Files
DD3-LoRa-Bridge-MultiSender/docs/engineering/local-hil-setup.md
T

5.7 KiB

Safe local DD3 HIL setup

Safety boundary

This harness is for two USB-connected LilyGO boards and, optionally, an electrically isolated USB-UART or optical meter simulator. It does not authorize direct connection to mains-referenced meter conductors. Do not change wiring, GPIO14 role selection, straps, eFuses, secure boot, flash encryption, NVS, credentials, or udev/OS permissions as part of this workflow.

The firmware remains role-neutral: GPIO14 is sampled at boot exactly as before. The hil environment inherits lilygo-t3-v1-6-1 (the current 433 MHz debug build) and adds only ENABLE_HIL_TRACE=1. Production environments are unchanged.

Dependencies and Linux serial access

PlatformIO is accepted from PATH or ~/.platformio/penv/bin/pio. Install Python dependencies into a repository-local environment:

python3 -m venv .venv-hil
.venv-hil/bin/pip install -r requirements-hil.txt

Inspect access with id -nG and stat -c '%A %U %G %n' /dev/ttyACM0. On this machine the user is already in dialout, and the discovered ports are root:dialout with group read/write. The tools never run sudo, modify group membership, change permissions, or install udev rules. If access is absent, stop and ask the system administrator to apply the site's normal policy, then start a new login session.

Discover and approve stable ports

Discovery lists metadata and symlinks but does not open a device:

.venv-hil/bin/python tools/hil/discover.py

Prefer /dev/serial/by-id names. Copy .hil/local.example.toml to ignored .hil/local.toml only after visually confirming the two targets. Set the sender and receiver path, then set approved_access = true only after approving the first serial open. Set approved_flash = true only after separately approving flashing. The tools refuse missing, duplicate, unstable, or unapproved ports. Never commit local /dev paths.

Current discovery on 2026-07-13 found two uniquely identified board candidates and one ambiguous device:

usb-1a86_USB_Single_Serial_5644005006-if00 -> ttyACM0
usb-1a86_USB_Single_Serial_5644020877-if00 -> ttyACM1
usb-1a86_USB_Serial-if00-port0             -> ttyUSB0 (ambiguous; do not access)

Serial role evidence, not the filename, is authoritative. A valid mapping requires a HIL: role event from both devices and must agree with the configured names.

Build, flash, and capture

Building never accesses serial devices and does not require local port configuration:

.venv-hil/bin/python tools/hil/build.py

After explicit access and flash approval is recorded:

.venv-hil/bin/python tools/hil/flash_pair.py --yes-flash
.venv-hil/bin/python tools/hil/capture_pair.py --reset --seconds 180
.venv-hil/bin/python tools/hil/run_baseline.py --yes-flash --seconds 180

flash_pair.py builds the HIL environment once, then uploads the same environment to each approved stable path. It does not issue a full-chip or NVS erase; normal uploading necessarily rewrites the bootloader, partition-table, boot-app, and application address ranges shown in the flash log. --reset pulses the boards' normal USB auto-reset circuit only after capture readers are active. Ctrl+C closes both serial handles and retains all bytes captured so far.

Each run writes beneath artifacts/hil/<UTC timestamp>-<label>/: exact per-device .raw streams, timestamped .log streams, merged events.jsonl, build/flash logs, report.md, and junit.xml. A report fails unless device evidence correlates a sender batch ID through receiver reassembly, decode, ACK transmission, and sender ACK reception.

Meter simulator

Configure ports.meter_simulator only for a separately identified and approved isolated USB-UART/optical adapter connected to the board's safe optical UART boundary. The simulator uses 9600 7E1:

.venv-hil/bin/python tools/hil/meter_simulator.py valid
.venv-hil/bin/python tools/hil/meter_simulator.py malformed_then_valid

Available fixtures cover timeout, truncation, missing terminator, malformed/missing OBIS, invalid/rollback/jump meter seconds, oversize, slow bytes, and malformed-then-valid recovery. Do not point this configuration at either LilyGO console port or an unidentified adapter.

One-shot LoRa fault injection

Fault hooks exist only in the hil build, are disabled at boot, and are consumed once. The capture tool arms them through the already approved console after the requested delay:

.venv-hil/bin/python tools/hil/capture_pair.py --seconds 180 --fault drop_chunk --fault-after 40
.venv-hil/bin/python tools/hil/run_baseline.py --yes-flash --fault suppress_ack --seconds 240
.venv-hil/bin/python tools/hil/run_fault_suite.py --seconds 600

Supported hooks: drop, duplicate, or post-CRC corrupt one chunk; suppress or delay one ACK response; and send one ACK response with a wrong batch ID. Clear pending hooks with HILCMD:clear. Production builds contain no active hook implementation.

Interpret and restore

Regenerate reports with:

.venv-hil/bin/python tools/hil/report.py artifacts/hil/<timestamp>
HIL_ARTIFACT=artifacts/hil/<timestamp> .venv-hil/bin/pytest -v -m hil hil_tests/

FAIL means required device evidence is missing or contradictory. Meter evidence becomes BLOCKED when a real meter or approved isolated simulator is silent/unavailable. Never convert a blocked or missing-evidence result into a pass manually.

To restore normal firmware, use the correct existing production environment for the board's radio (lilygo-t3-v1-6-1-prod or lilygo-t3-v1-6-1-868-prod) and the same approved flash procedure/port boundary. Reconfirm the environment and both target paths before uploading; restoring production is also a flash operation and requires explicit approval.