Skip to content

improving fetch stability and speedup rendering - #104

Open
lmoiseichuk wants to merge 12 commits into
MatixYo:mainfrom
lmoiseichuk:main
Open

lmoiseichuk wants to merge 12 commits into
MatixYo:mainfrom
lmoiseichuk:main

Conversation

@lmoiseichuk

Copy link
Copy Markdown

ADS-B fetch: one connect attempt again, a stack sized to its use, and no per-frame airport maths

Fixes #…

1. Make one connect attempt per ADS-B fetch again

#94 replaced the retrying performGetWithPoll() with a single attempt and a 5 s
connect timeout. #90's branch took main in at dc969f6 and resolved the conflict
by keeping performGetWithPoll(), and #90 brought it back to main at 78958a5.
It retries http.GET() every 5 ms until a 6 s deadline whenever the connect is
refused -- and a TLS setup that cannot get its buffers (mbedtls_ssl_setup,
-32512) comes back as "connection refused".

before after
TLS setup attempts per fetch 162-177, every one failing 1
failures while fetching ~925 a minute one per fetch interval
TCP connects to opendata.adsb.fi while failing ~28 a second 1 per fetch
outcome HTTP -11 after the 6 s deadline returns at once; the next fetch retries

Latent on the 240x240 board; it fires whenever a connect is refused or TLS
cannot get heap. Seen on a fork driving a 360x360 panel, where it failed every
fetch.

2. Size the ADS-B fetch task's stack from its measured use

worst stack use, measured over successful fetches (full TLS handshake, chunked body, filtered parse) 8,720 B
task stack 16,384 -> 12,288 B (3,568 B, 29 %, spare)
heap returned 4,096 B

For scale: Arduino-ESP32 2.0.14's mbedTLS (MAX_CONTENT_LEN=16384) allocates two
16,717 B record buffers per connection, and a whole fetch peaks at ~52 KB.

3. Skip measuring airports outside the square around the range

drawLargeAirportRunways measured every runway's airport on every frame -- a
cosf and a sqrtf for each of 1,706 runways, in software floating point on
the ESP32-C3: 1,701 distances a frame for the one airport in range at the
default location. Now the square around the range is worked out once per frame
in the table's 1e-7 degree units; an airport outside it is skipped with integer
comparisons, and the few inside are measured by the unchanged code.

measured on the board before after
runway overlay per frame, 25 km range 71 ms 17 ms
runway overlay per frame, 10 km range 82 ms 28 ms
full frame (360x360) 233 ms 178 ms
distances computed per frame 1,701 1

Same airports in range as before in 35 cases (five locations, both sides of the
180th meridian included, at the preset fetch radii and 100/200/700 km) and in
478 more with an airport a millionth of the radius inside or outside the
circle's edge. +21 lines, no existing line changed.

Tested

ESP32-C3 SuperMini, espressif32@6.5.0 / Arduino-ESP32 2.0.14. Each commit applies
cleanly to main 139a3ac and builds supermini. Board measurements were taken on a
360x360 build (a fork), since no 240x240 board was at hand; the airport and stack
changes don't depend on the panel. On that fork, with these commits, the fetch
ran 88 of 88 times over ten minutes with 0 TLS failures, and free heap levelled
off.

lmoiseichuk and others added 12 commits September 6, 2026 16:25
Every length in radar_theme.h was a constexpr tuned for a 240x240 panel.
They are now variables that initMetrics() computes from tft.width() and
tft.height(), scaled against a 240 reference, so a different panel gets
the same design rather than a second set of hand-tuned constants.

Values that are not lengths -- the ring count, the track horizon in
seconds, the reference range in km, plain ratios -- stay constexpr.

The k-prefixed names and their storage in the header keep the shape the
colours in this file have always had, so no call site changes.

status_screens.cpp gets the same treatment: centre, spinner radius, text
panel width, line gaps and VLW point sizes all scale.

No behaviour change on the 240x240 build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selected with -DPLANE_RADAR_PANEL_GC9B72, env supermini_gc9b72. The
default supermini env and the GC9A01 wiring are untouched.

LovyanGFX gained Panel_GC9B72 in 1.2.28, so the lib_deps floor moves up
from ^1.2.7. The panel needs a backlight line, which the 1.28" modules
tie on at the module: it sits on GPIO5 through Light_PWM so it can be
dimmed if the 3V3 rail sags under WiFi transmit -- a 2.1" backlight draws
several times what the 1.28" one does. LEDC channel 0, because the C3 has
only six channels and the 7 used in most LovyanGFX examples is out of
range on it.

The 360x360 frame buffer is 259,200 B at 16 bits, which does not fit on
an ESP32-C3 once WiFi is up -- measured, 188,404 B largest free block.
The frame sprite now walks a depth ladder and lands on 8-bit RGB332 at
129,600 B. There is deliberately no 4bpp rung: LovyanGFX has no
RGB-to-palette-index converter, so a blended write to a palette sprite
goes through copy_bit_affine and corrupts every antialiased primitive we
draw, fillSmoothCircle and drawWideLine among them. RGB332 keeps them.

Unlike the GC9A01 modules this panel must not be inverted; its init
sequence does not set inversion itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were hard-coded off. They are now per-panel pins in config.h,
defaulting to -1 on the GC9A01 where neither is broken out.

kDisplayPinMiso feeds LovyanGFX's bus config. It makes the panel
readable, which the antialiased primitives need on the direct-to-panel
path taken when no frame sprite fits, and it silences the "SPI Does not
have default pins on ESP32C3" error that -1 provoked at every boot.

kDisplayPinTe is polled by displayWaitForFrameStart() before each
pushSprite. No SPI panel in LovyanGFX consumes TE -- getScanLine()
returns -1 for all of them -- so the edge is caught here instead. A full
360x360 frame is 259,200 B on the wire, roughly 104 ms at 20 MHz, which
is long enough to tear without it. The wait gives up after 25 ms so a
miswired line cannot stall the main loop.

Also drops the ESP32-C6 material from the README. It was documenting
hardware that is not supported yet and needs a PlatformIO platform this
project does not use; the wiring table is now the one board that works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in upstream's ADS-B pipeline, dead reckoning at 4 Hz, the TLS fix, the
portal re-render and the RGB332 frame buffer.

One conflict, in ensureFrameSprite(): this branch tried 16bpp first and fell
back to 8bpp; upstream (MatixYo#100) now always uses RGB332 to free the frame's 57 KB
at 240x240. Resolved in upstream's favour -- one byte a pixel on every panel,
which at 360x360 is 129,600 B instead of 259,200 -- keeping this branch's
fallback to drawing straight to the panel when even that does not fit, and its
note on why no palette depth is tried.

Both supermini and supermini_gc9b72 build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
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
The fetch task was given 16384 B of stack. Its high-water mark after
successful fetches on an ESP32-C3 left 7,648 B of that unused, so a fetch
-- TLS handshake, chunked body, filtered parse -- peaks at about 8.7 KB.
A fetch whose connect fails peaks lower, at about 7.8 KB.

The stack is allocated from the same heap the fetch's TLS connection
needs for its two 16,717 B record buffers, so 7.6 KB of slack there is
7.6 KB less for TLS. Give the task 12288 B: about 3.5 KB over the
measured peak, and 4 KB back to the heap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
drawLargeAirportRunways measured the distance to each runway's airport
on every frame: a cosf and a sqrtf per runway, for 1,706 runways, all
soft-float on the ESP32-C3. At the default location that is 1,701
distances a frame for the one airport in range, and on a 360x360 panel
it took 71 ms of a 233 ms frame.

Work out the square around the range once per frame, in the airport
table's own units of 1e-7 degree, and compare each airport with it
before measuring: an airport outside the square is out of range, and
integer comparisons say so. Only the few inside it are measured, by the
same code as before, so the same airports are drawn. A margin of 0.001
degree keeps rounding from turning away an airport the distance test
would take.

Measured on the board: the runway overlay takes 17 ms instead of 71 at
the 25 km range and 28 ms instead of 82 at 10 km, and a frame 178 ms
instead of 233; one airport passes the square, and is measured once a
frame. The airports in range are the same as before in 35 cases -- five
locations, both sides of the 180th meridian among them, at the four
preset fetch radii and at 100, 200 and 700 km -- and in 478 more with an
airport a millionth of the radius inside or outside the circle's north,
south, east or west point. Without the margin, 81 of those 478 would
have been turned away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
On a 360x360 panel the frame sprite is 129,600 B even at 8bpp. With it,
WiFi, the LAN config portal and the fetch task, about 48 KB of heap was
free before each ADS-B fetch, in no block over 29 KB. The TLS connection
allocates two 16,717 B record buffers and peaks at about 52 KB, so every
fetch failed in mbedtls_ssl_setup with -32512 and no aircraft were shown.

Cap the frame sprite at what the 240x240 panel's full frame takes,
57,600 B. A panel whose frame is larger is composited in the fewest equal
horizontal bands that fit: three of 120 rows, 43,200 B, at 360x360. Each
band has the whole radar drawn into it -- grid, runways, aircraft and
labels, clipped to the band -- and is pushed complete, so every pixel
still goes straight from the old frame to the new one and nothing
flickers. The aircraft are placed once per frame, so one that straddles
two bands is drawn at the same point in both. Rendered both ways on the
board, a banded frame matched a full one on all 129,600 pixels, with
aircraft, tags and dots across both seams.

The 240x240 panel's frame fits the cap, so it keeps its full-frame
sprite and draws exactly as before, as one band.

The placement is static, which costs 1,792 B of RAM on both panels. It
has to outlive every band's grid drawing, and held on the loop task's
stack it cut that task's spare stack from 5,900 B to 2,972 B.

On the 360x360 board, all 88 fetches in ten minutes succeeded, in
1.6-1.9 s each. Before each: 131-134 KB free, largest block 69,620 B
(144 KB and 122,868 B before the first). The free heap fell by about
3 KB over the first two minutes and then held at 131.1 KB for the other
eight; the lowest it reached was 77,460 B.

Banding costs render time. With no aircraft and the fetch idle, a banded
frame takes 210 ms, where the full frame took 178 ms at the parent
commit -- also with none, as no fetch succeeded there. Aircraft are
drawn into every band: 26-29 ms for three, about 35 ms for four. Between
fetches a frame stays under 246 ms, inside the 250 ms redraw interval,
so the radar still redraws at about 4 Hz. While a fetch runs, its TLS
handshake and parse share the ESP32-C3's one core with the render, and
a frame takes 300-380 ms on average and up to 460 ms. Over two-minute
runs the radar drew 3.5-3.6 frames a second.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
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
The fetch task was given 16384 B of stack. Its high-water mark after
successful fetches on an ESP32-C3 left 7,648 B of that unused, so a fetch
-- TLS handshake, chunked body, filtered parse -- peaks at about 8.7 KB.
A fetch whose connect fails peaks lower, at about 7.8 KB.

The stack is allocated from the same heap the fetch's TLS connection
needs for its two 16,717 B record buffers, so 7.6 KB of slack there is
7.6 KB less for TLS. Give the task 12288 B: about 3.5 KB over the
measured peak, and 4 KB back to the heap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
drawLargeAirportRunways measured the distance to each runway's airport
on every frame: a cosf and a sqrtf per runway, for 1,706 runways, all
soft-float on the ESP32-C3. At the default location that is 1,701
distances a frame for the one airport in range, and on a 360x360 panel
it took 71 ms of a 233 ms frame.

Work out the square around the range once per frame, in the airport
table's own units of 1e-7 degree, and compare each airport with it
before measuring: an airport outside the square is out of range, and
integer comparisons say so. Only the few inside it are measured, by the
same code as before, so the same airports are drawn. A margin of 0.001
degree keeps rounding from turning away an airport the distance test
would take.

Measured on the board: the runway overlay takes 17 ms instead of 71 at
the 25 km range and 28 ms instead of 82 at 10 km, and a frame 178 ms
instead of 233; one airport passes the square, and is measured once a
frame. The airports in range are the same as before in 35 cases -- five
locations, both sides of the 180th meridian among them, at the four
preset fetch radii and at 100, 200 and 700 km -- and in 478 more with an
airport a millionth of the radius inside or outside the circle's north,
south, east or west point. Without the margin, 81 of those 478 would
have been turned away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL3fMjXoiVk9VGMHmQkfyW
The three ADS-B fixes as they go upstream -- one connect attempt per
fetch, the fetch task's stack sized from its use, and the square gate in
front of the airport distance test -- rebuilt on upstream/main. This
main already had the same patches on top of the GC9B72 work, so the
merge leaves the tree unchanged and rewrites neither history.

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.

1 participant