Files
2026-08-04 11:55:02 -07:00

110 lines
4.4 KiB
Markdown
Raw Permalink 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.
# Desktop simulator
Runs the firmware's UI on a Linux desktop so layout work does not need a
flash-and-look cycle. `main/ui/ui_manager.c` is compiled **unmodified** — the
same source the device runs.
## Build
```bash
cmake -S sim -B sim/build -G Ninja
cmake --build sim/build
```
LVGL comes from `managed_components/lvgl__lvgl`, which the ESP-IDF component
manager populates. If that directory is missing, CMake says so; run
`. ~/esp/esp-idf/export.sh && idf.py reconfigure` once to fetch it.
No ESP-IDF environment is needed to *build* the simulator — only a host
compiler, CMake, Ninja and `libsdl2-dev`.
## Run
```bash
./sim/build/thermostat_sim # SDL window, mouse acts as touch
./sim/build/thermostat_sim --png shot.png # render one frame, write a PNG, exit
./sim/build/thermostat_sim --png shot.png --settle-ms 1500
```
The `--png` mode is what makes UI review possible without a human at a screen:
it renders headless, so it works over SSH and in CI, and the resulting image
can be inspected directly.
Clicking a light tile in the window calls the same `ui_set_light_cb` path the
firmware uses; the simulator applies the change locally and logs the command,
so tiles respond with no Home Assistant present.
## Snapshots
The interesting screens — rain falling, a light dimmed, a sensor dropping out
— do not happen on demand, and live data changes underneath a before/after
comparison. So capture real responses and render those:
```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
```
Snapshots are plain JSON and are meant to be hand-edited into states that have
not occurred yet — `sim/snapshots/evening.json` was made that way, to check
the on/dimmed/unavailable tile styling while every real light was off.
`ha_snapshot.py` does **not** carry its own copy of the Jinja template. It
reads it from `thermostat_sim --print-template`, which the preprocessor builds
from `main/api/ha_template.h`, so a snapshot cannot be captured with a
template the firmware does not use.
## How it stays honest
A simulator that renders differently from the panel is worse than none, so
these are deliberately pinned to the device:
| Property | Value | Enforced by |
|---|---|---|
| Resolution | 800×480 | `PANEL_H_RES/V_RES` in `sim_main.c` |
| Colour depth | RGB565 | `LV_COLOR_DEPTH 16` in `lv_conf.h` |
| Render mode | `DIRECT` | `lv_conf.h` / `headless_display_create()` |
| LVGL version | whatever IDF resolved | builds the same `managed_components` tree |
| Fonts | Montserrat 12–48 | `lv_conf.h` **and** `sdkconfig.defaults` |
**The font lists must be edited together.** Enabling a size in one build and
not the other makes the simulator lie: text silently falls back to a different
size and every metric shifts.
## What it does not simulate
- **PSRAM bandwidth, the bounce-buffer refill ISR, and RGB scan timing.** The
left-edge artefacts in the root README are invisible here by construction.
This tool answers "does the layout look right", never "does the panel keep
up". Timing-sensitive changes still need hardware.
- **Touch hardware.** The mouse is a stand-in for the GT911: one contact
point, pixel-accurate, no palm rejection, and none of the I2C contention the
real panel has with the backlight controller.
- **Anything in `main/drivers/`.** Those touch real peripherals and are never
compiled here.
## Layout
| Path | Purpose |
|---|---|
| `sim_main.c` | Entry point: display setup, PNG capture, mock data |
| `lvgl_port_sim.c` | Host implementation of the firmware's `lvgl_port.h` |
| `compat/` | Stand-ins for the ESP-IDF headers shared sources include |
| `lv_conf.h` | Host LVGL config, mirroring the device's Kconfig settings |
`compat/` exists so firmware sources compile as-is instead of growing
`#ifdef`s. If a newly shared source fails to compile, extend `compat/` — do
not fork the firmware source.
## Gotchas
- **Montserrat has no `·` (U+00B7).** It renders as a tofu box.
`weather_get_string()` uses one, but its output only ever reaches a log
line. Anything routed to an on-screen label must avoid it.
- **`lodepng_encode32_file()` does not work here.** LVGL patches its bundled
lodepng to do file I/O through `lv_fs`, which needs a registered driver and
drive-letter paths. `sim_main.c` encodes to memory and uses `fwrite`.