56 std::cout <<
"=== FIRE lifecycle ===" << std::endl;
60 const uint32_t kDuration = 700;
61 const uint32_t kExtended = 1400;
64 std::vector<std::pair<bool, long long>> notices;
66 const auto t0 = Clock::now();
67 std::atomic<long long> expired_at{-1};
72 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0)
76 std::lock_guard<std::mutex>
lk(m);
78 active, std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0)
82 std::this_thread::sleep_for(std::chrono::milliseconds(kDuration + 400));
84 const long long at = expired_at.load();
85 check(
at >= 0,
"fire timer expired");
92 check(
at >=
static_cast<long long>(kDuration) - 5 &&
93 at <=
static_cast<long long>(kDuration) + 50,
94 "expiry honoured the CONFIGURED duration, not the built-in 6000 ms default");
96 std::lock_guard<std::mutex>
lk(m);
97 check(notices.size() == 2,
"exactly two controller notifications (start, stop)");
98 check(!notices.empty() && notices[0].first,
"first notification is FIRE_START");
99 check(notices.size() > 1 && !notices[1].first,
"second notification is FIRE_STOP");
100 check(notices.size() > 1 && notices[1].second >=
static_cast<long long>(kDuration) - 5,
101 "FIRE_STOP is sent at expiry, not early");
106 const uint32_t kDuration = 500;
107 const uint32_t kExtended = 1200;
110 const auto t0 = Clock::now();
111 std::atomic<long long> expired_at{-1};
114 expired_at = std::chrono::duration_cast<std::chrono::milliseconds>(
119 std::this_thread::sleep_for(std::chrono::milliseconds(300));
121 std::this_thread::sleep_for(std::chrono::milliseconds(kExtended + 500));
123 const long long at = expired_at.load();
124 check(
at >= 0,
"extended fire still expires");
126 check(
at >
static_cast<long long>(kDuration) + 150,
127 "extend pushed expiry past the original duration");
130 check(
at >= 300 +
static_cast<long long>(kExtended) - 20 &&
131 at <= 300 +
static_cast<long long>(kExtended) + 60,
132 "extended window is extended_ms measured from the extend call");
138 std::atomic<int> notify_count{0};
139 std::atomic<bool> expired{
false};
148 std::this_thread::sleep_for(std::chrono::milliseconds(150));
150 std::this_thread::sleep_for(std::chrono::milliseconds(800));
151 check(!expired.load(),
"leaving fire early cancels the expiry transition");
152 check(notify_count.load() == 2,
"early exit still notifies the controller (start + stop)");
161 std::atomic<long long> expired_at{-1};
162 const auto t0 = Clock::now();
165 expired_at = std::chrono::duration_cast<std::chrono::milliseconds>(
169 nullptr,
"Flow Test"});
170 std::this_thread::sleep_for(std::chrono::milliseconds(100));
171 check(fm.
extend() ==
false,
"extend() is refused when extended_ms is 0");
172 std::this_thread::sleep_for(std::chrono::milliseconds(400));
173 const long long at = expired_at.load();
174 check(
at >= 295 &&
at <= 350,
"the refused extend left the original window intact");
175 check(fm.
extend() ==
false,
"extend() is refused once the hold is over");
186 "an unknown name resolves to UNKNOWN — the hazard a State-typed target avoids");
188 "stateId() gives the controller the fire id without a second copy of the enum");
190 "stateId() reports unknown rather than guessing an id");
198 const uint32_t kDuration = 800;
201 std::atomic<bool> in_fire{
true};
202 std::atomic<int> expiries{0};
203 const auto t0 = Clock::now();
211 bool left_early =
false;
212 long long left_at = -1;
213 while (std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).
count() <
214 static_cast<long long>(kDuration) + 400) {
215 const long long now =
216 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
217 if (!in_fire.load()) {
220 if (now <
static_cast<long long>(kDuration) - 100)
223 std::this_thread::sleep_for(std::chrono::milliseconds(20));
225 check(!left_early,
"stayed in fire for the whole configured window (no early exit)");
226 check(left_at >= 0,
"left fire at the end of the window");
227 check(expiries.load() == 1,
"the expiry transition ran exactly once, not repeatedly");
228 check(fm.
isActive() ==
false,
"the hold timer is inactive once the burn has ended");
236 const uint16_t
port = 45913;
237 fs_alias::path dir = fs_alias::temp_directory_path() /
"daq_fire_pwm_test";
238 fs_alias::remove_all(dir);
239 fs_alias::create_directories(dir);
241 std::ofstream act(dir /
"state_machine_actuators.csv");
242 act <<
",Idle,Fire\n";
243 act <<
"Plain Valve,OPEN,OPEN\n";
244 act <<
"Throttle,OPEN,OPEN\n";
246 const std::string cfg =
247 "[network]\nactuator_cmd_port = " + std::to_string(
port) +
248 "\n\n[actuator_service]\nbind_address = \"0.0.0.0\"\n\n"
249 "[boards.act_board]\ntype = \"ACTUATOR\"\nip = \"127.0.0.1\"\nboard_id = 12\n"
250 "enabled = true\nnum_actuators = 10\n\n"
256 "[actuator_roles]\n\"Plain Valve\" = [\"NC\", 1, 12]\n"
257 "\"Throttle\" = [\"NO\", 2, 12, \"pwm_fuel\"]\n";
259 int sock = socket(AF_INET, SOCK_DGRAM, 0);
261 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof(reuse));
262 struct sockaddr_in addr{};
263 addr.sin_family = AF_INET;
264 addr.sin_port = htons(
port);
265 inet_pton(AF_INET,
"127.0.0.1", &addr.sin_addr);
266 bind(sock,
reinterpret_cast<struct sockaddr*
>(&addr),
sizeof(addr));
267 struct timeval tv{.tv_sec = 0, .tv_usec = 300000};
268 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
270 auto channelsFor = [&](
State st) {
273 ac.
load(cfg, (dir /
"state_machine_actuators.csv").
string());
275 std::set<uint8_t> seen;
277 for (
int i = 0; i < 6; ++i) {
278 ssize_t
n = recv(sock, buf,
sizeof(buf), 0);
281 daq::PacketHeader hdr;
282 std::vector<daq::ActuatorCommand> cmds;
284 for (
const auto& c : cmds)
285 seen.insert(c.actuator_id);
290 const auto idle = channelsFor(State::IDLE);
292 "outside fire the sequencer commands BOTH the plain valve and the assigned one");
294 const auto fire = channelsFor(State::FIRE);
295 check(fire.count(1),
"in fire the sequencer still commands its own valves");
296 check(!fire.count(2),
297 "in fire the sequencer stops commanding the controller-assigned valve — one "
298 "writer drives it during a burn");
301 fs_alias::remove_all(dir);
304 std::cout << (
g_failures == 0 ?
"\nPASSED" :
"\nFAILED") << std::endl;