first commit

This commit is contained in:
matt committed 2026-08-03 17:47:33 -07:00
commit 393592e327
1 file changed
+405
+405
View File
@@ -0,0 +1,405 @@
# Rotary Thermostat — Product Requirements
ESP-IDF firmware turning an Elecrow CrowPanel 2.1" Rotary Display into a
multi-room thermostat for a gas furnace, reading Zigbee room sensors over HTTP
and driving the furnace's heat call via a relay.
**Status:** not started. This document is the brief for the implementing agent.
---
## 1. Goal
Replace a conventional 24VAC thermostat with a device that:
- reads temperature from Zigbee sensors in multiple rooms, via an existing
Zigbee hub's HTTP API
- decides which room's reading governs (manual / scheduled / formula)
- calls for heat by closing a dry contact between `R` and `W`
- presents setpoint, current temperature and status on a 480×480 round display
- takes setpoint input from the rotary encoder
Control logic runs **on the ESP32**, not in Home Assistant. The house must keep
heating when the hub or HA is down, degrading to a safe fallback.
---
## 2. Hardware
### 2.1 Board — Elecrow CrowPanel 2.1" HMI Rotary Display
Verified on the actual unit (2026-08-03) with `esptool`:
| Property | Value |
|---|---|
| SoC | ESP32-S3 (QFN56) rev v0.2 |
| Flash | 16MB (GigaDevice `c8:4018`) |
| PSRAM | 8MB embedded (AP_3v3) |
| USB | native USB-Serial/JTAG (`303a:1001`) — **no CH340** |
| Port | `/dev/ttyACM0` |
| MAC | `1c:db:d4:b3:5c:74` |
| Power | 5V / 1A |
Currently running stock Arduino firmware (arduino-lib-builder, IDF v4.4.6,
Oct 2023). Nothing has been written to it.
### 2.2 Peripherals — FROM VENDOR DOCS, NOT YET VERIFIED ON HARDWARE
> **Read section 8.1 before trusting any of these numbers.** The previous
> project in this family lost most of a session to a pinout taken from the
> wrong board variant. Confirm every value against Elecrow's own source before
> writing driver code.
- **Panel:** ST7701S, 480×480 round IPS. Requires a **~30-command SPI
initialisation sequence** before the RGB bus produces anything. A bare RGB
configuration displays nothing.
- **Touch:** CST816 capacitive (*not* GT911 — different register map and
protocol from the 7" project).
- **I/O expander:** PCF8574 @ I2C `0x21`.
- **Rotary encoder:** A = GPIO42, B = GPIO4; button on expander P5
(INPUT_PULLUP).
| Signal | GPIO |
|---|---|
| CS | 16 |
| DE | 40 |
| HSYNC | 15 |
| VSYNC | 7 |
| PCLK | 41 (18 MHz) |
| R0–R4 | 46, 3, 8, 18, 17 |
| G0–G5 | 14, 13, 12, 11, 10, 9 |
| B0–B4 | 5, 45, 48, 47, 21 |
| I2C SDA / SCL | 38 / 39 |
**PCF8574 allocation:**
| Pin | Use |
|---|---|
| P0 | Touch reset |
| P1 | **free** |
| P2 | Touch interrupt |
| P3 | LCD power |
| P4 | LCD reset |
| P5 | Encoder button |
| P6 | **free** |
| P7 | **free** |
### 2.3 Available I/O
The RGB panel consumes ~21 GPIOs, so almost nothing native is exposed.
Externally available:
- **I2C connector** (SDA 38 / SCL 39), ZX-MX 1.25-4P
- **UART0 and UART1** connectors
- **12P FPC** (power + programming)
- **PCF8574 P1, P6, P7**
I2C is the expansion path, not GPIO. Three spare expander pins is enough for
this project; a second expander or I2C relay board can be added on the
connector if more are needed.
PCF8574 outputs **sink** well (~25 mA) but barely source — use **active-low
triggered** relay modules.
### 2.4 HVAC system
- Gas/oil forced-air furnace, 24VAC control. **Heat only, no cooling.**
- **3-wire** system.
- Presence of a `C` (common) wire **unconfirmed** — see section 7.
Third wire is most likely `G` (fan). On a heat-only system independent fan
control is rarely needed — the furnace board runs the blower automatically
during a heat call — so `G` can usually be repurposed as `C` by moving it to
the `C` terminal at the furnace control board. This powers the thermostat with
no new cable. **Confirm terminal labels at the furnace and photograph the
existing wiring before changing anything.**
If `C` is unavailable, power the board from a separate 5V USB supply. Do **not**
attempt 24VAC "power stealing" — this board needs ~5W and power stealing
supplies roughly a tenth of that; it will cause furnace chatter or
short-cycling.
### 2.5 Output wiring
```
PCF8574 P1 ──► relay module IN (active low)
R (24VAC) ──► relay COM
W (heat) ◄── relay NO closes to call for heat
```
Use an **opto-isolated** relay module with a separate JD-VCC jumper so the
24VAC side is genuinely isolated. Relay must be **normally open**.
**Never connect `R` to `C`** — dead short across the transformer; blows the
fuse on the furnace control board.
---
## 3. Architecture
```
Zigbee sensors ──► Zigbee hub (HTTP/JSON) ──► ESP32 polls over WiFi
│
setpoint + control loop
│
PCF8574 P1 ──► relay ──► R/W
```
Suggested component layout (mirroring the sibling project, section 9):
```
main/
├── main.c
├── app_config.h gitignored; credentials + hub URL
├── app_config.h.example tracked template
├── drivers/
│ ├── i2c_bus.{c,h} shared bus: PCF8574, touch, any added sensor
│ ├── expander/ PCF8574 — touch reset, LCD power/reset, relay
│ ├── display/ ST7701S init + RGB panel
│ ├── touch/ CST816
│ └── encoder/ rotary + button
├── hvac/
│ ├── relay.{c,h} heat call, with the safety interlocks of section 5
│ └── control.{c,h} setpoint, hysteresis, room selection
├── net/
│ ├── wifi_manager.{c,h}
│ └── zigbee_client.{c,h} polls hub, exposes readings with timestamps
├── ui/
│ ├── lvgl_port.{c,h} tick, task, locking
│ └── ui_manager.{c,h}
└── storage/
└── settings.{c,h} NVS persistence
```
---
## 4. Functional requirements
### 4.1 Room temperature acquisition
- Poll the Zigbee hub's HTTP endpoint on a fixed interval (default 60s;
configurable).
- Parse JSON with cJSON.
- Every reading carries **a value and a timestamp** (`last_seen` from the hub
if provided, otherwise local receipt time).
- Expose battery level per sensor if the hub reports it, and surface a low
battery warning on screen.
- A failed poll must not clear previously good readings — only age them.
### 4.2 Room selection
Three modes, selectable from the UI:
1. **Manual** — encoder picks the governing room.
2. **Schedule** — time-of-day rules (e.g. bedroom overnight, TV room evenings).
3. **Formula** — `min` across rooms ("coldest room wins") is the recommended
default; `average` also acceptable. Min-based control means nobody is cold,
at the cost of overheating elsewhere.
Manual override must always be reachable regardless of active mode.
### 4.3 Control loop
- Setpoint adjustable by rotary encoder, 0.5°C steps, sane bounds (e.g. 5–30°C).
- **Hysteresis** (deadband) to prevent chatter — default ±0.5°C. Call for heat
below `setpoint − deadband`, stop at `setpoint + deadband`.
- Evaluate at a fixed cadence; do not react to every incoming reading.
### 4.4 Display / UI
- Circular layout suited to a 480×480 round panel — nothing important near the
edges.
- Show: current governing temperature, setpoint, active room, mode, heat-call
state, and any fault (stale sensor, hub unreachable, low battery).
- Encoder rotation adjusts setpoint; button press cycles screens or confirms.
- LVGL 9.x.
### 4.5 Persistence
Survive reboot via NVS: setpoint, mode, selected room, schedule.
---
## 5. Safety requirements
**These are not optional and must be implemented in the relay layer itself, not
left to UI code.**
1. **Fail-open.** Normally-open relay. Crash, reset, brownout or power loss
must leave heat **off**.
2. **Watchdog-gated output.** The relay is held closed only while a control
task refreshes a timer. If the task stops, the relay drops automatically. A
hung firmware must not be able to hold a heat call.
3. **Maximum on-time cap.** Absolute limit per heat call (suggest 60 min).
Exceeding it drops the relay and raises a fault.
4. **Minimum off-time.** At least 5 minutes between heat calls (anti
short-cycle).
5. **Stale-data policy.** Define explicitly for "selected room hasn't reported
in N minutes" (suggest N = 20):
- stale data must **never** hold the relay closed
- it must **never** silently mean no heat either (frozen pipes)
- fall back in order: another room's fresh reading → a local sensor if
fitted → a fixed conservative setpoint (suggest 16°C), showing a clear
fault on screen throughout
6. **Bounded startup state.** Relay off until the control loop has at least one
fresh reading or has entered the documented fallback.
### 5.1 Risk calibration
For accuracy rather than alarm: the furnace's **high-limit and rollout switches
are independent of the thermostat** and remain in circuit. A stuck heat call
results in an overheated house and a large gas bill, with the furnace cycling
on its own limit — not a fire. The 24VAC control side is genuinely low voltage.
This does not remove the need for the interlocks above, but it does mean the
realistic failure mode is discomfort and cost.
---
## 6. Non-functional
- Must keep heating with the hub, MQTT broker or Home Assistant down, per the
fallback ladder in 5.5.
- WiFi reconnect must be automatic and must not block the control loop.
- All network I/O off the LVGL task.
- Boot to a usable, safe state in under ~10 seconds.
---
## 7. Blocking inputs still needed from the user
The implementing agent should request these before writing the relevant module.
**Do not guess at any of them.**
1. **Zigbee hub HTTP API shape.** A sample response is ideal:
```bash
curl -s 'http://<hub>/<endpoint>'
```
Needed: URL pattern, how a sensor is identified (friendly name? IEEE
address?), the JSON field holding temperature, whether a timestamp /
`last_seen` is included, whether auth is required, and battery reporting if
available.
2. **Confirmation of the 3 thermostat wires** — specifically whether the third
is `G` (repurposable as `C`) or already `C`.
3. **Room names / sensor IDs** for the TV room and bedroom.
4. **Units** — °C or °F for display.
---
## 8. Implementation notes and known pitfalls
### 8.1 Verify the pinout against vendor source first — non-negotiable
The sibling 7" project lost most of a session to a pinout taken from the wrong
board variant (CrowPanel 7" *Basic* vs *Advance* — same brand, same wiki, and
not one pin in common). Symptoms were a black screen and an apparently empty
I2C bus, and every software hypothesis tested came back negative because the
code was correct and the hardware description was wrong.
Before writing the display driver:
- Clone Elecrow's repo for **this** board and read the actual example source —
not the wiki prose, not a forum post, not a search summary.
- Lift the **ST7701S init sequence verbatim.** It is ~30 hex commands; one
wrong byte gives garbage or nothing, with no useful diagnostic.
- Cross-check against a second independent source (Elecrow's ESPHome example
is good) before trusting it.
- If the panel is black, **re-verify the pinout before debugging the software.**
A cheap early sanity check: bring up I2C first and scan the bus. Finding the
PCF8574 at `0x21` and the CST816 confirms the board is alive and the I2C pins
are right, independently of the display.
### 8.2 ESP-IDF / LVGL pitfalls already paid for on the sibling project
- **`lv_init()` must run before creating any LVGL object.** Calling
`lv_display_create()` first faults with `LoadProhibited` inside the TLSF
allocator — a confusing way to discover an ordering bug. Guard it with
`lv_is_initialized()`.
- **USB-Serial-JTAG occupies GPIO19/20** on the ESP32-S3. IDF enables it as a
secondary console by default. On the sibling board that silently blocked the
touch I2C bus. **On this board USB-JTAG is the only console link**, so do
*not* set `CONFIG_ESP_CONSOLE_SECONDARY_NONE=y` — you will lose all serial
output.
- **Do not enable `CONFIG_SPIRAM_FETCH_INSTRUCTIONS` / `CONFIG_SPIRAM_RODATA`**
with an RGB panel. They contend with the LCD for PSRAM bandwidth: black
screen and stalled TLS handshakes.
- **Bounce-buffer ISR vs flash writes.** The refill ISR reads PSRAM, which
shares the cache with flash on the S3. Any NVS/flash write while the panel is
running faults it with *"Cache disabled but cached memory region accessed"*.
Bring WiFi up **before** `display_init()`, and set
`CONFIG_ESP_WIFI_NVS_ENABLED=n`. Note this project writes settings to NVS at
runtime — either pause the panel around those writes or avoid bounce buffers.
- **Never write into the framebuffer being scanned.** Copying a partial
rectangle into the live framebuffer races the LCD's reads; the corruption
scales with bytes written and *persists*, because LVGL does not know those
pixels are wrong and never reinvalidates them. Use two framebuffers and swap.
- **LVGL does not bubble events to parents.** An event handler on the screen
object never sees taps landing on a child widget — which looks exactly like
dead touch zones. Poll the input device from an `lv_timer` for anything
screen-wide.
- **`lv_label_set_text()` invalidates unconditionally**, even for identical
text, forcing a repaint. Compare before setting.
- **PSRAM at 80MHz is marginal for an RGB panel.** The vendor's Arduino builds
use 120MHz, but on ESP-IDF 5.3 selecting `CONFIG_SPIRAM_SPEED_120M` raises
the shared MSPI core clock and forces flash to 120MHz too, which needs flash
High Performance Mode and a DC-aware bootloader. 480×480 needs roughly 40% of
the bandwidth 800×480 did, so this is likely to be a non-issue here — but
that is the lever if artefacts appear.
### 8.3 Toolchain
ESP-IDF **v5.3.2** at `~/esp/esp-idf`. `cmake` and `ninja` live in the IDF
virtualenv, not system-wide, so they are only on `PATH` after sourcing
`export.sh`. The Bash tool does not persist shell state between calls — source
it in the same compound command:
```bash
. ~/esp/esp-idf/export.sh && idf.py build
```
Flash and monitor on `/dev/ttyACM0`. Rootless podman on this machine is broken
(stale `/etc/subuid` after a user rename) — do not plan on containers.
---
## 9. Prior art
`~/workspace/thermostat-hw` — the sibling project on the CrowPanel 7" Advance.
Working, verified on hardware. Directly reusable patterns:
- `main/api/ambient_weather.c` — HTTP + cJSON polling with timeouts, fixed
response buffer and a chunked-encoding-safe event handler. **The Zigbee hub
client is the same shape.**
- `main/ui/lvgl_port.c` — LVGL tick, service task, recursive lock, and
pause/resume.
- `main/drivers/i2c_bus.c` — shared bus ownership with a diagnostic scan.
- `main/wifi/wifi_manager.c` — STA bring-up with bounded retries.
Its `README.md` documents the display architecture and the pitfalls above in
context.
`~/workspace/displayTest/FluidTouch` — third-party project supporting several
CrowPanel variants; useful reference for board-specific configuration and a
worked LovyanGFX setup.
---
## 10. Suggested phasing
Each phase should end in something verifiable on hardware.
| Phase | Deliverable | Verification |
|---|---|---|
| 1 | Project skeleton, WiFi, I2C bus + scan | PCF8574 at `0x21` and CST816 found in the log |
| 2 | ST7701S init + RGB panel | Colour bars on the round panel |
| 3 | LVGL port + basic UI | Setpoint renders, stable, no artefacts |
| 4 | Encoder + CST816 touch | Knob changes setpoint; taps register |
| 5 | Zigbee hub client | Room temps logged with timestamps and staleness |
| 6 | Control loop **+ all of section 5** | Relay logic exercised with a **bench LED/relay, not the furnace** |
| 7 | NVS persistence | Settings survive reboot |
| 8 | Schedule / formula modes | Room selection behaves per 4.2 |
| 9 | Install | Only after phase 6 has been proven on the bench |
**Do not connect to furnace wiring until the safety interlocks in section 5 are
implemented and demonstrated on the bench.**