Add network ADS-B adapter runner + fusion wiring (M3.2b)#15
Conversation
Wire the M3.2a network ADS-B provider into a running adapter and fuse its records with local ADS-B — the M3 exit criterion (PRD §32): an aircraft seen by both the local radio and an Internet feed appears as one fused track with correct provenance. - network_adsb.py: tiled AOI poll loop (tile_region + provider.fetch_region), polite inter-tile rate limit, cross-tile dedupe_observations, network TrackRecords with local_rf=False under the shared aircraft:icao:<hex> identity. Per-tile failure isolated (PartialSweep -> degraded); whole-sweep failure yields degraded + jittered backoff so last good tracks age out via fusion freshness. build_provider selects adsb.fi (live) or the fake feeder. - network_adsb_fake_feeder.py: in-process FakeAircraftProvider for the no-hardware path; roster overlaps local identities so fusion is demonstrable. - config.py: AETHER_NETWORK_ADSB toggle + provider / AOI center / radius / poll / rate-limit / timeout. AOI center defaults to null-island 0,0 — no station coordinates in the repo; the operator supplies their own. - main.py: run_network_adsb wired into the lifespan behind cfg.network_adsb. - Tests: 8 unit (tiled sweep, dedupe, per-tile + whole-sweep failure isolation, provider selection, fake roster) + 2 integration (local+network fuse into one track; network-only stays a network track), green against a real broker. - README: documented the no-hardware network + fusion demo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a network ADS-B adapter and an in-process fake provider to support Internet fusion of aircraft tracks without requiring live hardware or APIs. It integrates this adapter into the backend lifespan and configuration, updates the README, and adds integration and unit tests. The review feedback suggests tracking last_record_at cumulatively across sweeps, logging a warning when the live provider is enabled with default Null Island coordinates, and enforcing minimum politeness limits for the live production feed to prevent aggressive polling.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| received = 0 | ||
| backoff = INITIAL_BACKOFF_S |
There was a problem hiding this comment.
Initialize last_record_at outside the while True: loop so that it is tracked cumulatively across sweeps, matching the behavior of received. Currently, it is reset to None on every sweep, which causes the status record to report last_record_at=None whenever a sweep has zero visible aircraft, even if records were successfully received in previous sweeps.
received = 0
last_record_at: datetime | None = None
backoff = INITIAL_BACKOFF_S| deduped = dedupe_observations(observations) | ||
| last_record_at: datetime | None = None |
There was a problem hiding this comment.
| prov = provider if provider is not None else build_provider(cfg) | ||
| aoi = aoi_from_settings(cfg) |
There was a problem hiding this comment.
Log a warning if the network ADS-B adapter is enabled with a live provider but the AOI center is left at the default Null Island (0.0, 0.0). This is almost certainly a configuration mistake by the operator, and logging a warning helps them troubleshoot why no aircraft are appearing on the map.
| prov = provider if provider is not None else build_provider(cfg) | |
| aoi = aoi_from_settings(cfg) | |
| prov = provider if provider is not None else build_provider(cfg) | |
| aoi = aoi_from_settings(cfg) | |
| if aoi.center_lat == 0.0 and aoi.center_lon == 0.0 and prov.name != "fake": | |
| log.warning( | |
| "Network ADS-B is enabled but AOI center is at Null Island (0.0, 0.0). " | |
| "Please configure AETHER_NETWORK_ADSB_LAT and AETHER_NETWORK_ADSB_LON." | |
| ) |
| resolved_poll = poll_s if poll_s is not None else cfg.network_adsb_poll_s | ||
| resolved_rate = rate_limit_s if rate_limit_s is not None else cfg.network_adsb_rate_limit_s |
There was a problem hiding this comment.
Enforce minimum politeness limits for the live production feed (adsb.fi) to prevent accidental aggressive polling or DDoS due to misconfiguration.
resolved_poll = poll_s if poll_s is not None else cfg.network_adsb_poll_s
resolved_rate = rate_limit_s if rate_limit_s is not None else cfg.network_adsb_rate_limit_s
if prov.name == "adsb.fi":
# Enforce minimum politeness limits for the live production feed
resolved_poll = max(resolved_poll, 5.0)
resolved_rate = max(resolved_rate, 1.0)
Summary
Wires the M3.2a network ADS-B provider into a running adapter and fuses its records with local ADS-B — closing the M3 exit criterion (PRD §32): an aircraft seen by both the local radio and an Internet feed appears as one fused track with correct provenance.
network_adsb.py— tiled AOI poll loop (tile_region+provider.fetch_region), polite inter-tile rate limit, cross-tilededupe_observations, networkTrackRecords withlocal_rf=Falseunder the sharedaircraft:icao:<hex>identity. Per-tile failure is isolated (PartialSweep→ degraded); a whole-sweep failure yields degraded + jittered backoff so last-good tracks age out via fusion freshness.build_providerselectsadsb.fi(live) or the fake feeder.network_adsb_fake_feeder.py— in-processFakeAircraftProviderfor the no-hardware path; roster overlaps local identities so fusion is demonstrable end to end.config.py—AETHER_NETWORK_ADSBtoggle + provider / AOI center / radius / poll / rate-limit / timeout. AOI center defaults to null-island0,0— no station coordinates in the repo; the operator supplies their own.main.py—run_network_adsbwired into the lifespan behindcfg.network_adsb, mirroringrun_local_adsb.Test plan
a1b2c3fuse into one track (locally_received: true, two provenance entrieslocal_rfTrue+False,fusion.fused_count == 2, exactly one/api/stateentry); network-onlycafe01stays a single network track.🤖 Generated with Claude Code