New panel design
This commit is contained in:
1 parent
face2432e4
commit
b5d6b4a897
30 files changed
+4540
-171
No files matched your search
@@ -11,9 +11,10 @@ ESP-IDF firmware for the **Elecrow CrowPanel 7" Advance** (ESP32-S3-WROOM-1-N16R
|
||||
> **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.
|
||||
A wall dashboard driven by **Home Assistant** over the LAN: outdoor and indoor
|
||||
conditions from an Ambient Weather station, sunrise/sunset, rain, a five-day
|
||||
forecast, and on/off plus brightness control for six lights. Thermostat control
|
||||
(setpoint, HVAC relays, scheduling) is not implemented yet.
|
||||
|
||||
## Components
|
||||
|
||||
@@ -24,16 +25,32 @@ implemented yet.
|
||||
| `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 |
|
||||
| `main/api/ha_client.c` | Home Assistant REST transport (device only) |
|
||||
| `main/api/ha_data.c` | Response parsing and presentation helpers (**shared with the simulator**) |
|
||||
| `main/api/ha_config.h` | Entity IDs for this installation |
|
||||
| `main/api/ambient_weather.c` | Ambient Weather cloud client — **fallback only** |
|
||||
| `sim/` | Desktop simulator for the UI — see `sim/README.md` |
|
||||
| `tools/` | `ha_discover.py`, `ha_snapshot.py` |
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp main/app_config.h.example main/app_config.h
|
||||
# edit main/app_config.h: WiFi credentials + Ambient Weather keys
|
||||
# edit main/app_config.h: WiFi credentials + HA_TOKEN
|
||||
```
|
||||
|
||||
`main/app_config.h` is gitignored — it holds your WiFi password and API keys.
|
||||
`main/app_config.h` is gitignored — it holds your WiFi password and the Home
|
||||
Assistant token. Entity IDs are *not* secrets and live in
|
||||
`main/api/ha_config.h`, which is tracked.
|
||||
|
||||
Confirm the entity IDs still resolve after any change in HA:
|
||||
|
||||
```bash
|
||||
export HA_TOKEN='...' # HA: your user -> Security -> Long-lived tokens
|
||||
python3 tools/ha_discover.py
|
||||
```
|
||||
|
||||
A wrong entity ID fails **silently** as "unavailable" rather than as an error.
|
||||
|
||||
## Build & Flash
|
||||
|
||||
@@ -45,6 +62,50 @@ idf.py build flash monitor
|
||||
Requires ESP-IDF **v5.3 or newer** (the code uses the `i2c_master` driver API
|
||||
and `dma_burst_size`).
|
||||
|
||||
## Home Assistant
|
||||
|
||||
Everything on screen except the forecast comes from **one** POST to
|
||||
`/api/template`: about 430 bytes, measured at ~85 ms on the LAN. The Jinja
|
||||
template lives in `main/api/ha_template.h` and is built by the preprocessor
|
||||
from the same `HA_LIGHT_LIST` X-macro as the C table, so the request and the
|
||||
code that reads the reply cannot drift apart.
|
||||
|
||||
Three things fall out of doing it this way, and all three are load-bearing:
|
||||
|
||||
- **No SNTP, and no timezone database.** HA renders `sunrise`, `sunset`,
|
||||
`clock` and `date` as local-time strings. The board's own clock still starts
|
||||
at the epoch and it no longer matters — but note that means *nothing on this
|
||||
device knows the time*, so anything needing a real clock (scheduling, say)
|
||||
has to solve that separately. There is an unused RTC at I2C 0x51.
|
||||
- **No TLS.** Plain HTTP to a box on the same switch. The cloud client pays a
|
||||
multi-second handshake; this does not, which matters because the caller
|
||||
pauses rendering for the duration. Pointing `HA_HOST` at an https:// instance
|
||||
throws that away.
|
||||
- **Unavailable sensors arrive as JSON `null`,** never a sentinel number. `-1`
|
||||
mph of wind is impossible, but `-1` °F of dew point is not.
|
||||
|
||||
The forecast is a second, slower request (every 30th poll). It cannot be part
|
||||
of the template: HA removed the `forecast` attribute from weather entities in
|
||||
2024.4, and templates cannot call services.
|
||||
|
||||
### The wind sensors are dead
|
||||
|
||||
The station's anemometer has been offline since **2026-07-27**: `wind_speed`,
|
||||
`wind_gust`, `wind_direction` and `feels_like` all read `unavailable`, and the
|
||||
WIND tile shows `--` by design. It is deliberately *not* backfilled from the
|
||||
met.no forecast — regional modelled wind under a heading that means "my yard"
|
||||
is worse than showing nothing.
|
||||
|
||||
`sensor.moonlight_beach_max_gust` is a trap: it is **not** unavailable. It
|
||||
still reports its last value from 2026-07-28 and will render as a plausible
|
||||
live number. Anything that uses it needs a staleness check first.
|
||||
|
||||
### Fallback
|
||||
|
||||
When HA cannot be reached, `ambient_weather.c` fills in outdoor temperature,
|
||||
humidity and wind from the cloud API and the status line says so. Indoor, sun,
|
||||
forecast and lights have no cloud equivalent and hold their last values.
|
||||
|
||||
## Hardware (Advance)
|
||||
|
||||
### RGB panel (16-bit RGB565)
|
||||
@@ -109,12 +170,47 @@ 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.
|
||||
|
||||
## UI development
|
||||
|
||||
`main/ui/ui_manager.c` and `main/api/ha_data.c` are compiled **unmodified**
|
||||
into a desktop simulator, so layout work does not need a flash cycle:
|
||||
|
||||
```bash
|
||||
cmake -S sim -B sim/build -G Ninja && cmake --build sim/build
|
||||
./sim/build/thermostat_sim # SDL window, mouse = touch
|
||||
./sim/build/thermostat_sim --png /tmp/shot.png # headless capture
|
||||
```
|
||||
|
||||
Capture real states to iterate against, instead of waiting for reality:
|
||||
|
||||
```bash
|
||||
export HA_TOKEN=$(cat ~/.config/thermostat-hw/ha_token)
|
||||
python3 tools/ha_snapshot.py --name rainy
|
||||
./sim/build/thermostat_sim --data sim/snapshots/rainy.json \
|
||||
--forecast sim/snapshots/rainy.forecast.json
|
||||
```
|
||||
|
||||
See `sim/README.md` for what it does and does not reproduce. It says nothing
|
||||
about PSRAM bandwidth or panel timing.
|
||||
|
||||
> **The font lists in `sim/lv_conf.h` and `sdkconfig.defaults` must match.**
|
||||
> A size enabled in one build and not the other makes the simulator lie about
|
||||
> layout. Worse, `sdkconfig.defaults` only seeds a *new* `sdkconfig` — editing
|
||||
> it does nothing to an existing one, so change `sdkconfig` too (or delete it
|
||||
> and let it regenerate).
|
||||
|
||||
## 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.
|
||||
dead touch zones. Light tiles therefore carry their own handlers, and the
|
||||
labels inside them are left non-clickable so taps reach the tile.
|
||||
- **Montserrat has no `·` (U+00B7) or `—` (U+2014).** Both render as tofu
|
||||
boxes. `weather_get_string()` emits a middot, but only into a log line.
|
||||
Anything reaching a label must stay in Latin-1; the UI uses `--`.
|
||||
- **`LV_LABEL_LONG_DOT` needs an explicit height** to truncate. With a
|
||||
content-sized height the label wraps to a second line first and only then
|
||||
adds dots — landing on top of whatever is below it.
|
||||
- **`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()`.
|
||||
|
||||
Reference in new issue
Block a user