Firmware for ESP32-C3/S3 that connects via USB CDC to the hl-hostmon
host script,
polls JSON at fixed intervals, and renders a compact dashboard on a 1.54″ e-ink
(SSD1681, 200×200) using GxEPD2. It supports touch/auto page modes, Dallas (case)
temperature, optional Wi-Fi/OTA, and leaves room for a future PC fan PWM controller.
Current firmware: see
FW_VERSION
(set inplatformio.ini
→build_flags
)
- USB CDC client: sends
INFO
once, thenGET
at a configurable interval - Robust JSON framing (string/escape-aware) + ArduinoJson parsing
- Consistent UI with centered headers and right-aligned values
- Pages: Overview · Network · VMs/LXCs · Disks · Debug
- Touch / Auto page rotation, configurable poll interval
- Dallas 1-Wire temperature (“Case”)
- Wi-Fi / OTA optional (ArduinoOTA from Arduino IDE)
- Future: PC fan PWM on GPIO9 with tacho on GPIO5 (stubbed)
-
MCU: ESP32-C3 (native USB) or ESP32-S3 (works similarly)
-
Display: 1.54″ e-ink, SSD1681, 200×200
GxEPD2 model:GxEPD2_154_D67
-
Display wiring (as used in code):
Signal ESP32 pin CS 7 DC 1 RST 2 BUSY 3 SPI uses the board defaults (no explicit remap needed).
-
Touch: GPIO10 (active-low button or touch input)
-
Dallas: 1-Wire pin configurable (see
config.h
) -
Fan (planned): PWM on GPIO9, Tacho on GPIO5
This project is organized for PlatformIO. Example environment for ESP32-C3 USB:
; platformio.ini (excerpt)
[env:esp32c3-usb]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
build_flags =
-D FW_VERSION="0.1.4"
-D POLL_INTERVAL_MS=2000 ; request interval (ms)
-D LINK_TIMEOUT_S=30 ; "Online/Timeout" threshold on Overview (s)
-D RX_LINEBUF_BYTES=8192 ; serial framing buffer (one JSON object)
-D JSON_DOC_CAP=9216 ; ArduinoJson document capacity
; -D USE_WIFI=1 ; optional Wi-Fi
; -D USE_OTA=1 ; optional OTA (Arduino IDE)
lib_deps =
bblanchon/ArduinoJson
ZinggJM/GxEPD2
adafruit/Adafruit GFX Library
milesburton/DallasTemperature
Build / Upload
- USB upload (default):
PlatformIO: Upload
(envesp32c3-usb
) - OTA (optional): enable
USE_WIFI
andUSE_OTA
, set credentials inconfig.h
, then use Arduino IDE → Network Ports to upload.
Most knobs are compile-time flags (see build_flags
) with config.h
fallbacks:
POLL_INTERVAL_MS
— how often to sendGET
to the hostLINK_TIMEOUT_S
— Overview shows Online/Timeout based on last JSON ageRX_LINEBUF_BYTES
— serial framing buffer (increase for larger payloads)JSON_DOC_CAP
— ArduinoJson capacity (payload-dependent)USE_WIFI
,USE_OTA
— enable optional Wi-Fi/OTA
Runtime toggles live on the Debug page (e.g., debug flag) or via touch/auto mode.
All pages share a centered, bold header with an underline and right-aligned values.
- IP (primary IPv4)
- CPU — percent if available; fallback
L1 <load1>
whencpu.percent
isnull
- RAM —
used/total GiB
(binary GiB) - Case — local Dallas temperature
- Uptime —
Xd Yh
- Link —
Online (Xs)
orTimeout (Xs)
based on last JSON age
- IP
- Status (e.g.,
ok
) - Down / Up — rates from
net
totals (kbps
/Mbps
), split across two lines
- VMs:
running/total
- LXCs:
running/total
- Short lists (up to 2 each):
- name
on the left, status icon on the right
(filled square = running, outline = stopped). Names are ellipsis-truncated to fit.
- name
on the left- Temp right-aligned (10 px left of the icon),
-
if unknown - Activity icon on the far right (filled square = active, outline = idle)
- FW version
- RX age
- OK/ERR
- JSON len (bytes)
- Poll interval (s)
- Mode (TOUCH/AUTO)
- Debug flag
The device writes:
INFO
once on bootGET
everyPOLL_INTERVAL_MS
The host replies with a single JSON object per line (or with newlines—we frame by braces).
Fields used (examples, not exhaustive):
{
"uptime_s": 181284,
"hostname": "bkg-LPS",
"cpu": { "percent": null, "load1": 0.49, "load5": 0.17, "load15": 0.11 },
"ram": { "total_bytes": 16513433600, "used_bytes": 3926523904 },
"filesystems": [{ "mount": "/", "total_bytes": 100861726720, "used_bytes": 14402846720 }],
"proxmox": {
"vm_running": 1, "vm_total": 4,
"lxc_running": 0, "lxc_total": 0,
"vms": [{ "id": 100, "name": "UbuntuLTS24.04", "status": "running" }],
"lxcs": [{ "id": 201, "name": "dns", "status": "stopped" }]
},
"disks": [{ "name": "nvme0n1", "state": "active", "temp_C": null }],
"ip": {
"primary_ifname": "vmbr2",
"primary_ipv4": "192.168.100.103",
"gateway_ipv4": "192.168.100.1",
"ip_status": "ok"
},
"net": {
"window_s": 1,
"total_rx_bps": 83216, "total_tx_bps": 83240
}
}
Notes:
cpu.percent
may benull
; we fall back tocpu.load1
.ram.*_bytes
are parsed as 64-bit and shown as GiB.disks[*].state
:"active"
/"active/idle"
→ active icon; tempnull
→ “-”.net.total_rx_bps/total_tx_bps
preferred; we also accept*_Bps
and per-interface fallback.
- Structure: modular
src/pages/*
andsrc/modules/*
(Wi-Fi, Dallas, etc.) - UI helpers:
ui_theme.h
providesheader(...)
,content_top()
, andui::printRight(...)
- No serial spam: Serial is the host link; all debug info goes to the Debug page
- Extending pages: Prefer right-aligned values; measure and fit text to avoid collisions
- CPU shows “-”: host sent
"cpu":{"percent":null}
and no loads; addload1
or compute percent on host. - No Disks: ensure
disks
array present; temp may benull
(we show “-”). - Time-outs: Overview
Link
usesLINK_TIMEOUT_S
; bump if host polls slower. - JSON too large: check Debug → JSON len vs
RX_LINEBUF_BYTES
. Increase buffers if needed.
We use semver-ish tags (vX.Y.Z
). See CHANGELOG.md
for details.
This project is licensed under GPL-3.0-or-later. See LICENSE
and
THIRD_PARTY_LICENSES.md
.
Libraries:
- GxEPD2 (GPL-3.0), Adafruit GFX (BSD), ArduinoJson (MIT), DallasTemperature (MIT).
- ZinggJM for GxEPD2
- Adafruit for GFX
- Benoît Blanchon for ArduinoJson
- Miles Burton et al. for DallasTemperature