High-Level Overview
USER COMPUTER LAGERBOX (x86-64, Ubuntu 22.04+)
+---------------------------+ +----------------------------------------------+
| | | |
| $ lager supply psu1 | | Docker Container ("lager") |
| voltage 3.3 | Tailscale VPN | +----------------------------------------+ |
| --yes | (WireGuard) | | Flask/WebSocket Server | |
| | ───────────────> | | Python execution service | |
| $ lager python | SSH + HTTP | | Debug service (GDB) | |
| my_test.py | over encrypted | | | |
| | tunnel | | Hardware Service | |
+---------------------------+ | | | | |
| | v | |
| | Dispatchers --> Drivers | |
GITHUB ACTIONS RUNNER | | | | | |
+---------------------------+ | +-----|--------------|-------------------+ |
| | | | | |
| $ lager python | Tailscale VPN | USB / Serial / VISA / LAN |
| tests/ci/test.py | ───────────────> +--------|--------------|----------------------+
| --box $BOX_IP | (ephemeral key) | |
| | +--------|--------------|----------+
+---------------------------+ | INSTRUMENTS |
| |
| Power Supply Oscilloscope |
| LabJack T7 Debug Probe |
| Battery Sim USB Hub |
| E-Load Thermocouple |
+--------|-------------------------+
|
| Wires / probes / pins
v
+------------------+
| |
| DUT (Device |
| Under Test) |
| |
+------------------+
lager.* hardware libraries.
Terminology
| Term | Definition |
|---|---|
| CLI | The lager-cli Python package (installed via pip install lager-cli). A Click-based command-line tool that runs on the developer’s laptop. |
| Tailscale VPN | A WireGuard-based mesh VPN that creates an encrypted tunnel between the developer’s machine and the Lagerbox. |
| Lagerbox | Any x86-64 machine running Ubuntu 22.04 or newer, physically co-located with the test instruments. Runs a Docker container hosting the Flask/WebSocket server and hardware drivers. |
| Net | A logical name (e.g., psu1, uart0) that maps to a specific instrument + channel + address. Stored in /etc/lager/saved_nets.json on the box. |
| DUT | Device Under Test — the embedded board or product being tested. |
| Instrument | A piece of test equipment (power supply, oscilloscope, LabJack, debug probe, etc.) connected to the box via USB, serial, or LAN. |
lager python | CLI command that uploads a user-written Python script to the box for execution with full access to lager.* hardware libraries. |
Lagerbox Internals
LAGERBOX HOST (x86-64, Ubuntu 22.04+)
+-----------------------------------------------------------------------+
| |
| /etc/lager/ ~/third_party/ |
| saved_nets.json JLink_Linux_*/ (optional) |
| available_instruments.json customer-binaries/ (optional) |
| box_id |
| |
| Docker Container "lager" (--restart always) |
| +-------------------------------------------------------------------+ |
| | | |
| | Port 9000 -- Flask + SocketIO -- Main Box API | |
| | | (UART, supply, battery, instruments, | |
| | | nets, lock) | |
| | v | |
| | Port 5000 -- HTTP Server -- Python Execution Service | |
| | | | |
| | | receives impl script + LAGER_COMMAND_DATA | |
| | | env var (JSON), executes it, streams results | |
| | v | |
| | Port 8765 -- WebSocket Server -- Debug Service (GDB, OpenOCD) | |
| | | |
| | Port 8100 -- HTTP Server -- MCP Service (AI tool integration) | |
| | | |
| | Port 8080 -- Hardware Service (internal only, not exposed) | |
| | | | |
| | v | |
| | +------------------+ | |
| | | NetsCache | Thread-safe singleton | |
| | | (cache.py) | mtime-based invalidation | |
| | +--------+---------+ O(1) lookup by net name | |
| | | | |
| | v | |
| | +------------------+ | |
| | | Dispatchers | BaseDispatcher subclasses | |
| | | (per domain) | driver caching, net resolution | |
| | +--------+---------+ | |
| | | | |
| | v | |
| | +------------------+ | |
| | | Drivers | VISA/SCPI, pySerial, LJM, | |
| | | (per instrument)| pyOCD, aardvark_py, etc. | |
| | +--------+---------+ | |
| | | | |
| +--------------------+----------------------------------------------+ |
| | |
| USB / Serial / VISA / LAN |
+----------------------+------------------------------------------------+
|
v
INSTRUMENTS
Port Summary
| Port | Service | Exposed | Purpose |
|---|---|---|---|
| 9000 | Flask + SocketIO | Yes (VPN only) | Main box API: UART streaming, live supply/battery WebSockets, instrument discovery, net listing, box lock |
| 5000 | HTTP | Yes (VPN only) | Python execution service: receives CLI commands, runs impl scripts |
| 8765 | WebSocket | Yes (VPN only) | Debug sessions (GDB, flash, reset) |
| 8100 | HTTP | Yes (VPN only) | MCP service for AI tool integration |
| 8080 | Hardware Service | No (container-internal) | Instrument control via Device proxy |
| 8081 | HTTP | Yes (if PicoScope present) | Oscilloscope streaming UI |
| 8082-8085 | TCP / WebSocket | Yes (if PicoScope present) | Oscilloscope daemon (commands, browser streaming, database streaming, CLI WebSocket) |
| 8086+ | HTTP | Yes (if webcams present) | Webcam MJPEG streaming (one port per camera) |
| 22 | SSH | Yes | Direct SSH access for deployment and debugging |
Optional Control Plane Integration
Lager boxes expose an authenticated SSH-key-sync endpoint (POST /authorize-key on port 9000) that an external control plane can use to provision access without a human typing SSH commands. If /etc/lager/control_plane.json is present with an authorize_token, requests carrying that token as a Bearer token can add public keys to /etc/lager/authorized_keys.d/, which the box’s startup script and systemd path unit merge into ~/.ssh/authorized_keys.
Lager itself does not require or run a control plane — this is a hook, not a dependency. Leaving control_plane.json absent simply disables the endpoint.
Commercial control planes that build on this hook — for fleets that need org/RBAC/SSO, audit logging, and scheduling on top of Lager — are listed on the Professional Services directory.
Net Abstraction
A Net is the central abstraction that decouples CLI commands from physical hardware details.CLI command saved_nets.json entry Physical hardware
+---------------------+ +----------------------------+ +---------------------+
| | | { | | |
| lager supply psu1 | ----> | "name": "psu1", | ---> | Rigol DP832 |
| voltage 3.3 | | "type": "power-supply", | | Channel 1 |
| | | "channel": 1, | | VISA: USB0::... |
+---------------------+ | "instrument": { | +---------------------+
| "name": "rigol-dp832",|
| "address": "USB0::.."|
| }, |
| "params": { |
| "voltage_limit": 5.0 |
| } |
| } |
+----------------------------+
Supported Net Types
| Net Type | Instruments |
|---|---|
power-supply | Rigol DP800, Keithley 2200/2280, Keysight E36x00 |
battery | Keithley 2281S |
eload | Rigol DL3021 |
solar | EA PSI / EL series |
analog | Rigol MSO5000 (oscilloscope analog channel) |
logic | Rigol MSO5000 (logic analyzer channel) |
adc | LabJack T7, USB-202 |
dac | LabJack T7, USB-202 |
gpio | LabJack T7, USB-202 |
thermocouple | Phidget thermocouple |
watt | Yocto-Watt, Joulescope JS220 |
debug | J-Link, CMSIS-DAP, ST-Link (via pyOCD) |
uart | USB-to-serial adapters |
i2c | Aardvark, LabJack T7, FT232H |
spi | LabJack T7, FT232H |
arm | Rotrics Dexarm |
usb-hub | Acroname, YKUSH |
Execution Flows
CLI Command Execution
Step-by-step data path forlager supply psu1 voltage 3.3 --yes:
DEVELOPER LAPTOP NETWORK LAGERBOX
================ ======= ========
1. User runs:
$ lager supply psu1
voltage 3.3 --yes
|
v
2. CLI resolves box
- reads .lager/config
- finds box IP for "psu1"
|
v
3. CLI builds command JSON
|
v
4. CLI uploads impl script 5. SSH/HTTP over
cli/impl/power/supply.py ---------> Tailscale VPN --------> 6. Flask receives
+ sets env var (encrypted) request on :5000
LAGER_COMMAND_DATA=<JSON> |
v
7. Box executes
supply.py in
subprocess
|
v
8. Dispatcher looks up
"psu1" in NetsCache,
selects driver
|
v
9. Driver sends SCPI
command to instrument
|
v
14. CLI displays: <--------------- result JSON <----------- 10. Result streamed back
"Voltage set to 3.300V" streamed back
Custom Script Execution (lager python)
The lager python command uploads a user-written Python script to the box for execution. It uses the same Flask :5000 execution path as CLI commands.
$ lager python my_test.py --box mybox --env VOLTAGE=3.3 --timeout 300
lager.* hardware libraries, and output is streamed back in real time.
Physical Wiring
How instruments physically connect between the Lagerbox and the DUT: LAGERBOX
(x86-64, Ubuntu 22.04+)
+----------+
| |
USB-A | USB-A | USB-A LAN
port | port | port port
| | | | | |
| +----+-----+ | |
| | | |
+---------+ +----+----+ +--------+ +------------+
| | | | | | |
v v v v v v |
+--------+ +---------+ +------+ +--------+ +--------+ |
| LabJack| | Debug | | USB | |Aardvark| | Phidget| |
| T7 | | Probe | | Hub | | I2C/SPI| | Thermo | |
+---+----+ +----+----+ +--+---+ +---+----+ +---+----+ |
| | | | | |
| v v | | |
| SWD/JTAG USB to DUT | | |
| | | |
v v v v
+----------------------------------------------------------------+
| DUT (Device Under Test) |
| VCC, GND, SDA, SCL, SWD, TX, RX, GPIO, TEMP, USB |
+----------------------------------------------------------------+
|
+--------------------------------------------------------+
| VISA-over-LAN Instruments (on same LAN) |
| Rigol DP832, Rigol MSO5074, Keithley 2281S |
| Connected to DUT via banana jacks / BNC / probes |
+--------------------------------------------------------+
Connection Types
| Connection | Used For | Protocol |
|---|---|---|
| USB | LabJack, debug probes, serial, hubs | Vendor-specific, CDC-ACM |
| USB-VISA | Rigol/Keithley/Keysight instruments | USBTMC (SCPI) |
| LAN-VISA | Bench instruments on local network | VXI-11 / raw TCP (SCPI) |
| Serial (UART) | DUT communication | RS-232 / TTL via USB adapter |
| SWD / JTAG | Firmware flash, debug, reset | ARM debug (via probe) |
| I2C / SPI | Peripheral communication with DUT | I2C / SPI via Aardvark or LabJack |
GitHub Actions CI Integration
A CI runner is an ephemeral GitHub Actions VM that joins Tailscale and runslager commands exactly like a developer would — no special CI-specific infrastructure is needed.
GITHUB CLOUD TAILSCALE NETWORK HARDWARE BENCH
============ ================= ==============
+---------------------------+
| GitHub Actions Runner |
| (ubuntu-latest) |
| |
| 1. checkout code |
| 2. pip install lager-cli |
| 3. tailscale up -----------> Tailscale +------------------+
| (TAILSCALE_AUTHKEY) | Coordination --> | LAGERBOX |
| | Server | 100.x.y.z |
| 4. lager hello --.--------> | |
| --box $BOX_IP | encrypted tunnel | Docker container |
| | (WireGuard) | :5000 Flask |
| 5. lager python --.--------> | |
| tests/ci/test.py | +--------+---------+
| --box $BOX_IP | |
| | USB / VISA / LAN
+---------------------------+ |
+------+------+
| Instruments |
+------+------+
|
+------+------+
| DUT |
+-------------+
Required GitHub Secrets
| Secret | Purpose |
|---|---|
TAILSCALE_AUTHKEY | Ephemeral auth key so the runner can join the VPN |
LAGER_BOX_IP | Tailscale IP of the Lagerbox (e.g., 100.x.y.z) |
Workflow Example
name: Hardware-in-the-Loop Test
on:
workflow_dispatch:
concurrency:
group: hardware-test
cancel-in-progress: false
jobs:
hardware-test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Lager CLI
run: cd cli && pip install -q -e .
- name: Connect to hardware bench via Tailscale
uses: tailscale/github-action@v2
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
- name: Verify box connectivity
run: lager hello --box ${{ secrets.LAGER_BOX_IP }}
- name: Run hardware integration test
run: |
lager python tests/ci/demo_test.py \
--box ${{ secrets.LAGER_BOX_IP }} \
--add-file test/assets/firmware/nrf_blinky.hex \
--env FIRMWARE_PATH=nrf_blinky.hex \
--timeout 300
- name: Emergency cleanup
if: failure()
run: |
lager python tests/ci/cleanup.py \
--box ${{ secrets.LAGER_BOX_IP }} || true

