110 lines
4.4 KiB
Markdown
110 lines
4.4 KiB
Markdown
# 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`.
|