Setup & Basics
Test Script (recommended)
pip install pyserial
# Auto-detect port, run all tests
python tests/test_firmware.py
# Skip hardware-dependent manual groups
python tests/test_firmware.py --skip ota,cellular,ads1115
# Automated checks only
python tests/test_firmware.py --skip sensors,ads1115,mqtt,ota,websocket,sd,cellular
The script runs 13 test groups - 6 fully automated (parses serial output), 7 manual/assisted (prompts you to confirm what you observe). See tests/README.md for the full group list.
Test Environment
- Device: any supported board (see Hardware for board targets)
- Serial monitor: 115200 baud (
pio device monitorfrom a real terminal - not VSCode integrated terminal) - Web dashboard:
http://[device-ip]/ - MQTT monitor:
mosquitto_sub -h <broker> -p 8883 --cafile ca.crt -u <user> -P <pass> -t 'thesada/node/#' -v
0. Pre-flight: CA Certificate + First Flash
Before first flash, ensure data/ca.crt contains the correct root CA. For Let’s Encrypt encrypted brokers and GitHub OTA, the ISRG Root X1 covers both (but check before, could change anytime):
curl -s https://letsencrypt.org/certs/isrgrootx1.pem -o data/ca.crt
Verify:
openssl x509 -in data/ca.crt -noout -subject -issuer
# subject=CN=ISRG Root X1
# issuer=CN=ISRG Root X1 (self-signed root - correct)
Upload filesystem (includes ca.crt and config.json):
pio run -e esp32-owb --target uploadfs
If ca.crt is absent, the firmware falls back to a PROGMEM bundle baked into the build (common public TLS roots). MQTT and OTA stay cert-validated as long as the broker chain is covered by one of those roots. The log line is [INF][MQTT] /ca.crt missing - using baked PROGMEM CA bundle. If the broker chain is not covered by the bundle, the connection fails - upload the correct ca.crt.
Runtime ca.crt Upload (deployed devices)
If the device is already flashed and accessible over the network, upload ca.crt via the file API without reflashing:
# Upload ca.crt to LittleFS
curl -u admin:changeme -X POST \
'http://[ip]/api/file?path=/ca.crt&source=littlefs' \
-H 'Content-Type: application/octet-stream' \
--data-binary @data/ca.crt
# Restart to apply
curl -u admin:changeme -X POST \
'http://[ip]/api/cmd' \
-H 'Content-Type: application/json' \
-d '{"cmd":"restart"}'
Verify after reboot:
curl -u admin:changeme -X POST \
'http://[ip]/api/cmd' \
-H 'Content-Type: application/json' \
-d '{"cmd":"cat /ca.crt"}'
The PEM bundle should contain the roots that cover your broker and your OTA upstream. The PROGMEM fallback ships ISRG X1/X2 (Let’s Encrypt), DigiCert Global Root CA/G2/G3, and USERTrust ECC, which covers most public chains; use a flash-resident /ca.crt to scope trust tighter or to add a private CA. For cellular MQTT, the modem uploads the same trust source the WiFi path uses (file first, PROGMEM fallback otherwise) before connecting.
1. Boot + Config
| Check | Expected |
|---|---|
Serial shows thesada-fw vX.Y.Z |
Version matches thesada_config.h |
Serial shows [INF][Config] (no error) |
config.json parsed OK |
Serial shows [INF][WiFi] Connected to <ssid> |
WiFi connects to strongest configured SSID |
Serial shows [INF][MQTT] Connected |
MQTT broker reachable |
Serial shows [INF][Shell] Shell ready - xx commands |
Shell initialized - Commands available is depending on compiled modules |
Serial shows [INF][Lua] /scripts/main.lua executed |
Lua boot script ran |
Serial shows [INF][Lua] /scripts/rules.lua executed |
Lua rules loaded |
Serial shows [INF][Boot] Ready. Type 'help' for commands. |
Boot complete |
NTP log timestamps: once NTP syncs, log lines gain an ISO 8601 timestamp between the level and the tag:
[INF][2026-03-22T14:32:00Z][WiFi] Connected to myssid
Before sync the format is [INF][WiFi] .... Run net.ntp to confirm - it reports log timestamps: active or log timestamps: pending sync.
Quick check via shell:
selftest
Should show all [PASS] with at most a few [WARN] for optional items.
2. Heartbeat LED
| Check | Expected |
|---|---|
config.get device.heartbeat_s returns -1 |
LED stays off - disabled |
Set device.heartbeat_s to 10, restart |
[INF][Heartbeat] Ready - every 10s in boot log |
| Wait 10-12 s | Blue CHGLED pulses once (~150 ms) |
Set device.heartbeat_s to 3 (below minimum) |
Clamped to 5 s automatically |
Set device.heartbeat_s to -1, restart |
[INF][Heartbeat] Disabled - LED stays off |
3. Shell (serial + WebSocket)
The same commands work in both the serial terminal and the web terminal.
| Command | Expected output |
|---|---|
help |
Lists all commands with descriptions |
version |
thesada-fw vX.Y.Z (date time) |
heap |
Free: XXXXXX B Min: XXXXXX B Max alloc: XXXXXX B |
uptime |
0d 00:05:12 |
net.ip |
WiFi: connected + IP, SSID, RSSI, MAC |
net.ping 8.8.8.8 |
8.8.8.8 resolved to 8.8.8.8 |
net.ntp |
NTP: synced UTC: 2026-03-22T... + log timestamps: active |
net.mqtt |
MQTT: connected broker: ...:8883 + subscription table |
module.list |
Lists enabled modules with [x] |
fs.ls / |
LittleFS root listing |
fs.cat /config.json |
Config JSON content |
fs.write /test.txt hello |
Wrote 5 bytes to /test.txt |
fs.cat /test.txt |
hello |
fs.rm /test.txt |
Removed |
fs.df |
LittleFS + SD usage |
config.get mqtt.broker |
Broker hostname |
config.dump |
Full config JSON |
selftest |
[PASS] / [WARN] lines + === X passed, Y failed === |
unknown |
Unknown command: unknown |
4. Web Dashboard
| Check | Expected |
|---|---|
GET http://[ip]/api/info (no auth) |
{"version":"...","build":"...","device":"..."} |
GET http://[ip]/ with wrong password |
401, no dashboard |
| Dashboard loads with correct password | Sensor table visible |
| Sensor values update every ~60 s | Timestamp refreshes |
| Battery %, Battery V, Battery Charge State rows visible | Shows percent, voltage, Charging/Discharging |
| Battery % red when <= 20%, green when charging | Color coding works |
| MQTT status bar visible above sensor table | Green dot + MQTT connected + last publish time |
| MQTT disconnected state | Red dot + MQTT disconnected |
| Admin - Terminal tab | [connected] appears; live log lines flow in |
Log level filter set to WRN |
Only [WRN] lines visible; others hidden |
Log level filter set to ALL |
All log lines visible again |
| Clear button | Terminal output cleared |
Admin - Terminal - type version |
Firmware version returned via WebSocket |
Admin - Terminal - type help |
All commands listed |