STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_actuator_delays.cpp
Go to the documentation of this file.
1/**
2 * Actuator delay scheduling vs the 1 Hz republish.
3 *
4 * The delicate part of per-actuator delays is not the timer, it is the interaction with
5 * ActuatorCommander's continuous loop, which re-sends every actuator's position once a second so a
6 * rebooted board recovers. Two ways that fights a delayed command, both of which this pins:
7 *
8 * 1. The republish must NOT send the delayed actuator's settled position while its delay is
9 * pending — otherwise any delay longer than the republish period is unobservable, because the
10 * final value has already gone out. (Boards latch their last command, so the actuator is
11 * simply omitted and holds its pre-transition position.)
12 * 2. The republish must NOT cancel the pending schedule. Cancellation is keyed to real state
13 * transitions only; an earlier version bumped the generation on every apply and the delayed
14 * stage never fired.
15 *
16 * Drives the real ActuatorCommander over loopback UDP and asserts on what the "boards" receive.
17 */
18#include <arpa/inet.h>
19#include <netinet/in.h>
20#include <sys/socket.h>
21#include <unistd.h>
22
23#include <atomic>
24#include <chrono>
25#include <cstdio>
26#include <cstdlib>
27#include <filesystem>
28#include <fstream>
29#include <iostream>
30#include <mutex>
31#include <set>
32#include <string>
33#include <thread>
34#include <vector>
35
36#include "BoardListener.hpp"
39
40namespace fs = std::filesystem;
42using daqtest::Burst;
45using Clock = std::chrono::steady_clock;
46
47static int g_failures = 0;
48
49static void check(bool ok, const std::string& what) {
50 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
51 if (!ok)
52 g_failures++;
53}
54
55static std::string writeFixture(const fs::path& dir, uint16_t port, double slow_delay_s) {
56 std::ofstream act(dir / "state_machine_actuators.csv");
57 act << ",Idle,Armed\n";
58 act << "Fast Valve,CLOSE,OPEN\n";
59 act << "Slow Valve,CLOSE,OPEN\n";
60 act.close();
61
62 std::ofstream del(dir / "state_machine_actuator_delays.csv");
63 del << ",Idle,Armed\n";
64 del << "Fast Valve,0,0\n";
65 del << "Slow Valve,0," << slow_delay_s << "\n";
66 del.close();
67
68 return "[network]\nactuator_cmd_port = " + std::to_string(port) +
69 "\n\n[actuator_service]\nbind_address = \"0.0.0.0\"\n\n"
70 "[boards.act_board]\ntype = \"ACTUATOR\"\nip = \"127.0.0.1\"\nboard_id = 12\n"
71 "enabled = true\nnum_actuators = 10\n\n"
72 "[actuator_roles]\n\"Fast Valve\" = [\"NC\", 1, 12]\n\"Slow Valve\" = [\"NC\", 2, 12]\n";
73}
74
75int main() {
76 std::cout << "=== Actuator delay / republish interaction ===" << std::endl;
77
78 const uint16_t port = 45911;
79 const double kDelay = 1.4; // longer than the 1 s republish, so a republish lands mid-wait
80 fs::path dir = fs::temp_directory_path() / "daq_delay_test";
81 fs::remove_all(dir);
82 fs::create_directories(dir);
83 const std::string cfg = writeFixture(dir, port, kDelay);
84
85 // ── 1. A delayed actuator is withheld at t=0 and by the republish, then fires on time ──────
86 {
87 BoardListener listener(port);
88 if (!listener.start()) {
89 std::cerr << "could not bind UDP " << port << " — is something else using it?"
90 << std::endl;
91 return 1;
92 }
93
95 if (!ac.load(cfg, (dir / "state_machine_actuators.csv").string())) {
96 std::cerr << "ActuatorCommander::load failed" << std::endl;
97 listener.stop();
98 return 1;
99 }
100 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/true);
101 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
103 listener.stop();
104
105 const auto b = listener.bursts();
106 check(b.size() >= 3, "at least three bursts (entry, republish, delayed stage)");
107
108 bool entry_excludes_slow = !b.empty() && b[0].channels.count(1) && !b[0].channels.count(2);
109 check(entry_excludes_slow, "t=0 burst carries Fast Valve but NOT the delayed Slow Valve");
110
111 // Any burst before the delay elapses must still exclude channel 2.
112 bool early_leak = false;
113 for (const auto& x : b)
114 if (x.at_ms < static_cast<long long>(kDelay * 1000) - 150 && x.channels.count(2))
115 early_leak = true;
116 check(!early_leak, "no burst before the delay contains the delayed actuator");
117
118 // The republish that lands mid-wait must still have happened (proving it did not cancel
119 // the schedule and did not send the settled position).
120 bool mid_republish = false;
121 for (const auto& x : b)
122 if (x.at_ms > 800 && x.at_ms < 1300 && x.channels.count(1) && !x.channels.count(2))
123 mid_republish = true;
124 check(mid_republish, "republish at ~1 s re-sent Fast Valve and still skipped Slow Valve");
125
126 // The delayed stage fires near its deadline, carrying only the delayed actuator.
127 bool fired_on_time = false;
128 for (const auto& x : b) {
129 const long long target = static_cast<long long>(kDelay * 1000);
130 if (x.channels.count(2) && !x.channels.count(1) && x.at_ms > target - 200 &&
131 x.at_ms < target + 400)
132 fired_on_time = true;
133 }
134 check(fired_on_time, "delayed stage fired within tolerance carrying only Slow Valve");
135
136 // After firing, the republish covers both again.
137 bool settled = false;
138 for (const auto& x : b)
139 if (x.at_ms > static_cast<long long>(kDelay * 1000) + 100 && x.channels.count(1) &&
140 x.channels.count(2))
141 settled = true;
142 check(settled, "republish resumes covering both actuators once the stage has fired");
143 }
144
145 // ── 2. A new transition inside the delay window abandons the pending stage ────────────────
146 {
147 BoardListener listener(port);
148 if (!listener.start()) {
149 std::cerr << "could not re-bind UDP " << port << std::endl;
150 return 1;
151 }
152
154 ac.load(cfg, (dir / "state_machine_actuators.csv").string());
155 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/true);
156 std::this_thread::sleep_for(std::chrono::milliseconds(300));
157 // Idle has no delays, so this entry is entirely immediate — and must strand Armed's stage.
158 ac.startContinuousLoop(State::IDLE, /*allow_delays=*/true);
159 std::this_thread::sleep_for(std::chrono::milliseconds(2200));
161 listener.stop();
162
163 // Armed commands Slow Valve OPEN (hw 1 for an NC valve); Idle commands it CLOSE (hw 0).
164 // If the abandoned stage had fired we would see a lone channel-2 burst after the switch.
165 bool stale_stage = false;
166 for (const auto& x : listener.bursts())
167 if (x.at_ms > 500 && x.channels.count(2) && !x.channels.count(1))
168 stale_stage = true;
169 check(!stale_stage, "stage from the abandoned state never fires after a new transition");
170 }
171
172 // ── 3. The caller's OWN sequence must stagger, not just the loop's ────────────────────────
173 // Blocks 1 and 2 drive startContinuousLoop directly. SequencerService::transitionTo does not:
174 // it calls applyForState(to) first and starts the loop after. For a long time that direct call
175 // omitted the flag, and it defaults to false — "this is the 1 Hz republish, send settled
176 // positions" — so it shipped every actuator at t=0 and the staged schedule that followed was
177 // re-commanding valves already in position. Every configured delay was inert.
178 //
179 // This block pins the mode PAIR the caller now has to use: a staged entry, then a loop that
180 // only republishes. It cannot catch the regression itself — the wrong call lived in
181 // SequencerService, which this target does not link. test_hold_rules drives the real caller
182 // and asserts the stagger on the wire; that is the one that fails if this is ever undone.
183 {
184 BoardListener listener(port);
185 if (!listener.start()) {
186 std::cerr << "could not re-bind UDP " << port << std::endl;
187 return 1;
188 }
189
191 ac.load(cfg, (dir / "state_machine_actuators.csv").string());
192
193 // Exactly what transitionTo runs on a real state change.
194 ac.applyForState(State::ARMED, /*is_transition=*/true);
195 ac.startContinuousLoop(State::ARMED, /*allow_delays=*/false);
196
197 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
199 listener.stop();
200
201 const auto b = listener.bursts();
202 const long long target = static_cast<long long>(kDelay * 1000);
203
204 check(!b.empty() && b[0].channels.count(1),
205 "the transition's own apply sends the undelayed actuator at t=0");
206 check(!b.empty() && !b[0].channels.count(2),
207 "the transition's own apply does NOT send the delayed actuator at t=0");
208
209 const long long slow_open = daqtest::firstCommand(b, /*ch=*/2, /*hw=*/1);
210 check(slow_open >= target - 200 && slow_open <= target + 400,
211 "the delayed actuator was commanded at its delay, not immediately (at " +
212 std::to_string(slow_open) + " ms, want ~" + std::to_string(target) + ")");
213 }
214
215 fs::remove_all(dir);
216 std::cout << (g_failures == 0 ? "\nPASSED" : "\nFAILED") << std::endl;
217 return g_failures == 0 ? 0 : 1;
218}
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.
static const emmc_port_t port
Definition emmc_stm32.c:210
long target
Definition main.cpp:38
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
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....
One received command burst: when it arrived (ms since the listener started) and what it said.
static int g_failures
static void check(bool ok, const std::string &what)
static std::string writeFixture(const fs::path &dir, uint16_t port, double slow_delay_s)
int main()