19#include <netinet/in.h>
20#include <sys/socket.h>
40namespace fs = std::filesystem;
45using Clock = std::chrono::steady_clock;
49static void check(
bool ok,
const std::string& what) {
50 std::cout << (
ok ?
" ok " :
" FAIL ") << what << std::endl;
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";
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";
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";
76 std::cout <<
"=== Actuator delay / republish interaction ===" << std::endl;
78 const uint16_t
port = 45911;
79 const double kDelay = 1.4;
80 fs::path dir = fs::temp_directory_path() /
"daq_delay_test";
82 fs::create_directories(dir);
88 if (!listener.
start()) {
89 std::cerr <<
"could not bind UDP " <<
port <<
" — is something else using it?"
95 if (!ac.
load(cfg, (dir /
"state_machine_actuators.csv").string())) {
96 std::cerr <<
"ActuatorCommander::load failed" << std::endl;
101 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
105 const auto b = listener.
bursts();
106 check(
b.size() >= 3,
"at least three bursts (entry, republish, delayed stage)");
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");
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))
116 check(!early_leak,
"no burst before the delay contains the delayed actuator");
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");
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 &&
132 fired_on_time =
true;
134 check(fired_on_time,
"delayed stage fired within tolerance carrying only Slow Valve");
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) &&
142 check(settled,
"republish resumes covering both actuators once the stage has fired");
148 if (!listener.
start()) {
149 std::cerr <<
"could not re-bind UDP " <<
port << std::endl;
154 ac.
load(cfg, (dir /
"state_machine_actuators.csv").
string());
156 std::this_thread::sleep_for(std::chrono::milliseconds(300));
159 std::this_thread::sleep_for(std::chrono::milliseconds(2200));
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))
169 check(!stale_stage,
"stage from the abandoned state never fires after a new transition");
185 if (!listener.
start()) {
186 std::cerr <<
"could not re-bind UDP " <<
port << std::endl;
191 ac.
load(cfg, (dir /
"state_machine_actuators.csv").
string());
197 std::this_thread::sleep_for(std::chrono::milliseconds(2600));
201 const auto b = listener.
bursts();
202 const long long target =
static_cast<long long>(kDelay * 1000);
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");
211 "the delayed actuator was commanded at its delay, not immediately (at " +
212 std::to_string(slow_open) +
" ms, want ~" + std::to_string(
target) +
")");
216 std::cout << (
g_failures == 0 ?
"\nPASSED" :
"\nFAILED") << std::endl;
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
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
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 void check(bool ok, const std::string &what)
static std::string writeFixture(const fs::path &dir, uint16_t port, double slow_delay_s)