Setup & Basics

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 monitor from 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

Thesada - GPL-3.0-only (firmware) / CC BY-NC-SA 4.0 (docs) - License

This site uses Just the Docs, a documentation theme for Jekyll.