# 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`.