Turns the weather dashboard into an actual thermostat. Relays are not
fitted yet -- the GPIOs are driven and can be metered at the UART1-OUT
plug (idle 3.3V, energized 0V, active-low module assumed).
Control loop (main/control/thermostat.c) is pure logic: no ESP-IDF, no
network, no clock of its own. The caller passes a temperature and a
millisecond timestamp, which is what lets the simulator and the test
harness run the identical decision code.
Equipment protections, all verified by tools/thermostat_test.c:
deadband 1.0F stops the burner chattering at the setpoint
minimum on 3 min a shorter call is all ignition wear, no heat
minimum off 5 min back-to-back ignitions are what kill igniters
cycles/hour 5 standard gas default; catches a flapping sensor
boot lockout 60 s a boot loop must not machine-gun the gas valve
That test drives the state machine with synthetic time, so two hours of
furnace behaviour runs in milliseconds. It exits non-zero on failure and
is worth re-running after any edit to thermostat.c -- this switches a gas
valve.
Two deliberate decisions:
- G is never energized during a heat call. The furnace runs its own
blower with a fan-on delay that waits for the heat exchanger and a
fan-off delay that scavenges residual heat; driving G ourselves would
blow cold air at people and throw that heat away. G is manual
circulation only.
- The control loop starts before every early return in app_main. Those
returns are all "no WiFi" or "no HA token", and none of them is a
reason to leave a house unheated.
Relay pins are IO20 heat / IO19 fan on the UART1-OUT plug, measured on
this board since Elecrow documents the GPIO numbers but not the connector
pin order. Heat is on IO20 because it is USB D+, the line carrying the
hardware pull-up -- with an active-low module it fails toward "no heat"
rather than "heat stuck on" if the USB PHY is ever re-enabled. Both pins
were confirmed free of the onboard mic before use.
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG and CONFIG_ESP_PHY_ENABLE_USB are now
off to release them; the console is on UART0 via the CH340 regardless.
UI: the indoor card gains a status dot, mode and fan buttons, and a
setpoint column with finger-sized up/down targets. The reading grid drops
from six cells to four to make room -- wind was the one to cut, since the
station anemometer has been dead since 2026-07-27 and that cell has never
shown a number. Wind now rides on the outdoor cell's sub-line and will
reappear on its own if the sensor is fixed. Sunrise and sunset share a
cell, showing whichever comes next.
Known weakness, documented in README and top of TODO: indoor temperature
comes from Home Assistant over the network, so an HA outage ages the
reading until the 15-minute failsafe forces heat off. A local I2C sensor
removes the failure mode entirely and is the next thing to add. The
setpoint also resets to 68F on reboot, because persisting it means an NVS
write and a flash write while the panel scans faults the bounce-buffer
ISR.
Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
6.2 KiB
Markdown
124 lines
6.2 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: interactions schedule a one-shot full repaint 600ms
|
|
after the last touch, and every poll repaints too. Artefacts should now
|
|
visibly self-clean in under a second after touching, and within a poll
|
|
otherwise. If they *persist* beyond that, the repaint isn't reaching
|
|
them; if they merely *recur* during interaction, that is the PSRAM
|
|
bandwidth ceiling (README) and the honest remaining levers are fewer
|
|
redraws 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.
|