Skip to content

fix: ADS-B response truncation and radar longitude projection - #69

Open
davidjconnolly wants to merge 2 commits into
MatixYo:mainfrom
davidjconnolly:fix/adsb-json-truncation
Open

fix: ADS-B response truncation and radar longitude projection#69
davidjconnolly wants to merge 2 commits into
MatixYo:mainfrom
davidjconnolly:fix/adsb-json-truncation

Conversation

@davidjconnolly

@davidjconnolly davidjconnolly commented Jul 31, 2026

Copy link
Copy Markdown

Two independent bugs found while bringing this up on an ESP32-C3 Super Mini with the 1.28" round GC9A01, plus the build fix needed to flash anything at all. Each is a separate commit.


1. ~90% of ADS-B fetches fail with a JSON parse error

adsb: JSON parse error: InvalidInput
adsb: JSON parse error: InvalidInput
adsb: JSON parse error: IncompleteInput

Root cause

fetchUpdate() buffered the whole response body into a String before parsing. Instrumenting the read loop on hardware:

DBG read: why=deadline ms=10001 read=21040 len=11112 concat_fail=1 heap=30788 maxblk=9204
DBG size=21040 cl=[21040] te=[] ce=[]
DBG head=[{"ac":[ {"hex":"c012ed","type":"adsb_icao","flight":"ACA1511 ",...
  • read=21040 — the socket delivers the full body and it starts as valid JSON. Not a transport, TLS, or API problem, and not chunked encoding (te=[]).
  • heap=30788 / maxblk=9204 — ~30 kB free heap, but the largest contiguous block is ~9 kB. payload.reserve(21041) needs one contiguous 21 kB allocation and fails.
  • concat_fail=1 — the following String::concat() calls fail, so the buffer tops out at 10–14 kB and deserializeJson() gets a truncated body.
  • why=deadline ms=10001 — a second bug falls out of the first: payload.length() can never reach content_length, so the loop never takes its completion branch and burns the full 10 s timeout on every fetch.

This is heap fragmentation, not payload size as such — there is enough free heap for 21 kB, just never in one piece. Responses small enough to fit still succeed, which is the surviving ~10%.

Fix

Parse directly off the response stream, one aircraft at a time, with a DeserializationOption::Filter restricting the document to the fields the radar renders. Peak memory drops from ~21 kB contiguous to a few hundred bytes. A small PollingStream preserves the existing network-poll-hook behaviour during blocking reads and bounds the transfer with one deadline. Parsing stops early once kMaxAircraft is reached.

Verification

Before — ~90% failure. After — 25 consecutive fetches, 0 parse errors, each completing at the real 3 s poll interval instead of stalling 10 s:

adsb: 13 aircraft
adsb: 13 aircraft
adsb: 12 aircraft

2. Aircraft vanish at the 5 km and 10 km presets

Everything looked frozen at short ranges, while the 25 km preset worked fine.

Root cause

offsetKmFromCenter() converts longitude degrees to km with a flat 111 km/° and no cos(latitude) correction, so every east-west offset is overstated — ~38% at 43°N.

On hardware at the 10 km preset, radar centred at 43.6485,-79.4795:

DBG draw: n=4 inside=0 dots=4 outer=13.3km max=11.7km
POE662  dx=-14.5 dist=14.6km -> (4,104)     true dx=-10.5, true dist=10.7km (INSIDE)
RPA4438 dx=+16.2 dist=16.8km -> (250,153)   x=250 is off a 240px panel
PTR2264 dx=+16.3 dist=16.3km -> (251,125)

Aircraft genuinely inside the ring measure as beyond it and are demoted from a full symbol + callsign/type/altitude tag to a 2 px rim dot — inside=0, i.e. every aircraft. That reads as "the planes disappeared". Projected x can also land off-panel entirely. The 25 km preset masks it because its 33 km outer radius is wide enough that even inflated distances still fall inside.

Fix

Scale the longitude delta by cos(centre latitude).

Verification

Same location and traffic, after the fix:

DBG draw: n=8 inside=7 dots=1 outer=20.0km max=17.6km

Aircraft that previously rendered as rim dots now project inside the ring, and off-panel x coordinates are gone.


3. Build fix (required to flash anything)

LovyanGFX resolves to 1.2.26 under @^1.2.7 and now exposes fonts at global scope, so the three local aliases collide:

src/ui/status_screens.cpp:14:34: error: 'namespace fonts = lgfx::v1::lgfx::v1::fonts;' conflicts with a previous declaration

Removed the three namespace fonts = lgfx::v1::fonts; aliases; fonts:: already resolves without them. Pinning the library version instead would also work if you'd prefer that — happy to switch.

🤖 Generated with Claude Code

davidjconnolly and others added 2 commits July 30, 2026 21:25
Roughly 90% of ADS-B fetches failed with "JSON parse error: InvalidInput"
on the ESP32-C3 Super Mini.

The response body was buffered whole into a String before parsing. A busy
sector returns ~21 kB, but on-device diagnostics showed only ~30 kB of free
heap with a largest contiguous block of ~9 kB, so the reserve() and the
subsequent concat() calls failed. The socket delivered all ~21 kB, yet the
String only ever held 10-14 kB. Because payload.length() could then never
reach content_length, the read loop also spun out its full 10 s timeout on
every fetch before handing the truncated body to the parser.

Parse straight off the response stream instead, one aircraft at a time,
with an ArduinoJson filter limiting the document to the fields the radar
renders. Peak memory drops from ~21 kB contiguous to a few hundred bytes,
so a fragmented heap no longer matters.

A PollingStream wrapper preserves the existing behaviour of servicing the
network poll hook during blocking reads and bounds the transfer with a
single deadline.

Verified on hardware: 25 consecutive fetches, 0 parse errors, each now
completing at the 3 s poll interval rather than stalling for 10 s.

Also drop the `namespace fonts = lgfx::v1::fonts;` aliases, which collide
with the namespace LovyanGFX 1.2.26 exposes at global scope and broke the
build before anything could be flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
offsetKmFromCenter() converted longitude degrees to km with a flat
111 km/°, applying no cos(latitude) correction. Away from the equator
that overstates every east-west offset — ~38% at 43°N.

Two visible consequences at short ranges:

* Aircraft well inside the ring were measured as beyond it and demoted
  from a full symbol + callsign/type/altitude tag to a 2 px rim dot. On
  hardware at the 10 km preset every aircraft collapsed this way
  (n=4 inside=0 dots=4), which reads as "the planes disappeared".
* Projected x could land off the 240 px panel entirely (x=250, x=251).

The 25 km preset masked it: its 33 km outer radius is wide enough that
even inflated distances still fall inside the ring.

Verified on an ESP32-C3 Super Mini at Toronto latitude: aircraft that
previously rendered as rim dots now project inside the ring
(n=8 inside=7 dots=1), and off-panel x coordinates are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@davidjconnolly davidjconnolly changed the title fix: stream-parse ADS-B response to survive heap fragmentation fix: ADS-B response truncation and radar longitude projection Jul 31, 2026
@Reid-n0rc

Copy link
Copy Markdown

Hardware & testing methodology: ESP32-C3 Super Mini (esp32-c3-devkitm-1, RISC-V, 320KB RAM), 1.28" round GC9A01 240×240 SPI display, USB-C native serial. Built with PlatformIO, espressif32@6.5.0 platform, Arduino core framework-arduinoespressif32@3.20014.231204, -std=gnu++17. Firmware image: ~18.8% RAM / ~41% flash used. Connected to a real WiFi AP with live internet, pulling real ADS-B traffic from opendata.adsb.fi (tested from both Kansas and NYC-area locations). Flashed via pio run -t upload, monitored over USB serial (115200 baud, read directly via pyserial) for boot logs, fetch cadence, and crash/reboot banners.

Results: merged cleanly, no conflicts. Ran 30+ minutes cumulative across sessions with steady adsb: N aircraft fetches at the configured ~3s cadence, no truncation errors, no crashes traceable to this PR. Note: this PR's streaming JSON parser and cos(lat) projection fix are also a superset of #50, #53, #58, and #60 (same underlying fixes, different implementations) — I merged only this one and treated the other four as superseded. Solid, recommend merging as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants