Use case: drive UART and CAN and read the board's answer back¶
The problem. A verified flash says the image is on the chip, not that the
firmware works. The check is to send the board a command over its serial line,
or a frame on its CAN bus, and judge what comes back. Done with a terminal
program, cansend or candump, the agent reads free text, holds a port nobody
knows is held, and can transmit on a bus it was only meant to watch.
What Agentic Hardware-in-the-Loop (Agentic HIL) does instead. Serial ports
and CAN buses are named entries in the operator's configuration, and the agent
reaches them only through tools (com_session_start, com_write, com_read,
and can_session_start, can_send, can_read) or the matching plan steps
(uart_open, uart_write, uart_read, can_open, can_send, can_read).
A read in a plan carries a comparator, which is a claim about the answer. The
run judges the claim, and the report keeps the claim next to what the board
actually said, whether it matched or not.
Serial: a claim, and what the board said¶
The STM32 starter's firmware speaks a small line protocol on the ST-LINK's virtual COM port. Its nominal plan, with the comments removed:
version: 3
name: nucleo-f446re-nominal-status
steps:
- device: dut
action: flash
image_path: build/Debug/stm32-starter.elf
reset_after_flash: false
- device: dut_uart
action: uart_open
clear_buffer: true
- device: dut
action: reset
mode: run
- device: dut_uart
action: uart_read
comparator:
pattern: "\"event\":\"boot\""
timeout_s: 5
- device: dut_uart
action: uart_write
text: "STATUS\n"
- device: dut_uart
action: uart_read
comparator:
pattern: "\"state\":\"READY\",\"diagnostic\":\"NONE\""
timeout_s: 5
The port is opened before the reset and its buffer cleared, so the boot line is captured and every claim after it is a claim about this boot. The last read, as the run on Linux reported it (Agentic HIL 0.21.5, OpenOCD, 2026-09-15, record), abridged:
- 6
route dut_uart
action uart_read
result
Expected pattern matched the COM port output.
port_id dut_uart
timeout_s 5.0
bytes_received 39
reads 1
matched_text_truncated no
comparator
pattern "state":"READY","diagnostic":"NONE"
matched_text
text "state":"READY","diagnostic":"NONE"
encoding utf-8
When the answer is wrong¶
The starter's diagnostic plan makes the same start, then sends DIAG ON and
claims the board reports its self-test fault:
- device: dut_uart
action: uart_write
text: "DIAG ON\n"
- device: dut_uart
action: uart_read
comparator:
pattern: "\"state\":\"DEGRADED\",\"diagnostic\":\"E_SELF_TEST\""
timeout_s: 5
The firmware ships with a planted defect, and the same run reported, abridged:
Failed: comparator_unmet
...
- 6
route dut_uart
action uart_read
result
Expected pattern did not match the COM port output before this step's
timeout.
error_type comparator_unmet
timeout_s 5.0
bytes_received 39
reads 2
comparator
pattern "state":"DEGRADED","diagnostic":"E_SELF_TEST"
received_tail
text {"state":"READY","diagnostic":"NONE"}
The claim and the answer sit side by side: the board took DIAG ON and went on
reporting itself healthy. That points at the command parser, which compared
against DIAG ENABLE, a command the protocol never sends. One string literal
changed, no plan was touched, and the three plans went green on the rebuilt
image. The run had stopped at the failing step, closed the serial session in its
cleanup, and recovered the probe on its own (reap_processes, reset_halt,
probe_target, outcome recovered). The run on Windows (0.21.1, STM32CubeProgrammer
CLI, 2026-09-03,
record)
reported the same failure the same way, in text and in hex.
What holds the serial line¶
- A port is bound by its USB serial number as well as its device path, and the
session confirms that identity when it opens. A port name that has moved to
another board, after a replug or with a second adapter attached, is refused
as
com_port_identity_mismatch, with nothing opened and nothing written to either board. - An entry can keep DTR and RTS released for the session (
assert_dtr: false,assert_rts: false), so a session does not hold a board that wires them to its reset line in reset. The open itself is not proved untouched by that: on Linux, measured with an FT232R over 65 opens, it asserted DTR once for 239 to 943 microseconds before DTR was released for the rest of the session and at close. - Writing needs
allow_writeon the port. Reading needs no permission, because exclusivity carries that load: a run holds the port for its whole duration, so no second reader consumes the answer a claim was waiting for. - Writes are size-capped and reads are buffer-capped.
agentic-hil test-reactor --junit-xml <path>(from 0.21.2) writes one JUnit test case per step beside the JSON report, so a CI dashboard shows the read above as a failure of typecomparator_unmetand the steps after it as skipped. Testing has the mapping.
CAN: what is enforced, and what this page cannot show yet¶
A bus is declared the same way as a port:
can_buses:
dut_can:
adapter: "socketcan" # or "peak", or "process" for a custom bridge
channel: "can0"
bitrate: 500000
listen_only: true
permissions:
allow_write: false
socketcanandpeakrun through python-can, installed withagentic-hil[can]; aprocessbridge is a program you provide that speaks the bridge protocol.listen_only: trueis enforced, not recorded.peaksets PCAN's passive state and reads the mode back from the driver.socketcanreads the kernel's control mode and never sets it, so an interface that is up without listen-only refuses the session rather than joining the bus as an acknowledging node. Aprocessbridge has to confirm the mode in its answer toopen.- An adapter that cannot be held to it refuses with
can_listen_only_unsupportedbefore the bus is touched, or withcan_listen_only_unconfirmedafter asking, and closes again. There is no silent downgrade. - A send on a listen-only bus is refused as
can_listen_only_modebefore any driver call, whateverallow_writesays. A plan with acan_sendon such a bus is refused before its first hardware action. can_readwithoutmax_framesreads the bus's configuredmax_buffer_frames, which is also its ceiling. A read's comparator can name a frame id, claim a pattern over the data bytes and a numeric range for a value it captures; Testing has a plan that does.
None of the runs on this page opened a CAN bus. The starter's bench declares
none, and agentic-hil doctor said so:
CAN buses
None configured.
No published record shows the CAN tools driving a board yet. The rules above are specified in the Safety model and held by the repository's tests; this page does not show them on hardware.
Limits¶
- No expected failure. A plan cannot assert that a send is refused or that a line stays silent, because the first failing step ends the run.
- No waiting on a condition beyond one read. A read waits for its claim up to
its
timeout_sand no longer, and there is no branch, retry or until; from plan version 4,repeatruns a block a fixed number of times. - No per-step description that reaches the report, where a step is identified by its index, route and action.
- The rest is in what a plan cannot express yet.
Try it¶
The STM32 starter runs this loop end to end: three plans, one planted defect in the firmware, and the failing report as the agent's starting point. MCP tools has the serial and CAN tools an agent calls outside a plan, and Configuration the full port and bus entries.