641 parser = argparse.ArgumentParser(description=
"DiabloAvionics Board Simulator")
643 "--config", default=
"config/config.toml", help=
"Path to config.toml"
646 "--target", default=
"127.0.0.1", help=
"Target IP (DAQ Bridge / Backend)"
648 parser.add_argument(
"--port", type=int, default=5006, help=
"Target Port")
652 help=
"Only simulate boards of this type (e.g. PT, TC, LC, RTD, ACTUATOR)",
657 help=
"Reduce PT noise for spike validation (smoother signal)",
662 help=
"Write JSON stats (packets sent per board) to this file on shutdown",
668 help=
"Run for this many seconds then exit (default: run until Ctrl+C)",
673 help=
"Skip SETUP/SELF_TEST lifecycle, go directly to ACTIVE",
676 "--allow-ip-fallback",
678 help=
"Permit binding loopback when a config IP isn't on this host (sim_config.toml / "
679 "integration test only — NEVER with a hardware config; it would emit synthetic data "
680 "tagged with real board IDs).",
687 help=
"Uniform sensor scan rate for ALL board types, overriding the per-type "
688 "defaults (10 Hz, 50 Hz encoder). For CI, where the goal is verifying "
689 "nothing obviously broke rather than sustaining hardware rates. Keep "
690 "above the GUI envelope budget (points_per_second) or downsampling "
691 "becomes a pass-through and stops being tested.",
695 "--chunks-per-packet",
699 help=
"Batch N chunks (scans) per sensor packet, each with its own board-clock timestamp",
706 help=
"Random one-sided network delay in [0, MS] ms per sensor packet (seeded per board)",
713 help=
"Board crystal drift; sign alternates per board so both directions run",
720 help=
"Absolute board-clock offset (bogus clock)",
727 help=
"Reset every board's millis to ~0 once, after this many seconds",
732 help=
"Start board millis ~30 s before the uint32 wrap so a wrap occurs mid-run",
734 args = parser.parse_args()
736 if not os.path.exists(args.config):
737 print(f
"Error: Config file not found at {args.config}")
741 with open(args.config,
"rb")
as f:
742 config = tomli.load(f)
743 except Exception
as e:
744 print(f
"Error loading config: {e}")
747 boards = config.get(
"boards", {})
749 int(k): float(v)
for k, v
in config.get(
"sim_pt_targets", {}).items()
751 simulated_boards = []
753 print(f
"🚀 Starting Simulator - Target: {args.target}:{args.port}")
756 f
" Sim PT targets: {len(sim_pt_targets)} channels (e.g. ch1/ch5=500, ch6=4k)"
758 if not args.skip_startup:
759 print(
" Startup lifecycle: SETUP → SELF_TEST → ACTIVE (matching firmware)")
762 for name, board_cfg
in boards.items():
763 if not board_cfg.get(
"enabled",
True):
766 if name ==
"integration_startup":
771 and board_cfg.get(
"type",
"PT").upper() != args.only_type.upper()
780 low_noise=args.low_noise,
781 board_index=active_count,
782 sim_pt_targets=sim_pt_targets,
783 skip_startup=args.skip_startup,
784 sensor_hz=args.sensor_hz,
785 allow_ip_fallback=args.allow_ip_fallback,
787 chunks_per_packet=args.chunks_per_packet,
788 net_jitter_ms=args.net_jitter_ms,
789 clock_drift_ppm=args.clock_drift_ppm,
790 clock_offset_ms=args.clock_offset_ms,
791 reboot_after_s=args.reboot_after,
792 start_near_wrap=args.start_near_wrap,
796 simulated_boards.append(board)
798 state_label =
"ACTIVE" if args.skip_startup
else "SETUP"
800 f
" [{state_label:6}] {name:15} | Type: {board.board_type_str:4} | ID: {board.board_id:2} | Source: {board.ip}:{board.listen_port}"
803 if active_count == 0:
804 print(
"No enabled boards found in config!", flush=
True)
806 print(f
"{active_count} boards started.", flush=
True)
808 print(
"Simulator is running. Press Ctrl+C to stop.", flush=
True)
811 if not args.stats_file:
814 "total_sensor_packets": sum(b.packets_sent
for b
in simulated_boards),
817 for b
in simulated_boards:
818 active = b.active_connectors
819 stats[
"boards"][b.name] = {
820 "type": b.board_type_str,
821 "board_id": b.board_id,
822 "packets_sent": b.packets_sent,
823 "channels_per_packet": len(active),
824 "chunks_per_packet": b.timing.chunks_per_packet,
825 "total_sensor_updates": b.packets_sent
827 * b.timing.chunks_per_packet,
829 "clock_drift_ppm": b.drift_ppm,
830 "clock_offset_ms": b.timing.clock_offset_ms,
831 "net_jitter_ms": b.timing.net_jitter_ms,
832 "start_near_wrap": b.timing.start_near_wrap,
833 "rebooted_at": b._rebooted_at,
835 stats[
"total_sensor_updates"] = sum(
836 v[
"total_sensor_updates"]
for v
in stats[
"boards"].values()
840 tmp_path = args.stats_file +
".tmp"
841 with open(tmp_path,
"w")
as f:
842 json.dump(stats, f, indent=2)
843 os.replace(tmp_path, args.stats_file)
848 def _sigterm_handler(signum, frame):
849 for b
in simulated_boards:
853 print(f
"Stats written to {args.stats_file}", flush=
True)
856 signal.signal(signal.SIGTERM, _sigterm_handler)
860 time.sleep(args.duration)
861 print(f
"\nDuration ({args.duration}s) reached. Stopping...")
862 for b
in simulated_boards:
871 except KeyboardInterrupt:
872 print(
"\nStopping simulator...")
873 for b
in simulated_boards:
__init__(self, name, board_config, target_ip, target_port, low_noise=False, board_index=0, sim_pt_targets=None, skip_startup=False, timing=None, sensor_hz=None, allow_ip_fallback=False)