Simulator¶
The native_sim build runs the firmware and the real web UI on your own
computer - no ESP32, no flashing. Built-in apps, effects, weather overlays,
scripts and the whole HTTP API behave as they do on AWTRIX, and the live preview
grid in the web UI is the simulated panel.
It is the fastest way to develop against the API, try out apps and notifications, or preview the web UI, without touching hardware.
Build and run¶
pio run -e native_sim
.pio/build/native_sim/program # Linux/macOS
.pio\build\native_sim\program.exe # Windows
# then open http://localhost:8080
On Windows, if g++ is not on your PATH the build falls back to a portable
w64devkit - see Host toolchain.
Continuous integration builds the unit tests and the firmware, but not
native_sim (see Building from source), so
a green CI run is no promise that the simulator still compiles. Build it locally
to find out.
Command-line flags¶
| Flag | Default | Meaning |
|---|---|---|
--port <n> |
8080 |
TCP port for the web UI and API |
--data <dir> |
simdata |
Host directory used as the device filesystem |
--webui <file> |
webui/index.html |
The web UI HTML served at / |
--matrix |
- | Force the terminal matrix on |
--no-matrix |
- | Force the terminal matrix off |
On first run the simulator creates the data directory tree - simdata/ plus
ICONS/, PALETTES/, MELODIES/ and SCRIPTS/ subfolders - mirroring the
filesystem layout AWTRIX keeps in flash.
What runs, and what does not¶
The API behaves like the hardware: the same request produces the same response. It renders at the same 40 frames per second too, so scroll speeds, effects and GIF timing match what the panel does.
| On AWTRIX | In the simulator |
|---|---|
| Flash filesystem | the --data directory (default simdata/) |
| Stored settings and device configuration | simdata/settings.json, simdata/device.json |
| Embedded web UI | webui/index.html served from disk - edit it and hit F5 |
| Buttons, light sensor, battery, environment sensor | fake readings you drive over /sim/ |
| Buzzer and MP3 player | melodies and file plays are printed to stdout, not sounded |
| MQTT | the same client, against a real broker |
The following are not simulated. Where the corresponding API route still exists, it is accepted and then ignored:
- Wi-Fi and provisioning - no access point, no
wifi-scan(it returns[]). - Art-Net - no network DMX input.
- mDNS / UDP discovery and OTA -
/updateresponds501 notSupported. - The button webhook -
buttonCallbackis stored but never called, however you press the simulated buttons.
/api/v1/system accepts the network fields so the web UI behaves identically,
but they have no effect here. reboot, sleep, factory reset and firmware
update are logged and ignored - restart the binary, or delete files under
simdata/, instead. The simulator never asks for an HTTP login, whatever
authEnabled is set to.
MQTT¶
Set mqttHost and switch mqttEnabled on - on the web UI's MQTT tab, with
PUT /api/v1/system, or by editing simdata/device.json - then restart the
binary, because the MQTT configuration is read once at startup.
The simulator then connects to that broker exactly as AWTRIX does: availability,
capabilities, periodic state, Home Assistant discovery, the <prefix>/cmd/#
command topic and a script's mqtt.publish() / mqtt.subscribe() all work
against it. With mqttPrefix left empty the topics start with simulator/.
See MQTT automation for the setup and
MQTT topics for the topic list.
Poking the fake hardware - /sim/¶
Because there are no real buttons or sensors, the simulator exposes an extra
/sim/ route group to drive them. GET /sim lists the routes and their fields.
# Press a button (left | select | right). Body is optional.
curl -X POST -d '{}' http://localhost:8080/sim/button/left
curl -X POST -d '{"durationMs":400}' http://localhost:8080/sim/button/select
# Set the fake sensors.
curl -X PUT -H "Content-Type: application/json" \
-d '{"temperature":28.5,"humidity":55,"ldrRaw":300,"batteryPinMillivolts":2100}' \
http://localhost:8080/sim/sensors
Buttons - POST /sim/button/{left|select|right}. The optional body takes a
single durationMs (a whole number of milliseconds, minimum 40 - the press
must clear the 35 ms debounce). Omit the body and the press defaults to 80 ms,
above the debounce and below the 300 ms double-press window. Presses feed the
same debounce and double-press logic as AWTRIX, so app navigation, notification
dismiss and the double-press power toggle all respond. Send a body even when it
is empty (-d '{}'): a bare POST with no body leaves the connection waiting
for a Content-Length that never comes.
Sensors - PUT /sim/sensors (also PATCH) accepts exactly temperature,
humidity, ldrRaw and batteryPinMillivolts. Any subset is allowed; the loop
resamples them, so allow about 2 seconds before reading
/api/v1/device back. The default fake readings are
temperature 21.5, humidity 42, ldrRaw 1200 (mid-daylight on the 0–4095
ADC scale) and batteryPinMillivolts 2290.
batteryPinMillivolts is the raw voltage at the ADC pin, not a percentage: the
resistor divider and the discharge curve sit between it and the batteryPercent
and batteryVoltage you read back. The default 2290 corresponds to roughly
4.10 V and about 88 % at the default divider.
Both /sim routes reject what they don't understand with a 400 rather than
silently ignoring it - an unknown field is 400 unknownField, a body that is not
a JSON object is 400 invalidJson, and a durationMs that is not a whole number
or is below 40 is 400 invalidField. Nothing is applied unless the whole body
validates, so a rejected request never half-lands.
Host files: device.json and settings.json¶
Two files in the data directory hold what AWTRIX keeps in flash, in the same JSON schema as the API:
| File | Device equivalent | Written by |
|---|---|---|
simdata/settings.json |
the stored settings | the settings API / web UI |
simdata/device.json |
device (GPIO / network) config | the system API / web UI |
The app-loop order lives in simdata/apploop.json, radio stations in
simdata/radio.json, Berry sources and their stores in simdata/SCRIPTS/, and
uploaded icons, palettes and melodies in the matching subfolders. Pushed apps are
held in memory, as they are on AWTRIX, so nothing on disk corresponds to them and
restarting the binary clears them.
Hand-editing these files bypasses validation
The API and the web UI validate before they write; editing settings.json or
device.json by hand skips all of that. Values are applied on load with no
checks, and a file that is not valid JSON is discarded entirely - the
simulator silently falls back to defaults and your configuration is gone. If
it behaves strangely after a manual edit, delete the file and let it be
recreated.
Terminal matrix¶
Run the simulator in a terminal and it draws the matrix right there, in ANSI truecolor blocks, redrawn on every rendered frame. Unlike the web UI preview, which polls, the terminal shows every frame with the effective brightness applied, so auto-brightness and moodlight dimming are visible. Log lines scroll below a pinned matrix, and a status line reports the measured fps, the brightness and the current app.
The panel fills the window: each LED is blown up to the largest square block the
width and the free rows allow, separated by a thin grid like the one in the web
UI live preview. Resize the window and the matrix rescales within half a second;
COLUMNS / LINES override the detected size.
It needs a VT-capable terminal (Windows Terminal, Windows 10+ conhost, or any
POSIX terminal), and is on by default unless stdout is redirected. The default
32-wide panel fits from about 34 columns, and wider panels need more. Force it
with --matrix or turn it off with --no-matrix.