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
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
./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:
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
#ifdefs. 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 throughlv_fs, which needs a registered driver and drive-letter paths.sim_main.cencodes to memory and usesfwrite.