34namespace fs = std::filesystem;
40using Clock = std::chrono::steady_clock;
44static void check(
bool ok,
const std::string& what) {
45 std::cout << (
ok ?
" ok " :
" FAIL ") << what << std::endl;
67 const int saved = dup(fileno(stdout));
68 FILE* redirected = std::freopen(out.string().c_str(),
"w", stdout);
72 dup2(saved, fileno(stdout));
74 std::ifstream in(out);
75 return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
78static std::string
writeConfig(
const std::string& script, uint32_t timeout_ms,
79 const std::string& return_target,
80 const std::string& timeout_target) {
82 std::ofstream t(
g_dir /
"state_transitions.csv");
83 t <<
",Idle,Armed,Press Standby,Vent,Dyn State,Engine Abort\n";
84 t <<
"Idle,1,1,1,1,1,1\n";
85 t <<
"Armed,1,1,1,1,1,1\n";
86 t <<
"Press Standby,1,1,1,1,1,1\n";
87 t <<
"Vent,1,1,1,1,1,1\n";
88 t <<
"Dyn State,1,1,1,1,1,1\n";
89 t <<
"Engine Abort,1,1,1,1,0,1\n";
92 std::ofstream
a(
g_dir /
"state_machine_actuators.csv");
93 a <<
",Idle,Armed,Press Standby,Vent,Dyn State,Engine Abort\n";
94 a <<
"Vent Valve,CLOSE,CLOSE,CLOSE,CLOSE,CLOSE,CLOSE\n";
95 a <<
"Main Valve,CLOSE,CLOSE,CLOSE,CLOSE,CLOSE,CLOSE\n";
98 std::ofstream d(
g_dir /
"state_machine_actuator_delays.csv");
99 d <<
",Idle,Armed,Press Standby,Vent,Dyn State,Engine Abort\n";
100 d <<
"Vent Valve,0,0,0,0,0,0\n";
101 d <<
"Main Valve,0,0,0,0,0,0\n";
104 fs::create_directories(
g_dir /
"scripts");
106 std::ofstream s(
g_dir /
"scripts" /
"run.script");
111 std::ofstream c(cfg_path);
112 c <<
"[network]\nactuator_cmd_port = " <<
kActPort <<
"\n\n"
113 <<
"[database]\nport = 2\n\n"
114 <<
"[controller_service]\nhost = \"192.0.2.1\"\nport = 9999\n\n"
115 <<
"[server_heartbeat]\nbroadcast_ip = \"127.0.0.1\"\nbroadcast_port = " <<
kAbortPort
117 <<
"[boards.act_board]\ntype = \"ACTUATOR\"\nip = \"127.0.0.1\"\nboard_id = 12\n"
118 "enabled = true\nnum_actuators = 10\n\n"
119 <<
"[boards.pt_board]\ntype = \"PT\"\nip = \"127.0.0.1\"\nboard_id = 21\nenabled = true\n\n"
120 <<
"[sensor_roles_pt_board]\n\"Tank Pressure\" = 1\n\n"
121 <<
"[actuator_roles]\n\"Vent Valve\" = [\"NC\", 1, 12]\n\"Main Valve\" = [\"NC\", 2, 12]\n\n"
122 <<
"[state_machine]\n"
123 <<
"transitions_csv = \"" << (
g_dir /
"state_transitions.csv").
string() <<
"\"\n"
124 <<
"actuator_csv = \"" << (
g_dir /
"state_machine_actuators.csv").
string() <<
"\"\n"
125 <<
"actuator_delay_csv = \"" << (
g_dir /
"state_machine_actuator_delays.csv").
string()
127 <<
"[[states]]\nid = 1\nname = \"Idle\"\nis_boot = true\n\n"
128 <<
"[[states]]\nid = 2\nname = \"Armed\"\n\n"
129 <<
"[[states]]\nid = 3\nname = \"Press Standby\"\n\n"
130 <<
"[[states]]\nid = 4\nname = \"Vent\"\n\n"
131 <<
"[[states]]\nid = 17\nname = \"Engine Abort\"\nis_abort = true\n\n"
132 <<
"[[states]]\nid = 13\nname = \"Dyn State\"\n"
133 <<
"script_file = \"run.script\"\n"
134 <<
"script_timeout_ms = " << timeout_ms <<
"\n"
135 <<
"script_return_target = \"" << return_target <<
"\"\n"
136 <<
"script_timeout_target = \"" << timeout_target <<
"\"\n\n";
141 const auto t0 = Clock::now();
144 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
149 std::this_thread::sleep_for(std::chrono::milliseconds(2));
154 std::cout <<
"=== Dynamic states, executing ===" << std::endl;
156 g_dir = fs::temp_directory_path() /
"daq_script_runtime_test";
157 fs::remove_all(
g_dir);
158 fs::create_directories(
g_dir);
166 const State PRESS_STANDBY =
static_cast<State>(3);
169 const State ENGINE_ABORT =
static_cast<State>(17);
175 "open_valve(VENT_VALVE)\n"
177 "close_valve(VENT_VALVE)\n"
178 "transition_to(PRESS_STANDBY)\n",
179 5000,
"Idle",
"Idle");
187 const long long landed =
waitForState(svc, PRESS_STANDBY, 3000);
188 std::this_thread::sleep_for(std::chrono::milliseconds(100));
191 const auto b = listener.
bursts();
194 check(open >= 0,
"the script opened the vent valve");
195 check(close > 0 && (close - open) >= 430 && (close - open) <= 640,
196 "it closed " + std::to_string(close - open) +
" ms later (want ~500)");
197 check(landed >= 0,
"and the script's transition_to landed in Press Standby");
203 "open_valve(VENT_VALVE)\n"
205 "close_valve(VENT_VALVE)\n"
206 "transition_to(IDLE)\n",
207 8000,
"Idle",
"Idle");
214 std::this_thread::sleep_for(std::chrono::milliseconds(2300));
218 for (
const auto& burst : listener.
bursts()) {
219 auto it = burst.state.find(1);
220 if (it != burst.state.end() && it->second == 1)
224 check(opens >= 3,
"the open is re-asserted by the republish (saw " + std::to_string(opens) +
231 "open_valve(VENT_VALVE)\n"
233 "close_valve(VENT_VALVE)\n"
234 "transition_to(IDLE)\n",
235 9000,
"Idle",
"Idle");
240 abort_listener.
start();
243 std::this_thread::sleep_for(std::chrono::milliseconds(400));
245 const auto t0 = Clock::now();
247 const long long latency =
248 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
249 std::this_thread::sleep_for(std::chrono::milliseconds(100));
250 abort_listener.
stop();
254 check(latency < 200,
"the abort transition returned in " + std::to_string(latency) +
255 " ms despite a 5 s delay() in flight");
262 "open_valve(VENT_VALVE)\n"
264 "open_valve(MAIN_VALVE)\n"
266 "transition_to(IDLE)\n",
267 9000,
"Idle",
"Idle");
274 std::this_thread::sleep_for(std::chrono::milliseconds(200));
276 std::this_thread::sleep_for(std::chrono::milliseconds(1600));
280 check(main_open < 0,
"the statement after the transition never ran");
281 check(svc.
currentState() == ARMED,
"and the rig stayed where the operator put it");
288 "open_valve(VENT_VALVE)\n"
289 "while elapsed() < 60:\n"
291 "transition_to(IDLE)\n",
292 700,
"Armed",
"Vent");
298 check(landed >= 0,
"the runaway landed on its timeout target");
299 check(landed >= 500 && landed <= 1400,
300 "at about the configured 700 ms (" + std::to_string(landed) +
" ms)");
306 const std::string
path =
307 writeConfig(
"open_valve(VENT_VALVE)\ndelay(0.2)\nclose_valve(VENT_VALVE)\n", 9000,
313 const long long landed =
waitForState(svc, ARMED, 3000);
314 check(landed >= 0,
"a script with no transition_to lands on its return target");
325 "open_valve(VENT_VALVE)\n"
326 "if pressure(TANK_PRESSURE) > 100:\n"
327 " open_valve(MAIN_VALVE)\n"
329 3000,
"Armed",
"Vent");
337 const bool accepted = svc.
transitionTo(std::string(
"Dyn State"), 0, &reason);
338 std::this_thread::sleep_for(std::chrono::milliseconds(250));
341 check(!accepted,
"a script whose sensor has no fresh reading is refused entry");
342 check(reason.find(
"Tank Pressure") != std::string::npos,
343 "and the refusal names the sensor (\"" + reason +
"\")");
348 check(reason.find(
"produced no reading yet") != std::string::npos,
349 "and says WHICH failure it was, matching the log (\"" + reason +
"\")");
355 check(report.find(
":BLOCKED:") != std::string::npos,
356 "the status report calls the state blocked");
357 check(report.find(
"produced no reading yet") != std::string::npos,
358 "and carries the reason with it");
361 "and not one valve was commanded");
370 "open_valve(VENT_VALVE)\n"
372 "close_valve(VENT_VALVE)\n"
373 "transition_to(IDLE)\n",
374 9000,
"Idle",
"Idle");
386 "debug can command a valve in an ordinary state");
387 std::this_thread::sleep_for(std::chrono::milliseconds(150));
394 std::this_thread::sleep_for(std::chrono::milliseconds(1400));
400 "debug can still command a valve while a script is running");
401 std::this_thread::sleep_for(std::chrono::milliseconds(1400));
404 const auto b8 = listener.
bursts();
408 check(op_open >= 0,
"the operator's command reaches the board mid-script");
410 for (
const auto& burst : b8) {
411 auto it = burst.state.find(2);
412 if (it != burst.state.end() && it->second == 1)
415 check(opens >= 2,
"and the republish keeps re-asserting it, not the column's CLOSE");
419 "the pre-transition override did not survive entry; this open is the new one");
425 "debug mode still allows leaving a scripted state by a route the matrix forbids");
436 "while elapsed() > 100:\n"
437 " open_valve(VENT_VALVE)\n"
439 3000,
"Idle",
"Vent");
445 std::this_thread::sleep_for(std::chrono::milliseconds(400));
448 check(log.find(
"while false at entry") != std::string::npos,
449 "a loop whose condition is false at entry says so");
450 check(log.find(
"body never runs, 0 iterations") != std::string::npos,
451 "and reports that the body never ran");
452 check(log.find(
"script complete after 0 iteration(s)") != std::string::npos,
453 "and the completion line carries the iteration count");
459 "while elapsed() < 60:\n"
460 " open_valve(VENT_VALVE)\n"
462 600,
"Idle",
"Vent");
468 std::this_thread::sleep_for(std::chrono::milliseconds(1200));
471 check(log.find(
"while true at entry") != std::string::npos,
472 "a loop that runs announces entering");
473 check(log.find(
"iteration 1") != std::string::npos,
"and numbers each pass");
474 check(log.find(
"iteration 2") != std::string::npos,
"including later ones");
485 "open_valve(VENT_VALVE)\n"
487 "close_valve(VENT_VALVE)\n",
488 5000,
"Idle",
"Idle");
495 const auto t0 = Clock::now();
497 std::this_thread::sleep_for(std::chrono::milliseconds(600));
500 const auto b = listener.
bursts();
502 check(open >= 90,
"the script's first open waits out the column (" + std::to_string(open) +
503 " ms, want >= ~100)");
505 check(open >= 0 && open <= 260,
506 "and does not overshoot the lead-in (" + std::to_string(open) +
" ms)");
517 "open_valve(VENT_VALVE)\n"
519 9000,
"Idle",
"Idle");
526 std::this_thread::sleep_for(std::chrono::milliseconds(30));
528 const auto t0 = Clock::now();
531 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
534 "an abort mid-lead-in still lands in under 200 ms (" + std::to_string(ms) +
" ms)");
538 fs::remove_all(
g_dir);
540 std::cout << (
g_failures == 0 ?
"\nAll script-runtime checks passed.\n"
541 :
"\nFAILURES: " + std::to_string(
g_failures) +
"\n");
Collects UDP actuator packets, folding the 3x retransmit of one batch into a single burst.
std::vector< Burst > bursts() const
Top-level sequencer service.
bool setDebugMode(bool enabled)
Enable or disable debug mode.
bool init(const std::string &config_path="config/config.toml")
Load config, CSVs, connect to Elodin.
State currentState() const
bool transitionTo(const std::string &state_name)
Request a state transition by name (e.g.
std::string scriptStatusReport() const
What this process made of its dynamic states at startup, as newline-terminated text.
bool manualActuator(const std::string &name, int pos, std::string *refusal_reason=nullptr)
Debug mode only: manual OPEN/CLOSE for one role.
Parses state_transitions.csv and validates state transitions.
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
*brief create JSON pointer *sa reference_tokens const string_t & a
std::chrono::steady_clock Clock
long long firstCommand(const std::vector< Burst > &bursts, uint8_t ch, uint8_t hw, long long after_ms=-1)
When ch was first commanded to hw at or after after_ms.
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
std::string string() const
static constexpr uint16_t kAbortPort
static long long waitForState(SequencerService &svc, State want, int timeout_ms)
static constexpr uint16_t kActPort
static void check(bool ok, const std::string &what)
static std::string writeConfig(const std::string &script, uint32_t timeout_ms, const std::string &return_target, const std::string &timeout_target)
static std::string captureStdout(F &&body)
Channel 1 = Vent Valve (column always CLOSE — only a script opens it), 2 = Main Valve.
long long firstCommand(const std::vector< Burst > &bursts, uint8_t ch, uint8_t hw, long long after_ms=-1)
When ch was first commanded to hw at or after after_ms.