lager (Rust)
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;.
Quickstart
# Cargo.toml
[]
= { = "lager-net", = "0.2" }
// tests/boot.rs
use ;
LAGER_BOX_HOST=192.168.1.42
LagerBox::connect("hostname-or-ip") also works, with an optional
host:port or full URL.
Net types
| 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).
Features
| 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-net", = "0.2", = ["async"] }
Parallel tests and instrument safety
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.
Boxes behind an authenticating gateway
Boxes fronted by an authenticating reverse proxy reject unauthenticated
traffic with 401 + an X-Gateway-Auth-Url header (the same contract the
Lager CLI speaks). The crate handles this transparently:
-
CLI session reuse (zero config): after
lager login <auth_url>, the crate picks up the session from the CLI's token store (~/.lager_gateway_auth, orLAGER_GATEWAY_AUTH_FILE), attachesAuthorization: Bearerto every request — including debug-service and UART Socket.IO traffic — and refreshes expired access tokens automatically. The box→auth-server link is learned from the gateway's discovery header on first contact and the denied request is retried within the same call. -
Pinned token (CI): supply a token directly when there is no CLI login on the machine:
#or set the
LAGER_GATEWAY_TOKENenvironment variable.
Plain (ungated) boxes are unaffected: no header is sent and none of this
code runs. When a gateway asks for auth and no usable credential exists,
calls fail with Error::AuthRequired naming the auth server to log into.
Errors
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 boxAuthRequired— the box's gateway wants a bearer token and none is available: runlager login <auth_url>or setLAGER_GATEWAY_TOKENNotSupportedByBox— the net type is a documented stub (see below)
Firmware, flashing, and RTT
DebugNet drives a J-Link/OpenOCD debug probe through the box debug service
(port 8765, published on the box host):
let debug = lager.debug;
debug.connect?;
debug.erase?;
debug.flash?; // .hex/.elf/.bin inferred from extension
debug.reset?;
let head = debug.read_memory?;
// Stream RTT logs (blocking client) and assert on target output:
use ;
let mut lines = new.lines;
assert!;
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.
Not yet on the HTTP API
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.
Hardware smoke tests
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
# opt into net-specific tests:
LAGER_BOX_HOST=... LAGER_TEST_SUPPLY_NET=supply1
There is also a runnable example:
LAGER_BOX_HOST=192.168.1.42
Requirements
- A Lager box with software new enough to serve
POST /net/command(checklager.status()?.capabilities.net_command). - Rust 1.75+.
License
Apache-2.0