STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_script_positions.cpp
Go to the documentation of this file.
1/**
2 * Script positions: a running script layering valve commands on top of its state's CSV column.
3 *
4 * Three properties, each of which fails silently if it is wrong:
5 *
6 * 1. A script's valve position SURVIVES the 1 Hz republish. Commanding a valve once and walking
7 * away is the obvious implementation and it works for about a second, after which the
8 * republish re-sends the state's column and puts the valve back. Any script whose valve is
9 * open longer than a republish period would quietly stop working.
10 *
11 * 2. Valves the script never names still follow their column, stagger included. A script-owned
12 * flag applied too broadly would take the whole column out of the republish.
13 *
14 * 3. Taking a valve cancels its staged command. The state's own delays column can schedule a
15 * valve to move seconds AFTER the script has already commanded it; the stage lands last and
16 * the valve ends up opposite to what the script said, with nothing logged.
17 *
18 * Drives ActuatorCommander directly — there is no interpreter yet — and asserts on the wire,
19 * because a clock inside the commander is exactly what made the original delay bug invisible.
20 */
21#include <chrono>
22#include <filesystem>
23#include <fstream>
24#include <iostream>
25#include <string>
26#include <thread>
27
28#include "BoardListener.hpp"
31
32namespace fs = std::filesystem;
37
38static int g_failures = 0;
39
40static void check(bool ok, const std::string& what) {
41 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
42 if (!ok)
43 g_failures++;
44}
45
46/** Channel 1 = Vent Valve, channel 2 = Staged Valve (which the Armed column staggers). */
47static std::string writeFixture(const fs::path& dir, uint16_t port, double staged_delay_s) {
48 {
49 std::ofstream act(dir / "state_machine_actuators.csv");
50 act << ",Idle,Armed\n";
51 act << "Vent Valve,CLOSE,CLOSE\n"; // never opened by the column — only a script can
52 act << "Staged Valve,CLOSE,OPEN\n"; // opened by the column, on a delay
53 }
54 {
55 std::ofstream del(dir / "state_machine_actuator_delays.csv");
56 del << ",Idle,Armed\n";
57 del << "Vent Valve,0,0\n";
58 del << "Staged Valve,0," << staged_delay_s << "\n";
59 }
60 return "[network]\nactuator_cmd_port = " + std::to_string(port) +
61 "\n\n[actuator_service]\nbind_address = \"0.0.0.0\"\n\n"
62 "[boards.act_board]\ntype = \"ACTUATOR\"\nip = \"127.0.0.1\"\nboard_id = 12\n"
63 "enabled = true\nnum_actuators = 10\n\n"
64 "[actuator_roles]\n\"Vent Valve\" = [\"NC\", 1, 12]\n"
65 "\"Staged Valve\" = [\"NC\", 2, 12]\n";
66}
67
68/** How many distinct bursts commanded `ch` to `hw`. */
69static int countCommands(const std::vector<daqtest::Burst>& bursts, uint8_t ch, uint8_t hw) {
70 int n = 0;
71 for (const auto& b : bursts) {
72 auto it = b.state.find(ch);
73 if (it != b.state.end() && it->second == hw)
74 n++;
75 }
76 return n;
77}
78
79int main() {
80 std::cout << "=== Script positions vs the republish ===" << std::endl;
81
82 const uint16_t port = 45918;
83 const double kStagedDelay = 2.0; // well past the 1 s republish
84 fs::path dir = fs::temp_directory_path() / "daq_script_positions_test";
85 fs::remove_all(dir);
86 fs::create_directories(dir);
87 const std::string cfg = writeFixture(dir, port, kStagedDelay);
88 const std::string csv = (dir / "state_machine_actuators.csv").string();
89
90 // ── 1. A script position survives the republish ───────────────────────────────────────────
91 {
92 BoardListener listener(port);
93 if (!listener.start()) {
94 std::cerr << "could not bind UDP " << port << std::endl;
95 return 1;
96 }
98 if (!ac.load(cfg, csv)) {
99 std::cerr << "load failed" << std::endl;
100 listener.stop();
101 return 1;
102 }
103 // Vent Valve is CLOSE in the Armed column; the script opens it anyway.
104 ac.setScriptPosition("Vent Valve", 1);
105 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/true);
106 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
108 listener.stop();
109
110 const auto b = listener.bursts();
111 const int opens = countCommands(b, /*ch=*/1, /*hw=*/1);
112 const int closes = countCommands(b, /*ch=*/1, /*hw=*/0);
113 check(opens >= 3, "the script's OPEN is re-asserted by every republish (saw " +
114 std::to_string(opens) + ")");
115 check(closes == 0, "the republish never reverts it to the column's CLOSE");
116 }
117
118 // ── 2. Valves the script does not name still follow their column, stagger included ─────────
119 {
120 BoardListener listener(port);
121 listener.start();
123 ac.load(cfg, csv);
124 ac.setScriptPosition("Vent Valve", 1); // script owns ONLY this one
125 ac.applyForState(State::ARMED, /*is_transition=*/true);
126 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/false);
127 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
129 listener.stop();
130
131 const auto b = listener.bursts();
132 const long long staged_open = firstCommand(b, /*ch=*/2, /*hw=*/1);
133 check(staged_open >= 1700 && staged_open <= 2400,
134 "an unnamed valve still honours its 2 s stagger (opened at " +
135 std::to_string(staged_open) + " ms)");
136 }
137
138 // ── 3. Taking a valve cancels its staged command ──────────────────────────────────────────
139 //
140 // The Armed column opens Staged Valve at +2 s. The script closes it at ~50 ms. If the stage
141 // is not cancelled it lands at 2 s and re-opens a valve the script had deliberately shut.
142 {
143 BoardListener listener(port);
144 listener.start();
146 ac.load(cfg, csv);
147 ac.applyForState(State::ARMED, /*is_transition=*/true); // arms the +2 s stage
148 std::this_thread::sleep_for(std::chrono::milliseconds(50));
149 ac.setScriptPosition("Staged Valve", 0); // script takes it and closes it
150 ac.sendSingleActuator("Staged Valve", 0);
151 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/false);
152 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
154 listener.stop();
155
156 const auto b = listener.bursts();
157 const long long reopened = firstCommand(b, /*ch=*/2, /*hw=*/1, /*after_ms=*/100);
158 check(reopened < 0, "the stale +2 s stage never re-opens the valve the script closed");
159 check(countCommands(b, /*ch=*/2, /*hw=*/0) >= 2,
160 "and the republish keeps re-asserting the script's CLOSE");
161 }
162
163 // ── 4. A debug manual override beats a running script ─────────────────────────────────────
164 {
165 BoardListener listener(port);
166 listener.start();
168 ac.load(cfg, csv);
169 ac.setScriptPosition("Vent Valve", 1);
170 ac.setManualOverride("Vent Valve", 0); // operator takes it back
171 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/true);
172 std::this_thread::sleep_for(std::chrono::milliseconds(1200));
174 listener.stop();
175
176 const auto b = listener.bursts();
177 check(countCommands(b, /*ch=*/1, /*hw=*/1) == 0 &&
178 countCommands(b, /*ch=*/1, /*hw=*/0) >= 1,
179 "the operator's override wins over the script's position");
180 }
181
182 // ── 5. Clearing script positions hands the valve back to the column ───────────────────────
183 {
184 BoardListener listener(port);
185 listener.start();
187 ac.load(cfg, csv);
188 ac.setScriptPosition("Vent Valve", 1);
189 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/true);
190 std::this_thread::sleep_for(std::chrono::milliseconds(1200));
192 std::this_thread::sleep_for(std::chrono::milliseconds(1300));
194 listener.stop();
195
196 const auto b = listener.bursts();
197 check(countCommands(b, /*ch=*/1, /*hw=*/1) >= 1, "it was open while the script held it");
198 check(countCommands(b, /*ch=*/1, /*hw=*/0) >= 1,
199 "and back to the column's CLOSE once the script's positions were cleared");
200 }
201
202 fs::remove_all(dir);
203 std::cout << (g_failures == 0 ? "\nAll script-position checks passed.\n"
204 : "\nFAILURES: " + std::to_string(g_failures) + "\n");
205 return g_failures == 0 ? 0 : 1;
206}
Collects UDP actuator packets, folding the 3x retransmit of one batch into a single burst.
std::vector< Burst > bursts() const
Sends UDP actuator commands based on state transitions.
void stopContinuousLoop()
Stop the continuous re-send loop (blocks until the thread exits).
void applyForState(State state, bool is_transition=false)
Send actuator commands for the given state (one shot).
void startContinuousLoop(State state, bool allow_delays=true, bool defer_first_pass=false)
Start the 1 Hz continuous re-send loop for the given state.
bool load(const std::string &config_content, const std::string &csv_path, const std::string &delay_csv_path="")
Load actuator roles from config content and state→actuator map from CSV.
void setManualOverride(const std::string &name, int pos)
Debug: hold this logical position for the role until cleared or state transition.
void setScriptPosition(const std::string &name, int pos)
A running script's position for one valve — the script layering on top of its state's CSV column.
bool sendSingleActuator(const std::string &name, int pos)
Send one UDP packet for a single role (debug manual command).
void clearScriptPositions()
Drop every script position.
static const emmc_port_t port
Definition emmc_stm32.c:210
static bool ok
Definition flight_e2e.c:435
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
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.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
static int g_failures
static void check(bool ok, const std::string &what)
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.
static std::string writeFixture(const fs::path &dir, uint16_t port, double staged_delay_s)
Channel 1 = Vent Valve, channel 2 = Staged Valve (which the Armed column staggers).
static int countCommands(const std::vector< daqtest::Burst > &bursts, uint8_t ch, uint8_t hw)
How many distinct bursts commanded ch to hw.
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98