Files
thermostat-hw/TODO.md
T
mattandClaude ea1b8b8d15 Two indoor sensors, wind compass, and stop framebuffer cache thrashing
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>
2026-08-07 19:58:41 -07:00

6.3 KiB

TODO

Thermostat: next steps

  1. Fit the relay board. Wiring is in README "UART1-OUT plug". Until then the GPIOs are the only output — meter them (idle reads 3.3 V, energized 0 V) to confirm the control loop end to end.
  2. Local temperature sensor. The single biggest weakness: indoor temp comes from HA over the network, so an HA outage eventually forces heat off. An SHT30/BME280 on the I2C bus fixes it. Mount it away from the panel, which self-heats by several degrees.
  3. Persist the setpoint. Resets to 68 °F on every boot. NVS is not an option while the panel scans (cache fault); an HA input_number is.
  4. Watchdog. If the firmware hangs mid-call the relay stays latched until someone notices. The furnace's own high limit makes that uncomfortable rather than dangerous, but the task watchdog should reboot into the safe state.

Not yet verified on hardware

Flashed and booting clean (two-phase display init, icons on, first poll ~9s, no errors), but eyes and fingers haven't confirmed:

  1. Slider feel: press sends one command at the touch position immediately, drag is display-only tracking the finger, lift sends the final level. Poll data never moves a pressed knob.
  2. Trend arrows appear ~5 minutes after boot (indoor big card, outdoor and pressure cells) once the local ring has history. They are local-history based and reset on reboot — that is by design, not a bug.
  3. Artefact lifetime. Two "repaint to clean up" mitigations were tried and both made it worse -- see README Gotchas. They are gone, the 1Hz restyle that made the artefacts permanent is fixed, and the bounce buffer is up to 20 scanlines. If lines still come and go, the remaining honest levers are fewer redraws still, less WiFi interrupt load during rendering, or 120MHz PSRAM on a newer IDF.

Done

The six items from the 2026-08-03 review were completed in d4b2ab7 ("Work through the remaining review items in TODO.md"). See that commit message for the reasoning behind each. Summary:

  1. UI pause window shrunk — HTTP session kept alive between polls, timeout drops 30s→10s once warm. Measured on hardware: fetch went from ~1.3s to ~80ms, so lvgl_port_pause() no longer blocks rendering and touch for a meaningful period.
  2. Stale readings indicated — temperature dims and the status line shows age after two consecutive failures.
  3. Touch readout gated behind UI_DEBUG_TOUCH, default off.
  4. main/utils deleted — no callers.
  5. OTA partitions laid out — two 4MB slots, otadata, ~7.9MB unallocated.
  6. Cleanups — truncation logged once per response; weather task stack high-water mark measured (~2400 bytes spare of 6144).

Verified on hardware after the change: boots from the new OTA layout, three consecutive successful polls at 60s, no reboots.

Still open

1. Confirm the left-edge artefacts are actually gone

The pause window is now ~80ms instead of ~1.3s, which should make the artefacts much rarer — but this has not been confirmed by eye. The timing improvement was measured from the log; the artefacts can only be verified by watching the panel around the moment the status line updates.

If they persist, the remaining lever is pausing only around LVGL's flush (the actual PSRAM writes) rather than the whole network transaction. That is more invasive; do not start it until the current change has been observed to be insufficient.

2. OTA implementation

Partition layout exists; the download/apply logic does not. This is not a drop-in. An OTA write is a flash write, and the bounce-buffer refill ISR reads PSRAM, which shares the cache with flash on the S3 — downloading while the panel scans will fault it with "Cache disabled but cached memory region accessed". The implementation must stop the panel for the download and write, or reboot into a dedicated updater mode. See README "Partitions".

3. Backlight control (optional feature)

backlight_set() is only ever called once, with 255. A night-dim schedule or touch-to-wake is a natural small feature. Note the STC8H1K28 shares the I2C bus with the GT911, so brightness writes must stay in task context (never an ISR) and will contend with touch polls on the same bus.

4. Left-edge artefacts, still open

Three things have been tried. Two "repaint to clean up" mitigations made it worse and are gone (README Gotchas). The 1Hz restyle that made them permanent is fixed. Now: bounce buffer at 20 scanlines and bb_invalidate_cache on, so framebuffer streaming no longer thrashes the DCache.

If they persist after that, the mechanism is the raw PSRAM ceiling and the honest remaining levers are unattractive: 120MHz PSRAM (needs a newer IDF and a DC-aware bootloader), or a lower pixel clock (below ~15MHz the panel stops locking and the image rolls). Do not reach for another repaint.

5. The actual thermostat

Setpoint, HVAC relay control, scheduling and persistence are all still unimplemented — this remains a weather display. Note a separate project (~/workspace/rotaryThermo) has a PRD for building this on the CrowPanel 2.1" rotary display instead, driven by Zigbee room sensors. Decide which board hosts the thermostat before building it twice.

Facts already established — do not re-investigate

  • Build is clean on ESP-IDF v5.3 (. ~/esp/esp-idf/export.sh && idf.py build). App is ~1.4MB in a 4MB OTA slot.
  • main/app_config.h is gitignored and holds real credentials locally.
  • resp_buf bounds/NUL-termination in ambient_weather.c is correct.
  • GT911 read-then-clear-status ordering is correct.
  • Backlight duty inversion maps correctly at both endpoints (255→0x00 full, 0→0xF5 off).
  • The ° glyph exists in LVGL's built-in Montserrat fonts; the · separator that does not appears only in log strings, never on screen.
  • LVGL recursive-mutex locking is consistent across all UI entry points.
  • Init order in main.c is deliberate and load-bearing: display_alloc() before WiFi (contiguous bounce buffers from a virgin heap), WiFi/NVS before display_start() (flash writes finished before scan-out), backlight_init() before touch_init(). Never reorder.
  • The bounce buffer is enabled and load-bearing.
  • Board is N16R8 (16MB flash / 8MB octal PSRAM).
  • touch_get_indev(), display_get_panel() and lvgl_port_is_paused() are uncalled but deliberately kept as driver API surface.