diff --git a/retina_simulation/orchestrator.py b/retina_simulation/orchestrator.py index 26ab0b2..7075d01 100644 --- a/retina_simulation/orchestrator.py +++ b/retina_simulation/orchestrator.py @@ -697,6 +697,37 @@ def build_ground_truth_payload(aircraft_summaries: list[dict]) -> list[dict]: return payload_aircraft +def build_adsb_push_payload(aircraft_summaries: list[dict]) -> list[dict]: + """Remap world aircraft summaries to the server ADS-B push schema. + + Transponder-equipped aircraft only. This push IS the simulated ADS-B + broadcast, and a dark target by definition emits none — pushing it with + its object id standing in for the hex minted a fake transponder per dark + aircraft on the server, so every dark solve keyed mn-adsb-* and the dark + lane stayed permanently empty. Dark aircraft still reach the server + through the ground-truth push, where the object id is the intended key + (build_ground_truth_payload above). + """ + payload_aircraft = [] + for ac in aircraft_summaries: + hex_code = ac.get("adsb_hex") or "" + if not hex_code: + continue + speed_ms = ac.get("speed_ms", 0) + payload_aircraft.append( + { + "hex": hex_code, + "flight": "", + "lat": round(ac["lat"], 5), + "lon": round(ac["lon"], 5), + "alt_baro": round(ac["alt_km"] * 1000 / 0.3048), + "gs": round(speed_ms * 1.94384, 1), + "track": round(ac.get("heading", 0), 1), + } + ) + return payload_aircraft + + async def _push_ground_truth_live( orchestrator: FleetOrchestrator, base_url: str, @@ -971,25 +1002,7 @@ async def _push_adsb_live( try: aircraft_raw = orchestrator.world.get_aircraft_summary() - payload_aircraft = [] - for ac in aircraft_raw: - # Push ALL aircraft — ADS-B and dark/drone/anomalous alike. - # ADS-B aircraft use their transponder hex; others use object_id. - hex_code = ac.get("adsb_hex") or ac.get("id", "") - if not hex_code: - continue - speed_ms = ac.get("speed_ms", 0) - payload_aircraft.append( - { - "hex": hex_code, - "flight": "", - "lat": round(ac["lat"], 5), - "lon": round(ac["lon"], 5), - "alt_baro": round(ac["alt_km"] * 1000 / 0.3048), - "gs": round(speed_ms * 1.94384, 1), - "track": round(ac.get("heading", 0), 1), - } - ) + payload_aircraft = build_adsb_push_payload(aircraft_raw) if not payload_aircraft: continue diff --git a/tests/test_adsb_push_payload.py b/tests/test_adsb_push_payload.py new file mode 100644 index 0000000..2c081f4 --- /dev/null +++ b/tests/test_adsb_push_payload.py @@ -0,0 +1,56 @@ +"""ADS-B push payload schema (build_adsb_push_payload). + +The push is the simulated ADS-B broadcast, so it must carry transponder +aircraft only. The old inline loop substituted the object id for a missing +hex, which minted a fake transponder per dark aircraft on the server — every +dark solve then keyed mn-adsb-* and the server's dark lane stayed empty. +""" + +from retina_simulation.orchestrator import build_adsb_push_payload + + +def _summary(**overrides) -> dict: + base = { + "id": "obj-0001", + "lat": 34.85, + "lon": -82.4, + "alt_km": 9.5, + "heading": 270.0, + "speed_ms": 230.0, + "has_adsb": True, + "is_anomalous": False, + "object_type": "aircraft", + "adsb_hex": "a1b2c3", + "adsb_callsign": "ABC1234", + "anomaly_event": None, + } + base.update(overrides) + return base + + +class TestBuildAdsbPushPayload: + def test_transponder_aircraft_pushed_under_its_hex(self): + out = build_adsb_push_payload([_summary()]) + assert len(out) == 1 + entry = out[0] + assert entry["hex"] == "a1b2c3" + assert entry["lat"] == 34.85 + assert entry["alt_baro"] == round(9500 / 0.3048) + assert entry["gs"] == round(230.0 * 1.94384, 1) + assert entry["track"] == 270.0 + + def test_dark_aircraft_not_pushed(self): + """The regression: an aircraft with no adsb_hex must not appear at + all — above all not under its object id.""" + out = build_adsb_push_payload([_summary(has_adsb=False, adsb_hex=None, adsb_callsign=None)]) + assert out == [] + + def test_mixed_fleet_keeps_only_transponder_aircraft(self): + out = build_adsb_push_payload( + [ + _summary(), + _summary(id="obj-0002", has_adsb=False, adsb_hex=None), + _summary(id="obj-0003", adsb_hex="d4e5f6"), + ] + ) + assert [e["hex"] for e in out] == ["a1b2c3", "d4e5f6"]