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 base/data/ca.crt

Verify:

openssl x509 -in base/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 or wrong, TLS still connects but logs [WRN][MQTT] No CA cert - insecure. MQTT and OTA will work but without certificate verification.

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 @base/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 ISRG Root X1 (for Let’s Encrypt / GitHub OTA) and USERTrust ECC (for github.com). Both are needed for end-to-end GitHub OTA. For cellular MQTT, the modem uses the same /ca.crt file - if absent, it connects without CA verification and logs a warning.


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
mqtt MQTT: connected broker: ...:8883
module.list Lists enabled modules with [x]
fs.ls / LittleFS root listing
fs.cat /config.json Config JSON content
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 - CERN-OHL-P-2.0 / GPL-3.0-only / CC BY 4.0

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