# 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:

```bash
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:

```bash
.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:

```text
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:

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

After explicit access and flash approval is recorded:

```bash
.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`:

```bash
.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:

```bash
.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:

```bash
.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.
