Downstairs and upstairs are now separate readings. The station names them
backwards -- "temperature_4" is downstairs and "inside_temperature" is
upstairs -- so ha_config.h says which is which. Downstairs is the primary:
big number on the card, and it is what the furnace controls on, because it
is where people are and heat rises. Upstairs sits beside it, obviously
subordinate by size, and costs no vertical space in a layout that has none.
The anemometer came back on 2026-08-07, so wind gets its own column: a dial
whose needle points at the direction the wind is coming FROM (weather-vane
convention, matching the cardinal below it), the speed, and a 5-minute trend
arrow.
- The needle does not animate. A spin would look better, but every animated
frame is a redraw, and redrawing is what starves the bounce refill ISR.
- Below 0.5 mph it shows "Calm" and draws no needle. A vane that is not
being pushed reports whatever it last settled on, which on this station
is a steady 0 degrees -- a confident needle pointing due north at zero
wind. The live snapshot caught exactly that.
- The trend is local, not HA's 10-minute average, so the window is ours to
pick: 1-minute buckets over 5 minutes, threshold 1.5 mph.
That last point needed the trend engine to stop assuming one hour. Window,
bucket size, minimum age and threshold are now per-channel in a table, which
also removes the threshold arguments main.c was passing in.
Grid drops to 117px cells to make room, so the sun sub-line is just which
event it is and "RAIN TODAY" is "RAIN" -- both were overrunning.
On the artefacts: bounce mode is NOT a DMA read of the framebuffer. The
refill is a CPU memcpy in an ISR, which is why the driver skips the usual
write-back in draw_bitmap ("the data gets read by the CPU as well"). So the
panel streams 750KB per frame at ~44Hz through the DCache, evicting the
working set of everything else on the chip, continuously -- which is the real
reason this board is so sensitive to WiFi and to any extra rendering.
bb_invalidate_cache frees each chunk after it is copied. The header warns it
"can be dangerous if data is written from other core(s)"; that does not apply
here, because it invalidates the buffer being scanned and by design LVGL only
draws into the other one. Whether it is enough is for the panel to say.
Also gives tools/ha_snapshot.py a --token-file so the token no longer has to
travel through a shell command line to reach it.
Co-Authored-By: Claude <noreply@anthropic.com>
Keep the HTTP session alive between weather polls (TODO #1). Re-creating the
client each time forced a fresh TLS handshake inside the window where the UI
is frozen. Measured on hardware: the fetch went from ~1.3s to ~80ms, so the
pause is no longer long enough to matter once thermostat controls exist. The
timeout drops from 30s to 10s once a session is warm, since only a cold
handshake needs the longer budget. Any transport error or non-200 tears the
session down so a half-open connection cannot poison every later poll.
The response buffer is now reset per request rather than on
HTTP_EVENT_ON_CONNECTED -- a kept-alive connection does not re-fire that
event, so the buffer would have accumulated across polls.
Indicate stale readings (TODO #2). After two consecutive failures the
temperature dims and the status line shows the age. Age comes from tick
counts, not weather_data_t.last_update: there is no SNTP client here, so
time(NULL) starts at the epoch and any age derived from it is meaningless.
Gate the touch readout behind UI_DEBUG_TOUCH, default off (TODO #3). Kept the
note about LVGL not bubbling events to parents -- it documents a real trap.
Delete main/utils (TODO #4); it had no callers outside itself. Also refreshed
project_structure.txt, which was additionally missing i2c_bus and backlight.
Lay out two 4MB OTA slots (TODO #5) while no devices are fielded, and document
in the README that an OTA download cannot run while the panel is scanning --
the flash write faults the bounce-buffer ISR.
Log the truncation warning once per response instead of once per chunk, and
record the weather task's stack high-water mark (TODO #6): ~2400 bytes spare
of 6144.
Co-Authored-By: Claude <noreply@anthropic.com>