Skip to content

ADSB background fetch with http/1.1 - #90

Merged
MatixYo merged 5 commits into
MatixYo:mainfrom
pvanbaren:feature/adsb-pipeline
Sep 29, 2026
Merged

MatixYo merged 5 commits into
MatixYo:mainfrom
pvanbaren:feature/adsb-pipeline

Conversation

@pvanbaren

Copy link
Copy Markdown
Contributor

This pull request moves the ADSB fetch over to a background thread using http/1.1 chunked transfer, streaming+filtering the JSON, and updating the display at 4Hz using dead-reckoning between ADSB updates.

  • background thread + dead reckoning make the UI smoother and more responsive
  • http/1.1 chunked transfer fixes the issue which required forcing http/1.0 transfer mode
  • streaming the JSON data in avoids double-allocation of memory
  • filtering the JSON data to only the desired fields also reduces memory requirements

pvanbaren and others added 4 commits September 10, 2026 21:41
Aircraft positions only moved on each ~3 s ADS-B fetch, so motion was steppy.
Now extrapolate each aircraft from its last fetched fix along its ground track
(track_deg) at its ground speed (gs_knots) using elapsed time, and redraw at
4 Hz between fetches.

- adsb_client: record lastUpdateMs() at each successful fetch (the time the
  stored positions are valid).
- radar_display: extrapolatedLatLon() dead-reckons position using the same flat
  111 km/deg projection as the drawing path (round-trips exactly); drawAircraft
  uses it for symbols, tags, and beyond-ring dots.
- main loop: redraw every kRadarRedrawIntervalMs (250 ms) between fetches. The
  fetch interval goes 3 s -> 5 s, since smooth motion no longer depends on how
  often the data is refreshed.

Extrapolation also accounts for the age of the fix itself. adsb.fi reports
seen_pos: seconds since each aircraft's position was measured (often 10-30 s
old). Starting from the fetch time drew a stale fix lagging its true position,
so seen_pos is stored per aircraft (pos_age_ms, capped at 30 s) and added to
the elapsed time.

Positions snap to real data on each fetch. Motion pauses briefly during the
blocking fetch itself. Applies to both targets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fetch blocked the main loop for ~1-2 s every 3 s, pausing the 2 Hz
dead-reckoned redraw. Move it to a FreeRTOS task pinned to core 0 so the render
loop (core 1) keeps refreshing smoothly throughout the request.

- adsb_client: init() creates a mutex; fetchUpdate() parses into a local buffer
  and publish()es atomically under the lock; snapshotAircraft() copies the list
  + base timestamp under the lock for readers on another thread.
- radar_display: drawAircraft() renders from a locked snapshot; extrapolation
  takes the base time as a parameter.
- main: adsbFetchTask loops fetchUpdate every kAdsbFetchIntervalMs; the loop
  just redraws at 2 Hz. No more inline fetch / poll-fn.

Applies to both targets (on the single-core C3 the scheduler interleaves the
task with the loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fetch buffered the whole response body into a String and then handed
that to ArduinoJson, so peak heap held the payload and the document at the
same time -- and the document stored all ~40 fields of every aircraft
record when the radar reads only 15 of them.

Replace readResponseBodyWithPoll with PollingBodyReader, which ArduinoJson
pulls from directly. It drains the socket in 512-byte blocks and serves
single bytes, since the JSON deserializer never calls readBytes, and it
runs the network poll callback on every refill -- the reason HTTPClients
e2e94c6 forced HTTP/1.0 because the response body was arriving chunked and
the JSON parser choked on the chunk-size lines. The cause is that
getStreamPtr() hands back the raw socket -- HTTPClient de-chunks only inside
its own body readers, which this code can't use because they block without
running the network poll callback.

Decode the framing here instead. BodyFramer unwraps an identity or chunked
body from a byte source, stripping chunk sizes, extensions, the inter-chunk
CRLFs and any trailer section. It pulls a byte at a time, so a chunk boundary
landing anywhere in the socket's own buffering -- mid-hex-digit, or between
the CR and the LF -- needs no special handling. PollingBodyReader loses its
length bookkeeping to the framer and becomes PollingSocketSource, a plain
pump that keeps the poll-callback refill loop.

Transfer-Encoding has to be requested via collectHeaders() before the GET;
HTTPClient's own _transferEncoding is private.

Dropping useHTTP10 also re-enables keep-alive on the request, but nothing is
reused yet -- the client and HTTPClient are still torn down every fetch. The
post-parse drain() is groundwork for that: a parser stops at the closing
brace, and a connection reused without consuming the terminating chunk would
read it as the head of the next response.

The header is free of Arduino includes so the framing can be tested on a
host. Compiles clean on both arduino-esp32 2.x and 3.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MatixYo

MatixYo commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Looks good, could you resolve conflicts? @pvanbaren

@pvanbaren

Copy link
Copy Markdown
Contributor Author

I've resolved the conflicts. I kept the timeout (which fixed the retry storm) and filter changes from the main branch, otherwise merged in my changes to fetch on a background thread.

@MatixYo
MatixYo merged commit 78958a5 into MatixYo:main Sep 29, 2026
2 checks passed
lmoiseichuk added a commit to lmoiseichuk/ESP32-Plane-Radar that referenced this pull request Oct 8, 2026
MatixYo#94 (e23b82c) stopped a TLS retry storm by replacing performGetWithPoll
with a single GET and a 5000 ms connect timeout. When main was merged
into MatixYo#90's branch (dc969f6, "Merge branch 'main' into
feature/adsb-pipeline"), the conflict was resolved by keeping
performGetWithPoll, and MatixYo#90 took it back to main (78958a5). It calls
GET() again every 5 ms until a 6000 ms deadline whenever the result is
CONNECTION_REFUSED or NOT_CONNECTED.

A TLS setup that cannot allocate its record buffers comes back that way.
mbedtls_ssl_setup fails with -32512 (SSL - Memory allocation failed),
WiFiClientSecure::connect returns 0, and HTTPClient reports a refused
connection. Nothing frees heap between attempts, so the same allocation
fails every 5 ms until the deadline. The storm fires whenever the TLS
setup cannot get its heap, or a connect is refused. On the 240x240 board
the heap is there and it stays latent; it was seen on a fork driving a
360x360 panel, whose larger frame buffer leaves less heap. There each
fetch repeated the failed allocation about 170 times, gave up after 6 s
with "adsb: HTTP -11", and logged about 900 allocation failures a
minute.

Go back to MatixYo#94's behaviour: one GET with the 5000 ms connect timeout. A
failed connect is retried by the next fetch, kAdsbFetchIntervalMs later.
The background task, streaming parse and chunked framing from MatixYo#90 are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
lmoiseichuk added a commit to lmoiseichuk/ESP32-Plane-Radar that referenced this pull request Oct 8, 2026
MatixYo#94 (e23b82c) stopped a TLS retry storm by replacing performGetWithPoll
with a single GET and a 5000 ms connect timeout. When main was merged
into MatixYo#90's branch (dc969f6, "Merge branch 'main' into
feature/adsb-pipeline"), the conflict was resolved by keeping
performGetWithPoll, and MatixYo#90 took it back to main (78958a5). It calls
GET() again every 5 ms until a 6000 ms deadline whenever the result is
CONNECTION_REFUSED or NOT_CONNECTED.

A TLS setup that cannot allocate its record buffers comes back that way.
mbedtls_ssl_setup fails with -32512 (SSL - Memory allocation failed),
WiFiClientSecure::connect returns 0, and HTTPClient reports a refused
connection. Nothing frees heap between attempts, so the same allocation
fails every 5 ms until the deadline. The storm fires whenever the TLS
setup cannot get its heap, or a connect is refused. On the 240x240 board
the heap is there and it stays latent; it was seen on a fork driving a
360x360 panel, whose larger frame buffer leaves less heap. There each
fetch repeated the failed allocation about 170 times, gave up after 6 s
with "adsb: HTTP -11", and logged about 900 allocation failures a
minute.

Go back to MatixYo#94's behaviour: one GET with the 5000 ms connect timeout. A
failed connect is retried by the next fetch, kAdsbFetchIntervalMs later.
The background task, streaming parse and chunked framing from MatixYo#90 are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
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