Files
thermostat-hw/README.md
T
mattandClaude d4b2ab7f99 Work through the remaining review items in TODO.md
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>
2026-08-04 09:08:51 -07:00

154 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Thermostat HMI
ESP-IDF firmware for the **Elecrow CrowPanel 7" Advance** (ESP32-S3-WROOM-1-N16R8,
16MB flash + 8MB octal PSRAM, 800×480 IPS, GT911 capacitive touch).
> **Board variant matters.** Elecrow sells a *Basic* CrowPanel 7" and an
> *Advance* CrowPanel 7". They share a form factor and almost nothing else:
> every RGB data pin, every sync pin, the pixel clock, the porches, the sync
> polarities and the touch I2C pins are different, and the Advance drives its
> backlight over I2C instead of from a GPIO. This firmware targets the
> **Advance**. Building it against Basic documentation yields a black screen
> and a silent I2C bus.
Currently displays outdoor conditions from an Ambient Weather personal weather
station. Thermostat control (setpoint, HVAC relays, scheduling) is not
implemented yet.
## Components
| Path | Purpose |
|------|---------|
| `main/drivers/display` | RGB parallel panel via `esp_lcd`, double-buffered in PSRAM |
| `main/drivers/touch` | GT911 capacitive touch over I2C, registered as an LVGL input device |
| `main/ui/lvgl_port.c` | LVGL tick source, service task, and locking |
| `main/ui/ui_manager.c` | Screen layout and update entry points |
| `main/wifi` | WiFi station bring-up |
| `main/api` | Ambient Weather REST client |
## Setup
```bash
cp main/app_config.h.example main/app_config.h
# edit main/app_config.h: WiFi credentials + Ambient Weather keys
```
`main/app_config.h` is gitignored — it holds your WiFi password and API keys.
## Build & Flash
```bash
idf.py set-target esp32s3
idf.py build flash monitor
```
Requires ESP-IDF **v5.3 or newer** (the code uses the `i2c_master` driver API
and `dma_burst_size`).
## Hardware (Advance)
### RGB panel (16-bit RGB565)
| Signal | GPIO |
|--------|------|
| DE | 42 |
| VSYNC | 41 |
| HSYNC | 40 |
| PCLK | 39 |
| R0–R4 | 7, 17, 18, 3, 46 |
| G0–G5 | 9, 10, 11, 12, 13, 14 |
| B0–B4 | 21, 47, 48, 45, 38 |
Timings: 18 MHz pixel clock, HFP 8 / HPW 4 / HBP 8, VFP 8 / VPW 4 / VBP 8.
Both syncs idle high; PCLK is `pclk_active_neg`.
### I2C bus — GPIO 15 (SDA) / 16 (SCL)
| Address | Device |
|---------|--------|
| 0x30 | STC8H1K28 board controller — backlight **and GT911 reset** |
| 0x51 | RTC |
| 0x5D | GT911 touch |
The GT911 has **no ESP32 reset or interrupt GPIO** on this board. The
STC8H1K28 holds it in reset, so `backlight_init()` must run before
`touch_init()` or the touch controller NACKs every address.
STC8H1K28 protocol is single raw bytes, no register addressing: `0x19` wake,
then `0x10` and `0x18` to configure and release the GT911. Brightness is
`0x00`–`0xF5` **inverted** (`0x00` = full) on v1.3; v1.2 uses a narrower
`0x05`–`0x10` range instead.
## Display architecture
Two 800×480×2 framebuffers in PSRAM, LVGL in `RENDER_MODE_DIRECT`. LVGL draws
into whichever buffer the LCD is *not* scanning; the last flush of a refresh
hands that buffer to the panel, which switches to it. Nothing ever writes into
the buffer being displayed — copying into the live framebuffer races the LCD's
own reads, and the resulting corrupt pixels persist indefinitely because LVGL
does not know they are wrong and never reinvalidates them.
A 16-scanline bounce buffer stages each chunk in internal RAM. 20 scanlines
would be better but needs 64KB of internal RAM and fails to allocate once
WiFi is up.
### Known limitation: occasional left-edge artefacts
Short stale lines sometimes appear at the left edge, most often right after a
weather fetch. The panel needs ~34 MB/s sustained from PSRAM at the 18MHz
pixel clock, and 80MHz octal PSRAM is marginal for that. When the bounce
refill ISR runs late — WiFi interrupt activity is enough — the panel scans out
whatever was left in the buffer.
The pixel clock is not a usable lever: below roughly 40Hz refresh (about
15MHz) the panel stops locking and the image rolls vertically instead.
The real fix is 120MHz PSRAM, which is what the vendor's Arduino build uses
(`-DCONFIG_SPIRAM_SPEED_120M=1`). On ESP-IDF 5.3 that is not safely reachable:
selecting it raises the shared MSPI core clock, and IDF only ships flash timing
tables pairing 120MHz PSRAM with 120MHz flash. Flash above 80MHz requires
High Performance Mode and a DC-aware bootloader.
## Gotchas
- **LVGL does not bubble events to parents.** An event handler on the screen
object never sees taps that land on a child widget, which looks exactly like
dead touch zones. The touch readout polls the input device from an
`lv_timer` instead.
- **`lv_label_set_text()` invalidates unconditionally**, even when the text is
identical — so setting an unchanged value still forces a repaint. All label
updates go through `label_set_if_changed()`.
- **WiFi must be brought up before `display_init()`.** The bounce refill ISR
reads PSRAM, which shares the cache with flash; an NVS write while the panel
is running faults it with *"Cache disabled but cached memory region
accessed"*.
## Partitions
Two 4 MB OTA slots (`ota_0` / `ota_1`) with `otadata`, laid out in
`partitions.csv`. The app is ~1.4 MB, so the slots are generous — but the
layout is fixed once devices are fielded, and re-partitioning later means
reflashing each one over USB. ~7.9 MB of the 16 MB is left unallocated;
adding a partition to free space is safe, moving an existing one is not.
> **OTA is not yet implemented, and it is not a drop-in.** Writing an OTA image
> is a flash write, and the bounce-buffer refill ISR reads PSRAM, which shares
> the cache with flash on the S3 (see Memory notes). Downloading an update
> while the panel is scanning **will** fault that ISR. Any implementation must
> stop the panel for the download and write, or reboot into a dedicated
> updater mode first.
## Memory notes
Two 800×480×2 framebuffers (750 KB each) live in PSRAM. The 8 MB **octal**
PSRAM at 80 MHz is required — QSPI cannot sustain the RGB refresh.
Do **not** enable `CONFIG_SPIRAM_FETCH_INSTRUCTIONS` / `CONFIG_SPIRAM_RODATA`:
they contend with the panel for PSRAM bandwidth. The bounce buffer **is**
enabled (the panel cannot stream straight from PSRAM), which is exactly why
flash writes are dangerous while the panel runs: the refill ISR reads PSRAM,
which shares the cache with flash on the S3, so an NVS write faults it with
*"Cache disabled but cached memory region accessed"*. All flash writes happen
before `display_init()` and `CONFIG_ESP_WIFI_NVS_ENABLED` is off to keep the
WiFi stack from writing later.