STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
daq_bridge_main.cpp
Go to the documentation of this file.
1#include <arpa/inet.h>
2#include <netinet/in.h>
3#include <signal.h>
4#include <sys/socket.h>
5#include <unistd.h>
6
7#include <algorithm>
8#include <array>
9#include <atomic>
10#include <chrono>
11#include <cstring>
12#include <fstream>
13#include <functional>
14#include <iomanip>
15#include <iostream>
16#include <map>
17#include <set>
18#include <sstream>
19#include <string>
20#include <thread>
21#include <vector>
22
27#include "daq-protocol.h"
28#include "fsw/BoardTypeWire.hpp"
29
30namespace {
31constexpr uint8_t SERVER_HEARTBEAT_PACKET_TYPE = 2;
32
33std::vector<uint8_t> build_server_heartbeat_packet() {
34 std::vector<uint8_t> pkt(7);
35 uint32_t ts = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
36 std::chrono::steady_clock::now().time_since_epoch())
37 .count() &
38 0xFFFFFFFF);
40 pkt[1] = DIABLO_COMMS_VERSION;
41 pkt[2] = static_cast<uint8_t>(ts & 0xFF);
42 pkt[3] = static_cast<uint8_t>((ts >> 8) & 0xFF);
43 pkt[4] = static_cast<uint8_t>((ts >> 16) & 0xFF);
44 pkt[5] = static_cast<uint8_t>((ts >> 24) & 0xFF);
45 pkt[6] = 0; // engine_state = SAFE
46 return pkt;
47}
48
49// Forwards parsed board-log text to the Node backend over UDP (loopback). The
50// backend caches it in memory and pushes it to the GUI — logs deliberately do
51// NOT go to Elodin (no need to replay board logs for past runs).
52//
53// Envelope (little-endian): [board_id:u8][flags:u8][text_len:u16][text bytes].
55public:
56 BackendLogForwarder(const std::string& host, uint16_t port) {
57 sock_ = socket(AF_INET, SOCK_DGRAM, 0);
58 std::memset(&dest_, 0, sizeof(dest_));
59 dest_.sin_family = AF_INET;
60 dest_.sin_port = htons(port);
61 inet_pton(AF_INET, host.c_str(), &dest_.sin_addr);
62 }
64 if (sock_ >= 0)
65 close(sock_);
66 }
67
68 bool ready() const {
69 return sock_ >= 0;
70 }
71
72 void forward(uint8_t board_id, uint8_t flags, const uint8_t* text, uint16_t text_len) {
73 if (sock_ < 0)
74 return;
75 std::vector<uint8_t> env;
76 env.reserve(4u + text_len);
77 env.push_back(board_id);
78 env.push_back(flags);
79 env.push_back(static_cast<uint8_t>(text_len & 0xFF));
80 env.push_back(static_cast<uint8_t>((text_len >> 8) & 0xFF));
81 if (text && text_len)
82 env.insert(env.end(), text, text + text_len);
83 sendto(sock_, env.data(), env.size(), 0, reinterpret_cast<sockaddr*>(&dest_),
84 sizeof(dest_));
85 }
86
87private:
88 int sock_ = -1;
89 sockaddr_in dest_{};
90};
91} // namespace
95#include "config/Config.hpp"
100#include "net/DaqInterface.hpp"
105
106std::atomic<bool> running(true);
107
108void signal_handler(int /* sig */) {
109 running = false;
110 std::cout << "\n[DAQ Bridge] Shutting down..." << std::endl;
111}
112
113// Board type enum (matches DiabloAvionics / config [boards.*])
115
118 std::string ip;
119 /** How many channels this board is told to sample — the length of its
120 * active_connectors list, which is the only statement of that. Log-only here. */
123 int board_id; // Added board_id
124};
125
127 uint32_t interval_ms = 1000;
128 uint16_t broadcast_port = 5005;
129 std::string broadcast_ip = "255.255.255.255";
130 bool send_from_daq_bridge = true; // false when heartbeat_service is used
131};
132
133// Loopback fallback routing (integration test only): the simulator binds 127.0.0.<host-octet> per
134// board, where host-octet is the last octet of that board's config IP, so the bridge resolves those
135// packets by that octet — identity-based and immune to how the TOML parser happens to order
136// [boards] (toml++ sorts alphabetically; the Python sim iterates file order). Keyed by host octet.
137using BoardByOctet = std::map<int, BoardConfig>;
138
139/** Last octet of a dotted IPv4 string (e.g. "192.168.2.51" -> 51), or -1 if not parseable. */
140static int ipv4_host_octet(const std::string& ip) {
141 const auto dot = ip.rfind('.');
142 if (dot == std::string::npos || dot + 1 >= ip.size())
143 return -1;
144 const std::string tail = ip.substr(dot + 1);
145 if (tail.empty() || tail.find_first_not_of("0123456789") != std::string::npos)
146 return -1;
147 const int v = std::atoi(tail.c_str());
148 return (v >= 0 && v <= 255) ? v : -1;
149}
150
151// Minimal config parse: [database] host/port, [network] sensor_port/bind_ip, [server_heartbeat],
152// [boards.xxx] type/ip/enabled
153static void load_board_map_from_config(const std::string& config_path,
154 std::map<std::string, BoardConfig>& board_map,
155 BoardByOctet* out_board_by_octet, std::string& db_host,
156 uint16_t& db_port, uint16_t* out_sensor_port = nullptr,
157 std::string* out_bind_ip = nullptr,
158 ServerHeartbeatConfig* out_hb = nullptr,
159 fsw::time::TimeSyncConfig* out_ts = nullptr,
160 uint16_t* out_log_port = nullptr) {
161 db_host = "127.0.0.1";
162 db_port = 2240;
163 {
164 std::ifstream f(config_path);
165 if (!f.is_open())
166 return; // missing file → keep defaults, no boards (matches old behavior)
167 }
168
169 const fsw::config::Config c = fsw::config::load(config_path);
170 db_host = c.database.host;
171 db_port = c.database.port;
172 if (out_sensor_port)
173 *out_sensor_port = c.network.sensor_port;
174 if (out_bind_ip)
175 *out_bind_ip = c.network.bind_ip;
176 if (out_hb) {
177 out_hb->interval_ms = c.server_heartbeat.interval_ms;
178 out_hb->broadcast_port = c.server_heartbeat.broadcast_port;
179 out_hb->broadcast_ip = c.server_heartbeat.broadcast_ip;
180 out_hb->send_from_daq_bridge = c.server_heartbeat.send_from_daq_bridge;
181 }
182 if (out_ts)
183 *out_ts = c.time_sync;
184 if (out_log_port)
185 *out_log_port = c.logs.backend_udp_port;
186
187 auto kind = [](const std::string& t) {
188 if (t == "PT")
189 return BoardType::PT;
190 if (t == "LC")
191 return BoardType::LC;
192 if (t == "TC")
193 return BoardType::TC;
194 if (t == "RTD")
195 return BoardType::RTD;
196 if (t == "ACTUATOR")
197 return BoardType::ACTUATOR;
198 if (t == "ENCODER")
199 return BoardType::ENCODER;
200 if (t == "ENVIRONMENTAL")
201 return BoardType::ENVIRONMENTAL;
202 if (t == "BALL_VALVE")
203 return BoardType::BALL_VALVE;
204 return BoardType::UNKNOWN;
205 };
206 for (const auto& b : c.boards) {
207 if (b.ip.empty())
208 continue;
209 BoardType bt = kind(b.type);
210 if (bt == BoardType::UNKNOWN)
211 continue;
212 BoardConfig bc{bt, b.ip, static_cast<int>(b.active_connectors.size()), b.enabled,
213 b.board_id};
214 board_map[b.ip] = bc;
215 // Index the board by its config-IP host octet for the loopback fallback (see BoardByOctet).
216 if (out_board_by_octet && b.enabled) {
217 const int octet = ipv4_host_octet(b.ip);
218 if (octet >= 2) {
219 auto [it, inserted] = out_board_by_octet->emplace(octet, bc);
220 if (!inserted)
221 std::cerr << "[Config] WARN: loopback host octet " << octet << " (127.0.0."
222 << octet << ") is claimed by both [" << it->second.ip << "] and ["
223 << b.ip
224 << "]; keeping the first. Give boards config IPs with distinct "
225 "last octets."
226 << std::endl;
227 }
228 }
229 }
230 // NOTE: do NOT add 127.0.0.1→PT. When the simulator can't bind config IPs it binds
231 // 127.0.0.<host octet> (one per board); resolve_board_config_by_source_ip maps that back.
232}
233
234// Map discovery signature board_type (DAQv2 wire: 1=PT, 2=LC, 3=RTD, 4=TC, 5=ACT, 6=ENC, 7=ENV,
235// 8=BALL_VALVE)
237 switch (t) {
239 return BoardType::PT;
241 return BoardType::LC;
243 return BoardType::RTD;
245 return BoardType::TC;
247 return BoardType::ACTUATOR;
248 case 6:
249 return BoardType::ENCODER;
251 return BoardType::ENVIRONMENTAL;
253 return BoardType::BALL_VALVE;
254 default:
255 return BoardType::UNKNOWN;
256 }
257}
258
260 switch (t) {
261 case BoardType::PT:
263 case BoardType::LC:
265 case BoardType::RTD:
266 return fsw::daq_wire::kRtd;
267 case BoardType::TC:
269 case BoardType::ACTUATOR:
271 case BoardType::ENCODER:
272 return 6;
273 case BoardType::ENVIRONMENTAL:
275 case BoardType::BALL_VALVE:
277 default:
279 }
280}
281
282/**
283 * Resolve a packet's source IP to its config board. Real deployments send from the config IP, so
284 * the direct board_map lookup hits. The integration test can't bind the 192.168.2.* config IPs, so
285 * the simulator binds each board to 127.0.0.<host-octet> (the last octet of its config IP); those
286 * packets resolve by that octet via board_by_octet. This is identity-based — unlike the old
287 * index-based scheme it does NOT depend on the bridge and the sim iterating [boards] in the same
288 * order, which they don't (toml++ sorts alphabetically; the Python sim uses file order).
289 */
291 const std::string& source_ip, const std::map<std::string, BoardConfig>& board_map,
292 const BoardByOctet& board_by_octet) {
293 auto it = board_map.find(source_ip);
294 if (it != board_map.end() && it->second.enabled)
295 return &it->second;
296 if (source_ip.compare(0, 8, "127.0.0.") == 0) {
297 const int octet = ipv4_host_octet(source_ip);
298 if (octet >= 2) {
299 auto bo = board_by_octet.find(octet);
300 if (bo != board_by_octet.end() && bo->second.enabled)
301 return &bo->second;
302 }
303 }
304 return nullptr;
305}
306
307int main(int argc, char* argv[]) {
308 // Parse command line arguments
309 std::string config_path = "config/config.toml";
310 std::string bind_address = "0.0.0.0";
311 uint16_t bind_port = 5006; // DiabloAvionics default sensor port
312
313 if (argc > 1)
314 config_path = argv[1];
315 if (argc > 2)
316 bind_address = argv[2];
317 if (argc > 3)
318 bind_port = static_cast<uint16_t>(std::stoi(argv[3]));
319
320 std::cout << "=== DAQ Bridge - DiabloAvionics Packet Receiver ===" << std::endl;
321 std::cout << "Config: " << config_path << std::endl;
322 std::cout << "Listening on: " << bind_address << ":" << bind_port << std::endl;
323 std::cout << std::endl;
324
325 // Hard-fail on a missing config. Otherwise every downstream loader treats "not found" as an
326 // empty config: daq_bridge binds :5006, registers 0 VTables, prints a green ✅, and sits
327 // "active (running)" forever while nothing can ever arrive — a healthy-looking no-op that
328 // presents as a vague "pipeline down". (This is exactly what a sim run does when it points at a
329 // config/sim_config.toml that was never generated.) A visible exit is far better.
330 {
331 std::ifstream cfg_check(config_path);
332 if (!cfg_check.is_open()) {
333 std::cerr << "❌ FATAL: daq_bridge config not found: '" << config_path
334 << "'. Refusing to start as a 0-board no-op. For a simulated run the backend "
335 << "generates config/sim_config.toml at session start; for hardware it is "
336 << "config/config.toml (generated from config/profiles/*.toml)." << std::endl;
337 return 1;
338 }
339 }
340
341 signal(SIGINT, signal_handler);
342 signal(SIGTERM, signal_handler);
343 // A database restart must reach the socket error/reconnect path, not kill the bridge.
344 signal(SIGPIPE, SIG_IGN);
345
346 // ── Board IP → Type mapping from config ([boards.*]), DB host/port, network sensor_port ──
347 std::map<std::string, BoardConfig> board_map;
348 BoardByOctet board_by_octet;
349 std::string db_host;
350 uint16_t db_port;
351 uint16_t config_sensor_port = 0;
352 std::string config_bind_ip;
353 ServerHeartbeatConfig hb_config;
354 fsw::time::TimeSyncConfig time_sync_cfg;
355 uint16_t log_backend_port = 8092; // [logs] backend_udp_port default
356 try {
357 load_board_map_from_config(config_path, board_map, &board_by_octet, db_host, db_port,
358 &config_sensor_port, &config_bind_ip, &hb_config, &time_sync_cfg,
359 &log_backend_port);
360 } catch (const std::invalid_argument& error) {
361 std::cerr << "FATAL: " << error.what() << std::endl;
362 return 1;
363 }
364 std::cout << "[TimeSync] mode="
365 << (time_sync_cfg.mode == fsw::time::TimeSyncConfig::Mode::BoardClock ? "board-clock"
366 : "arrival")
367 << " window=" << time_sync_cfg.window_seconds << "s" << std::endl;
368 if (config_sensor_port != 0)
369 bind_port = config_sensor_port;
370 if (!config_bind_ip.empty())
371 bind_address = config_bind_ip;
372
373 // Nothing named a NIC explicitly — find the one holding the board subnet. Without this the
374 // sensor listener accepts board traffic on every interface of the shared apps box, and every
375 // reply leaves on whichever one the route table prefers.
376 if (bind_address.empty() || bind_address == "0.0.0.0") {
377 const auto nic = fsw::net::resolveDaqBindAddress(fsw::config::load(config_path), "DAQ");
378 if (!nic.ok)
379 return 1;
380 bind_address = nic.address;
381 }
382
383 std::cout << "[Config] Loopback fallback (127.0.0.<host-octet>): " << board_by_octet.size()
384 << " enabled boards" << std::endl;
385 std::cout << "[Config] Board routing table (from " << config_path << "):" << std::endl;
386 for (const auto& [ip, cfg] : board_map) {
387 const char* type_str = "UNKNOWN";
388 switch (cfg.type) {
389 case BoardType::PT:
390 type_str = "PT";
391 break;
392 case BoardType::LC:
393 type_str = "LC";
394 break;
395 case BoardType::TC:
396 type_str = "TC";
397 break;
398 case BoardType::RTD:
399 type_str = "RTD";
400 break;
401 case BoardType::ACTUATOR:
402 type_str = "ACTUATOR";
403 break;
404 case BoardType::ENCODER:
405 type_str = "ENCODER";
406 break;
407 case BoardType::ENVIRONMENTAL:
408 type_str = "ENVIRONMENTAL";
409 break;
410 case BoardType::BALL_VALVE:
411 type_str = "BALL_VALVE";
412 break;
413 default:
414 break;
415 }
416 std::cout << " " << ip << " → " << type_str << " (" << cfg.active_channels << " channels)"
417 << (cfg.enabled ? " ✅" : " ❌") << std::endl;
418 }
419
420 // ── System mode ──
421 bool is_flight_daq = (config_path.find("flight") != std::string::npos);
422 fsw::config::SystemState system_state =
424 std::cout << "[System] Mode: " << (is_flight_daq ? "FLIGHT" : "GROUND") << std::endl;
425
426 // ── FSW Config Manager ──
427 std::cout << "[FSW] Initializing configuration manager..." << std::endl;
428 auto fsw_config = std::make_unique<fsw::fsw::FSWConfigManager>();
429 // Same NIC the sensor pipeline uses. This used to be a hardcoded "0.0.0.0" two lines from
430 // where [network].bind_ip was already parsed, so the FSW config port accepted traffic from —
431 // and replied on — any interface on the box.
432 if (!fsw_config->initialize(bind_address, 5008)) {
433 std::cerr << "❌ Failed to initialize FSW config manager" << std::endl;
434 return 1;
435 }
436 fsw_config->set_system_state(system_state);
437
438 // Add static IPs to FSW config manager
439 for (const auto& [ip, cfg] : board_map) {
440 if (cfg.board_id >= 0) {
441 fsw_config->set_board_static_ip(cfg.board_id, ip);
442 }
443 }
444
445 // ── Board Discovery ──
448 std::string network_interface = "eth0";
449 std::string target_subnet = is_flight_daq ? "192.168.3." : "192.168.2.";
450
451 std::string cmd = "ip -4 addr show | grep -B 2 'inet.*" + target_subnet +
452 "' | grep -E '^[0-9]+:' | head -1 | awk -F: '{print $2}' | tr -d ' '";
453 FILE* fp = popen(cmd.c_str(), "r");
454 if (fp) {
455 char iface[64] = {0};
456 if (fgets(iface, sizeof(iface), fp)) {
457 size_t len = strlen(iface);
458 if (len > 0 && iface[len - 1] == '\n')
459 iface[len - 1] = '\0';
460 if (strlen(iface) > 0)
461 network_interface = iface;
462 }
463 pclose(fp);
464 }
465 std::cout << "[Discovery] Interface: " << network_interface << std::endl;
466
467 std::string base_ip = is_flight_daq ? "192.168.3.0" : "192.168.2.0";
468 discovery.initialize(network_interface, base_ip, 100, 150);
469 for (const auto& [ip, cfg] : board_map) {
470 if (cfg.board_id >= 0 && cfg.board_id <= 255)
471 discovery.set_static_ip_for_board(static_cast<uint8_t>(cfg.board_id), ip);
472 }
474 config_manager.load_base_config(config_path);
475
476 // ── Sensor Pipeline ──
477 fsw::streams::SensorFramePipeline pipeline(bind_address, bind_port);
478 if (!pipeline.is_ready()) {
479 std::cerr << "❌ Failed to initialize sensor pipeline: " << pipeline.last_error()
480 << std::endl;
481 return 1;
482 }
483 std::cout << "✅ Sensor pipeline ready on port " << bind_port << std::endl;
484
485 // Board-log forwarder → Node backend (loopback UDP; not Elodin).
486 BackendLogForwarder log_forwarder("127.0.0.1", log_backend_port);
487 if (log_forwarder.ready())
488 std::cout << "✅ Board-log forwarder → 127.0.0.1:" << log_backend_port << std::endl;
489 else
490 std::cerr << "⚠️ Board-log forwarder socket failed (logs won't reach backend)" << std::endl;
491
492 if (pipeline.set_broadcast(true)) {
493 if (hb_config.send_from_daq_bridge)
494 std::cout << "✅ Broadcast enabled for SERVER_HEARTBEAT" << std::endl;
495 else
496 std::cout << "✅ SERVER_HEARTBEAT from heartbeat_service (daq_bridge skipping)"
497 << std::endl;
498 } else {
499 std::cerr << "⚠️ Failed to enable broadcast (heartbeat may not reach boards)" << std::endl;
500 }
501
502 // Proactively send SENSOR_CONFIG to all configured sense boards (PT/TC/RTD/LC).
503 // Boards need SENSOR_CONFIG to transition from WaitingForServer → Active and start streaming.
504 int proactive_count = 0;
505 for (const auto& [ip, cfg] : board_map) {
506 if (cfg.board_id < 0 || !cfg.enabled || cfg.type == BoardType::ACTUATOR ||
507 cfg.type == BoardType::ENVIRONMENTAL || cfg.type == BoardType::BALL_VALVE)
508 continue;
509 daq::BoardHeartbeatPacket synthetic{};
510 synthetic.board_id = static_cast<uint8_t>(cfg.board_id);
511 synthetic.engine_state = daq::EngineState::SAFE;
512 synthetic.board_state = daq::BoardState::SETUP;
513 daq::PacketHeader syn_hdr{};
514 syn_hdr.packet_type = daq::PacketType::BOARD_HEARTBEAT;
515 syn_hdr.version = DIABLO_COMMS_VERSION;
516 syn_hdr.timestamp =
517 static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
518 std::chrono::steady_clock::now().time_since_epoch())
519 .count() &
520 0xFFFFFFFF);
521 std::string mac = "00:00:00:00:" + std::to_string(cfg.board_id) + ":00";
522 fsw_config->process_board_heartbeat(syn_hdr, synthetic, ip, mac);
523 proactive_count++;
524 }
525 if (proactive_count > 0)
526 std::cout << "✅ Proactive SENSOR_CONFIG sent to " << proactive_count << " sense boards"
527 << std::endl;
528
530
531 // ── Inline Calibration (Elodin only supports 1 stream subscriber, so calibration runs here) ──
533 pt_calibration.load_calibration();
534 fsw::calibration::SensorCalibrationManager tc_calibration("TC", "°C", 3);
535 tc_calibration.load_calibration(
536 "scripts/calibration/calibrations/tc",
537 "external/DiabloAvionics/TC_Board/Calibration/tc_calibration.csv");
538 fsw::calibration::SensorCalibrationManager rtd_calibration("RTD", "°C", 3);
539 rtd_calibration.load_calibration(
540 "scripts/calibration/calibrations/rtd",
541 "external/DiabloAvionics/RTD_Board/Calibration/rtd_calibration.csv");
542 fsw::calibration::SensorCalibrationManager lc_calibration("LC", "lbf", 3);
543 lc_calibration.load_calibration(
544 "scripts/calibration/calibrations/lc",
545 "external/DiabloAvionics/LC_Board/Calibration/lc_calibration.csv");
546 router.set_pt_calibration(&pt_calibration);
547 router.set_tc_calibration(&tc_calibration);
548 router.set_rtd_calibration(&rtd_calibration);
549 router.set_lc_calibration(&lc_calibration);
550 std::cout << "✅ Sensor router initialized (with inline calibration: PT="
551 << pt_calibration.get_calibrated_count() << " channels)" << std::endl;
552
553 // ── Elodin Client (host/port from config [database]) ──
554 fsw::elodin::ElodinClient elodin_client;
555 fsw::routing::HeartbeatRouter heartbeat_router(elodin_client);
556 bool elodin_connected = false;
557 // Collect active boards with local channels from config (board-namespaced)
558 auto active_boards = fsw::config::load_active_boards(config_path);
559 const auto& pt_boards = active_boards[BoardType::PT];
560 const auto& act_boards = active_boards[BoardType::ACTUATOR];
561 const auto& tc_boards = active_boards[BoardType::TC];
562 const auto& rtd_boards = active_boards[BoardType::RTD];
563 const auto& env_boards = active_boards[BoardType::ENVIRONMENTAL];
564 const auto& lc_boards = active_boards[BoardType::LC];
565 const auto& enc_boards = active_boards[BoardType::ENCODER];
566 const auto& bv_boards = active_boards[BoardType::BALL_VALVE];
567
568 std::vector<uint8_t> config_board_ids;
569 for (const auto& [ip, cfg] : board_map) {
570 if (cfg.enabled && cfg.board_id > 0 && cfg.board_id <= 254)
571 config_board_ids.push_back(static_cast<uint8_t>(cfg.board_id));
572 }
573
574 if (elodin_client.connect(db_host, db_port)) {
575 elodin_connected = true;
576 std::cout << "✅ Connected to Elodin database" << std::endl;
577 // Register RAW VTables (board-namespaced TYPE<n>.CH<m> names)
578 if (!fsw::elodin::DatabaseConfig::register_tables(elodin_client, pt_boards, act_boards,
579 tc_boards, rtd_boards, lc_boards,
580 enc_boards)) {
581 std::cerr << "⚠️ RAW VTable registration failed" << std::endl;
582 }
583 // Register CALIBRATED VTables
585 elodin_client, pt_boards, tc_boards, rtd_boards, lc_boards, enc_boards, act_boards);
586 if (!bv_boards.empty() &&
588 std::cerr << "⚠️ Ball valve RAW VTable registration failed" << std::endl;
589 // Register BOARD_HEARTBEAT and SELF_TEST VTables only for boards in config
590 fsw::elodin::DatabaseConfig::register_heartbeat_tables(elodin_client, config_board_ids);
592 fsw::elodin::DatabaseConfig::register_self_test_tables(elodin_client, config_board_ids);
593 // Drain any response from DB after registration; otherwise recv buffer fills and TABLE
594 // writes stall after ~3s
595 std::array<uint8_t, 4096> drain_buf;
596 auto drain_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1500);
597 while (std::chrono::steady_clock::now() < drain_deadline && elodin_client.is_connected()) {
598 while (elodin_client.read_data(drain_buf.data(), drain_buf.size()) > 0) {
599 }
600 std::this_thread::sleep_for(std::chrono::milliseconds(20));
601 }
602 std::cout << "[Elodin] Drain complete, ready for TABLE data" << std::endl;
603 } else {
604 std::cerr << "⚠️ Elodin not connected (packets will be parsed but not published)"
605 << std::endl;
606 }
607
608 std::cout << "\n📡 Listening for DiabloAvionics packets...\n" << std::endl;
609
610 // ── Main Loop ──
611 size_t packet_count = 0;
612 size_t elodin_publish_count = 0;
613 size_t elodin_drop_count = 0;
614 std::map<std::string, size_t> packets_per_board; // IP → count
615 std::set<std::string> unknown_ips;
616 auto last_stats_time = std::chrono::steady_clock::now();
617 auto last_reconnect_time = std::chrono::steady_clock::now();
618 auto last_heartbeat_send = std::chrono::steady_clock::now();
619
620 // Per-board clock sync: reconstructs true sample times from chunk board
621 // millis (spreading + offset estimation + wrap/reboot handling). Keyed by
622 // source IP. See time/BoardClockSync.hpp; [time_sync] in config.toml.
623 fsw::time::BoardClockSync clock_sync(time_sync_cfg);
624 auto last_sync_log = std::chrono::steady_clock::now();
625
626 while (running) {
627 auto now = std::chrono::steady_clock::now();
628 // Broadcast SERVER_HEARTBEAT only when heartbeat_service is not used
629 if (hb_config.send_from_daq_bridge) {
630 auto elapsed_ms =
631 std::chrono::duration_cast<std::chrono::milliseconds>(now - last_heartbeat_send)
632 .count();
633 if (elapsed_ms >= static_cast<int64_t>(hb_config.interval_ms)) {
635 ssize_t sent = pipeline.send_to(hb_config.broadcast_ip, hb_config.broadcast_port,
636 pkt.data(), pkt.size());
637 if (sent > 0)
638 last_heartbeat_send = now;
639 }
640 }
641
642 auto batch = pipeline.poll();
643 if (!batch.has_value()) {
644 if (auto env = pipeline.get_last_environmental()) {
645 const BoardConfig* cfg =
646 resolve_board_config_by_source_ip(env->source_ip, board_map, board_by_octet);
647 const bool configured =
648 cfg && cfg->type == BoardType::ENVIRONMENTAL && cfg->board_id > 0 &&
649 cfg->board_id <= 255 &&
650 std::any_of(env_boards.begin(), env_boards.end(), [&](const auto& board) {
651 return board.board_id == cfg->board_id &&
652 std::find(board.channels.begin(), board.channels.end(), 1) !=
653 board.channels.end();
654 });
655 if (configured) {
656 ++packet_count;
657 ++packets_per_board[env->source_ip];
658 const uint64_t timestamp_ns =
659 std::chrono::duration_cast<std::chrono::nanoseconds>(
660 std::chrono::system_clock::now().time_since_epoch())
661 .count();
663 timestamp_ns, env->temperature_c, env->pressure_pa, env->humidity_rh,
664 env->sample_timestamp_ms);
665 if (elodin_connected && elodin_client.is_connected() &&
666 elodin_client.publish({0x25, static_cast<uint8_t>(cfg->board_id)}, msg))
667 ++elodin_publish_count;
668 else
669 ++elodin_drop_count;
670 }
671 }
672 // When last packet was a BOARD_HEARTBEAT, run discovery and broadcast config to that
673 // board
674 auto hb = pipeline.get_last_heartbeat();
675 if (hb) {
676 discovery.process_board_announcement(hb->data.data(), hb->data.size(),
677 hb->source_ip);
678 daq::PacketHeader ph;
679 daq::BoardHeartbeatPacket hb_body;
680 if (daq::parse_board_heartbeat_packet(hb->data.data(), hb->data.size(), ph,
681 hb_body)) {
682 uint8_t board_type_wire = fsw::daq_wire::kUnknown;
683 int elodin_board_id = -1;
685 hb->source_ip, board_map, board_by_octet)) {
686 board_type_wire = config_board_type_to_wire_u8(hb_cfg->type);
687 if (hb_cfg->board_id >= 0)
688 elodin_board_id = hb_cfg->board_id;
689 }
690 std::hash<std::string> hasher;
691 uint32_t ip_hash = static_cast<uint32_t>(hasher(hb->source_ip));
692 uint32_t sig_id =
693 (static_cast<uint32_t>(board_type_wire) << 8) | hb_body.board_id;
694 std::ostringstream mac;
695 mac << std::hex << std::setw(2) << std::setfill('0') << ((ip_hash >> 16) & 0xFF)
696 << ":" << std::setw(2) << ((ip_hash >> 8) & 0xFF) << ":" << std::setw(2)
697 << (ip_hash & 0xFF) << ":" << std::setw(2) << ((ip_hash >> 24) & 0xFF)
698 << ":" << std::setw(2) << ((sig_id >> 8) & 0xFF) << ":" << std::setw(2)
699 << (sig_id & 0xFF);
700 fsw_config->process_board_heartbeat(ph, hb_body, hb->source_ip, mac.str());
701
702 uint64_t hb_receive_ts_ns =
703 std::chrono::duration_cast<std::chrono::nanoseconds>(
704 std::chrono::steady_clock::now().time_since_epoch())
705 .count();
706 heartbeat_router.process_heartbeat(ph, hb_body, board_type_wire,
707 hb_receive_ts_ns, elodin_board_id);
708 }
709 // NOTE: config.toml.auto generation removed. Board discovery remains in-memory
710 // for this process only; other services must use config/config.toml.
711 }
712
713 // ── Board LOGS (type 15): forward to backend, do NOT publish to Elodin ──
714 auto log = pipeline.get_last_log();
715 if (log) {
716 daq::PacketHeader log_hdr;
717 uint8_t flags = 0;
718 const uint8_t* text = nullptr;
719 uint16_t text_len = 0;
720 if (daq::parse_log_packet(log->data.data(), log->data.size(), log_hdr, flags, text,
721 text_len)) {
722 // Board attribution by source IP (same mapping as sensor/heartbeat paths).
723 uint8_t board_id = 0;
725 log->source_ip, board_map, board_by_octet)) {
726 if (log_cfg->board_id >= 0)
727 board_id = static_cast<uint8_t>(log_cfg->board_id);
728 }
729 log_forwarder.forward(board_id, flags, text, text_len);
730 }
731 }
732
733 std::this_thread::sleep_for(std::chrono::microseconds(500));
734
735 // Drain Elodin socket: we only write TABLE packets; if the DB sends anything back
736 // (acks, errors, etc.) and we never read, the TCP recv buffer fills and the connection
737 // stalls after ~10–20s. Non-blocking read and discard keeps the connection alive.
738 if (elodin_connected && elodin_client.is_connected()) {
739 std::array<uint8_t, 4096> drain_buf;
740 while (elodin_client.read_data(drain_buf.data(), drain_buf.size()) > 0) {
741 /* discard */
742 }
743 }
744
745 // Try reconnect every 5 seconds if disconnected. NOT gated on elodin_connected:
746 // that flag is only set by a successful FIRST connect, so gating on it meant a bridge
747 // that lost the startup race to elodin-db never retried and published nothing for the
748 // life of the process.
749 if (!elodin_client.is_connected()) {
750 auto since = std::chrono::steady_clock::now() - last_reconnect_time;
751 if (std::chrono::duration_cast<std::chrono::seconds>(since).count() >= 5) {
752 last_reconnect_time = std::chrono::steady_clock::now();
753 if (elodin_client.reconnect()) {
754 std::cout << "✅ Reconnected to Elodin — re-registering VTables"
755 << std::endl;
756 // Publishing is gated on this flag everywhere below; without setting it
757 // here a bridge that only connected on a RETRY would stay silent.
758 elodin_connected = true;
760 elodin_client, pt_boards, act_boards, tc_boards, rtd_boards, lc_boards,
761 enc_boards);
763 elodin_client, pt_boards, tc_boards, rtd_boards, lc_boards, enc_boards,
764 act_boards);
765 if (!bv_boards.empty())
767 bv_boards);
769 config_board_ids);
771 env_boards);
773 config_board_ids);
774 }
775 }
776 }
777 continue;
778 }
779
780 packet_count++;
781 const std::string& source_ip = pipeline.last_source_ip();
782 packets_per_board[source_ip]++;
783
784 // Use system_clock (epoch) so timestamps align with JS Date.now() — prevents
785 // boot-relative vs epoch confusion that causes plot spikes when sources mix.
786 uint64_t receive_timestamp_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
787 std::chrono::system_clock::now().time_since_epoch())
788 .count();
789
790 // ── Route based on source IP → board type (config, then 127.0.0.x simulator fallback,
791 // then discovery, else treat as PT) ──
792 auto board_it = board_map.find(source_ip);
793 const BoardConfig* effective_cfg =
794 resolve_board_config_by_source_ip(source_ip, board_map, board_by_octet);
795 // Use board type from config even when disabled — we still publish actuator/PT data to DB
796 BoardType board_type = board_it != board_map.end()
797 ? board_it->second.type
798 : (effective_cfg ? effective_cfg->type : BoardType::UNKNOWN);
799 if (board_type == BoardType::UNKNOWN) {
800 auto discovered = discovery.get_board_by_ip(source_ip);
801 if (discovered) {
802 board_type = discovery_board_type_to_enum(discovered->signature.board_type);
803 if (board_type != BoardType::UNKNOWN)
804 board_map[source_ip] = {board_type, source_ip, 10, true, -1};
805 }
806 if (board_type == BoardType::UNKNOWN) {
807 if (unknown_ips.find(source_ip) == unknown_ips.end()) {
808 unknown_ips.insert(source_ip);
809 std::cout << "[Discovery] Unknown board at " << source_ip
810 << " — treating as PT so data is written to DB" << std::endl;
811 }
812 board_type = BoardType::PT;
813 }
814 }
815
816 // ── Publish SELF_TEST back to Elodin ──
817 if (elodin_connected && elodin_client.is_connected() && !batch.value().self_tests.empty()) {
818 elodin_client.begin_batch();
819 for (const auto& st_packet : batch.value().self_tests) {
820 // Self-test UDP has no board_id on wire; map source IP → config board_id.
821 // Must match sensor routing: simulators use 127.0.0.<host-octet> (board_by_octet)
822 // while board_map is keyed by config IPs — without effective_cfg, board_id stayed 0
823 // and no [0x60,*] rows reached Elodin (UI showed UNTESTED despite ACTIVE).
824 uint8_t board_id = 0;
825 if (effective_cfg && effective_cfg->board_id >= 0)
826 board_id = static_cast<uint8_t>(effective_cfg->board_id);
827 else {
828 auto cfg_it = board_map.find(source_ip);
829 if (cfg_it != board_map.end() && cfg_it->second.board_id >= 0)
830 board_id = static_cast<uint8_t>(cfg_it->second.board_id);
831 }
832 if (board_id == 0) {
833 auto discovered = discovery.get_board_by_ip(source_ip);
834 if (discovered)
835 board_id = discovered->signature.board_id;
836 }
837 // Integration fallback: startup sim commonly uses loopback board IPs
838 // like 127.0.0.60. If config/discovery mapping is unavailable, derive
839 // board_id from the last octet so SELF_TEST still reaches Elodin.
840 if (board_id == 0 && source_ip.compare(0, 8, "127.0.0.") == 0) {
841 int ip_octet = (source_ip.size() >= 9) ? std::atoi(source_ip.c_str() + 8) : 0;
842 if (ip_octet > 0 && ip_octet <= 255) {
843 board_id = static_cast<uint8_t>(ip_octet);
844 }
845 }
846
847 if (board_id != 0) {
848 using SelfTestElodinMsg = comms::CommsMessage<uint64_t, uint8_t, uint8_t>;
849
850 // Each sensor result gets its own VTable [0x60+sensor_id, board_id]
851 // so Elodin VTableStream delivers every result (shared tables only
852 // deliver the last-written row to subscribers).
853
854 // adc_good → sensor_id=0 (TDAC)
855 {
856 std::array<uint8_t, 2> pkt_id = {0x60, board_id};
857 SelfTestElodinMsg msg;
858 msg.setField<0>(receive_timestamp_ns);
859 msg.setField<1>(static_cast<uint8_t>(0));
860 msg.setField<2>(st_packet.adc_good);
861 elodin_client.publish(pkt_id, msg);
862 }
863
864 for (const auto& res : st_packet.results) {
865 if (res.sensor_id == 0)
866 continue;
867 uint8_t high = static_cast<uint8_t>(0x60 + (res.sensor_id & 0x0F));
868 std::array<uint8_t, 2> pkt_id = {high, board_id};
869 SelfTestElodinMsg msg;
870 msg.setField<0>(receive_timestamp_ns);
871 msg.setField<1>(res.sensor_id);
872 msg.setField<2>(res.result);
873 elodin_client.publish(pkt_id, msg);
874 }
875 }
876 }
877 if (elodin_client.flush_batch())
878 elodin_publish_count++;
879
880 std::array<uint8_t, 4096> drain_buf;
881 while (elodin_client.read_data(drain_buf.data(), drain_buf.size()) > 0) {
882 }
883 }
884
885 // ── Begin batch: all publishes from this packet go into one buffer ──
886 bool publishing = elodin_connected && elodin_client.is_connected();
887 if (publishing)
888 elodin_client.begin_batch();
889
890 // Board-namespaced packet IDs: low byte = (board_number-1)*0x10 + local_channel
891 // Match GUI / sequencer: board_id % 10 == 0 → use slot 10 (boards 10, 20, …).
892 uint8_t board_number = 1;
893 if (effective_cfg) {
894 int bn = static_cast<int>(effective_cfg->board_id % 10);
895 board_number = static_cast<uint8_t>(bn == 0 ? 10 : bn);
896 }
897 uint8_t board_offset =
898 static_cast<uint8_t>((static_cast<unsigned>(board_number) - 1u) * 0x20u);
899
900 // ── Clock sync: per-chunk corrected timestamps ──────────────────────
901 // Samples arrive chunk-major, each carrying its chunk's board millis
902 // (sample_timestamp_ms). Collect the distinct chunk timestamps in send
903 // order, reconstruct their true epoch-ns times (spreading + per-board
904 // offset; in "arrival" mode this returns receive_timestamp_ns for
905 // every chunk — the legacy behavior, byte-for-byte).
906 std::map<uint32_t, uint64_t> chunk_time_ns;
907 {
908 std::vector<uint32_t> chunk_ts;
909 for (const auto& sample : batch.value().pt_samples) {
910 if (chunk_ts.empty() || chunk_ts.back() != sample.sample_timestamp_ms)
911 chunk_ts.push_back(sample.sample_timestamp_ms);
912 }
913 if (!chunk_ts.empty()) {
914 auto stamped = clock_sync.stamp_packet(source_ip, receive_timestamp_ns, chunk_ts);
915 for (size_t i = 0; i < chunk_ts.size(); i++)
916 chunk_time_ns[chunk_ts[i]] = stamped[i];
917 }
918 }
919
920 // Helper lambda: build board-namespaced packet and publish a raw sample
921 auto publish_raw_sample = [&](uint8_t type_hi, const auto& sample) {
922 uint8_t pkt_lo = static_cast<uint8_t>(board_offset + sample.channel_id);
923 std::array<uint8_t, 2> pkt_id = {type_hi, pkt_lo};
925 auto ts_it = chunk_time_ns.find(sample.sample_timestamp_ms);
926 msg.setField<0>(ts_it != chunk_time_ns.end() ? ts_it->second : receive_timestamp_ns);
927 msg.setField<1>(sample.channel_id);
928 msg.setField<2>(std::array<uint8_t, 3>{0, 0, 0});
929 msg.setField<3>(sample.raw_adc_counts);
930 msg.setField<4>(sample.sample_timestamp_ms);
931 msg.setField<5>(sample.status_flags);
932 if (publishing)
933 elodin_client.publish(pkt_id, msg);
934 };
935
936 switch (board_type) {
937 case BoardType::PT: {
938 for (const auto& sample : batch.value().pt_samples)
939 publish_raw_sample(0x20, sample);
940 break;
941 }
942 case BoardType::ACTUATOR: {
943 for (const auto& sample : batch.value().pt_samples)
944 publish_raw_sample(0x30, sample);
945 break;
946 }
947 case BoardType::LC: {
948 for (const auto& sample : batch.value().pt_samples)
949 publish_raw_sample(0x23, sample);
950 break;
951 }
952 case BoardType::TC: {
953 for (const auto& sample : batch.value().pt_samples)
954 publish_raw_sample(0x21, sample);
955 break;
956 }
957 case BoardType::RTD: {
958 for (const auto& sample : batch.value().pt_samples)
959 publish_raw_sample(0x22, sample);
960 break;
961 }
962 case BoardType::ENCODER: {
963 for (const auto& sample : batch.value().pt_samples)
964 publish_raw_sample(0x24, sample);
965 break;
966 }
967 case BoardType::BALL_VALVE: {
968 // Valve position (raw 12-bit counts of a 4-20 mA signal). Its own type_hi so a
969 // ball valve never shares a key with an ACTUATOR board on the same slot.
970 for (const auto& sample : batch.value().pt_samples)
971 publish_raw_sample(0x33, sample);
972 break;
973 }
974 default:
975 break;
976 }
977
978 // ── Flush entire batch as one TCP write ──
979 if (publishing) {
980 if (elodin_client.flush_batch()) {
981 elodin_publish_count++;
982 } else {
983 elodin_drop_count++;
984 }
985 // Drain any response from DB so recv buffer doesn't fill and stall the connection
986 std::array<uint8_t, 4096> drain_buf;
987 while (elodin_client.read_data(drain_buf.data(), drain_buf.size()) > 0) {
988 }
989 }
990
991 // ── Periodic stats (every 2 seconds) ──
992 // Periodic clock-sync diagnostics (logs only; [time_sync] log_interval_s).
994 time_sync_cfg.log_interval_s > 0) {
995 auto sync_elapsed = std::chrono::steady_clock::now() - last_sync_log;
996 if (std::chrono::duration_cast<std::chrono::seconds>(sync_elapsed).count() >=
997 static_cast<int64_t>(time_sync_cfg.log_interval_s)) {
998 last_sync_log = std::chrono::steady_clock::now();
999 for (const auto& key : clock_sync.boards()) {
1000 const auto st = clock_sync.stats(key);
1001 std::cout << "[TimeSync] board=" << key << std::fixed << std::setprecision(1)
1002 << " offset=" << st.offset_ms << "ms jitter=" << st.jitter_ms
1003 << "ms resyncs=" << st.resyncs << " clamped=" << st.clamp_fallbacks
1004 << " locked=" << (st.locked ? "yes" : "no") << std::defaultfloat
1005 << std::endl;
1006 }
1007 }
1008 }
1009
1010 auto elapsed = std::chrono::steady_clock::now() - last_stats_time;
1011 if (std::chrono::duration_cast<std::chrono::seconds>(elapsed).count() >= 2) {
1012 last_stats_time = std::chrono::steady_clock::now();
1013
1014 std::cout << "\n[Stats] Total: " << packet_count << " pkts";
1015 for (const auto& [ip, cnt] : packets_per_board) {
1016 const BoardConfig* cfg =
1017 resolve_board_config_by_source_ip(ip, board_map, board_by_octet);
1018 const char* tag = "???";
1019 if (cfg) {
1020 switch (cfg->type) {
1021 case BoardType::PT:
1022 tag = "PT";
1023 break;
1024 case BoardType::LC:
1025 tag = "LC";
1026 break;
1027 case BoardType::TC:
1028 tag = "TC";
1029 break;
1030 case BoardType::RTD:
1031 tag = "RTD";
1032 break;
1033 case BoardType::ACTUATOR:
1034 tag = "ACT";
1035 break;
1036 case BoardType::ENCODER:
1037 tag = "ENC";
1038 break;
1039 case BoardType::BALL_VALVE:
1040 tag = "BV";
1041 break;
1042 default:
1043 break;
1044 }
1045 }
1046 std::cout << " | " << tag << "(" << ip << "):" << cnt;
1047 }
1048 if (elodin_connected && elodin_client.is_connected())
1049 std::cout << " | DB: ✅ " << elodin_publish_count << "ok/" << elodin_drop_count
1050 << "drop";
1051 else if (elodin_connected)
1052 std::cout << " | DB: ⚠️ disconnected (reconnecting)";
1053 else
1054 std::cout << " | DB: ❌";
1055 std::cout << std::endl;
1056
1057 // Print raw PT ADC counts instead of calibrated pressures (removed logic)
1058 if (board_type == BoardType::PT && !batch.value().pt_samples.empty()) {
1059 std::cout << "[PT Raw] ";
1060 for (const auto& sample : batch.value().pt_samples) {
1061 std::cout << "CH" << (int)sample.channel_id << ":" << sample.raw_adc_counts
1062 << " ";
1063 }
1064 std::cout << std::endl;
1065 }
1066 }
1067 }
1068
1069 // Shutdown
1070 auto boards = discovery.get_discovered_boards();
1071 std::cout << "[Discovery] Found " << boards.size() << " boards" << std::endl;
1072 std::cout << "[DAQ Bridge] Shutdown complete" << std::endl;
1073 return 0;
1074}
Per-board clock sync for DAQ sample timestamping.
static volatile uint32_t tail
Definition board_uart9.c:11
static std::atomic< bool > running
void forward(uint8_t board_id, uint8_t flags, const uint8_t *text, uint16_t text_len)
BackendLogForwarder(const std::string &host, uint16_t port)
bool load_calibration()
Auto-load calibration from default paths Default: factory CSV first, then newest GUI JSON overlays ev...
size_t get_calibrated_count() const
Get number of calibrated channels.
bool load_calibration(const std::string &json_dir, const std::string &csv_path)
Auto-discover & load from default directories.
Board Discovery and Configuration Manager.
void start_discovery(DiscoveryMode mode=DiscoveryMode::HYBRID)
Start discovery process.
void set_static_ip_for_board(uint8_t board_id, const std::string &ip)
Set static IP for board_id (from config).
void process_board_announcement(const uint8_t *data, size_t size, const std::string &source_ip)
Process incoming board announcement packet.
std::optional< DiscoveredBoard > get_board_by_ip(const std::string &ip)
Get board by IP address.
bool initialize(const std::string &network_interface, const std::string &base_ip, uint8_t ip_range_start=100, uint8_t ip_range_end=200)
Initialize discovery system.
std::vector< DiscoveredBoard > get_discovered_boards()
Get all discovered boards.
Dynamic Configuration Manager.
bool load_base_config(const std::string &config_path)
Load base configuration.
static bool register_calibrated_tables(ElodinClient &client, const std::vector< BoardChannels > &pt_boards, const std::vector< BoardChannels > &tc_boards, const std::vector< BoardChannels > &rtd_boards, const std::vector< BoardChannels > &lc_boards, const std::vector< BoardChannels > &enc_boards, const std::vector< BoardChannels > &act_boards)
Register CALIBRATED VTables.
static bool register_environmental_tables(ElodinClient &client, const std::vector< BoardChannels > &boards)
static bool register_heartbeat_tables(ElodinClient &client, const std::vector< uint8_t > &board_ids)
Register BOARD_HEARTBEAT VTables for specific board IDs.
static bool register_tables(ElodinClient &client, const std::vector< BoardChannels > &pt_boards, const std::vector< BoardChannels > &act_boards, const std::vector< BoardChannels > &tc_boards, const std::vector< BoardChannels > &rtd_boards, const std::vector< BoardChannels > &lc_boards, const std::vector< BoardChannels > &enc_boards)
Register RAW sensor / actuator VTables.
static bool register_self_test_tables(ElodinClient &client, const std::vector< uint8_t > &board_ids)
Register SELF_TEST VTables for specific board IDs.
static bool register_ball_valve_tables(ElodinClient &client, const std::vector< BoardChannels > &bv_boards)
Register ball valve RAW position VTables [0x33] (daq_bridge).
Standalone Elodin database client.
bool flush_batch()
Flush (send) the accumulated batch in a single TCP write.
bool is_connected() const
Check if connected.
ssize_t read_data(void *buffer, size_t max_len)
Read incoming data packet from Elodin (non-blocking).
bool publish(uint16_t message_id, const MessageType &message)
Publish a message to a table.
void begin_batch()
Begin batching — subsequent publish() calls go into an internal buffer.
bool connect(const std::string &host, uint16_t port=2240)
Connect to Elodin database.
bool reconnect()
Attempt to reconnect using the last-used host/port.
Routes incoming BOARD_HEARTBEAT packets to Elodin DB.
BoardEvent process_heartbeat(const daq::PacketHeader &header, const daq::BoardHeartbeatPacket &heartbeat, uint8_t board_type_wire, uint64_t receive_ts_ns, int elodin_board_id_if_known=-1)
Process one BOARD_HEARTBEAT and publish to Elodin.
Routes decoded sensor frames to Elodin tables.
UDP pipeline: DAQv2-Comms board packets → internal SensorBatch.
std::optional< EnvironmentalSample > get_last_environmental()
const std::string & last_source_ip() const
std::optional< daq_comms::protocol::SensorBatch > poll()
std::optional< LastLog > get_last_log()
std::optional< LastHeartbeat > get_last_heartbeat()
ssize_t send_to(const std::string &dest_ip, uint16_t dest_port, const uint8_t *data, size_t size)
std::vector< uint64_t > stamp_packet(const std::string &board_key, uint64_t arrival_ns, const std::vector< uint32_t > &chunk_ms)
Stamp one packet's chunks for one board.
std::vector< std::string > boards() const
All boards seen (for periodic diagnostics logging).
BoardStats stats(const std::string &board_key) const
Stats for one board (default-constructed if unknown).
#define DIABLO_COMMS_VERSION
Definition daq-protocol.h:4
static void load_board_map_from_config(const std::string &config_path, std::map< std::string, BoardConfig > &board_map, BoardByOctet *out_board_by_octet, std::string &db_host, uint16_t &db_port, uint16_t *out_sensor_port=nullptr, std::string *out_bind_ip=nullptr, ServerHeartbeatConfig *out_hb=nullptr, fsw::time::TimeSyncConfig *out_ts=nullptr, uint16_t *out_log_port=nullptr)
fsw::config::ActiveBoardKind BoardType
static BoardType discovery_board_type_to_enum(uint8_t t)
static uint8_t config_board_type_to_wire_u8(BoardType t)
static int ipv4_host_octet(const std::string &ip)
Last octet of a dotted IPv4 string (e.g.
void signal_handler(int)
std::map< int, BoardConfig > BoardByOctet
static const BoardConfig * resolve_board_config_by_source_ip(const std::string &source_ip, const std::map< std::string, BoardConfig > &board_map, const BoardByOctet &board_by_octet)
Resolve a packet's source IP to its config board.
static const emmc_port_t port
Definition emmc_stm32.c:210
byte mac[6]
Definition main.cpp:31
static uint8_t board_id
Definition main.cpp:30
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
json_pointer res
Definition json.hpp:13957
size_type count(const key_type &key) const
Definition json.hpp:19264
std::vector< uint8_t > build_server_heartbeat_packet()
comms::CommsMessage< uint64_t, float, uint32_t, float, uint32_t > EnvironmentalMessage
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, uint32_t, uint32_t, uint8_t > RawPTMessage
Raw Pressure Transducer sample message for Elodin.
bool parse_log_packet(const uint8_t *buffer, size_t buffer_size, PacketHeader &header_out, uint8_t &flags_out, const uint8_t *&text_out, uint16_t &text_len_out)
Parses a LOGS packet (type 15) from buffer.
bool parse_board_heartbeat_packet(const uint8_t *buffer, size_t buffer_size, PacketHeader &header_out, BoardHeartbeatPacket &data_out)
Parses a Board Heartbeat packet from buffer.
SystemState
System state (GSE or Flight).
std::map< ActiveBoardKind, std::vector< elodin::BoardChannels > > load_active_boards(const std::string &config_path)
Parse [boards.
ActiveBoardKind
Board kinds from [boards.
Config load(const std::string &path)
Parse config.toml.
Definition Config.cpp:442
constexpr uint8_t kRtd
constexpr uint8_t kActuator
constexpr uint8_t kThermocouple
constexpr uint8_t kPressureTransducer
constexpr uint8_t kBallValve
constexpr uint8_t kLoadCell
constexpr uint8_t kUnknown
Legacy DAQv2 wire numeric board_type (Elodin / discovery); heartbeat body no longer carries type.
constexpr uint8_t kEnvironmental
DaqBindResult resolveDaqBindAddress(const ::fsw::config::Config &cfg, const char *who)
Decide which local address board traffic should leave from, and log the decision.
static bool elapsed(uint32_t now, uint32_t then, uint32_t interval)
Definition self_test.c:10
int active_channels
How many channels this board is told to sample — the length of its active_connectors list,...
CommsMessage - Template-based message serialization.
void setField(ValueType &&value)
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
ServerHeartbeatConfig server_heartbeat
Definition Config.hpp:267
DatabaseConfig database
Definition Config.hpp:266
std::vector< BoardConfig > boards
Definition Config.hpp:272
fsw::time::TimeSyncConfig time_sync
Definition Config.hpp:270
NetworkConfig network
Definition Config.hpp:265
LogsConfig logs
Definition Config.hpp:269
uint16_t backend_udp_port
Definition Config.hpp:52
int main()
std::string board(const std::string &name, const std::string &id, bool enabled=true)