307int main(
int argc,
char* argv[]) {
309 std::string config_path =
"config/config.toml";
310 std::string bind_address =
"0.0.0.0";
311 uint16_t bind_port = 5006;
314 config_path = argv[1];
316 bind_address = argv[2];
318 bind_port =
static_cast<uint16_t
>(std::stoi(argv[3]));
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;
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;
344 signal(SIGPIPE, SIG_IGN);
347 std::map<std::string, BoardConfig> board_map;
351 uint16_t config_sensor_port = 0;
352 std::string config_bind_ip;
355 uint16_t log_backend_port = 8092;
358 &config_sensor_port, &config_bind_ip, &hb_config, &time_sync_cfg,
360 }
catch (
const std::invalid_argument& error) {
361 std::cerr <<
"FATAL: " << error.what() << std::endl;
364 std::cout <<
"[TimeSync] mode="
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;
376 if (bind_address.empty() || bind_address ==
"0.0.0.0") {
380 bind_address = nic.address;
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";
401 case BoardType::ACTUATOR:
402 type_str =
"ACTUATOR";
404 case BoardType::ENCODER:
405 type_str =
"ENCODER";
407 case BoardType::ENVIRONMENTAL:
408 type_str =
"ENVIRONMENTAL";
410 case BoardType::BALL_VALVE:
411 type_str =
"BALL_VALVE";
416 std::cout <<
" " << ip <<
" → " << type_str <<
" (" << cfg.active_channels <<
" channels)"
417 << (cfg.enabled ?
" ✅" :
" ❌") << std::endl;
421 bool is_flight_daq = (config_path.find(
"flight") != std::string::npos);
424 std::cout <<
"[System] Mode: " << (is_flight_daq ?
"FLIGHT" :
"GROUND") << std::endl;
427 std::cout <<
"[FSW] Initializing configuration manager..." << std::endl;
428 auto fsw_config = std::make_unique<fsw::fsw::FSWConfigManager>();
432 if (!fsw_config->initialize(bind_address, 5008)) {
433 std::cerr <<
"❌ Failed to initialize FSW config manager" << std::endl;
436 fsw_config->set_system_state(system_state);
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);
448 std::string network_interface =
"eth0";
449 std::string target_subnet = is_flight_daq ?
"192.168.3." :
"192.168.2.";
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");
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;
465 std::cout <<
"[Discovery] Interface: " << network_interface << std::endl;
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)
479 std::cerr <<
"❌ Failed to initialize sensor pipeline: " << pipeline.
last_error()
483 std::cout <<
"✅ Sensor pipeline ready on port " << bind_port << std::endl;
487 if (log_forwarder.ready())
488 std::cout <<
"✅ Board-log forwarder → 127.0.0.1:" << log_backend_port << std::endl;
490 std::cerr <<
"⚠️ Board-log forwarder socket failed (logs won't reach backend)" << std::endl;
494 std::cout <<
"✅ Broadcast enabled for SERVER_HEARTBEAT" << std::endl;
496 std::cout <<
"✅ SERVER_HEARTBEAT from heartbeat_service (daq_bridge skipping)"
499 std::cerr <<
"⚠️ Failed to enable broadcast (heartbeat may not reach boards)" << std::endl;
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)
509 daq::BoardHeartbeatPacket synthetic{};
510 synthetic.board_id =
static_cast<uint8_t
>(cfg.board_id);
513 daq::PacketHeader syn_hdr{};
517 static_cast<uint32_t
>(std::chrono::duration_cast<std::chrono::milliseconds>(
518 std::chrono::steady_clock::now().time_since_epoch())
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);
525 if (proactive_count > 0)
526 std::cout <<
"✅ Proactive SENSOR_CONFIG sent to " << proactive_count <<
" sense boards"
536 "scripts/calibration/calibrations/tc",
537 "external/DiabloAvionics/TC_Board/Calibration/tc_calibration.csv");
540 "scripts/calibration/calibrations/rtd",
541 "external/DiabloAvionics/RTD_Board/Calibration/rtd_calibration.csv");
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="
556 bool elodin_connected =
false;
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];
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));
574 if (elodin_client.
connect(db_host, db_port)) {
575 elodin_connected =
true;
576 std::cout <<
"✅ Connected to Elodin database" << std::endl;
579 tc_boards, rtd_boards, lc_boards,
581 std::cerr <<
"⚠️ RAW VTable registration failed" << std::endl;
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;
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) {
600 std::this_thread::sleep_for(std::chrono::milliseconds(20));
602 std::cout <<
"[Elodin] Drain complete, ready for TABLE data" << std::endl;
604 std::cerr <<
"⚠️ Elodin not connected (packets will be parsed but not published)"
608 std::cout <<
"\n📡 Listening for DiabloAvionics packets...\n" << std::endl;
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;
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();
624 auto last_sync_log = std::chrono::steady_clock::now();
627 auto now = std::chrono::steady_clock::now();
631 std::chrono::duration_cast<std::chrono::milliseconds>(now - last_heartbeat_send)
633 if (elapsed_ms >=
static_cast<int64_t
>(hb_config.
interval_ms)) {
636 pkt.data(), pkt.size());
638 last_heartbeat_send = now;
642 auto batch = pipeline.
poll();
643 if (!batch.has_value()) {
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();
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())
663 timestamp_ns, env->temperature_c, env->pressure_pa, env->humidity_rh,
664 env->sample_timestamp_ms);
666 elodin_client.
publish({0x25, static_cast<uint8_t>(cfg->board_id)}, msg))
667 ++elodin_publish_count;
678 daq::PacketHeader ph;
679 daq::BoardHeartbeatPacket hb_body;
683 int elodin_board_id = -1;
685 hb->source_ip, board_map, board_by_octet)) {
687 if (hb_cfg->board_id >= 0)
688 elodin_board_id = hb_cfg->board_id;
690 std::hash<std::string> hasher;
691 uint32_t ip_hash =
static_cast<uint32_t
>(hasher(hb->source_ip));
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)
700 fsw_config->process_board_heartbeat(ph, hb_body, hb->source_ip,
mac.str());
702 uint64_t hb_receive_ts_ns =
703 std::chrono::duration_cast<std::chrono::nanoseconds>(
704 std::chrono::steady_clock::now().time_since_epoch())
707 hb_receive_ts_ns, elodin_board_id);
716 daq::PacketHeader log_hdr;
718 const uint8_t* text =
nullptr;
719 uint16_t text_len = 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);
729 log_forwarder.forward(
board_id, flags, text, text_len);
733 std::this_thread::sleep_for(std::chrono::microseconds(500));
739 std::array<uint8_t, 4096> drain_buf;
740 while (elodin_client.
read_data(drain_buf.data(), drain_buf.size()) > 0) {
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();
754 std::cout <<
"✅ Reconnected to Elodin — re-registering VTables"
758 elodin_connected =
true;
760 elodin_client, pt_boards, act_boards, tc_boards, rtd_boards, lc_boards,
763 elodin_client, pt_boards, tc_boards, rtd_boards, lc_boards, enc_boards,
765 if (!bv_boards.empty())
782 packets_per_board[source_ip]++;
786 uint64_t receive_timestamp_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
787 std::chrono::system_clock::now().time_since_epoch())
792 auto board_it = board_map.find(source_ip);
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) {
803 if (board_type != BoardType::UNKNOWN)
804 board_map[source_ip] = {board_type, source_ip, 10,
true, -1};
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;
812 board_type = BoardType::PT;
817 if (elodin_connected && elodin_client.
is_connected() && !batch.value().self_tests.empty()) {
819 for (
const auto& st_packet : batch.value().self_tests) {
825 if (effective_cfg && effective_cfg->
board_id >= 0)
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);
835 board_id = discovered->signature.board_id;
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);
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);
864 for (
const auto&
res : st_packet.results) {
865 if (
res.sensor_id == 0)
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);
878 elodin_publish_count++;
880 std::array<uint8_t, 4096> drain_buf;
881 while (elodin_client.
read_data(drain_buf.data(), drain_buf.size()) > 0) {
886 bool publishing = elodin_connected && elodin_client.
is_connected();
892 uint8_t board_number = 1;
894 int bn =
static_cast<int>(effective_cfg->
board_id % 10);
895 board_number =
static_cast<uint8_t
>(bn == 0 ? 10 : bn);
897 uint8_t board_offset =
898 static_cast<uint8_t
>((
static_cast<unsigned>(board_number) - 1u) * 0x20u);
906 std::map<uint32_t, uint64_t> chunk_time_ns;
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);
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];
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);
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);
933 elodin_client.
publish(pkt_id, msg);
936 switch (board_type) {
937 case BoardType::PT: {
938 for (
const auto& sample : batch.value().pt_samples)
939 publish_raw_sample(0x20, sample);
942 case BoardType::ACTUATOR: {
943 for (
const auto& sample : batch.value().pt_samples)
944 publish_raw_sample(0x30, sample);
947 case BoardType::LC: {
948 for (
const auto& sample : batch.value().pt_samples)
949 publish_raw_sample(0x23, sample);
952 case BoardType::TC: {
953 for (
const auto& sample : batch.value().pt_samples)
954 publish_raw_sample(0x21, sample);
957 case BoardType::RTD: {
958 for (
const auto& sample : batch.value().pt_samples)
959 publish_raw_sample(0x22, sample);
962 case BoardType::ENCODER: {
963 for (
const auto& sample : batch.value().pt_samples)
964 publish_raw_sample(0x24, sample);
967 case BoardType::BALL_VALVE: {
970 for (
const auto& sample : batch.value().pt_samples)
971 publish_raw_sample(0x33, sample);
981 elodin_publish_count++;
986 std::array<uint8_t, 4096> drain_buf;
987 while (elodin_client.
read_data(drain_buf.data(), drain_buf.size()) > 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() >=
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
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();
1014 std::cout <<
"\n[Stats] Total: " << packet_count <<
" pkts";
1015 for (
const auto& [ip, cnt] : packets_per_board) {
1018 const char* tag =
"???";
1020 switch (cfg->type) {
1030 case BoardType::RTD:
1033 case BoardType::ACTUATOR:
1036 case BoardType::ENCODER:
1039 case BoardType::BALL_VALVE:
1046 std::cout <<
" | " << tag <<
"(" << ip <<
"):" << cnt;
1049 std::cout <<
" | DB: ✅ " << elodin_publish_count <<
"ok/" << elodin_drop_count
1051 else if (elodin_connected)
1052 std::cout <<
" | DB: ⚠️ disconnected (reconnecting)";
1054 std::cout <<
" | DB: ❌";
1055 std::cout << std::endl;
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
1064 std::cout << std::endl;
1071 std::cout <<
"[Discovery] Found " << boards.size() <<
" boards" << std::endl;
1072 std::cout <<
"[DAQ Bridge] Shutdown complete" << std::endl;