First-class Rust access to Lager nets, so embedded
developers can write their entire hardware-in-the-loop test suite in Rust
and run it with cargo test — no Python required.
The crate is a pure HTTP/JSON client of the Lager box's API (port 9000): power supplies, battery simulators, e-loads, solar simulators, GPIO, ADC, DAC, thermocouples, watt meters, energy analyzers, SPI, I2C, USB hub ports, robot arms, webcams, routers, and streaming UART — plus the box-level capabilities (its own BLE adapter, WiFi interface, and BluFi ESP32 provisioning). Debug-probe nets (flash / erase / reset / memory reads / RTT) talk to the box's debug service on port 8765.
The package publishes as
lager-net(the barelagername is taken on crates.io), but the library target is namedlager, so your code readsuse lager::LagerBox;.
# Cargo.toml
[dev-dependencies]
lager = { package = "lager-net", version = "0.1" }// tests/boot.rs
use lager::{LagerBox, Level};
#[test]
fn dut_boots_at_3v3() -> lager::Result<()> {
let lager = LagerBox::from_env()?; // reads LAGER_BOX_HOST
let supply = lager.supply("supply1");
let boot_ok = lager.gpio("boot_ok");
supply.set_voltage(3.3)?;
supply.enable()?;
// Hardware-timed wait on the box; returns elapsed seconds.
let t = boot_ok.wait_for_level(Level::High, 5.0)?;
println!("booted in {t:.3}s");
supply.disable()
}LAGER_BOX_HOST=192.168.1.42 cargo testLagerBox::connect("hostname-or-ip") also works, with an optional
host:port or full URL.
| Handle | Constructor | Highlights |
|---|---|---|
Supply |
lager.supply(name) |
set_voltage, set_current, enable/disable, OCP/OVP, state() |
Battery |
lager.battery(name) |
set_soc, set_voc, model/capacity/mode, state() |
Eload |
lager.eload(name) |
set(EloadMode::Cc, 0.5), setpoint, state() |
Solar |
lager.solar(name) |
set/stop PV simulation, irradiance/set_irradiance, voc, mpp_voltage/mpp_current, resistance, temperature |
Gpio |
lager.gpio(name) |
input, output, toggle, wait_for_level (hardware-timed) |
Adc / Dac |
lager.adc(name) / lager.dac(name) |
read(); set(volts) |
Thermocouple |
lager.thermocouple(name) |
read() in °C |
WattMeter |
lager.watt_meter(name) |
power/current/voltage/all(duration) |
EnergyAnalyzer |
lager.energy_analyzer(name) |
read_energy, read_stats |
Spi |
lager.spi(name) |
configure, read, write, read_write, transfer |
I2c |
lager.i2c(name) |
configure, scan, read, write, write_read |
UsbPort |
lager.usb(name) |
enable/disable/toggle/state |
Arm |
lager.arm(name) |
position, move_to/move_by, go_home, motor enable/disable, set_acceleration |
Webcam |
lager.webcam(name) |
start/stop MJPEG stream, url, status |
Router |
lager.router(name) |
system_info, interfaces/clients/leases, block_internet, generic command(action, params) |
DebugNet |
lager.debug(name) |
connect, flash, erase, reset, read_memory, info/status, rtt (blocking) |
Uart |
lager.uart(name)? (feature uart) |
streaming read, write, wait_for(b"boot ok", ...) |
Box-level capabilities (the box's own hardware, no net name):
| Handle | Constructor | Highlights |
|---|---|---|
Ble |
lager.ble() |
scan/scan_named, info/connect (GATT enumeration), disconnect |
Wifi |
lager.wifi() |
status, scan, connect(ssid, password), delete |
Blufi |
lager.blufi() |
scan, connect, provision(device, ssid, password), wifi_scan, status, version |
Discovery and box health: lager.nets(), lager.health(), lager.status()
(status().capabilities.net_command tells you the box image is new enough;
net_command_roles / ble_command / wifi_command / blufi_command report
the newer arm/webcam/router roles and box-level endpoints).
| Feature | Default | What you get |
|---|---|---|
blocking |
yes | LagerBox on ureq — tiny dependency tree, no tokio |
async |
no | AsyncLagerBox on reqwest/tokio; same methods, .awaited |
uart |
no | Uart streaming sessions over the box's Socket.IO /uart namespace |
Both clients execute the exact same request builders and response parsers
(the wire module), so the two transports cannot drift apart.
lager = { package = "lager-net", version = "0.1", features = ["async"] }The box serializes access per physical instrument: every net command runs
under a per-device lock in the box's single-owner hardware service, so
parallel cargo test threads can never interleave I/O on one instrument
(e.g. a LabJack shared across GPIO/ADC/SPI nets, or a Keithley shared by
supply and battery roles). Tests sharing a net still observe each other's
state changes — partition nets across tests, or run
cargo test -- --test-threads=1 when that matters.
Timeout budgets mirror the Lager CLI: quick commands use 10 s; watt/energy
integration windows and wait_for_level widen (or drop) the client timeout
automatically so a healthy long measurement is never aborted mid-flight.
Everything returns lager::Result<T> with a single Error enum:
Connection— box unreachable (network/Tailscale/box offline)Timeout— the box stalled past the (already widened) budgetBox { status, message }— the box refused or the hardware failedUnsupportedByBox— HTTP 501: the box image predates this endpoint; update the boxNotSupportedByBox— the net type is a documented stub (see below)
DebugNet drives a J-Link/OpenOCD debug probe through the box debug service
(port 8765, published on the box host):
let debug = lager.debug("debug1");
debug.connect()?;
debug.erase()?;
debug.flash("firmware.hex")?; // .hex/.elf/.bin inferred from extension
debug.reset(false)?;
let head = debug.read_memory(0x0800_0000, 16)?;
// Stream RTT logs (blocking client) and assert on target output:
use std::io::{BufRead, BufReader};
let mut lines = BufReader::new(debug.rtt()?).lines();
assert!(lines.next().transpose()?.unwrap().contains("boot ok"));If the debug service is reached through an SSH tunnel, point the crate at it
with LagerBox::builder(host).debug_service_url("http://127.0.0.1:8765") or
the LAGER_DEBUG_SERVICE_URL env var.
Oscilloscope / logic-analyzer workflows are not exposed on any box HTTP API
yet. Scope ships as a documented stub whose methods return
Error::NotSupportedByBox; see
MISSING_ENDPOINTS.md for the endpoint sketch.
The crate's own test suite is hermetic (cargo test runs against a mock
box). To exercise a real box:
LAGER_BOX_HOST=192.168.1.42 cargo test --test hardware -- --ignored
# opt into net-specific tests:
LAGER_BOX_HOST=... LAGER_TEST_SUPPLY_NET=supply1 cargo test --test hardware -- --ignoredThere is also a runnable example:
LAGER_BOX_HOST=192.168.1.42 cargo run --example power_cycle -- supply1 adc1- A Lager box with software new enough to serve
POST /net/command(checklager.status()?.capabilities.net_command). - Rust 1.75+.
Apache-2.0