For embedded teams building ECUs, robots, vehicles and networked devices who want the same integration tests to run before and after hardware exists. A network simulator, a YAML test runner and a SIL/HIL harness — unified by one idea: write the test once.
Built for reproducibility
Deterministic by design
An integer-microsecond clock, ECUs stepped in config order — a PASS→FAIL flip is always caused by your change, never by the runner.
One suite, many targets
The same YAML tests drive host-compiled firmware (SIL), the virtual simulation on your laptop, a live CAN bus over SocketCAN — bare, or with a restbus of simulated peers running beside your real ECU — or an Ethernet (UDP) network. Without rewriting anything.
DBC and netmap native
Parse BO_/SG_/VAL_ for CAN, or a netmap of named fields at byte offsets for UDP — Intel/Motorola byte order, signed values, scaling and symbolic value tables.
YAML test DSL
send, set_signal, wait, expect, fault — plus send_udp, set_field, expect_udp, fault_udp for Ethernet. Assert equals, greater_than, less_than, present, absent within us/ms/s deadlines.
Reports that explain
HTML/JSON results with the reason each test failed, and exit codes: 0 all pass · 1 test failures · 2 usage errors.
One-command CLI
init scaffolds vehicle.yaml, DBC and tests; simulate prints the deterministic bus trace; test and report close the loop.
How it works
Write tests in YAML
Plain YAML describes stimulus and assertions against real DBC signals — no C harness, no bespoke framework.
Run them against your real firmware (SIL)
Host-compiled firmware implementing the Ecu trait runs on the virtual bus. Each simulated step has a wall-clock budget (default 100 ms) — an overrun fails the test instead of hanging it.
Or on a virtual network
A deterministic simulator of your network — nodes defined in YAML or Rust — runs on any laptop. The bundle includes an EV powertrain demo: inject overvoltage, brake presses, dropped frames.
Re-run against real hardware
The identical suite drives a physical ECU over SocketCAN, or talks to Ethernet nodes over a real UDP socket. On a bare socket, set_signal, set_field and fault are rejected with a clear error — not silently ignored. Switch the interface to restbus and your simulated peers (config nodes, reactive vECUs) run beside the real ECU as a hardware-in-the-loop rest bus.
See it in action
$ embrig test examples/ev-powertrain/vehicle.yaml PASS brake_press_disables_motor (360 ms) PASS bus_carries_periodic_frames (230 ms) PASS charger_faults_on_stale_battery (910 ms) PASS nominal_conditions_enable_motor (320 ms) PASS overvoltage_disables_motor (410 ms) 5 passed, 0 failed
$ embrig simulate examples/ev-powertrain/vehicle.yaml --duration 600ms --verbose 0 0x100 [8] A0 0F 00 00 5A 02 01 00 0 0x110 [8] 00 00 00 00 00 00 00 00 0 0x120 [8] 01 00 00 00 00 00 00 00 0 0x210 [8] 00 00 00 00 00 00 00 00 0 0x220 [8] 00 00 00 00 00 00 00 00 0 0x230 [8] 03 00 00 00 00 00 00 00 50000 0x220 [8] 01 00 00 00 00 00 00 00 50000 0x230 [8] 03 00 00 00 00 00 00 00 100000 0x100 [8] A0 0F 00 00 5A 02 01 00 … deterministic integer-µs clock, ECUs stepped in config order
# bump the VCU overvoltage threshold from 450 V to 460 V in vehicle.yaml $ embrig test examples/ev-powertrain/vehicle.yaml PASS brake_press_disables_motor (360 ms) PASS bus_carries_periodic_frames (230 ms) PASS charger_faults_on_stale_battery (910 ms) PASS nominal_conditions_enable_motor (320 ms) FAIL overvoltage_disables_motor (410 ms) 0x220.motor_enable expected false, got 1 4 passed, 1 failed # the flip is caused by your change, never by the runner
From laptop to hardware
Write the test once. Increase the realism of the environment without rewriting the test.
Get started in two minutes
No hardware. No CAN adapter. No setup.
A Rust workspace — clone it, run one command.
cargo run --bin embrig -- test examples/ev-powertrain/vehicle.yaml
# → 5 passed, 0 failed
Then go further — same suite, more realism:
# Software-in-the-loop: your firmware on the virtual bus
cargo run --example sil_firmware --package embrig-sil
# → 2 passed, 0 failed
# … or a diff-drive rover: drive, e-stop, over-speed fail-safe
cargo run --example robot_sil --package embrig-sil
# → 4 passed, 0 failed
# … or the Ethernet rover: telemetry, overrides, drop/corrupt faults
cargo run --example udp_rover --package embrig-test
# → 4 passed, 0 failed
# Scaffold your own project
cargo run --bin embrig -- init my-project
cargo run --bin embrig -- test my-project/vehicle.yaml
# Drive a real CAN bus (build with the socketcan feature)
cargo build --features socketcan
cargo run --features socketcan --bin embrig -- \
test my-project/vehicle.yaml --interface vcan0
# … or add a simulated rest bus beside your real ECU
# (interface "hil" is declared in vehicle.yaml with type: restbus)
cargo run --features socketcan --bin embrig -- \
test my-project/vehicle.yaml --interface hil
A test in plain YAML
name: overvoltage_disables_motor
timeout: 5s
steps:
- wait: { time: 300ms }
- set_signal: { ecu: battery, id: 0x100, signal: voltage, value: 460.0 }
- expect: { id: 0x220, signal: motor_enable, equals: false, within: 1s }
- expect: { id: 0x230, signal: state, equals: "SAFE", within: 1s }
The same file runs in the simulator, against SIL firmware, and on the bus. Over 450 V the VCU refuses to enable the motor — a PASS→FAIL flip proves the ECU behaves, not the runner.
Development
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --features socketcan # 178 tests, incl. 11 CLI black-box
cargo run --example sil_firmware --package embrig-sil
cargo run --example robot_sil --package embrig-sil
cargo run --example udp_rover --package embrig-test
scripts/vcan-smoke.sh # optional: exercise the real SocketCAN path on vcan0
Guides: how to SIL-test your firmware · how to HIL-test your firmware · how to test Ethernet (UDP) nodes