STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_script_runtime.cpp
Go to the documentation of this file.
1/**
2 * Dynamic states, executing. Asserted on the wire, because a clock inside the sequencer is exactly
3 * what made the original actuator-delay bug invisible for so long.
4 *
5 * The cases that matter, and what each one catches if it goes wrong:
6 *
7 * - The vent pulse (motivating example 1) lands its close 500 ms after its open. Catches delay()
8 * built on sleep_for, whose drift only becomes visible over many iterations.
9 * - A valve a script opens is still open two republish periods later. Catches open_valve wired to
10 * sendSingleActuator alone: it works for a second and then the republish quietly reverts it.
11 * - An abort during a script reaches the boards in well under 200 ms. Catches the script running
12 * on the command worker, which would block every command including the abort for its whole
13 * runtime — the single worst failure this feature can have.
14 * - A transition out mid-delay stops the script dead. Catches a script outliving its own state.
15 * - A runaway loop lands on its timeout target, not its return target. Catches the two exits
16 * being conflated.
17 *
18 * Hermetic: loopback UDP, [database].port = 2 so no Elodin is needed, controller on TEST-NET-1.
19 */
20#include <unistd.h>
21
22#include <chrono>
23#include <cstdio>
24#include <filesystem>
25#include <fstream>
26#include <iostream>
27#include <string>
28#include <thread>
29
30#include "BoardListener.hpp"
33
34namespace fs = std::filesystem;
40using Clock = std::chrono::steady_clock;
41
42static int g_failures = 0;
43
44static void check(bool ok, const std::string& what) {
45 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
46 if (!ok)
47 g_failures++;
48}
49
51static constexpr uint16_t kActPort = 45919;
52static constexpr uint16_t kAbortPort = 15007;
53
54/** Channel 1 = Vent Valve (column always CLOSE — only a script opens it), 2 = Main Valve. */
55
56/**
57 * Run `body` with stdout redirected to a file, and give back what it printed.
58 *
59 * The diagnostics these tests pin are log lines and nothing else — they exist so an operator can
60 * tell a loop that never ran from one that ran and timed out, which are the two outcomes that
61 * looked identical on the stand. Asserting on the text is the only way to keep them honest.
62 */
63template <typename F>
64static std::string captureStdout(F&& body) {
65 const fs::path out = g_dir / "stdout_capture.txt";
66 std::fflush(stdout);
67 const int saved = dup(fileno(stdout));
68 FILE* redirected = std::freopen(out.string().c_str(), "w", stdout);
69 (void)redirected;
70 body();
71 std::fflush(stdout);
72 dup2(saved, fileno(stdout));
73 close(saved);
74 std::ifstream in(out);
75 return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
76}
77
78static std::string writeConfig(const std::string& script, uint32_t timeout_ms,
79 const std::string& return_target,
80 const std::string& timeout_target) {
81 {
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";
90 }
91 {
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";
96 }
97 {
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";
102 }
103
104 fs::create_directories(g_dir / "scripts");
105 {
106 std::ofstream s(g_dir / "scripts" / "run.script");
107 s << script;
108 }
109
110 const fs::path cfg_path = g_dir / "config.toml";
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
116 << "\n\n"
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()
126 << "\"\n\n"
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";
137 return cfg_path.string();
138}
139
140static long long waitForState(SequencerService& svc, State want, int timeout_ms) {
141 const auto t0 = Clock::now();
142 for (;;) {
143 const auto ms =
144 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
145 if (svc.currentState() == want)
146 return ms;
147 if (ms > timeout_ms)
148 return -1;
149 std::this_thread::sleep_for(std::chrono::milliseconds(2));
150 }
151}
152
153int main() {
154 std::cout << "=== Dynamic states, executing ===" << std::endl;
155
156 g_dir = fs::temp_directory_path() / "daq_script_runtime_test";
157 fs::remove_all(g_dir);
158 fs::create_directories(g_dir);
159
160 // Ids as THIS fixture declares them, never the compiled enumerators. State::PRESS_STANDBY is
161 // 20 and State::VENT is 13 in the enum, while here Press Standby is 3 and 13 is Dyn State —
162 // so comparing against the enumerator would silently ask about a different state, and one of
163 // those comparisons would even pass for the wrong reason. Ids are keys, not an ordering.
164 const State IDLE = static_cast<State>(1);
165 const State ARMED = static_cast<State>(2);
166 const State PRESS_STANDBY = static_cast<State>(3);
167 const State VENT = static_cast<State>(4);
168 const State DYN = static_cast<State>(13);
169 const State ENGINE_ABORT = static_cast<State>(17);
170 (void)DYN;
171
172 // ── 1. The vent pulse: open, hold 500 ms, close, leave ────────────────────────────────────
173 {
174 const std::string path = writeConfig(
175 "open_valve(VENT_VALVE)\n"
176 "delay(0.5)\n"
177 "close_valve(VENT_VALVE)\n"
178 "transition_to(PRESS_STANDBY)\n",
179 5000, "Idle", "Idle");
180
182 svc.init(path);
183 BoardListener listener(kActPort);
184 listener.start();
185
186 svc.transitionTo(std::string("Dyn State"));
187 const long long landed = waitForState(svc, PRESS_STANDBY, 3000);
188 std::this_thread::sleep_for(std::chrono::milliseconds(100));
189 listener.stop();
190
191 const auto b = listener.bursts();
192 const long long open = firstCommand(b, /*ch=*/1, /*hw=*/1);
193 const long long close = firstCommand(b, /*ch=*/1, /*hw=*/0, /*after_ms=*/open + 1);
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");
198 }
199
200 // ── 2. A script's valve survives more than one republish period ───────────────────────────
201 {
202 const std::string path = writeConfig(
203 "open_valve(VENT_VALVE)\n"
204 "delay(2.5)\n"
205 "close_valve(VENT_VALVE)\n"
206 "transition_to(IDLE)\n",
207 8000, "Idle", "Idle");
208
210 svc.init(path);
211 BoardListener listener(kActPort);
212 listener.start();
213 svc.transitionTo(std::string("Dyn State"));
214 std::this_thread::sleep_for(std::chrono::milliseconds(2300));
215 listener.stop();
216
217 int opens = 0;
218 for (const auto& burst : listener.bursts()) {
219 auto it = burst.state.find(1);
220 if (it != burst.state.end() && it->second == 1)
221 opens++;
222 }
223 // Without a script position this is exactly 1, and the republish closes it a second later.
224 check(opens >= 3, "the open is re-asserted by the republish (saw " + std::to_string(opens) +
225 " over 2.3 s)");
226 }
227
228 // ── 3. An abort during a running script is not delayed by it ──────────────────────────────
229 {
230 const std::string path = writeConfig(
231 "open_valve(VENT_VALVE)\n"
232 "delay(5)\n"
233 "close_valve(VENT_VALVE)\n"
234 "transition_to(IDLE)\n",
235 9000, "Idle", "Idle");
236
238 svc.init(path);
239 BoardListener abort_listener(kAbortPort);
240 abort_listener.start();
241
242 svc.transitionTo(std::string("Dyn State"));
243 std::this_thread::sleep_for(std::chrono::milliseconds(400));
244
245 const auto t0 = Clock::now();
246 svc.transitionTo(std::string("Engine Abort"));
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();
251
252 // The 200 ms bar test_abort_ordering uses. A script on the command worker would make this
253 // the remaining 4.6 s of its delay.
254 check(latency < 200, "the abort transition returned in " + std::to_string(latency) +
255 " ms despite a 5 s delay() in flight");
256 check(svc.currentState() == ENGINE_ABORT, "and the rig is in Engine Abort");
257 }
258
259 // ── 4. Transitioning out mid-script stops it dead ─────────────────────────────────────────
260 {
261 const std::string path = writeConfig(
262 "open_valve(VENT_VALVE)\n"
263 "delay(1.0)\n"
264 "open_valve(MAIN_VALVE)\n" // must never happen: we leave before this
265 "delay(5)\n"
266 "transition_to(IDLE)\n",
267 9000, "Idle", "Idle");
268
270 svc.init(path);
271 BoardListener listener(kActPort);
272 listener.start();
273 svc.transitionTo(std::string("Dyn State"));
274 std::this_thread::sleep_for(std::chrono::milliseconds(200));
275 svc.transitionTo(std::string("Armed"));
276 std::this_thread::sleep_for(std::chrono::milliseconds(1600));
277 listener.stop();
278
279 const long long main_open = firstCommand(listener.bursts(), /*ch=*/2, /*hw=*/1);
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");
282 }
283
284 // ── 5. A runaway lands on the TIMEOUT target, not the return target ───────────────────────
285 {
286 // Distinct targets, so which one it took is unambiguous.
287 const std::string path = writeConfig(
288 "open_valve(VENT_VALVE)\n"
289 "while elapsed() < 60:\n"
290 " delay(0.05)\n"
291 "transition_to(IDLE)\n",
292 700, /*return=*/"Armed", /*timeout=*/"Vent");
293
295 svc.init(path);
296 svc.transitionTo(std::string("Dyn State"));
297 const long long landed = waitForState(svc, VENT, 3000);
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)");
301 check(svc.currentState() != ARMED, "and NOT on the return target");
302 }
303
304 // ── 6. Running off the end lands on the RETURN target ─────────────────────────────────────
305 {
306 const std::string path =
307 writeConfig("open_valve(VENT_VALVE)\ndelay(0.2)\nclose_valve(VENT_VALVE)\n", 9000,
308 /*return=*/"Armed", /*timeout=*/"Vent");
309
311 svc.init(path);
312 svc.transitionTo(std::string("Dyn State"));
313 const long long landed = waitForState(svc, ARMED, 3000);
314 check(landed >= 0, "a script with no transition_to lands on its return target");
315 check(svc.currentState() != VENT, "and NOT on the timeout target");
316 }
317
318 // ── 7. A sensor with no fresh reading refuses the transition, before anything moves ───────
319 //
320 // No Elodin is running in this test, so the feed never receives anything. The transition-time
321 // precheck is what turns that into a refusal at the button rather than a script that enters
322 // the state, opens a valve, and only then discovers it cannot read what it needs.
323 {
324 const std::string path = writeConfig(
325 "open_valve(VENT_VALVE)\n"
326 "if pressure(TANK_PRESSURE) > 100:\n"
327 " open_valve(MAIN_VALVE)\n"
328 "delay(0.2)\n",
329 3000, /*return=*/"Armed", /*timeout=*/"Vent");
330
332 svc.init(path);
333 BoardListener listener(kActPort);
334 listener.start();
335
336 std::string reason;
337 const bool accepted = svc.transitionTo(std::string("Dyn State"), 0, &reason);
338 std::this_thread::sleep_for(std::chrono::milliseconds(250));
339 listener.stop();
340
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 + "\")");
344 // The SPECIFIC diagnosis, not a flattened one. This used to reply "<role> has no fresh
345 // calibrated reading" for all four outcomes, so an operator could not tell a sensor that
346 // never arrived from one that went stale or was never calibrated — three different things
347 // to go and fix, and only the log knew which.
348 check(reason.find("produced no reading yet") != std::string::npos,
349 "and says WHICH failure it was, matching the log (\"" + reason + "\")");
350
351 // The same fact, reachable by the GUI: SCRIPTS reports the live gate as BLOCKED so a
352 // greyed button can explain itself. Only load failures were ever reported here before,
353 // and the sensor gate is the one an operator actually meets.
354 const std::string report = svc.scriptStatusReport();
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");
359 check(svc.currentState() == IDLE, "the rig did not move");
360 check(firstCommand(listener.bursts(), /*ch=*/1, /*hw=*/1) < 0,
361 "and not one valve was commanded");
362 }
363
364 // ── 8. Debug mode and a scripted state ────────────────────────────────────────────────────
365 //
366 // Three rules, and only the middle one is new — the other two fall out of how transitions
367 // already work, which is exactly why they are worth pinning before someone "simplifies" them.
368 {
369 const std::string path = writeConfig(
370 "open_valve(VENT_VALVE)\n"
371 "delay(4)\n"
372 "close_valve(VENT_VALVE)\n"
373 "transition_to(IDLE)\n",
374 9000, /*return=*/"Idle", /*timeout=*/"Idle");
375
377 svc.init(path);
378 svc.setDebugMode(true);
379
380 // (a) Entering clears whatever the operator had commanded by hand. Same as every other
381 // state: the incoming state's column is the truth, and a leftover override would silently
382 // outrank it.
383 BoardListener before(kActPort);
384 before.start();
385 check(svc.manualActuator("Main Valve", 1),
386 "debug can command a valve in an ordinary state");
387 std::this_thread::sleep_for(std::chrono::milliseconds(150));
388 before.stop();
389 check(firstCommand(before.bursts(), /*ch=*/2, /*hw=*/1) >= 0, "and it reaches the board");
390
391 BoardListener listener(kActPort);
392 listener.start();
393 svc.transitionTo(std::string("Dyn State"));
394 std::this_thread::sleep_for(std::chrono::milliseconds(1400));
395
396 // (b) A hand command during a script is ALLOWED, and the operator outranks the script.
397 // They are fighting over a valve and the script cannot notice — but the person at the
398 // panel is the last line, and taking that away to protect a script is the wrong trade.
399 check(svc.manualActuator("Main Valve", 1),
400 "debug can still command a valve while a script is running");
401 std::this_thread::sleep_for(std::chrono::milliseconds(1400));
402 listener.stop();
403
404 const auto b8 = listener.bursts();
405 // Channel 2 is CLOSE in the Dyn State column, so the only thing that can open it is the
406 // operator — and it must stay open across a republish rather than being reverted.
407 const long long op_open = firstCommand(b8, /*ch=*/2, /*hw=*/1);
408 check(op_open >= 0, "the operator's command reaches the board mid-script");
409 int opens = 0;
410 for (const auto& burst : b8) {
411 auto it = burst.state.find(2);
412 if (it != burst.state.end() && it->second == 1)
413 opens++;
414 }
415 check(opens >= 2, "and the republish keeps re-asserting it, not the column's CLOSE");
416 // The override the operator set BEFORE entering was still cleared by the transition —
417 // that part is unchanged, and is what every other state does.
418 check(op_open > 900,
419 "the pre-transition override did not survive entry; this open is the new one");
420
421 // (c) Leaving is unrestricted in debug mode. Dyn State -> Engine Abort is a 0 in this
422 // fixture's matrix, so this only succeeds because debug mode relaxes the transition check
423 // — which is the escape hatch that makes (b) acceptable.
424 check(svc.transitionTo(std::string("Engine Abort")),
425 "debug mode still allows leaving a scripted state by a route the matrix forbids");
426 check(svc.currentState() == ENGINE_ABORT, "and the rig gets there");
427 }
428
429 // ── 9. A loop that never runs says so, with the numbers that decided it ───────────────────
430 //
431 // On the stand this was indistinguishable from a working run: the state entered, exited, and
432 // the journal said only "start" then "script complete". Whether the body executed once or
433 // never was unknowable, and the two mean opposite things.
434 {
435 const std::string path = writeConfig(
436 "while elapsed() > 100:\n" // false immediately — elapsed() starts at 0
437 " open_valve(VENT_VALVE)\n"
438 " delay(0.05)\n",
439 3000, /*return=*/"Idle", /*timeout=*/"Vent");
440
442 svc.init(path);
443 const std::string log = captureStdout([&] {
444 svc.transitionTo(std::string("Dyn State"));
445 std::this_thread::sleep_for(std::chrono::milliseconds(400));
446 });
447
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");
454 }
455
456 // ── 10. A loop that runs reports each pass and how it ended ───────────────────────────────
457 {
458 const std::string path = writeConfig(
459 "while elapsed() < 60:\n"
460 " open_valve(VENT_VALVE)\n"
461 " delay(0.05)\n",
462 600, /*return=*/"Idle", /*timeout=*/"Vent");
463
465 svc.init(path);
466 const std::string log = captureStdout([&] {
467 svc.transitionTo(std::string("Dyn State"));
468 std::this_thread::sleep_for(std::chrono::milliseconds(1200));
469 });
470
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");
475 }
476
477 // ── 11. The script waits for the state's own column before its first command ──────────────
478 //
479 // Both reach the board as UDP, but the boards poll ONE datagram per loop() and
480 // hotfire_config.h sets LOOP_DELAY_MS = 10, so the column's three retransmits take ~30 ms to
481 // drain and the script's first command queues behind them. On the stand this read as a valve
482 // not staying open for as long as the script asked.
483 {
484 const std::string path = writeConfig(
485 "open_valve(VENT_VALVE)\n"
486 "delay(0.3)\n"
487 "close_valve(VENT_VALVE)\n",
488 5000, "Idle", "Idle");
489
491 svc.init(path);
492 BoardListener listener(kActPort);
493 listener.start();
494
495 const auto t0 = Clock::now();
496 svc.transitionTo(std::string("Dyn State"));
497 std::this_thread::sleep_for(std::chrono::milliseconds(600));
498 listener.stop();
499
500 const auto b = listener.bursts();
501 const long long open = firstCommand(b, /*ch=*/1, /*hw=*/1);
502 check(open >= 90, "the script's first open waits out the column (" + std::to_string(open) +
503 " ms, want >= ~100)");
504 // Still prompt: a lead-in that drifted into the hundreds would eat a short state's budget.
505 check(open >= 0 && open <= 260,
506 "and does not overshoot the lead-in (" + std::to_string(open) + " ms)");
507 (void)t0;
508 }
509
510 // ── 12. An abort during the lead-in is not delayed by it ──────────────────────────────────
511 //
512 // The wait is spent on the SCRIPT's thread, never the command worker, so a queued abort never
513 // sits behind it. Sleeping in doTransitionTo instead would have put 100 ms straight into the
514 // 200 ms budget test_abort_ordering pins.
515 {
516 const std::string path = writeConfig(
517 "open_valve(VENT_VALVE)\n"
518 "delay(5)\n",
519 9000, "Idle", "Idle");
520
522 svc.init(path);
523 svc.transitionTo(std::string("Dyn State"));
524
525 // Land inside the lead-in, before the script has run a single statement.
526 std::this_thread::sleep_for(std::chrono::milliseconds(30));
527
528 const auto t0 = Clock::now();
529 svc.transitionTo(std::string("Engine Abort"));
530 const long long ms =
531 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
532
533 check(ms < 200,
534 "an abort mid-lead-in still lands in under 200 ms (" + std::to_string(ms) + " ms)");
535 check(svc.currentState() == ENGINE_ABORT, "and the rig is in the abort state");
536 }
537
538 fs::remove_all(g_dir);
539
540 std::cout << (g_failures == 0 ? "\nAll script-runtime checks passed.\n"
541 : "\nFAILURES: " + std::to_string(g_failures) + "\n");
542 return g_failures == 0 ? 0 : 1;
543}
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.
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.
State
Definition main.cpp:72
static bool ok
Definition flight_e2e.c:435
@ IDLE
Definition gnss_frame.c:6
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
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 int g_failures
static constexpr uint16_t kAbortPort
static fs::path g_dir
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.
int main()