STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
config_broadcast_service_main.cpp
Go to the documentation of this file.
1/**
2 * Config Broadcast Service — C++ ACTUATOR_CONFIG / SENSOR_CONFIG broadcaster.
3 *
4 * Builds packets from config.toml and calibration JSON, sends via UDP to boards.
5 * Flight-ready replacement for Python config_broadcast_service.py.
6 *
7 * Usage: ./config_broadcast_service [--config PATH] [--interval-ms MS]
8 */
9
10#include <arpa/inet.h>
11#include <netinet/in.h>
12#include <signal.h>
13#include <sys/socket.h>
14#include <unistd.h>
15
16#include <algorithm>
17#include <atomic>
18#include <cerrno>
19#include <chrono>
20#include <cmath>
21#include <cstring>
22
23// These packets are byte-packed: a u32 lands at offset 2, and the actuator entries step by 7, so
24// &buf[off] is routinely not 4-aligned. Writing through a uint32_t* there is undefined behaviour
25// (UBSan: "store to misaligned address ... requires 4 byte alignment", caught on all five call
26// sites during an integration run). memcpy compiles to the same single store on x86-64 and ARM64
27// and is defined everywhere, so this is a correctness fix with no code-generation cost.
28static inline void store_u32(uint8_t* dst, uint32_t v) {
29 std::memcpy(dst, &v, sizeof(v));
30}
31#include <fstream>
32#include <iostream>
33#include <map>
34#include <set>
35#include <sstream>
36#include <string>
37#include <vector>
38
39#include "config/Config.hpp"
41#include "net/DaqInterface.hpp"
42
43namespace {
44std::atomic<bool> g_running{true};
45
46void signalHandler(int /*sig*/) {
47 std::cout << "\n[ConfigBroadcast] Shutting down..." << std::endl;
48 g_running = false;
49}
50
51constexpr uint8_t SENSOR_CONFIG = 5;
52constexpr uint8_t ACTUATOR_CONFIG = 6;
53constexpr uint16_t DEFAULT_LISTEN_PORT = 5005;
54
55std::string trim(const std::string& s) {
56 size_t a = s.find_first_not_of(" \t\r\n\"");
57 size_t b = s.find_last_not_of(" \t\r\n\"");
58 return (a == std::string::npos) ? "" : s.substr(a, b - a + 1);
59}
60
61uint32_t ipToU32Le(const std::string& ip) {
62 int a = 0, b = 0, c = 0, d = 0;
63 if (sscanf(ip.c_str(), "%d.%d.%d.%d", &a, &b, &c, &d) != 4)
64 return 0;
65 return (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(b) << 16) |
66 (static_cast<uint32_t>(c) << 8) | static_cast<uint32_t>(d);
67}
68
69uint32_t ipToU32Be(const std::string& ip) {
70 return ipToU32Le(ip);
71}
72
73struct BoardInfo {
74 int id;
75 std::string ip;
76 std::string type;
77 bool enabled;
81 // Serial-print / log-stream mode byte (0..3): 0 USB Tier-1, 1 USB verbose,
82 // 2 stream Tier-1, 3 stream Tier-1+2. Sent verbatim as the config packet's
83 // enable_serial_printing byte; the firmware interprets it as the mode.
85 std::vector<int> active_connectors;
86 uint16_t listen_port;
87};
88
89void parseVentAbortFromCsv(const std::string& csv_path, std::map<std::string, int>& vent_map,
90 std::map<std::string, int>& abort_map) {
91 std::ifstream f(csv_path);
92 if (!f.is_open())
93 return;
94 std::string line;
95 if (!std::getline(f, line))
96 return;
97 std::vector<std::string> headers;
98 std::istringstream iss(line);
99 std::string cell;
100 while (std::getline(iss, cell, ','))
101 headers.push_back(trim(cell));
102
103 int vent_col = -1, abort_col = -1;
104 for (size_t i = 1; i < headers.size(); ++i) {
105 if (headers[i] == "Vent")
106 vent_col = static_cast<int>(i);
107 if (headers[i] == "Engine Abort")
108 abort_col = static_cast<int>(i);
109 }
110 if (vent_col < 0 || abort_col < 0)
111 return;
112
113 while (std::getline(f, line)) {
114 std::vector<std::string> cells;
115 std::istringstream is(line);
116 while (std::getline(is, cell, ','))
117 cells.push_back(trim(cell));
118 if (cells.size() <= static_cast<size_t>(std::max(vent_col, abort_col)))
119 continue;
120 std::string name = cells[0];
121 if (name.empty() || name == "Test Actuator 2")
122 continue;
123 std::string v = cells[vent_col];
124 std::string a = cells[abort_col];
125 std::transform(v.begin(), v.end(), v.begin(), ::toupper);
126 std::transform(a.begin(), a.end(), a.begin(), ::toupper);
127 vent_map[name] = (v == "OPEN") ? 1 : 0;
128 abort_map[name] = (a == "OPEN") ? 1 : 0;
129 }
130}
131
132// One UDP packet to send: (packet type, bytes, dest IP, dest port).
133using ConfigPacket = std::tuple<uint8_t, std::vector<uint8_t>, std::string, uint16_t>;
134
135// Abort thresholds the calibration service computed through each sensor's REAL model (cubic /
136// robust / physics, with a physics fallback for sparse cal), keyed by uid -> {target_psi, raw_adc}.
137// The calibration service is the authority — it has every model loaded and re-emits on each
138// capture/clear/reload. config_broadcast prefers these and only falls back to its own inline
139// physics inversion when a uid is absent (calibration service starting up / down) or its recorded
140// PSI no longer matches the live config (an abort_pts edit the service hasn't re-emitted for yet).
141std::map<uint16_t, std::pair<double, uint32_t>> readAbortThresholds() {
142 std::map<uint16_t, std::pair<double, uint32_t>> out;
143 const char* candidates[] = {
144 "scripts/calibration/calibrations/abort_thresholds.txt",
145 "../scripts/calibration/calibrations/abort_thresholds.txt",
146 "../../scripts/calibration/calibrations/abort_thresholds.txt",
147 };
148 for (const char* path : candidates) {
149 std::ifstream f(path);
150 if (!f.is_open())
151 continue;
152 std::string line;
153 while (std::getline(f, line)) {
154 if (line.empty() || line[0] == '#')
155 continue;
156 std::istringstream is(line);
157 unsigned uid = 0;
158 double psi = 0.0;
159 unsigned long adc = 0;
160 if (is >> uid >> psi >> adc)
161 out[static_cast<uint16_t>(uid)] = {psi, static_cast<uint32_t>(adc)};
162 }
163 break; // first found wins
164 }
165 return out;
166}
167
168// Read config.toml fresh and build the full SENSOR_CONFIG/ACTUATOR_CONFIG packet set.
169// Called every broadcast cycle so board edits (roles, active_connectors, voltage_reference,
170// abort thresholds, enable flags) go live without restarting the service or a session.
171// Returns an empty vector on any failure (missing/truncated file, no designated survivor);
172// the caller keeps the last-good set rather than dropping board config for a cycle.
173//
174// ── THIS IS THE ONE DELIBERATE EXCEPTION to the draft-at-session-start rule ───────────────────
175// Everywhere else, config reaches a running rig at exactly one point: the backend deploys the
176// active profile to config/ at session start, and the pipeline services then read it once at
177// boot. This service does not follow that rule, on purpose. It is always-on rather than
178// session-gated, and board config (including [abort_pts] trip thresholds and boards.*.enabled)
179// is expected to reach hardware without cycling a session.
180//
181// Do not "fix" this into consistency without deciding what should happen to boards between runs
182// — an always-on service that reads once at boot would need a service restart to pick up an
183// edit, which is a third apply point and worse than either option. See
184// docs/CONFIGURATION_GUIDE.md.
185std::vector<ConfigPacket> buildPackets(const std::string& config_path) {
186 const fsw::config::Config cfg = fsw::config::load(config_path);
187
188 // Map typed boards -> local BoardInfo (expand active_connectors, synthesize ip like the old
189 // parser).
190 std::vector<BoardInfo> boards;
191 for (const auto& b : cfg.boards) {
192 // active_connectors is the only statement of which channels exist — no expansion
193 // from a config-only num_sensors, which never had a counterpart on the wire. The
194 // packet's num_sensors byte is just this list's length (see build_sensor_config).
195 const std::vector<int>& active = b.active_connectors;
196 std::string ip = b.ip;
197 if (ip.empty() && b.board_id > 0)
198 ip = "192.168.2." + std::to_string(b.board_id);
199 boards.push_back({b.board_id, ip, b.type, b.enabled, b.designated_survivor,
200 b.necessary_for_abort, b.voltage_reference, b.enable_serial_printing,
201 active, b.listen_port});
202 }
203
204 std::string designated_ip;
205 int designated_id = -1;
206 for (const auto& b : boards) {
207 if (b.enabled && b.type == "ACTUATOR" && b.designated_survivor) {
208 designated_ip = b.ip;
209 designated_id = b.id;
210 break;
211 }
212 }
213 // No designated survivor → treat as a bad/partial read and keep last-good.
214 if (designated_ip.empty())
215 return {};
216
217 std::map<int, std::string> board_id_to_ip;
218 for (const auto& b : boards)
219 if (b.id > 0)
220 board_id_to_ip[b.id] = b.ip;
221
222 std::map<std::string, int> vent_map, abort_map;
223 std::string csv_path = cfg.state_machine.actuator_csv;
224 const char* csv_fbs[] = {
225 "config/state_machine_actuators.csv",
226 "../config/state_machine_actuators.csv",
227 "../../config/state_machine_actuators.csv",
228 };
229 for (const char* fb : csv_fbs) {
230 std::ifstream t(fb);
231 if (t.is_open()) {
232 csv_path = fb;
233 break;
234 }
235 }
236 parseVentAbortFromCsv(csv_path, vent_map, abort_map);
237
238 const std::map<std::string, double>& abort_pts = cfg.abort_pts;
239
240 // A ball valve is left out of the abort list. In a no-connection abort its own board has lost
241 // the server too, and it opens the valve itself 5 s after the last server heartbeat and
242 // ignores a command to close until heartbeats return (BallValve_Hotfire), so a survivor's
243 // vent/abort command could not hold it anywhere else. Its fail-open is its abort behaviour.
244 std::map<std::string, std::tuple<int, int, bool>> actuator_roles;
245 for (const auto& [name, r] : cfg.actuator_roles)
246 if (r.channel >= 1 && r.channel <= 255 && !r.ball_valve)
247 actuator_roles[name] = {r.channel, r.board_id > 0 ? r.board_id : 12, r.is_no};
248
249 // Autonomous overpressure abort thresholds: invert each abort PSI to a raw ADC code through the
250 // SAME per-sensor model the calibration service streams to the GUI, so a board trips at the ADC
251 // that matches the pressure the operator sees. This used to invert a factory
252 // PTCalibrationManager that the physics-or-nothing deploy does not ship (and that deliberately
253 // excludes the operator cubic) — every lookup returned null, each abort sensor was silently
254 // skipped, and the ACTUATOR_CONFIG packet carried zero thresholds: the hardware overpressure
255 // net was simply absent. A threshold that cannot be computed is now logged loudly and omitted
256 // (no board trip for that one sensor) rather than shipped wrong or dropped in silence.
257 //
258 // Only ratiometric-physics (0.5-4.5 V) inversion is implemented:
259 // adc = (0.1 + 0.8 * psi/full_scale) * 2^31, the
260 // exact inverse of the calibration service's convert_ratiometric_pt_to_pressure. A sensor whose
261 // model is cubic/robust/blend, or that sits on a 4-20 mA board, is refused here (set its
262 // calibration_model to "physics" for a board trip, or it has none) — inverting the operator
263 // cubic or the current-loop curve would duplicate that math and is left as a follow-up. Both
264 // shipped rigs' abort PTs are ratiometric physics.
265 const auto cal_thresholds = readAbortThresholds();
266 std::vector<std::tuple<uint32_t, uint8_t, uint32_t>> abort_pt_list;
267 std::set<std::string> abort_warnings;
268 for (const auto& [sensor_name, threshold_psi] : abort_pts) {
269 const std::string tag = "[ConfigBroadcast] abort_pts \"" + sensor_name + "\": ";
270 // The role -> board -> channel walk lives in fsw::config::find_pt_role now, so the
271 // sequencer's pressure subscriber resolves a sensor by name the same way this does rather
272 // than growing its own copy with its own off-by-one.
273 const auto ref = fsw::config::find_pt_role(cfg, sensor_name);
274 if (!ref) {
275 abort_warnings.insert(tag + "no PT sensor_role declares this name — NO board trip");
276 continue;
277 }
278 if (ref->t7 != nullptr) {
279 abort_warnings.insert(tag + "is a LabJack T7 input — no board can trip on it");
280 continue;
281 }
282 const auto& b = *ref->board;
283 const std::string& board_key = ref->board_key;
284 const int channel = ref->channel;
285 if (channel < 1 || channel > 255) {
286 abort_warnings.insert(tag + "channel out of range — no board abort threshold");
287 continue;
288 }
289 const uint16_t uid = static_cast<uint16_t>(b.board_id * 100 + channel);
290 // Prefer the calibration service's model-correct threshold, as long as the PSI it was
291 // computed for still matches the live config (it re-emits on capture/clear/reload, not
292 // on a bare abort_pts edit). This is the path that covers cubic/robust and operator
293 // cal.
294 {
295 auto cit = cal_thresholds.find(uid);
296 if (cit != cal_thresholds.end() && std::abs(cit->second.first - threshold_psi) < 0.5) {
297 abort_pt_list.push_back(
298 {ipToU32Le(b.ip), static_cast<uint8_t>(channel), cit->second.second});
299 continue;
300 }
301 }
302 // Fallback (calibration service not up yet, or the abort PSI changed and it hasn't
303 // re-emitted): invert physics inline. Interface + model + full-scale resolved exactly
304 // as calibration_main does, so a physics threshold lands on the same curve the operator
305 // reads.
306 const bool is_loop = b.has_hp_pt_keys || b.pt_type == "4-20 mA absolute";
307 std::string model = is_loop ? "physics" : "cubic"; // interface-aware default
308 if (const auto* models = cfg.calibration_model_for("calibration_model_" + board_key)) {
309 auto mit = models->find(sensor_name);
310 if (mit != models->end())
311 model = mit->second;
312 }
313 double full_scale = is_loop ? b.hp_pt_full_scale_psi : 1000.0;
314 if (const auto* fss = cfg.full_scale_for("calibration_full_scale_" + board_key)) {
315 auto fit = fss->find(sensor_name);
316 if (fit != fss->end() && fit->second > 0.0)
317 full_scale = fit->second;
318 }
319
320 if (model != "physics" || is_loop) {
321 abort_warnings.insert(tag + "model \"" + model + (is_loop ? " (4-20 mA)" : "") +
322 "\" cannot be inverted for a board trip — set "
323 "calibration_model = \"physics\", or this sensor has NO trip");
324 continue;
325 }
326 if (!(full_scale > 0.0) || !(threshold_psi > 0.0)) {
327 abort_warnings.insert(tag + "non-positive full_scale/threshold — NO board trip");
328 continue;
329 }
330 constexpr double ADC_MAX = 2147483648.0; // 2^31
331 // 0.5-4.5 V at 5 V excitation: 10 % of full code at zero, 90 % at full scale.
332 double adc =
333 std::clamp((0.1 + 0.8 * (threshold_psi / full_scale)) * ADC_MAX, 0.0, ADC_MAX - 1.0);
334 abort_pt_list.push_back(
335 {ipToU32Le(b.ip), static_cast<uint8_t>(channel), static_cast<uint32_t>(llround(adc))});
336 }
337 // Log each distinct abort-threshold problem once while it persists, and note recovery — never
338 // spam the every-cycle rebuild. Dropping resolved entries lets a re-break warn again.
339 {
340 static std::set<std::string> s_last_abort_warnings;
341 for (const auto& w : abort_warnings)
342 if (s_last_abort_warnings.count(w) == 0)
343 std::cerr << "⚠️ " << w << std::endl;
344 if (abort_warnings.empty() && !s_last_abort_warnings.empty())
345 std::cout << "[ConfigBroadcast] all abort_pts thresholds resolved" << std::endl;
346 s_last_abort_warnings = abort_warnings;
347 }
348
349 auto build_actuator_config = [&](int is_abort_controller,
350 int enable_serial) -> std::vector<uint8_t> {
351 std::vector<std::tuple<uint32_t, uint8_t, uint8_t, uint8_t>> abort_actuators;
352 for (const auto& [name, tup] : actuator_roles) {
353 int ch = std::get<0>(tup), bid = std::get<1>(tup);
354 std::string ip = board_id_to_ip.count(bid) ? board_id_to_ip[bid] : designated_ip;
355 uint8_t vent = static_cast<uint8_t>(vent_map.count(name) ? vent_map[name] : 0);
356 uint8_t abort = static_cast<uint8_t>(abort_map.count(name) ? abort_map[name] : 0);
357 abort_actuators.push_back({ipToU32Le(ip), static_cast<uint8_t>(ch), vent, abort});
358 }
359
360 size_t N = std::min(abort_actuators.size(), size_t(255));
361 size_t X = std::min(abort_pt_list.size(), size_t(255));
362 size_t body = 1 + 1 + N * 7 + 1 + X * 9 + 1;
363 size_t total = 6 + body;
364
365 std::vector<uint8_t> buf(total);
366 buf[0] = ACTUATOR_CONFIG;
367 buf[1] = 0;
368 store_u32(&buf[2], 0);
369
370 size_t off = 6;
371 buf[off++] = static_cast<uint8_t>(is_abort_controller);
372 buf[off++] = static_cast<uint8_t>(N);
373 for (size_t i = 0; i < N; ++i) {
374 auto [ip, aid, vent, abort] = abort_actuators[i];
375 store_u32(&buf[off], ip);
376 off += 4;
377 buf[off++] = aid;
378 buf[off++] = vent;
379 buf[off++] = abort;
380 }
381 buf[off++] = static_cast<uint8_t>(X);
382 for (size_t i = 0; i < X; ++i) {
383 auto [ip, sid, adc] = abort_pt_list[i];
384 store_u32(&buf[off], ip);
385 off += 4;
386 buf[off++] = sid;
387 store_u32(&buf[off], adc);
388 off += 4;
389 }
390 buf[off] = static_cast<uint8_t>(enable_serial); // mode byte 0..3
391 return buf;
392 };
393
394 auto build_sensor_config = [&](const BoardInfo& b) -> std::vector<uint8_t> {
395 std::vector<uint8_t> channels;
396 for (int c : b.active_connectors)
397 if (c >= 1 && c <= 255)
398 channels.push_back(static_cast<uint8_t>(c));
399 size_t num = std::min(channels.size(), size_t(255));
400 size_t body = 1 + num + 1 + 1 + (b.necessary_for_abort ? 4 : 0) + 1;
401 size_t total = 6 + body;
402
403 std::vector<uint8_t> buf(total);
404 buf[0] = SENSOR_CONFIG;
405 buf[1] = 0;
406 store_u32(&buf[2], 0);
407
408 size_t off = 6;
409 buf[off++] = static_cast<uint8_t>(num);
410 for (size_t i = 0; i < num; ++i)
411 buf[off++] = channels[i];
412 buf[off++] = static_cast<uint8_t>(std::min(2, std::max(0, b.voltage_reference)));
413 buf[off++] = b.necessary_for_abort ? 1 : 0;
414 if (b.necessary_for_abort) {
415 uint32_t ip_be = ipToU32Be(designated_ip);
416 buf[off] = (ip_be >> 24) & 0xFF;
417 buf[off + 1] = (ip_be >> 16) & 0xFF;
418 buf[off + 2] = (ip_be >> 8) & 0xFF;
419 buf[off + 3] = ip_be & 0xFF;
420 off += 4;
421 }
422 buf[off] = static_cast<uint8_t>(b.enable_serial_printing); // mode byte 0..3
423 return buf;
424 };
425
426 std::vector<ConfigPacket> packets;
427 for (const auto& b : boards) {
428 if (!b.enabled)
429 continue;
430 if (b.type == "ACTUATOR") {
431 int is_abort = (b.id == designated_id) ? 1 : 0;
432 auto pkt = build_actuator_config(is_abort, b.enable_serial_printing);
433 if (!pkt.empty())
434 packets.push_back({ACTUATOR_CONFIG, pkt, b.ip, b.listen_port});
435 } else if (b.type == "BALL_VALVE") {
436 // The ball valve firmware reads only the log-mode byte; it is never the abort
437 // controller and ignores the abort lists.
438 auto pkt = build_actuator_config(0, b.enable_serial_printing);
439 if (!pkt.empty())
440 packets.push_back({ACTUATOR_CONFIG, pkt, b.ip, b.listen_port});
441 } else if (b.type == "PT" || b.type == "TC" || b.type == "RTD" || b.type == "LC" ||
442 b.type == "ENCODER") {
443 auto pkt = build_sensor_config(b);
444 packets.push_back({SENSOR_CONFIG, pkt, b.ip, b.listen_port});
445 }
446 }
447 return packets;
448}
449
450} // namespace
451
452int main(int argc, char* argv[]) {
453 std::string config_path = "config/config.toml";
454 int interval_ms = -1; // -1 = use config
455
456 for (int i = 1; i < argc; ++i) {
457 std::string arg = argv[i];
458 if (arg == "--config" && i + 1 < argc)
459 config_path = argv[++i];
460 else if (arg == "--interval-ms" && i + 1 < argc)
461 interval_ms = std::max(500, std::atoi(argv[++i]));
462 else if (arg == "--help" || arg == "-h") {
463 std::cout << "Usage: " << argv[0] << " [--config PATH] [--interval-ms MS]\n";
464 return 0;
465 }
466 }
467
468 std::string config_content;
469 {
470 std::ifstream f(config_path);
471 if (!f.is_open()) {
472 for (const auto& fp : {"config/config.toml", "../config/config.toml"}) {
473 f.open(fp);
474 if (f.is_open()) {
475 config_path = fp;
476 break;
477 }
478 }
479 }
480 if (f.is_open()) {
481 std::ostringstream ss;
482 ss << f.rdbuf();
483 config_content = ss.str();
484 }
485 }
486
487 if (config_content.empty()) {
488 std::cerr << "[ConfigBroadcast] No config loaded" << std::endl;
489 return 1;
490 }
491
492 // Precedence: defaults < config < CLI (--interval-ms). Config value clamped to >=500 ms.
493 if (interval_ms < 0) {
494 const fsw::config::Config cfg = fsw::config::load(config_path);
495 interval_ms = std::max(500, static_cast<int>(cfg.config_broadcast_interval_ms));
496 }
497
498 signal(SIGINT, signalHandler);
499 signal(SIGTERM, signalHandler);
500
501 // Packets (including abort thresholds) are rebuilt from the live config every cycle (below) so
502 // board and calibration edits apply with no restart / no session.
503 auto packets = buildPackets(config_path);
504 if (packets.empty())
505 std::cerr << "[ConfigBroadcast] No packets from config yet (no designated_survivor?); "
506 "retrying each cycle"
507 << std::endl;
508
509 int sock = socket(AF_INET, SOCK_DGRAM, 0);
510 if (sock < 0) {
511 std::cerr << "[ConfigBroadcast] socket() failed" << std::endl;
512 return 1;
513 }
514
515 // Pin the egress NIC. This service unicasts to each board, so the destination already names
516 // one subnet — but nothing stopped the kernel choosing a different interface to reach it on a
517 // host with several. Resolved from the config as loaded at startup: the send loop re-reads
518 // config.toml every cycle for board edits, and a NIC change mid-run is not a board edit.
519 {
520 const auto nic =
521 fsw::net::resolveDaqBindAddress(fsw::config::load(config_path), "ConfigBroadcast");
522 if (!nic.ok) {
523 close(sock);
524 return 1;
525 }
526 fsw::net::bindToDaqInterface(sock, nic.address, "ConfigBroadcast");
527 }
528
529 struct sockaddr_in dest;
530 memset(&dest, 0, sizeof(dest));
531 dest.sin_family = AF_INET;
532
533 std::cout << "[ConfigBroadcast] Started — interval=" << interval_ms
534 << "ms (C++ standalone, live config reload)" << std::endl;
535 std::cout << "[ConfigBroadcast] " << packets.size() << " packet types" << std::endl;
536
537 unsigned long total_sent = 0;
538 // Per-destination send accounting. The single global counter could not distinguish
539 // "all 8 boards got their config" from "the same board got it 8 times".
540 std::map<std::string, unsigned long> tx_ok, tx_fail, tx_bad_addr;
541 std::map<std::string, size_t> tx_bytes;
542 std::map<std::string, uint8_t> tx_type;
543 auto last_log = std::chrono::steady_clock::now();
544
545 while (g_running) {
546 // Re-read config.toml and rebuild each cycle so board edits go live. Keep the
547 // last-good set if a read is empty/mid-write (buildPackets returns {} on failure).
548 auto fresh = buildPackets(config_path);
549 if (!fresh.empty())
550 packets.swap(fresh);
551
552 for (const auto& [pkt_type, pkt, ip, listen_port] : packets) {
553 const std::string dest_key = ip + ":" + std::to_string(listen_port);
554 if (inet_pton(AF_INET, ip.c_str(), &dest.sin_addr) != 1) {
555 tx_bad_addr[dest_key]++;
556 continue;
557 }
558 dest.sin_port = htons(listen_port);
559 errno = 0;
560 ssize_t sent = sendto(sock, pkt.data(), pkt.size(), 0,
561 reinterpret_cast<struct sockaddr*>(&dest), sizeof(dest));
562 if (sent == static_cast<ssize_t>(pkt.size())) {
563 total_sent++;
564 tx_ok[dest_key]++;
565 tx_bytes[dest_key] = pkt.size();
566 tx_type[dest_key] = pkt_type;
567 } else {
568 // A short or failed send is the whole bug class we are chasing: the old code
569 // counted only successes, so a silently dropped packet looked identical to a
570 // delivered one. Report errno the first time and then once per 10s per dest.
571 tx_fail[dest_key]++;
572 int e = errno;
573 if (tx_fail[dest_key] == 1 || tx_fail[dest_key] % 10 == 0) {
574 std::cerr << "[ConfigBroadcast] ✗ sendto(" << dest_key
575 << ") type=" << (int)pkt_type << " len=" << pkt.size() << " returned "
576 << sent << " errno=" << e << " (" << std::strerror(e) << ")"
577 << std::endl;
578 }
579 }
580 }
581
582 auto now = std::chrono::steady_clock::now();
583 if (std::chrono::duration<double>(now - last_log).count() >= 10.0) {
584 std::cout << "[ConfigBroadcast] Sent " << total_sent << " packets total ("
585 << packets.size() << " dests this cycle)" << std::endl;
586 for (const auto& [dkey, n] : tx_ok) {
587 std::cout << " → " << dkey << " type=" << (int)tx_type[dkey]
588 << " len=" << tx_bytes[dkey] << " ok=" << n << " fail=" << tx_fail[dkey]
589 << std::endl;
590 }
591 for (const auto& [dkey, n] : tx_fail)
592 if (tx_ok.find(dkey) == tx_ok.end())
593 std::cout << " → " << dkey << " ok=0 fail=" << n << std::endl;
594 for (const auto& [dkey, n] : tx_bad_addr)
595 std::cout << " → " << dkey << " INVALID ADDRESS, never sent (n=" << n << ")"
596 << std::endl;
597 last_log = now;
598 }
599
600 for (int i = 0; g_running && i < interval_ms; i += 100)
601 usleep(100000);
602 }
603
604 close(sock);
605 std::cout << "[ConfigBroadcast] Stopped." << std::endl;
606 return 0;
607}
static const uint32_t channels[BOARD_ADC_COUNT]
Definition board_adc.c:7
static void signalHandler(int)
static void store_u32(uint8_t *dst, uint32_t v)
Config Broadcast Service — C++ ACTUATOR_CONFIG / SENSOR_CONFIG broadcaster.
static std::atomic< bool > g_running
static bool is_abort_controller
Definition main.cpp:63
static STAR_MCP3201 adc(MCP3201_CS_PIN, &mcp_spi)
*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
size_type count(const key_type &key) const
Definition json.hpp:19264
std::tuple< uint8_t, std::vector< uint8_t >, std::string, uint16_t > ConfigPacket
std::vector< ConfigPacket > buildPackets(const std::string &config_path)
std::map< uint16_t, std::pair< double, uint32_t > > readAbortThresholds()
void parseVentAbortFromCsv(const std::string &csv_path, std::map< std::string, int > &vent_map, std::map< std::string, int > &abort_map)
std::optional< PtRoleRef > find_pt_role(const Config &cfg, const std::string &role)
Resolve one role by its canonical config name.
Config load(const std::string &path)
Parse config.toml.
Definition Config.cpp:442
bool bindToDaqInterface(int sock, const std::string &address, const char *who)
bind() an already-created socket to address on an ephemeral port.
DaqBindResult resolveDaqBindAddress(const ::fsw::config::Config &cfg, const char *who)
Decide which local address board traffic should leave from, and log the decision.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
int main()
static boot_report_t r
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98