The artefacts went from occasional to continuous, and the cause was my own
mitigation for them.
lv_obj_set_local_style_prop() does NOT skip identical values -- it calls
lv_obj_refresh_style() unconditionally, which invalidates the object. A
comment in ui_set_stale() claimed the opposite and that assumption spread.
ui_update_thermostat() is called once a second by the control task and was
writing seven style properties every time, so the panel was redrawing
something forever, and glitched lines were being regenerated as fast as
anything could clear them.
Two full-screen invalidates made it worse still: one after every HA poll,
one 600ms after every touch. The premise was that LVGL never reinvalidates
pixels it does not know are wrong, so repainting would clean them up. But
the README already had the answer -- the corruption scales with how much is
written, because LVGL's PSRAM writes compete with the panel's DMA reads out
of the same PSRAM. A full repaint is 768KB of exactly the contention that
causes the problem. Both are removed.
What is left is the thing that actually helps: write less.
- ui_update_thermostat() returns without taking the lock when nothing
moved, and otherwise touches only the widgets whose values changed.
- ui_set_stale() is guarded on the previous value.
- Condition and trend icons only get a colour write when the glyph itself
changed.
Also raises the bounce buffer from 16 to 20 scanlines. The comment saying
20 "fails to allocate once WiFi is up" was true when display_init() ran
after wifi_init_sta(); it does not any more. display_alloc() takes the
buffers from a virgin heap with ~200KB free, so the extra 12.8KB is
affordable, and each larger chunk buys the refill ISR more time to fetch the
next one before the LCD needs it. That attacks the mechanism rather than the
symptom. ~24KB internal heap remains once WiFi is up; the "Free internal
heap" log line is the thing to watch if it needs to come back down.
Both gotchas are now in the README so the next person does not reach for a
repaint again.
Co-Authored-By: Claude <noreply@anthropic.com>
123 lines
6.1 KiB
Markdown
123 lines
6.1 KiB
Markdown
# 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. The wind sensors are dead (hardware, not software)
|
|
|
|
The station anemometer has read `unavailable` since 2026-07-27 — wind speed,
|
|
gust, direction and feels-like. The WIND tile shows `--` by design and will
|
|
keep doing so until the hardware is fixed. Nothing to change in the firmware;
|
|
noted so it is not re-diagnosed as a parsing bug. See README "Home Assistant".
|
|
|
|
### 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.
|