Skip to main content

Hardware-in-the-loop testing (hwtest/)

A pre-release harness that catches the bugs a compiler cannot — the ones that only appear on real timing and real silicon (reboot mid-record, dropped-BLE truncation, delete-during-upload splicing, verified trim, crash-safe delete, OTA). It drives a real device and asserts on the firmware's own serial log plus the bytes the server actually stored.

11 scenarios USB serial + BLE CI-gated exit code sate CLI

:::tip Use the sate CLI Everything below is wrapped by a single professional command — sate — which also flashes firmware and diagnoses hardware faults:

sate test # run the recorder harness (--sim for no board)
sate test -t pendant # pendant over BLE
sate flash recorder # build + flash (auto-detects the port)
sate flash pendant # build + flash (Seeed core)
sate devices --ble # list serial ports + scan the pendant
sate doctor --device # reset the board + diagnose real hardware faults

Install it with pip install -e hwtest (gives a sate command), or run in place with ./hwtest/sate …. sate <command> -h shows options. :::

What it checks

ScenarioGuardsDevice
boot_healthBoots to [MEM] ready, no crash/hang (LV_TICK_CUSTOM trap)Recorder
reboot_resumeA button take interrupted by a reboot auto-resumesRecorder
byte_matchUploaded bytes on the server == bytes the device sentRecorder
verified_trimSD audio is freed only after the server confirms itRecorder
delete_journalA delete interrupted by a reboot heals (no hidden takes)Recorder
delete_during_uploadDelete during an upload doesn't splice two takesRecorder
pendant_advertisePendant is discoverable + connectsPendant
pendant_streamLoud stream flows near-live, no nap-wipe / dropped-packet stallPendant
pendant_stopNotifies stop after 0x00Pendant
pendant_batteryBattery reads 0–100 with a sane charging bitPendant
pendant_findmeFind-me flashes the LEDsPendant

Two transports

  • Recorder — tested over USB serial + device-api. The harness resets the board (RTS/DTR), drives record/reboot/delete, reads the serial log ([MEM] ready, [CONN] resume … session …, [CONN] uploaded … (N bytes), [REC] healed interrupted delete), and confirms bytes via GET /sessions/verify.
  • Pendant — tested over BLE (the Mac acts as the BLE central, the app's role): connect, send 0x01/0x00/0x02, measure the PCM stream + battery.

Setup

pip install -e hwtest # installs the `sate` command + deps
cp hwtest/config.example.toml hwtest/config.toml # fill in serial port, server, pendant name
sate doctor # confirm pyserial/bleak/arduino-cli are ready

The recorder firmware must be built CDCOnBoot=cdc,USBMode=hwcdc (else no serial). For the pendant, grant Bluetooth permission to Terminal/Python and unpair it from the phone (one BLE central at a time).

Run

sate test # recorder, all scenarios
sate test -t pendant # pendant over BLE
sate test --sim # self-test the harness, no board
sate test --only byte_match,reboot_resume # a subset
sate gui # native window (or double-click "SATE Hardware Test.app")
sate dashboard # browser dashboard

The GUI has a PORT picker (auto-scans /dev/cu.usbmodem*) for the recorder and a PENDANT scan for BLE. Bench steps that need a human (press RECORD, make noise) appear as a Done ▸ button. Exit code is non-zero on any FAIL/ERROR, so it drops into CI / a pre-flash gate.

The raw entry points still work if you prefer (python3 hwtest/run.py --config …, gui.py, dashboard.py).

Diagnose a board — sate doctor --device

Before (or instead of) a full run, probe the actual hardware. sate doctor --device resets the board, reads its boot log, and reports real faults with fixes:

sate doctor --device # recorder over serial
sate doctor -d -t pendant # pendant over BLE (advertising, connect, battery)
It flagsFrom
No serial outputwrong CDC build / unpowered / wrong port
Crash · panic · brownout · boot-loopGuru Meditation, Backtrace, repeated boot banners
SD card init failureSD_MMC.begin/setPins failed
Audio codec failureES8311 init failed
PSRAM not detected / low contiguous heappsram free = 0, largest < 40 KB
Setup hangnever reached [MEM] ready
Unclaimed / failing registrationprovisioned=0, register … code<0

It exits non-zero on any fault, so it also gates a flash.

Limitations (honest)

  • boot_health proves setup() finished; a frozen LVGL still prints [MEM] ready, so add a post-boot UI-liveness marker for full boot-hang coverage.
  • delete_during_upload and a true brownout need precise timing (best with a power relay); the prompted versions are "assisted".
  • Full pendant → app → server upload is the app's job; the harness checks the pendant firmware's BLE stream + control directly.