A PC stats dashboard for the Lilka v2 handheld (ESP32-S3, 240x280 ST7789)
- A Lilka v2 board.
- A microSD card, formatted FAT32, with a
config.jsonnext to the firmware (see "Where config.json lives" below, andsd_card_template/for the format). - A reachable Prometheus server (
prometheus.urlinconfig.json) already scraping whatever exporter(s) provide the metrics you want. - WiFi: either already configured through the Lilka's stock
keiralauncher (its WiFi setup screen saves credentials to NVS, which this firmware reads automatically), or supplied viawifi.ssid/wifi.passwordinconfig.jsonas a fallback.
{
"wifi": { "ssid": "...", "password": "..." },
"prometheus": { "url": "http://192.168.1.50:9090", "poll_interval_ms": 3000 },
"clock": { "timezone_offset_min": 120, "ntp_server": "pool.ntp.org" },
"queries": [
{ "label": "CPU", "query": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[1m])) * 100)" },
{ "label": "RAM", "query": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100" },
{ "label": "GPU", "query": "avg(nvidia_smi_utilization_gpu_ratio) * 100" }
]
}wifi— optional if NVS already has credentials from keira; required otherwise.prometheus.url— required. No trailing slash needed (it's stripped if present).clock— optional, both keys default to UTC /pool.ntp.org. Only used to render the fallback clock screen (see below); has no effect on the gauges themselves.queries— optional array, one object per gauge:{"label": "...", "query": "..."}.- Omit
queriesentirely to get the built-in node_exporter (Linux) defaults for CPU/RAM/GPU (seedefaultMetrics()insrc/app_config.cpp). There is no GPU-metric convention in node_exporter itself, so that default assumesnvidia_gpu_exporteris also being scraped — override it for AMD/Intel GPUs or a different exporter. - Include as many entries as you want — CPU/RAM/GPU, temperatures, disk usage, network
throughput, container counts, whatever your Prometheus already has.
labelis clipped to 6 characters to stay legible on a gauge. - Set an entry's
"query"to""to keep it in the list but turn it off (shows "off", never queried) rather than deleting it. - Every query is expected to resolve to a single scalar, generally 0-100 (percentages read best on the bar, but any number is displayed).
- Omit
- More gauges than fit on screen at once are paginated, not dropped: the dashboard shows as
many rows as comfortably fit (usually 3-4) and you flip pages with LEFT/RIGHT. The footer
shows
pg 2/3whenever there's more than one page.
This is a standalone PlatformIO project (not a keira app/plugin):
cd lilka_monitoring
pio run -t uploadIt uses the lilka SDK from the PlatformIO registry (lib_deps = lilka) and the
boards/lilka_v2.json board definition, matching the SDK's own
firmware template.
- A — force an immediate refresh (dashboard) / back to dashboard (status screen).
- LEFT / RIGHT — switch page when there are more gauges than fit on one screen.
- START — toggle the status screen (WiFi/Prometheus connection details, per-metric status, last error).
- A single query fails (e.g. only the GPU exporter is down): that gauge shows
N/A/ERRon its own; every other gauge keeps updating normally. - Prometheus itself is unreachable (every configured query fails in the same poll cycle):
the dashboard replaces itself with a full-screen local clock (synced over NTP once WiFi
connects) plus a "Prometheus unreachable, retrying..." message, instead of a screen full of
ERR. It switches back to the gauges automatically as soon as a poll succeeds again. - No
queriesconfigured (an explicit empty"queries": []): a short message tells you to add entries toconfig.jsoninstead of showing an empty dashboard. - WiFi drops: reconnects automatically (retried every 30s) without blocking the UI.
- Prometheus is polled over plain HTTP with a 4s timeout per query; a slow/unreachable server degrades the update rate but never hangs the input loop indefinitely.