/serial/`
(or `.../port/`) address instead of a `/dev/ttyUSB*` path, so they survive
tty renumbering, reboots, and port moves.
* While a cable is assigned, it is **no longer offered as a generic UART
device** — the serial line belongs to the instrument.
Some cheap USB-serial clones share one serial number (or have none). If
`assign` reports multiple matching cables, pin the assignment to a physical
box port with `--port` instead — the trade-off is that moving the cable to a
different port breaks the assignment.
**Removing an assignment:**
```bash theme={null}
lager nets assign --remove --serial 00000006 --box my-lager-box
```
The cable is offered as a generic UART device again. **Nets live and die with
their assignment**: any saved nets bound to the assignment's `serial://`
address are deleted automatically and reported in the output. The same cascade
applies when re-assigning a cable to a different instrument (or switching its
identity from `--serial` to `--port`) — only a baud-only re-assign keeps the
existing nets.
**Currently assignable devices:** Rigol DP711 (single-channel RS-232 power
supply). Run `lager nets assign --list` to see the catalog your box supports.
### `delete`
Delete a specific net by its name and type.
```bash theme={null}
lager nets delete NAME NET_TYPE [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the net to delete
* `NET_TYPE` - Type of the net (supply, debug, adc, i2c, spi, etc.)
**Options:**
* `--box TEXT` - Lagerbox name or IP
* `--yes` - Skip confirmation prompt
**Example:**
```bash theme={null}
# Delete a supply net (with confirmation)
lager nets delete supply1 supply --box my-lager-box
# Delete without confirmation
lager nets delete temp_sensor adc --box my-lager-box --yes
# Delete an I2C net
lager nets delete i2c_bus i2c --box my-lager-box --yes
```
### `delete-all`
Delete all saved nets on a Lager Box. **This is a dangerous operation.**
```bash theme={null}
lager nets delete-all [OPTIONS]
```
**Options:**
* `--box TEXT` - Lagerbox name or IP
* `--yes` - Skip confirmation prompt
**Example:**
```bash theme={null}
# Delete all nets (requires confirmation)
lager nets delete-all --box my-lager-box
# Delete all nets without prompting
lager nets delete-all --box my-lager-box --yes
```
### `rename`
Rename an existing net.
```bash theme={null}
lager nets rename NAME NEW_NAME [OPTIONS]
```
**Arguments:**
* `NAME` - Current name of the net
* `NEW_NAME` - New name for the net (must be unique)
**Options:**
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
lager nets rename supply1 main_power --box my-lager-box
```
### `tui`
Launch an interactive terminal-based UI for managing nets. The TUI provides a visual interface for viewing, creating, and deleting nets.
```bash theme={null}
lager nets tui [OPTIONS]
```
**Options:**
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
lager nets tui --box my-lager-box
```
**TUI Features:**
* Browse all connected instruments and their channels
* Create new nets with guided prompts
* Pick custom LabJack pins when adding i2c/spi nets — a pin dialog opens with
the defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI: CS=FIO0/SCK=FIO1/
MOSI=FIO2/MISO=FIO3); any DIO pin can be chosen per signal, CS can be set to
none for 3-pin SPI, and pins already used by saved nets show a warning
* Assign custom serial devices (RS-232 instruments) to their USB cables — the
interactive twin of [`assign`](#assign), including the optional
create-the-net step (`--as-net`)
* Delete existing nets
* View net details and instrument information
* Keyboard navigation
### `set-script`
Attach a debug script — either a JLinkScript or an OpenOCD `.cfg`/`.tcl` — to an existing debug net. The file is stored on the box and used automatically during connect, flash, erase, and reset operations.
The backend (J-Link vs. OpenOCD) is auto-detected from two signals:
1. **The probe's USB VID** on the net's `address` field (J-Link → `jlink`; ST-Link, FTDI, CMSIS-DAP, etc. → `openocd`).
2. **The file** — extension first (`.JLinkScript` → jlink; `.cfg`/`.tcl`/`.ocd` → openocd), with a content sniff as a tie-breaker for extensionless files or stdin.
If the two signals disagree, `set-script` refuses with a clear error and asks you to pick one via `--backend`.
A debug net only carries **one** script at a time. If the other field is already set, `set-script` clears it and prints a yellow notice on stderr so nothing disappears silently.
```bash theme={null}
lager nets set-script NAME SCRIPT_PATH [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the debug net
* `SCRIPT_PATH` - Path to the script file, or `-` to read from stdin
**Options:**
* `--backend [jlink|openocd]` - Force a specific backend instead of auto-detecting (required if the probe and file disagree)
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
# Attach a J-Link script to a J-Link net (auto-detected)
lager nets set-script debug1 ./custom_connect.JLinkScript --box my-lager-box
# Attach an OpenOCD config to an FTDI/ST-Link/CMSIS-DAP net (auto-detected)
lager nets set-script debug1 ./probe.cfg --box my-lager-box
# Read the script from stdin
cat probe.cfg | lager nets set-script debug1 - --box my-lager-box
# Force a specific backend when the file or probe is ambiguous
lager nets set-script debug1 ./generic_script --backend openocd --box my-lager-box
```
### `remove-script`
Remove the debug script (J-Link or OpenOCD) attached to a debug net.
```bash theme={null}
lager nets remove-script NAME [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the debug net
**Options:**
* `--backend [jlink|openocd]` - Only remove the named backend's script (default: remove whichever is set)
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
# Remove whichever script is attached
lager nets remove-script debug1 --box my-lager-box
# Only remove the J-Link script (leave any OpenOCD config in place)
lager nets remove-script debug1 --backend jlink --box my-lager-box
```
### `show-script`
Display the contents of the debug script attached to a debug net. The script content is written to stdout (so `> out.cfg` works); a one-line summary like `# OpenOCD config, 1247 bytes` is written to stderr so interactive use tells you which backend's script you're looking at without polluting redirects.
```bash theme={null}
lager nets show-script NAME [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the debug net
**Options:**
* `--backend [jlink|openocd]` - Only show the named backend's script (default: show whichever is set)
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
# Display the attached script (J-Link or OpenOCD)
lager nets show-script debug1 --box my-lager-box
# Save to a local file (the stderr banner doesn't end up in the file)
lager nets show-script debug1 --box my-lager-box > script.txt
```
### `show`
Display all fields of a saved net, including user-provided metadata (`purpose`,
`notes`, `tags`) set with `describe`.
```bash theme={null}
lager nets show NAME [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the net
**Options:**
* `--json` - Output as raw JSON
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
lager nets show battery1 --box my-lager-box
lager nets show battery1 --json --box my-lager-box
```
### `describe`
Set metadata on a saved net so AI agents (and humans) understand what the net does
on the DUT. Introduced in **lager 0.24.0**; the fields feed agent-assisted testing
via the [MCP server](/source/reference/mcp/overview). At least one of `--purpose`,
`--notes`, or `--tag` (or `--clear-tags`) must be provided.
```bash theme={null}
lager nets describe NAME [OPTIONS]
```
**Arguments:**
* `NAME` - Name of the net
**Options:**
* `-p`, `--purpose TEXT` - One sentence: what this net does on the DUT
* `-n`, `--notes TEXT` - Optional notes (gotchas, jumper positions, scope probe points)
* `-t`, `--tag TEXT` - Tag for categorisation/matching (repeatable)
* `--clear-tags` - Remove all existing tags before adding new ones
* `--box TEXT` - Lagerbox name or IP
**Example:**
```bash theme={null}
# Describe a net's purpose and add tags
lager nets describe battery1 \
--purpose "Main battery rail powering the MCU" \
--tag power --tag critical \
--box my-lager-box
# Replace the existing tags
lager nets describe battery1 --clear-tags --tag power --box my-lager-box
# View what was set
lager nets show battery1 --box my-lager-box
```
## Net Types Reference
| Net Type | Description | Typical Instruments |
| ------------------------------- | ------------------- | --------------------------------------------------------------------------------------- |
| `power-supply` (alias `supply`) | Power supply output | Rigol DP800, Rigol DP711 (via [`assign`](#assign)), Keithley 2200/2280, Keysight E36200 |
| `battery` (alias `batt`) | Battery simulator | Keithley 2281S |
| `solar` | Solar simulator | EA PSI/EL series |
| `eload` | Electronic load | Rigol DL3021 |
| `debug` | Debug probe | J-Link, CMSIS-DAP, ST-Link |
| `adc` | Analog input | LabJack T7 |
| `dac` | Analog output | LabJack T7 |
| `gpio` | Digital I/O | LabJack T7, MCC USB-202 |
| `i2c` | I2C bus | LabJack T7, Aardvark |
| `spi` | SPI bus | LabJack T7, Aardvark |
| `scope` | Oscilloscope | Rigol MSO5000, PicoScope |
| `uart` | Serial port | Prolific USB, SiLabs CP210x |
| `usb` | USB port control | Acroname hub, YKUSH |
| `camera` | Video capture | Logitech BRIO |
| `arm` | Robot arm | Rotrics Dexarm |
| `watt-meter` | Power measurement | Yocto Watt |
| `thermocouple` | Temperature sensor | Phidget thermocouples |
## Debug Script Workflow
Both J-Link and OpenOCD debug probes can carry a custom script for handling reset sequences, clock initialization, board-specific signal pinning, or other device-specific behavior. Lager stores one script per debug net (either a JLinkScript or an OpenOCD `.cfg`/`.tcl`, never both) and applies it automatically during connect, flash, erase, and reset operations.
```bash theme={null}
# Attach a J-Link script to a J-Link probe (auto-detected from .JLinkScript extension)
lager nets set-script debug1 ./my_device.JLinkScript --box my-lager-box
# Attach an OpenOCD config to an FTDI / ST-Link / CMSIS-DAP probe (auto-detected)
lager nets set-script debug1 ./probe.cfg --box my-lager-box
# Verify what's attached (stderr says which backend, stdout has the content)
lager nets show-script debug1 --box my-lager-box
# Scripts are used automatically for all debug operations:
lager debug debug1 flash --hex firmware.hex --box my-lager-box
lager debug debug1 gdbserver --box my-lager-box
# Remove the script when no longer needed
lager nets remove-script debug1 --box my-lager-box
```
You can also attach a script at net creation time:
```bash theme={null}
lager nets add debug1 debug STM32F407VG USB::001::002 \
--jlink-script ./my_device.JLinkScript --box my-lager-box
lager nets add debug2 debug d2763 USB::003::004 \
--openocd-config ./probe.cfg --box my-lager-box
```
You can also configure J-Link scripts per-project in the local `.lager` config file:
```json theme={null}
{
"DEBUG": {
"debug1": "./scripts/my_device.JLinkScript"
}
}
```
When both a net-level script (via `set-script`) and a project-level script (via `.lager` config) exist, the project-level script takes priority.
## Examples
```bash theme={null}
# List all nets
lager nets --box my-lager-box
# Create a new power supply net
lager nets add vdd_main power-supply 1 TCPIP::192.168.1.100::INSTR --box my-lager-box
# Create I2C and SPI bus nets
lager nets add i2c_sensors i2c 0 USB::470026574 --box my-lager-box
lager nets add spi_flash spi 0 USB::2238595116 --box my-lager-box
# Auto-create all available nets
lager nets add-all --box my-lager-box --yes
# Delete a specific net
lager nets delete old_supply supply --box my-lager-box --yes
# Rename a net
lager nets rename supply1 main_power --box my-lager-box
# Launch interactive manager
lager nets tui --box my-lager-box
# Bulk create from JSON file
lager nets add-batch testbed-nets.json --box my-lager-box
# Manage debug-probe scripts (J-Link or OpenOCD, auto-detected)
lager nets set-script debug1 ./custom_init.JLinkScript --box my-lager-box
lager nets set-script debug2 ./probe.cfg --box my-lager-box
lager nets show-script debug1 --box my-lager-box
lager nets remove-script debug1 --box my-lager-box
```
## Notes
* Net names are globally unique regardless of type
* Use `lager instruments --box ` to see available instruments and channels
* The TUI provides the easiest way to set up nets for the first time
* Use `add-all` to quickly configure a new Lager Box with sensible defaults
* I2C and SPI nets are supported on LabJack T7 and Aardvark adapters
* Debug scripts (both J-Link and OpenOCD) are base64-encoded for storage and decoded automatically during debug operations
* A debug net carries at most one script (`jlink_script` or `openocd_config`); `set-script` enforces this by clearing the other field when present
# CLI Overview
Source: https://docs.lagerdata.com/source/reference/cli/overview
Introduction to the Lager Command-Line Interface (CLI) for hardware control and automation.
The Lager Command-Line Interface (CLI) provides a powerful and scriptable way to interact with your Lager Box and connected hardware directly from your terminal. It is the ideal tool for manual control, shell scripting, and integration into CI/CD pipelines.
## Core Concepts
The CLI follows a standard `GROUP COMMAND` structure. Most commands operate on a specific Lager Box (Lagerbox), which is specified using the `--box` option.
```bash theme={null}
# General command structure
lager [GLOBAL_OPTIONS] [ARGS]...
# Example: Read an ADC value from a specific Lager Box
lager adc SENSOR_1 --box my-lager-box
# Example: Set power supply voltage
lager supply VDD_MAIN voltage 3.3 --box my-lager-box
```
### Nets
Most hardware commands operate on **nets** - named abstractions representing physical test points or signals. Nets map friendly names to instrument channels.
```bash theme={null}
# List available nets
lager nets --box my-lager-box
# Use a net in a command
lager supply supply1 voltage 3.3 --box my-lager-box
```
## Key Command Groups
Below is a summary of the main command groups available in the Lager CLI.
### Lager Box Management
* **[Boxes](/source/reference/cli/boxes)**: Manage Lager Box configurations (add, delete, sync, import/export)
* **[Hello](/source/reference/cli/hello)**: Verify connectivity to your Lager Box
* **[Update](/source/reference/cli/update)**: Update Lager Box software
* **[SSH](/source/reference/cli/ssh)**: Direct SSH access to Lager Box
* **[Logs](/source/reference/cli/logs)**: View Lager Box service logs
### Configuration
* **[Instruments](/source/reference/cli/instruments)**: List connected test equipment
* **[Nets](/source/reference/cli/nets)**: Create and manage nets (test point abstractions)
* **[Defaults](/source/reference/cli/defaults)**: Set default Lager Box and net configurations
### Power & Simulation
* **[Supply](/source/reference/cli/supply)**: Control programmable power supplies
* **[Battery](/source/reference/cli/battery)**: Simulate battery characteristics (SOC, voltage, capacity)
* **[Solar](/source/reference/cli/solar)**: Control solar panel simulators
* **[E-Load](/source/reference/cli/eload)**: Control electronic loads (CC/CV/CR/CP modes)
* **[Watt](/source/reference/cli/watt)**: Read power consumption from watt meters
* **[Energy](/source/reference/cli/energy)**: Integrate energy/charge and compute power statistics (Joulescope JS220)
### Measurement
* **[Scope](/source/reference/cli/scope)**: Control oscilloscopes for waveform capture
* **[Logic](/source/reference/cli/logic)**: Control logic analyzers with protocol decoding
* **[ADC](/source/reference/cli/adc)**: Read analog voltage values
* **[Thermocouple](/source/reference/cli/tc)**: Read temperature sensors
### I/O & Communication
* **[GPI](/source/reference/cli/gpi)**: Read digital inputs
* **[GPO](/source/reference/cli/gpo)**: Write digital outputs
* **[DAC](/source/reference/cli/dac)**: Analog output voltage control
* **[UART](/source/reference/cli/uart)**: Serial communication
* **[USB](/source/reference/cli/usb)**: USB port power control
* **[BLE](/source/reference/cli/ble)**: Bluetooth Low Energy scanning
### Development
* **[Debug](/source/reference/cli/debug)**: Flash firmware, GDB server, memory access, RTT logging
* **[Python](/source/reference/cli/python)**: Execute Python scripts on Lager Box
* **[Exec](/source/reference/cli/exec)**: Run build/test commands in a local Docker dev container
* **[Devenv](/source/reference/cli/devenv)**: Configure the local Docker development environment
* **[Binaries](/source/reference/cli/binaries)**: Run custom binaries on Lager Box
### Utilities
* **[Webcam](/source/reference/cli/webcam)**: Video capture and streaming
* **[Arm](/source/reference/cli/arm)**: Control robotic arm positioning
## Example: Test Script Workflow
This example shell script demonstrates a typical hardware test workflow.
```bash theme={null}
#!/bin/bash
# Define variables
LAGER_BOX="my-test-rig"
FIRMWARE_PATH="build/my_app.hex"
VOLTAGE_NET="supply1"
SENSOR_NET="adc1"
# 1. Flash the latest firmware
echo "--> Flashing firmware..."
lager debug flash --hex "$FIRMWARE_PATH" --box "$LAGER_BOX"
# 2. Power on the device
echo "--> Enabling power..."
lager supply "$VOLTAGE_NET" voltage 3.3 --box "$LAGER_BOX" --yes
lager supply "$VOLTAGE_NET" enable --box "$LAGER_BOX"
sleep 2
# 3. Take a sensor reading
echo "--> Reading sensor..."
READING=$(lager adc "$SENSOR_NET" --box "$LAGER_BOX")
echo "Sensor reading: $READING V"
# 4. Check if value is within expected range
if (( $(echo "$READING > 1.0" | bc -l) )) && (( $(echo "$READING < 2.0" | bc -l) )); then
echo "PASS: Value within expected range"
else
echo "FAIL: Value out of range!"
exit 1
fi
# 5. Power down
echo "--> Disabling power..."
lager supply "$VOLTAGE_NET" disable --box "$LAGER_BOX" --yes
echo "--> Test complete."
```
## Setting Default Lager Box
To avoid specifying `--box` on every command, set a default Lager Box:
```bash theme={null}
# Set default Lager Box
lager defaults add --box my-lager-box
# Now commands use the default
lager supply supply1 voltage 3.3
lager adc adc1
```
## Global Options
All commands support these global options:
| Option | Description |
| ------------ | ---------------------------- |
| `--box TEXT` | Lager Box name or IP address |
| `--help` | Show help for any command |
| `--version` | Show CLI version |
## Tips
* Use `lager --help` to see all options for any command
* Most commands support `--yes` to skip confirmation prompts
* Set defaults with `lager defaults add` to reduce typing
* Use the `tui` subcommand (where available) for interactive control
* Commands that read values (like `adc`, `soc`) can be used in scripts
# Python
Source: https://docs.lagerdata.com/source/reference/cli/python
Run Python scripts on box
Run Python scripts inside the container on the specified box for test automation, hardware control, and data processing.
## Syntax
```bash theme={null}
lager python [OPTIONS] [RUNNABLE] [ARGS]...
```
## Global Options
| Option | Short | Type | Default | Description |
| ------------------- | ----- | -------- | --------- | ------------------------------------------------------------------------ |
| `--box TEXT` | | String | | Lagerbox name or IP address |
| `--env FOO=BAR` | | Multiple | | Set environment variables for the script |
| `--passenv VAR` | | Multiple | | Pass environment variables from current shell |
| `--kill TEXT` | | String | | Kill a specific running process by its process ID |
| `--kill-all` | | Flag | | Kill all running scripts |
| `--signal SIGNAL` | | Choice | `SIGTERM` | Signal to use with `--kill`/`--kill-all` |
| `--download FILE` | | Multiple | | Download files from box after script completes |
| `--allow-overwrite` | | Flag | | Allow overwriting existing local files with `--download` |
| `--timeout SECONDS` | | Integer | 0 (none) | Maximum runtime in seconds |
| `--detach` | `-d` | Flag | | Run in detached mode (background) |
| `--port PORT` | `-p` | Multiple | | Forward ports to the Python process |
| `--add-file FILE` | | Multiple | | Add extra files to upload with script |
| `--reattach TEXT` | | String | | Reattach to a detached process by its process ID |
| `--continue TEXT` | | String | | Resume a script paused at a breakpoint, by its process ID |
| `--console TEXT` | | String | | Connect to the interactive console of a paused script, by its process ID |
| `--help` | | Flag | | Show help message and exit |
**Arguments:**
* `RUNNABLE` - Python script file or directory to execute (required unless a process-management flag such as `--kill`, `--kill-all`, `--reattach`, `--continue`, or `--console` is used)
* `ARGS` - Additional arguments passed to the script
**Signal choices for `--kill`/`--kill-all`:** SIGINT, SIGQUIT, SIGABRT, SIGKILL, SIGUSR1, SIGUSR2, SIGTERM, SIGSTOP
## Basic Usage
```bash theme={null}
# Run a Python script on the box
lager python script.py --box my-lager-box
# Run with arguments
lager python test.py --box my-lager-box -- --verbose --target DUT1
# Run a directory as a module
lager python my_test_suite/ --box my-lager-box
```
## Environment Variables
### Setting Variables
```bash theme={null}
# Set explicit environment variables
lager python test.py --box my-lager-box --env API_KEY=abc123 --env DEBUG=true
# Pass variables from your current shell
export SECRET_TOKEN=xyz
lager python test.py --box my-lager-box --passenv SECRET_TOKEN
```
### Auto-Injected Variables
The following environment variables are automatically available inside your script:
| Variable | Description |
| ---------------------- | ------------------------------------------------------------- |
| `LAGER_OUTPUT_CHANNEL` | File path for structured output (see Structured Output below) |
| `LAGER_PROCESS_ID` | Unique UUID for this execution |
| `LAGER_RUNNABLE` | Path to the script being executed |
| `LAGER_BOX` | Box name (if `--box` was provided) |
## File Downloads
Download files generated by your script after it completes:
```bash theme={null}
# Download a single file
lager python data_processor.py --box my-lager-box --download results.csv
# Download multiple files
lager python test.py --box my-lager-box --download report.json --download log.txt
# Allow overwriting existing local files
lager python test.py --box my-lager-box --download results.csv --allow-overwrite
```
Files are downloaded to the current working directory using the basename of the remote path. If a local file with the same name already exists, the command fails unless `--allow-overwrite` is set. Gzip-compressed files are automatically decompressed during download.
## Detached Mode
Run scripts in the background without waiting for output:
```bash theme={null}
# Start a long-running script in the background
lager python long_running.py --box my-lager-box --detach
```
In detached mode:
* The command returns immediately after the script starts
* No stdout/stderr is streamed back
* The script continues running on the box
* Use `--kill` to stop it later
## Killing Running Scripts
```bash theme={null}
# Kill with default SIGTERM
lager python --box my-lager-box --kill
# Kill with specific signal
lager python --box my-lager-box --kill --signal SIGKILL
```
## Port Forwarding
Forward network ports from the box to your local machine:
```bash theme={null}
# Forward port 8080
lager python web_server.py --box my-lager-box -p 8080
# Forward with different local/remote ports
lager python server.py --box my-lager-box -p 8080:80
# Forward with protocol
lager python server.py --box my-lager-box -p 8080:80/tcp
```
Port format: `SRC_PORT[:DST_PORT][/PROTOCOL]`
## Timeout
Limit script execution time:
```bash theme={null}
# Kill after 5 minutes
lager python analysis.py --box my-lager-box --timeout 300
```
When a script exceeds the timeout:
* First, SIGTERM is sent (exit code 124)
* If the script doesn't exit, SIGKILL is sent (exit code 137)
## Structured Output
Scripts running on the box can send structured data back to the CLI using the `lager.core.output()` function. This uses a dedicated output channel (file descriptor 3) separate from stdout/stderr.
### Box-Side API
```python theme={null}
from lager.core import output, OutputEncoders
# Output a Python dict (default: pickle encoding)
output({'status': 'pass', 'measurement': 3.14})
# Output as JSON
output({'voltage': 3.3, 'current': 0.5}, encoder=OutputEncoders.JSON)
# Output as YAML
output({'device': 'PSU-1', 'readings': [1.0, 2.0]}, encoder=OutputEncoders.YAML)
# Output raw binary data
output(image_bytes, encoder=OutputEncoders.Raw)
```
**Available encoders:**
| Encoder | ID | Use Case |
| ----------------------- | -- | --------------------------- |
| `OutputEncoders.Raw` | 1 | Binary data (images, files) |
| `OutputEncoders.Pickle` | 2 | Python objects (default) |
| `OutputEncoders.JSON` | 3 | JSON-serializable data |
| `OutputEncoders.YAML` | 4 | YAML-serializable data |
Structured output is printed to the CLI console as it arrives. Standard stdout and stderr are streamed separately in real time.
## Module Includes
If your script depends on local modules, configure includes in a `.lager` file in your project:
```yaml theme={null}
# .lager (in project root)
includes:
my_lib: /path/to/my_lib
fixtures: /path/to/fixtures
```
The CLI searches up the directory tree for a `.lager` file, then zips the script along with all include directories before uploading to the box.
```bash theme={null}
# Script can import from included directories
lager python test.py --box my-lager-box
```
In `test.py`:
```python theme={null}
from my_lib import helpers
from fixtures import test_data
```
## Additional Files
Upload extra files alongside your script:
```bash theme={null}
lager python flash_and_test.py --box my-lager-box --add-file firmware.hex --add-file config.json
```
Files are available in the same directory as your script on the box.
## Exit Codes
| Exit Code | Meaning |
| --------- | ----------------------------------------- |
| 0 | Success |
| -1 | Failed to retrieve exit code from box |
| 124 | Script terminated by SIGTERM (timeout) |
| 137 | Script killed by SIGKILL (timeout, force) |
| Other | Script's own exit code |
## Examples
```bash theme={null}
# Basic script execution
lager python script.py --box my-lager-box
# Run with environment variables
lager python test.py --box my-lager-box --env API_KEY=abc123 --env DEBUG=true
# Run in detached mode
lager python long_running.py --box my-lager-box --detach
# Download files after completion
lager python data_processor.py --box my-lager-box --download results.csv
# Kill running script
lager python --box my-lager-box --kill
# Run with port forwarding
lager python web_server.py --box my-lager-box -p 8080
# Run with timeout
lager python analysis.py --box my-lager-box --timeout 300
# Upload extra files with script
lager python flash_test.py --box my-lager-box --add-file firmware.hex
# Pass arguments to the script
lager python test.py --box my-lager-box -- --device DUT1 --verbose
```
### Hardware Test Script Example
```python theme={null}
# test_power.py - run with: lager python test_power.py --box my-lager-box
from lager import Net, NetType
from lager.core import output, OutputEncoders
import time
# Get the power supply net
psu = Net.get("PSU_CH1", type=NetType.PowerSupply)
# Set voltage and enable
psu.voltage(value=3.3, ovp=3.6)
psu.enable()
time.sleep(1)
# Read state
state = psu.get_full_state()
print(f"Voltage: {state['voltage']}V, Current: {state['current']}A")
# Send structured results
output({
'test': 'power_on',
'voltage': state['voltage'],
'current': state['current'],
'status': 'pass' if state['voltage'] > 3.0 else 'fail'
}, encoder=OutputEncoders.JSON)
# Cleanup
psu.disable()
```
## Notes
* Use `--env` for script-specific configuration values
* Use `--passenv` for secrets/tokens from your current shell
* `--download` retrieves files only after script completion (not during)
* Port forwarding syntax: `SRC_PORT[:DST_PORT][/PROTOCOL]`
* After script execution, the hardware cache on the box is automatically cleared to release VISA connections
* Output is streamed in real time via a multiplexed HTTP protocol with keepalive (20-second interval)
***
## Installing Python packages
Scripts run inside the Lager Python container on the box. To install packages
your scripts depend on, use the declarative `lager box-config pip` commands,
which record the packages in the box config and rebuild the container so they
persist across `lager python` runs and box updates.
```bash theme={null}
# Add one or more packages
lager box-config pip add pandas requests --box my-box
# List configured packages
lager box-config pip list --box my-box
# Remove a package
lager box-config pip remove numpy --box my-box
# Apply the changes (rebuild the container)
lager box-config apply --box my-box
```
See the [Box Config reference](/source/reference/cli/box-config) for the full
declarative provisioning workflow (pip/cargo/npm packages, apt, udev, mounts,
env, and more).
The standalone `lager pip` command was removed and folded into
`lager box-config pip`.
# Router
Source: https://docs.lagerdata.com/source/reference/cli/router
Manage routers as Lager nets
Register and control network routers as Lager nets. A router net wraps a router's
REST API (e.g. a MikroTik hAP) so you can inspect interfaces, list wireless
clients and DHCP leases, toggle interfaces, block internet access, and reset the
router to a clean baseline — useful for connectivity and network-resilience tests.
Introduced in **lager 0.10.0**. The default instrument type is `MikroTik_hAP`.
## Syntax
```bash theme={null}
lager router COMMAND [ARGS] [OPTIONS]
```
Every subcommand accepts `--box BOX` (Lagerbox name or IP; uses the default box if
omitted). Most operate on a `NETNAME` — the name of a router net previously
registered with `add-net`.
## Commands
| Command | Description |
| --------------------- | ----------------------------------------------------- |
| `add-net` | Register a router as a net on the box |
| `connect` | Verify connectivity to a router net |
| `interfaces` | List network interfaces |
| `wireless-interfaces` | List wireless interfaces and their configuration |
| `wireless-clients` | List connected wireless clients |
| `dhcp-leases` | List DHCP leases (devices that received IP addresses) |
| `system-info` | Get system resource information |
| `reboot` | Reboot the router |
| `enable-interface` | Enable a wireless interface |
| `disable-interface` | Disable a wireless interface |
| `block-internet` | Block all internet access (drops forwarded traffic) |
| `reset` | Reset the router net to a clean baseline state |
| `run` | Run an arbitrary router REST API GET call |
***
## Command Reference
### `add-net`
Register a router as a net on the box.
```bash theme={null}
lager router add-net NAME --address IP [OPTIONS]
```
| Option | Default | Description |
| -------------- | -------------- | ------------------------- |
| `--address` | (required) | IP address of the router |
| `--username` | `admin` | Router username |
| `--password` | (empty) | Router password |
| `--instrument` | `MikroTik_hAP` | Router instrument type |
| `--use-ssl` | off | Use HTTPS instead of HTTP |
| `--box` | | Lagerbox name or IP |
```bash theme={null}
lager router add-net router1 --address 192.168.88.1 --username admin --password secret --box my-lager-box
```
### `connect`
Verify connectivity to a router net.
```bash theme={null}
lager router connect router1 --box my-lager-box
```
### `interfaces`
List network interfaces on a router net.
```bash theme={null}
lager router interfaces router1 --box my-lager-box
```
### `wireless-interfaces`
List wireless interfaces and their configuration.
```bash theme={null}
lager router wireless-interfaces router1 --box my-lager-box
```
### `wireless-clients`
List currently connected wireless clients.
```bash theme={null}
lager router wireless-clients router1 --box my-lager-box
```
### `dhcp-leases`
List DHCP leases — devices that have received IP addresses.
```bash theme={null}
lager router dhcp-leases router1 --box my-lager-box
```
### `system-info`
Get system resource information from a router net.
```bash theme={null}
lager router system-info router1 --box my-lager-box
```
### `reboot`
Reboot a router net. Prompts for confirmation unless `--yes` is passed.
```bash theme={null}
lager router reboot router1 --box my-lager-box
lager router reboot router1 --yes --box my-lager-box
```
### `enable-interface` / `disable-interface`
Enable or disable a wireless interface by name.
```bash theme={null}
lager router enable-interface router1 wlan1 --box my-lager-box
lager router disable-interface router1 wlan1 --box my-lager-box
```
### `block-internet`
Block all internet access on a router net (drops forwarded traffic). Use `reset`
to restore access.
```bash theme={null}
lager router block-internet router1 --box my-lager-box
```
### `reset`
Reset a router net to a clean baseline state: removes all test-tagged firewall
rules, bandwidth limits, and access-list entries; re-enables DHCP and all wireless
interfaces. If `--ssid` and `--password` are provided, a fresh baseline WPA2
network is applied. Prompts for confirmation unless `--yes` is passed.
| Option | Description |
| ------------ | ----------------------------------------------- |
| `--ssid` | Baseline SSID to restore on wireless interfaces |
| `--password` | Baseline WPA2 password |
| `--yes` | Skip the confirmation prompt |
```bash theme={null}
lager router reset router1 --box my-lager-box
lager router reset router1 --ssid HomeNet --password secret123 --box my-lager-box
```
### `run`
Run an arbitrary router REST API GET call. `PATH` is the API path relative to
`/rest`, e.g. `/ip/address`.
```bash theme={null}
lager router run router1 /ip/address --box my-lager-box
```
***
## See Also
* [Nets](/source/reference/cli/nets) — manage instrument and device nets
* [WiFi](/source/reference/cli/wifi) — manage the Lager Box's own WiFi settings
# Oscilloscope
Source: https://docs.lagerdata.com/source/reference/cli/scope
Control oscilloscope settings and capture waveforms
Control oscilloscope nets through the Lager CLI for waveform capture, triggering, measurements, and streaming.
## Syntax
```bash theme={null}
lager scope [NETNAME] [OPTIONS] COMMAND [ARGS]...
```
If `NETNAME` is omitted, lists all available scope nets on the box.
## Global Options
| Option | Description |
| ----------- | ---------------------------------------------------------------------------- |
| `--box BOX` | Lagerbox name or IP address |
| `--mcu MCU` | MCU identifier (passed to the backend; use when multiple MCUs share a scope) |
| `--help` | Show help message and exit |
## Commands
| Command | Description | Hardware |
| ----------- | ------------------------------------------------- | --------------- |
| `enable` | Enable oscilloscope channel | Both |
| `disable` | Disable oscilloscope channel | Both |
| `start` | Start waveform capture (continuous or single) | Both |
| `stop` | Stop waveform capture | Both |
| `force` | Force trigger manually (bypass trigger condition) | Both |
| `autoscale` | Automatically adjust vertical scale and timebase | Rigol only |
| `coupling` | Set channel coupling mode (dc, ac, or gnd) | Rigol only |
| `probe` | Set probe attenuation ratio | Rigol only |
| `scale` | Set vertical scale (volts per division) | Rigol only |
| `timebase` | Set horizontal timebase (seconds per division) | Rigol only |
| `measure` | Measure waveform characteristics | Rigol only |
| `trigger` | Configure trigger settings | Both (see note) |
| `cursor` | Control scope cursor positions | Rigol only |
| `stream` | Stream oscilloscope data with web visualization | PicoScope only |
Edge trigger works with both PicoScope and Rigol. Protocol triggers (I2C, SPI, UART) and pulse width trigger are Rigol only.
***
## Validation Ranges
The CLI validates input values before sending commands to the oscilloscope:
| Parameter | Minimum | Maximum | Unit |
| ------------------- | ------------ | ----------- | ------- |
| Vertical scale | 0.001 | 100.0 | V/div |
| Horizontal timebase | 1e-9 (1 ns) | 50.0 | s/div |
| Capture duration | 0.001 (1 ms) | 3600 (1 hr) | seconds |
| Sample count | 1 | 100,000,000 | samples |
***
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Rigol | MSO5000 series | 4 analog + 16 digital channels, SCPI/VISA control, advanced triggers (edge, pulse, I2C, SPI, UART), measurements, cursors |
| PicoScope | 2000 series | Real-time streaming, web-based visualization, CSV export, edge trigger |
### Feature Comparison
| Feature | Rigol MSO5000 | PicoScope 2000 |
| ------------------- | --------------------------- | ----------------- |
| Channels | 4 analog + 16 digital | Up to 4 analog |
| Real-time streaming | No (single capture) | Yes (continuous) |
| Measurements | 11 built-in types | Via streaming/CSV |
| Trigger types | Edge, pulse, I2C, SPI, UART | Edge only |
| Cursor control | Manual XY cursors | Web UI cursors |
| Autoscale | Yes | No |
| Web visualization | No | Yes (HTML5) |
| Data export | Via measurements | CSV streaming |
| Control method | SCPI over USB/LAN | WebSocket daemon |
***
## Basic Control Commands
### `enable`
Enable oscilloscope channel for the specified net.
```bash theme={null}
lager scope NET_NAME enable [--box BOX] [--mcu MCU]
```
### `disable`
Disable oscilloscope channel.
```bash theme={null}
lager scope NET_NAME disable [--box BOX] [--mcu MCU]
```
### `start`
Start waveform capture (continuous or single).
```bash theme={null}
lager scope NET_NAME start [--box BOX] [--mcu MCU] [--single]
```
**Options:**
* `--single` - Capture single waveform then stop (one-shot mode)
### `stop`
Stop waveform capture.
```bash theme={null}
lager scope NET_NAME stop [--box BOX] [--mcu MCU]
```
### `force`
Force trigger manually, bypassing the trigger condition. Useful when the signal doesn't meet trigger criteria.
```bash theme={null}
lager scope NET_NAME force [--box BOX] [--mcu MCU]
```
### `autoscale`
Automatically adjust vertical scale and horizontal timebase for optimal display (Rigol only).
```bash theme={null}
lager scope NET_NAME autoscale [--box BOX] [--mcu MCU]
```
***
## Channel Configuration (Rigol)
### `coupling`
Set the input coupling mode for the oscilloscope channel.
```bash theme={null}
lager scope NET_NAME coupling MODE [--box BOX] [--mcu MCU]
```
**Arguments:**
* `MODE` - Coupling mode: `dc`, `ac`, or `gnd`
| Mode | Description |
| ----- | -------------------------------- |
| `dc` | Pass both DC and AC components |
| `ac` | Block DC component, pass AC only |
| `gnd` | Ground reference (0V baseline) |
### `probe`
Set the probe attenuation ratio for accurate voltage measurements.
```bash theme={null}
lager scope NET_NAME probe RATIO [--box BOX] [--mcu MCU]
```
**Arguments:**
* `RATIO` - Probe attenuation: `1`, `10`, `100`, or `1000`
```bash theme={null}
lager scope ANALOG1 probe 10 --box my-lager-box # 10:1 probe
lager scope ANALOG1 probe 1 --box my-lager-box # Direct connection (1:1)
lager scope ANALOG1 probe 100 --box my-lager-box # 100:1 high-voltage probe
```
### `scale`
Set the vertical scale (volts per division) for the oscilloscope channel.
```bash theme={null}
lager scope NET_NAME scale VOLTS_PER_DIV [--box BOX] [--mcu MCU]
```
**Arguments:**
* `VOLTS_PER_DIV` - Vertical scale in volts per division (0.001 to 100.0)
```bash theme={null}
lager scope ANALOG1 scale 1.0 --box my-lager-box # 1V/div
lager scope ANALOG1 scale 0.5 --box my-lager-box # 500mV/div
lager scope ANALOG1 scale 0.1 --box my-lager-box # 100mV/div
lager scope ANALOG1 scale 0.001 --box my-lager-box # 1mV/div (minimum)
```
### `timebase`
Set the horizontal timebase (seconds per division) for the oscilloscope.
```bash theme={null}
lager scope NET_NAME timebase SEC_PER_DIV [--box BOX] [--mcu MCU]
```
**Arguments:**
* `SEC_PER_DIV` - Horizontal timebase in seconds per division (1e-9 to 50.0)
```bash theme={null}
lager scope ANALOG1 timebase 0.001 --box my-lager-box # 1ms/div
lager scope ANALOG1 timebase 0.0001 --box my-lager-box # 100us/div
lager scope ANALOG1 timebase 0.000001 --box my-lager-box # 1us/div
lager scope ANALOG1 timebase 0.000000001 --box my-lager-box # 1ns/div (minimum)
```
***
## Measure Subcommands (Rigol)
Measure waveform characteristics on Rigol oscilloscopes. PicoScope users should use the streaming capture commands to export waveform data for analysis.
**Common Options for all measure commands:**
| Option | Description |
| ----------- | ------------------------------------------ |
| `--box BOX` | Lagerbox name or IP |
| `--mcu MCU` | MCU identifier |
| `--display` | Display measurement on oscilloscope screen |
| `--cursor` | Enable measurement cursor on screen |
### Time Measurements
#### `measure period`
Measure waveform period.
```bash theme={null}
lager scope NET_NAME measure period [--box BOX] [--display] [--cursor]
```
#### `measure freq`
Measure waveform frequency.
```bash theme={null}
lager scope NET_NAME measure freq [--box BOX] [--display] [--cursor]
```
#### `measure pulse-width-pos`
Measure positive pulse width.
```bash theme={null}
lager scope NET_NAME measure pulse-width-pos [--box BOX] [--display] [--cursor]
```
#### `measure pulse-width-neg`
Measure negative pulse width.
```bash theme={null}
lager scope NET_NAME measure pulse-width-neg [--box BOX] [--display] [--cursor]
```
#### `measure duty-cycle-pos`
Measure positive duty cycle (percentage of time signal is high).
```bash theme={null}
lager scope NET_NAME measure duty-cycle-pos [--box BOX] [--display] [--cursor]
```
#### `measure duty-cycle-neg`
Measure negative duty cycle (percentage of time signal is low).
```bash theme={null}
lager scope NET_NAME measure duty-cycle-neg [--box BOX] [--display] [--cursor]
```
### Voltage Measurements
#### `measure vpp`
Measure peak-to-peak voltage.
```bash theme={null}
lager scope NET_NAME measure vpp [--box BOX] [--display] [--cursor]
```
#### `measure vmax`
Measure maximum voltage.
```bash theme={null}
lager scope NET_NAME measure vmax [--box BOX] [--display] [--cursor]
```
#### `measure vmin`
Measure minimum voltage.
```bash theme={null}
lager scope NET_NAME measure vmin [--box BOX] [--display] [--cursor]
```
#### `measure vavg`
Measure average voltage.
```bash theme={null}
lager scope NET_NAME measure vavg [--box BOX] [--display] [--cursor]
```
#### `measure vrms`
Measure RMS (root mean square) voltage.
```bash theme={null}
lager scope NET_NAME measure vrms [--box BOX] [--display] [--cursor]
```
***
## Trigger Subcommands
Configure trigger settings. Edge trigger works with both PicoScope and Rigol. Protocol triggers (I2C, SPI, UART) and pulse trigger are Rigol only.
**Common Trigger Options:**
| Option | Description | Default |
| --------------- | ---------------------------------------------------------- | ----------- |
| `--mode` | Trigger mode: `normal`, `auto`, `single` | `normal` |
| `--coupling` | Coupling mode: `dc`, `ac`, `low_freq_rej`, `high_freq_rej` | `dc` |
| `--source NET` | Trigger source net | Current net |
| `--level VOLTS` | Trigger level in volts | - |
### `trigger edge`
Set edge trigger configuration. Works with both PicoScope and Rigol.
```bash theme={null}
lager scope NET_NAME trigger edge [OPTIONS]
```
**Options (in addition to common):**
| Option | Description |
| --------- | ------------------------------------------ |
| `--slope` | Trigger slope: `rising`, `falling`, `both` |
**Examples:**
```bash theme={null}
# Rising edge at 1.5V
lager scope ANALOG1 trigger edge --slope rising --level 1.5 --box my-lager-box
# Falling edge in single capture mode
lager scope ANALOG1 trigger edge --slope falling --level 0.5 --mode single --box my-lager-box
# Either edge with AC coupling
lager scope ANALOG1 trigger edge --slope both --coupling ac --level 0 --box my-lager-box
```
### `trigger pulse`
Set pulse width trigger (Rigol only). Triggers when pulse width meets specified conditions.
```bash theme={null}
lager scope NET_NAME trigger pulse [OPTIONS]
```
**Options (in addition to common):**
| Option | Description | Default |
| ----------------- | --------------------------- | ---------- |
| `--trigger-on` | Condition (see table below) | `positive` |
| `--upper SECONDS` | Upper pulse width limit | - |
| `--lower SECONDS` | Lower pulse width limit | - |
**Trigger-on Conditions:**
| Condition | Description |
| ------------------ | ---------------------------------------- |
| `positive` | Positive pulse |
| `negative` | Negative pulse |
| `positive_greater` | Positive pulse wider than upper limit |
| `negative_greater` | Negative pulse wider than upper limit |
| `positive_less` | Positive pulse narrower than upper limit |
| `negative_less` | Negative pulse narrower than upper limit |
**Examples:**
```bash theme={null}
# Detect short positive glitches (< 100us)
lager scope ANALOG1 trigger pulse --trigger-on positive_less --upper 0.0001 --box my-lager-box
# Detect long negative pulses (> 1ms)
lager scope ANALOG1 trigger pulse --trigger-on negative_greater --upper 0.001 --level 1.0 --box my-lager-box
```
### `trigger i2c`
Set I2C protocol trigger (Rigol only). Triggers on I2C bus events.
```bash theme={null}
lager scope NET_NAME trigger i2c [OPTIONS]
```
**Options (in addition to common mode/coupling):**
| Option | Description | Default |
| ------------------- | ---------------------------------------- | ------------ |
| `--source-scl NET` | SCL source net | - |
| `--source-sda NET` | SDA source net | - |
| `--level-scl VOLTS` | SCL trigger level | - |
| `--level-sda VOLTS` | SDA trigger level | - |
| `--trigger-on` | Condition (see table below) | `start` |
| `--address HEX` | I2C address (hex) | - |
| `--addr-width` | Address width: `7`, `8`, `10` bits | `7` |
| `--data HEX` | Data pattern to match (hex) | - |
| `--data-width INT` | Data width in bits | `8` |
| `--direction` | Direction: `read`, `write`, `read_write` | `read_write` |
**Trigger-on Conditions:**
| Condition | Description |
| ----------- | ------------------------ |
| `start` | Start condition |
| `restart` | Repeated start condition |
| `stop` | Stop condition |
| `ack_miss` | Missing ACK (NACK) |
| `address` | Specific address match |
| `data` | Specific data match |
| `addr_data` | Address + data match |
**Examples:**
```bash theme={null}
# Trigger on I2C start condition
lager scope ANALOG1 trigger i2c \
--source-scl SCL_NET --source-sda SDA_NET \
--trigger-on start --box my-lager-box
# Trigger on specific I2C address (7-bit)
lager scope ANALOG1 trigger i2c \
--source-scl SCL_NET --source-sda SDA_NET \
--trigger-on address --address 0x48 --box my-lager-box
# Trigger on I2C write to address 0x76 with data 0xF4
lager scope ANALOG1 trigger i2c \
--source-scl SCL_NET --source-sda SDA_NET \
--trigger-on addr_data --address 0x76 --data 0xF4 \
--direction write --box my-lager-box
# Trigger on NACK (missing acknowledgment)
lager scope ANALOG1 trigger i2c \
--source-scl SCL_NET --source-sda SDA_NET \
--trigger-on ack_miss --box my-lager-box
```
### `trigger spi`
Set SPI protocol trigger (Rigol only). Triggers on SPI bus events.
```bash theme={null}
lager scope NET_NAME trigger spi [OPTIONS]
```
**Options (in addition to common mode/coupling):**
| Option | Description | Default |
| ------------------------- | ------------------------------- | -------- |
| `--source-mosi-miso NET` | MOSI/MISO source net | - |
| `--source-sck NET` | SCK (clock) source net | - |
| `--source-cs NET` | CS (chip select) source net | - |
| `--level-mosi-miso VOLTS` | MOSI/MISO trigger level | - |
| `--level-sck VOLTS` | SCK trigger level | - |
| `--level-cs VOLTS` | CS trigger level | - |
| `--trigger-on` | Condition: `timeout`, `cs` | `cs` |
| `--data HEX` | Data pattern to match (hex) | - |
| `--data-width INT` | Data width in bits | `8` |
| `--clk-slope` | Clock edge: `rising`, `falling` | `rising` |
| `--cs-idle` | CS idle state: `high`, `low` | `high` |
| `--timeout SECONDS` | Timeout value in seconds | - |
**Examples:**
```bash theme={null}
# Trigger on CS assertion
lager scope ANALOG1 trigger spi \
--source-mosi-miso MOSI_NET --source-sck SCK_NET --source-cs CS_NET \
--trigger-on cs --box my-lager-box
# Trigger on SPI data pattern with falling clock edge
lager scope ANALOG1 trigger spi \
--source-mosi-miso MOSI_NET --source-sck SCK_NET --source-cs CS_NET \
--data 0xFF --clk-slope falling --box my-lager-box
```
### `trigger uart`
Set UART protocol trigger (Rigol only). Triggers on UART serial events.
```bash theme={null}
lager scope NET_NAME trigger uart [OPTIONS]
```
**Options (in addition to common):**
| Option | Description | Default |
| ------------------ | ------------------------------------------- | ------- |
| `--baud INT` | Baud rate | `9600` |
| `--parity` | Parity: `none`, `even`, `odd` | `none` |
| `--stop-bits` | Stop bits: `1`, `1.5`, `2` | `1` |
| `--data-width INT` | Data width in bits | `8` |
| `--trigger-on` | Condition: `start`, `stop`, `data`, `error` | `start` |
| `--data HEX` | Data pattern to match (hex) | - |
**Examples:**
```bash theme={null}
# Trigger on UART start bit at 115200 baud
lager scope ANALOG1 trigger uart \
--source UART_TX --baud 115200 --trigger-on start --box my-lager-box
# Trigger on specific UART data byte
lager scope ANALOG1 trigger uart \
--source UART_TX --baud 9600 --trigger-on data --data 0x55 --box my-lager-box
# Trigger on UART framing error
lager scope ANALOG1 trigger uart \
--source UART_RX --baud 115200 --trigger-on error --box my-lager-box
```
***
## Cursor Subcommands (Rigol)
Control manual XY cursors on Rigol oscilloscopes for precise time and voltage measurements. Cursors A and B can be positioned independently, and the oscilloscope calculates the delta between them.
### `cursor set-a` / `cursor set-b`
Set the absolute position of cursor A or B.
```bash theme={null}
lager scope NET_NAME cursor set-a [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU]
lager scope NET_NAME cursor set-b [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU]
```
**Options:**
| Option | Description |
| ------ | ------------------------------- |
| `--x` | X coordinate (time position) |
| `--y` | Y coordinate (voltage position) |
### `cursor move-a` / `cursor move-b`
Move a cursor by a relative offset from its current position.
```bash theme={null}
lager scope NET_NAME cursor move-a [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU]
lager scope NET_NAME cursor move-b [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU]
```
**Options:**
| Option | Description |
| ------ | --------------------------- |
| `--x` | Relative X movement (delta) |
| `--y` | Relative Y movement (delta) |
### `cursor hide`
Hide the cursor display.
```bash theme={null}
lager scope NET_NAME cursor hide [--box BOX] [--mcu MCU]
```
**Examples:**
```bash theme={null}
# Position cursors for time measurement
lager scope ANALOG1 cursor set-a --x -0.001 --box my-lager-box
lager scope ANALOG1 cursor set-b --x 0.001 --box my-lager-box
# Fine-tune cursor B position
lager scope ANALOG1 cursor move-b --x 0.0001 --box my-lager-box
# Set voltage measurement cursors
lager scope ANALOG1 cursor set-a --y 0.5 --box my-lager-box
lager scope ANALOG1 cursor set-b --y 3.3 --box my-lager-box
# Hide cursors when done
lager scope ANALOG1 cursor hide --box my-lager-box
```
***
## Stream Subcommands (PicoScope)
Stream oscilloscope data in real time from PicoScope devices. The streaming system uses a dedicated daemon on the Lager Box with a web-based visualization interface.
### Daemon Architecture
The PicoScope streaming daemon communicates over multiple ports:
| Port | Purpose |
| ---- | ------------------------------------- |
| 8080 | HTTP server for web visualization UI |
| 8082 | WebSocket command channel (browser) |
| 8083 | WebTransport streaming (browser data) |
| 8085 | CLI command port (WebSocket) |
### `stream start`
Start oscilloscope streaming acquisition with web visualization.
```bash theme={null}
lager scope NET_NAME stream start [OPTIONS]
```
**Options:**
| Option | Short | Description | Default |
| ----------------- | ----- | ------------------------------------ | -------- |
| `--channel` | `-c` | Channel: `A`, `B`, `1`, `2` | `A` |
| `--volts-per-div` | `-v` | Vertical scale (V/div) | `1.0` |
| `--time-per-div` | `-t` | Horizontal scale (s/div) | `0.001` |
| `--trigger-level` | | Trigger threshold voltage | `0.0` |
| `--trigger-slope` | | Slope: `rising`, `falling`, `either` | `rising` |
| `--capture-mode` | | Mode: `auto`, `normal`, `single` | `auto` |
| `--coupling` | | Coupling: `dc`, `ac` | `dc` |
| `--quiet` | `-q` | Minimal output | |
| `--json` | | JSON output format | |
| `--verbose` | | Verbose debugging output | |
**Examples:**
```bash theme={null}
# Start streaming on channel A with default settings
lager scope PICO1 stream start --box my-lager-box
# Start with specific configuration
lager scope PICO1 stream start -c A -v 2.0 -t 0.0001 --box my-lager-box
# Start with trigger configuration
lager scope PICO1 stream start \
--trigger-level 1.5 --trigger-slope rising \
--capture-mode normal --box my-lager-box
# Start in single capture mode with AC coupling
lager scope PICO1 stream start \
--capture-mode single --coupling ac --box my-lager-box
```
### `stream stop`
Stop oscilloscope streaming acquisition.
```bash theme={null}
lager scope NET_NAME stream stop [--box BOX]
```
### `stream status`
Check oscilloscope streaming daemon status.
```bash theme={null}
lager scope NET_NAME stream status [--box BOX]
```
### `stream web`
Open web browser for real-time oscilloscope visualization.
```bash theme={null}
lager scope NET_NAME stream web [--box BOX] [--port PORT]
```
**Options:**
* `--port` - HTTP server port (default: 8080)
The web interface provides an HTML5 oscilloscope display with real-time waveform rendering via WebTransport.
### `stream capture`
Capture oscilloscope waveform data to a CSV file.
```bash theme={null}
lager scope NET_NAME stream capture [OPTIONS]
```
**Options:**
| Option | Short | Description | Default |
| ------------ | ----- | ---------------------------------------- | ---------------- |
| `--output` | `-o` | CSV output file path | `scope_data.csv` |
| `--duration` | `-d` | Capture duration in seconds (0.001-3600) | `1.0` |
| `--samples` | `-n` | Maximum samples to capture (1-100M) | unlimited |
| `--quiet` | `-q` | Minimal output | |
| `--json` | | JSON output format | |
| `--verbose` | | Verbose debugging output | |
**Examples:**
```bash theme={null}
# Capture 1 second of data (default)
lager scope PICO1 stream capture --box my-lager-box
# Capture 5 seconds to specific file
lager scope PICO1 stream capture -o waveform.csv -d 5.0 --box my-lager-box
# Capture up to 1M samples
lager scope PICO1 stream capture -n 1000000 -o data.csv --box my-lager-box
# Capture with JSON output format
lager scope PICO1 stream capture --json --box my-lager-box
```
### `stream config`
Configure oscilloscope streaming settings without starting/stopping acquisition.
```bash theme={null}
lager scope NET_NAME stream config [OPTIONS]
```
**Options:**
| Option | Short | Description |
| ------------------------ | ----- | ------------------------------------------ |
| `--channel` | `-c` | Channel: `A`, `B`, `1`, `2` |
| `--volts-per-div` | `-v` | Volts per division |
| `--time-per-div` | `-t` | Time per division (seconds) |
| `--trigger-level` | | Trigger level (volts) |
| `--trigger-source` | | Trigger source channel: `A`, `B`, `1`, `2` |
| `--trigger-slope` | | Slope: `rising`, `falling`, `either` |
| `--capture-mode` | | Mode: `auto`, `normal`, `single` |
| `--coupling` | | Coupling: `dc`, `ac` |
| `--enable` / `--disable` | | Enable or disable channel |
**Examples:**
```bash theme={null}
# Change vertical scale while streaming
lager scope PICO1 stream config -v 0.5 --box my-lager-box
# Switch to channel B
lager scope PICO1 stream config -c B --enable --box my-lager-box
# Change trigger settings
lager scope PICO1 stream config --trigger-level 2.0 --trigger-slope falling --box my-lager-box
# Switch to single capture mode
lager scope PICO1 stream config --capture-mode single --box my-lager-box
```
***
## Examples
### Basic Rigol Workflow
```bash theme={null}
# Enable scope channel
lager scope ANALOG1 enable --box my-lager-box
# Auto-scale to find signal
lager scope ANALOG1 autoscale --box my-lager-box
# Fine-tune settings
lager scope ANALOG1 scale 0.5 --box my-lager-box
lager scope ANALOG1 timebase 0.001 --box my-lager-box
# Start continuous capture
lager scope ANALOG1 start --box my-lager-box
# Take measurements
lager scope ANALOG1 measure freq --display --box my-lager-box
lager scope ANALOG1 measure vpp --display --box my-lager-box
# Single capture mode
lager scope ANALOG1 start --single --box my-lager-box
# Stop capture
lager scope ANALOG1 stop --box my-lager-box
```
### Channel Configuration
```bash theme={null}
# Configure for 10x probe at 500mV/div, 1ms/div
lager scope ANALOG1 probe 10 --box my-lager-box
lager scope ANALOG1 scale 0.5 --box my-lager-box
lager scope ANALOG1 timebase 0.001 --box my-lager-box
# Set AC coupling for audio signals
lager scope ANALOG1 coupling ac --box my-lager-box
# Force trigger when signal doesn't meet criteria
lager scope ANALOG1 force --box my-lager-box
```
### Measurements
```bash theme={null}
# Time measurements
lager scope ANALOG1 measure freq --display --box my-lager-box
lager scope ANALOG1 measure period --box my-lager-box
lager scope ANALOG1 measure duty-cycle-pos --box my-lager-box
# Voltage measurements
lager scope ANALOG1 measure vpp --box my-lager-box
lager scope ANALOG1 measure vrms --display --box my-lager-box
lager scope ANALOG1 measure vmax --box my-lager-box
```
### Triggering
```bash theme={null}
# Edge trigger
lager scope ANALOG1 trigger edge --slope rising --level 1.5 --box my-lager-box
# Pulse width trigger (glitch detection)
lager scope ANALOG1 trigger pulse --trigger-on positive_less --upper 0.0001 --box my-lager-box
# UART trigger at 115200 baud
lager scope ANALOG1 trigger uart \
--source UART_NET --baud 115200 --trigger-on start --box my-lager-box
# I2C trigger on address 0x48
lager scope ANALOG1 trigger i2c \
--source-scl SCL --source-sda SDA \
--trigger-on address --address 0x48 --box my-lager-box
# SPI trigger on chip select
lager scope ANALOG1 trigger spi \
--source-mosi-miso MOSI --source-sck SCK --source-cs CS \
--trigger-on cs --box my-lager-box
```
### Cursor Measurements
```bash theme={null}
# Measure time between two events
lager scope ANALOG1 cursor set-a --x -0.001 --box my-lager-box
lager scope ANALOG1 cursor set-b --x 0.002 --box my-lager-box
# Measure voltage difference
lager scope ANALOG1 cursor set-a --y 0.0 --box my-lager-box
lager scope ANALOG1 cursor set-b --y 3.3 --box my-lager-box
# Clean up
lager scope ANALOG1 cursor hide --box my-lager-box
```
### PicoScope Streaming
```bash theme={null}
# Start streaming with default settings
lager scope PICO1 stream start --box my-lager-box
# Open web visualization
lager scope PICO1 stream web --box my-lager-box
# Capture data to file
lager scope PICO1 stream capture -o waveform.csv -d 5.0 --box my-lager-box
# Adjust settings while streaming
lager scope PICO1 stream config -v 2.0 -t 0.0001 --box my-lager-box
# Check streaming status
lager scope PICO1 stream status --box my-lager-box
# Stop streaming
lager scope PICO1 stream stop --box my-lager-box
```
***
## Command Structure
```
scope [NETNAME] [--box BOX]
Basic Control
├── enable [--mcu]
├── disable [--mcu]
├── start [--mcu] [--single]
├── stop [--mcu]
└── force [--mcu]
Channel Configuration (Rigol)
├── scale VOLTS_PER_DIV [--mcu]
├── coupling {dc|ac|gnd} [--mcu]
├── probe {1|10|100|1000} [--mcu]
├── timebase SECONDS_PER_DIV [--mcu]
└── autoscale [--mcu]
Measurements (Rigol)
└── measure
├── period [--display] [--cursor]
├── freq [--display] [--cursor]
├── vpp [--display] [--cursor]
├── vmax [--display] [--cursor]
├── vmin [--display] [--cursor]
├── vrms [--display] [--cursor]
├── vavg [--display] [--cursor]
├── pulse-width-pos [--display] [--cursor]
├── pulse-width-neg [--display] [--cursor]
├── duty-cycle-pos [--display] [--cursor]
└── duty-cycle-neg [--display] [--cursor]
Triggers
└── trigger
├── edge [--slope] [--level] [--mode] [--coupling] [--source]
├── pulse [--trigger-on] [--upper] [--lower] [--level] ...
├── i2c [--source-scl] [--source-sda] [--trigger-on] [--address] ...
├── spi [--source-mosi-miso] [--source-sck] [--source-cs] ...
└── uart [--baud] [--parity] [--trigger-on] [--data] ...
Cursors (Rigol)
└── cursor
├── set-a [--x] [--y]
├── set-b [--x] [--y]
├── move-a [--x] [--y]
├── move-b [--x] [--y]
└── hide
Streaming (PicoScope)
└── stream
├── start [-c] [-v] [-t] [--trigger-level] [--trigger-slope] ...
├── stop
├── status
├── web [--port]
├── capture [-o] [-d] [-n] [--quiet] [--json] [--verbose]
└── config [-c] [-v] [-t] [--enable/--disable] ...
```
***
## Notes
* Net names refer to names assigned when setting up your testbed with `lager nets`
* The `--mcu` option is available on all commands and is passed to the backend; use it when multiple MCUs share a scope channel
* Streaming commands are only available for PicoScope devices
* Measurement and cursor commands are only available for Rigol devices
* Edge trigger is the only trigger type supported on both PicoScope and Rigol
* Web visualization requires port 8080 to be accessible on the Lager Box
* Validation rejects values outside the allowed ranges before sending to hardware
* Use `lager nets` to see available scope nets
## See Also
* [Logic Analyzer](/source/reference/cli/logic) -- Digital signal capture and protocol decode
* [Python Scope API](/source/reference/python/scope) -- Automate oscilloscope operations in Python scripts
# Solar Simulation
Source: https://docs.lagerdata.com/source/reference/cli/solar
Control solar panel simulator settings and output
Control solar panel simulator Nets through the Lager CLI. Solar simulation enables testing of solar-powered devices by simulating various irradiance conditions, temperatures, and panel characteristics.
## Syntax
```bash theme={null}
lager solar [OPTIONS] NET_NAME COMMAND [ARGS]...
```
## Global Options
| Option | Description |
| ----------- | --------------------------- |
| `--box BOX` | Lagerbox name or IP address |
| `--help` | Show help message and exit |
## Commands
| Command | Description |
| ------------- | ------------------------------------------ |
| `set` | Initialize and start solar simulation mode |
| `stop` | Stop solar simulation mode |
| `irradiance` | Set or read irradiance (W/m²) |
| `mpp-current` | Read maximum power point current (A) |
| `mpp-voltage` | Read maximum power point voltage (V) |
| `resistance` | Set or read dynamic panel resistance (Ω) |
| `temperature` | Read cell temperature (°C) |
| `voc` | Read open-circuit voltage (V) |
## Command Reference
### `set`
Initialize and start the solar simulation mode.
```bash theme={null}
lager solar NET_NAME set [--box BOX]
```
This command configures the power supply to operate in solar panel simulation mode, enabling I-V curve emulation.
### `stop`
Stop the solar simulation mode and return to normal operation.
```bash theme={null}
lager solar NET_NAME stop [--box BOX]
```
### `irradiance`
Set or read the irradiance level in watts per square meter (W/m²).
```bash theme={null}
lager solar NET_NAME irradiance [VALUE] [--box BOX]
```
**Arguments:**
* `VALUE` - Irradiance value in W/m² (0.0 - 1500.0). If omitted, reads current value.
**Examples:**
```bash theme={null}
# Set irradiance to 1000 W/m² (standard test condition)
lager solar SOLAR1 irradiance 1000
# Read current irradiance
lager solar SOLAR1 irradiance
```
### `mpp-current`
Read the maximum power point (MPP) current in amps.
```bash theme={null}
lager solar NET_NAME mpp-current [--box BOX]
```
Returns the current at which the simulated solar panel produces maximum power.
### `mpp-voltage`
Read the maximum power point (MPP) voltage in volts.
```bash theme={null}
lager solar NET_NAME mpp-voltage [--box BOX]
```
Returns the voltage at which the simulated solar panel produces maximum power.
### `resistance`
Set or read the dynamic panel resistance in ohms.
```bash theme={null}
lager solar NET_NAME resistance [VALUE] [--box BOX]
```
**Arguments:**
* `VALUE` - Resistance value in Ω (0.1 - 100.0). If omitted, reads current value.
### `temperature`
Read the simulated cell temperature in degrees Celsius.
```bash theme={null}
lager solar NET_NAME temperature [--box BOX]
```
### `voc`
Read the open-circuit voltage (Voc) in volts.
```bash theme={null}
lager solar NET_NAME voc [--box BOX]
```
Returns the voltage when no load is connected to the simulated solar panel.
***
## Examples
```bash theme={null}
# Start solar simulation mode
lager solar SOLAR_PANEL set --box my-lager-box
# Set irradiance to standard test condition (1000 W/m²)
lager solar SOLAR_PANEL irradiance 1000
# Read MPP voltage and current
lager solar SOLAR_PANEL mpp-voltage
lager solar SOLAR_PANEL mpp-current
# Read open-circuit voltage
lager solar SOLAR_PANEL voc
# Set panel resistance
lager solar SOLAR_PANEL resistance 5.0
# Stop solar simulation
lager solar SOLAR_PANEL stop
```
***
## Supported Hardware
| Manufacturer | Model Series | Features |
| ------------ | ------------- | -------------------------------------------- |
| EA | PSI/EL series | Two-quadrant operation, I-V curve simulation |
| EA | PSB 10060-60 | Bidirectional power supply |
| EA | PSB 10080-60 | Bidirectional power supply |
***
## Solar Panel Simulation Concepts
### I-V Curve
Solar panel simulators create an I-V (current-voltage) characteristic curve that mimics real solar panel behavior:
* **Short-circuit current (Isc)**: Maximum current when terminals are shorted
* **Open-circuit voltage (Voc)**: Voltage with no load
* **Maximum Power Point (MPP)**: Optimal operating point for maximum power
### Standard Test Conditions (STC)
Industry standard for solar panel testing:
* Irradiance: 1000 W/m²
* Cell temperature: 25°C
* Air mass: AM1.5
***
## Notes
* Solar simulation requires compatible bidirectional power supplies
* The EA PSI/EL series supports two-quadrant operation for realistic simulation
* Use `lager nets` to see available solar nets on your box
* Irradiance values above 1500 W/m² are outside the valid range
# SPI
Source: https://docs.lagerdata.com/source/reference/cli/spi
Perform SPI data transfers
Perform SPI (Serial Peripheral Interface) data transfers with devices connected to a Lagerbox. SPI is a synchronous serial protocol using four lines: SCLK (clock), MOSI (master out), MISO (master in), and CS (chip select).
## Syntax
```bash theme={null}
lager spi [NETNAME] [OPTIONS] [SUBCOMMAND]
```
## Arguments
| Argument | Description |
| --------- | ---------------------------------------------------------------------------- |
| `NETNAME` | SPI net name (optional if default is set via `lager defaults add --spi-net`) |
## Options
| Option | Description |
| ----------- | --------------------------- |
| `--box BOX` | Lagerbox name or IP address |
When invoked without a subcommand, lists SPI nets on the box (or shows configuration for the specified net).
***
## Subcommands
### `config`
Configure SPI bus parameters. Settings persist across subsequent commands.
```bash theme={null}
lager spi NETNAME config [OPTIONS]
```
| Option | Description |
| ------------------------ | -------------------------------------------------------------------- |
| `--box BOX` | Lagerbox name or IP address |
| `--mode 0\|1\|2\|3` | SPI mode (clock polarity and phase) |
| `--frequency FREQ` | Clock frequency (e.g., `1M`, `500k`, `5M`) |
| `--bit-order msb\|lsb` | Bit order: MSB first or LSB first |
| `--word-size 8\|16\|32` | Word size in bits |
| `--cs-active low\|high` | Chip select active polarity |
| `--cs-mode auto\|manual` | CS assertion mode: `auto` (hardware) or `manual` (user-managed GPIO) |
**SPI Modes:**
| Mode | CPOL | CPHA | Description |
| ---- | ---- | ---- | --------------------------------------------- |
| 0 | 0 | 0 | Clock idle low, data sampled on rising edge |
| 1 | 0 | 1 | Clock idle low, data sampled on falling edge |
| 2 | 1 | 0 | Clock idle high, data sampled on falling edge |
| 3 | 1 | 1 | Clock idle high, data sampled on rising edge |
**Examples:**
```bash theme={null}
# Configure SPI mode 0 at 5MHz
lager spi MY_SPI config --mode 0 --frequency 5M
# Set 16-bit word size with LSB-first
lager spi MY_SPI config --word-size 16 --bit-order lsb
# Use manual CS mode (for Aardvark with separate GPIO for CS)
lager spi MY_SPI config --cs-mode manual
```
***
### `transfer`
Perform a full-duplex SPI transfer. Sends data while simultaneously receiving response. If provided data is shorter than `NUM_WORDS`, the remaining words are padded with the fill value. If longer, data is truncated.
```bash theme={null}
lager spi NETNAME transfer NUM_WORDS [OPTIONS]
```
| Argument | Description |
| ----------- | --------------------------- |
| `NUM_WORDS` | Number of words to transfer |
| Option | Description | Default |
| ----------------------- | ------------------------------------- | ------- |
| `--box BOX` | Lagerbox name or IP address | |
| `--data DATA` | Hex data to transmit (e.g., `0x9f01`) | |
| `--data-file PATH` | File containing data to transmit | |
| `--fill VALUE` | Fill value for padding | `0xFF` |
| `--mode 0\|1\|2\|3` | SPI mode override | |
| `--frequency FREQ` | Clock frequency override | |
| `--bit-order msb\|lsb` | Bit order override | |
| `--word-size 8\|16\|32` | Word size override | |
| `--cs-active low\|high` | CS polarity override | |
| `--keep-cs` | Keep CS asserted after transfer | `false` |
| `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` |
**Examples:**
```bash theme={null}
# Read device ID: send 0x9F command, read 3 response bytes (4 words total)
lager spi MY_SPI transfer --data 0x9f 4
# Send data at 5MHz
lager spi MY_SPI transfer --data "01 02 03 04" --frequency 5M 4
# Output as JSON
lager spi MY_SPI transfer --data 0x9f 4 --format json
# Keep CS asserted for multi-part transfer
lager spi MY_SPI transfer --data 0x03 --keep-cs 1
```
***
### `read`
Read data from an SPI slave device. Sends fill bytes while clocking in the response.
```bash theme={null}
lager spi NETNAME read NUM_WORDS [OPTIONS]
```
| Argument | Description |
| ----------- | ----------------------- |
| `NUM_WORDS` | Number of words to read |
| Option | Description | Default |
| ----------------------- | ------------------------------------- | ------- |
| `--box BOX` | Lagerbox name or IP address | |
| `--fill VALUE` | Fill byte sent while reading | `0xFF` |
| `--mode 0\|1\|2\|3` | SPI mode override | |
| `--frequency FREQ` | Clock frequency override | |
| `--bit-order msb\|lsb` | Bit order override | |
| `--word-size 8\|16\|32` | Word size override | |
| `--cs-active low\|high` | CS polarity override | |
| `--keep-cs` | Keep CS asserted after transfer | `false` |
| `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` |
**Examples:**
```bash theme={null}
# Read 5 bytes from SPI slave
lager spi MY_SPI read 5
# Read with 0x00 fill instead of default 0xFF
lager spi MY_SPI read 5 --fill 0x00
# Read 4 words at 16-bit word size
lager spi MY_SPI read 4 --word-size 16
```
***
### `write`
Write data to an SPI slave device. Performs a full-duplex transfer and displays the received response.
```bash theme={null}
lager spi NETNAME write DATA [OPTIONS]
```
| Argument | Description |
| -------- | ---------------------------------------- |
| `DATA` | Hex data to write (e.g., `0x9f01020304`) |
| Option | Description | Default |
| ----------------------- | ------------------------------------- | ------- |
| `--box BOX` | Lagerbox name or IP address | |
| `--mode 0\|1\|2\|3` | SPI mode override | |
| `--frequency FREQ` | Clock frequency override | |
| `--bit-order msb\|lsb` | Bit order override | |
| `--word-size 8\|16\|32` | Word size override | |
| `--cs-active low\|high` | CS polarity override | |
| `--keep-cs` | Keep CS asserted after transfer | `false` |
| `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` |
**Examples:**
```bash theme={null}
# Send JEDEC ID command and read response
lager spi MY_SPI write 0x9f01020304
# Write with mode override
lager spi MY_SPI write 0x0102 --mode 3
# Write and keep CS low for continued transfer
lager spi MY_SPI write 0x03000000 --keep-cs
```
***
## Hex Data Formats
Data arguments accept multiple hex formats. Parsing behavior depends on word size:
**8-bit word size (default):**
| Format | Example | Parsed As |
| --------------------- | ---------- | -------------------- |
| Prefixed continuous | `0x9f01` | `[0x9f, 0x01]` |
| Unprefixed continuous | `9f01` | `[0x9f, 0x01]` |
| Space-separated | `9f 01 02` | `[0x9f, 0x01, 0x02]` |
| Comma-separated | `9f,01,02` | `[0x9f, 0x01, 0x02]` |
**16-bit or 32-bit word size:**
| Format | Example | Parsed As |
| --------------- | --------------- | ------------------ |
| Continuous | `0x1234` | `[0x1234]` |
| Space-separated | `0x1234 0x5678` | `[0x1234, 0x5678]` |
Values are validated against the configured word size range.
***
## Fill Value Format
The `--fill` option accepts hex or decimal values:
| Format | Example | Value |
| -------------- | ------- | ----- |
| Hex prefixed | `0xff` | 255 |
| Hex unprefixed | `ff` | 255 |
| Decimal | `255` | 255 |
***
## Frequency Format
Clock frequencies accept numeric values with optional suffixes:
| Format | Example | Value |
| ---------- | ----------- | ------- |
| Plain Hz | `1000000` | 1 MHz |
| kHz suffix | `500k` | 500 kHz |
| MHz suffix | `5M` | 5 MHz |
| Hz suffix | `1000000hz` | 1 MHz |
***
## Supported Hardware
| Adapter | Pins | CS Control | Notes |
| ------------ | --------------------------------------- | ------------------ | ------------------------------------------------------ |
| LabJack T7 | Configurable FIO pins (e.g., FIO0-FIO3) | Manual GPIO | \~450 kHz max (throttle=0); CS managed via GPIO writes |
| Aardvark USB | Fixed MOSI/MISO/SCK/SS | Hardware or manual | Up to 8 MHz |
| FT232H | Configurable | MPSSE-based | FTDI MPSSE SPI |
***
## Net Configuration
SPI nets are configured in `saved_nets.json` on the box. Example net record:
```json theme={null}
{
"name": "my_spi",
"role": "spi",
"instrument": "labjack_t7",
"pin": "FIO0-FIO3",
"params": {
"cs_pin": 0,
"clk_pin": 1,
"mosi_pin": 2,
"miso_pin": 3,
"mode": 0,
"frequency_hz": 1000000,
"word_size": 8,
"bit_order": "msb"
}
}
```
For Aardvark adapters:
```json theme={null}
{
"name": "my_spi",
"role": "spi",
"instrument": "aardvark",
"pin": "SPI0",
"params": {
"mode": 0,
"frequency_hz": 1000000,
"word_size": 8,
"bit_order": "msb"
}
}
```
***
## Output Formats
| Format | Description |
| ------- | --------------------------------------------- |
| `hex` | Space-separated hex values (e.g., `9f 01 02`) |
| `bytes` | Raw byte values |
| `json` | JSON object with data array and metadata |
***
## Examples
```bash theme={null}
# List all SPI nets on a box
lager spi --box my-lager-box
# Show configuration for a specific net
lager spi MY_SPI --box my-lager-box
# Configure SPI mode and speed
lager spi MY_SPI config --mode 0 --frequency 5M
# Read flash JEDEC ID (send 0x9F command, read 3 bytes)
lager spi MY_SPI transfer --data 0x9f 4
# Read 256 bytes from flash
lager spi MY_SPI read 256
# Write a command byte
lager spi MY_SPI write 0x06
# Multi-step transfer with CS held low
lager spi MY_SPI write 0x03 --keep-cs
lager spi MY_SPI read 4
```
***
## Troubleshooting
### No Response from Device
* Verify MOSI, MISO, SCLK, and CS wiring
* Check SPI mode matches the device datasheet
* Confirm CS polarity (`--cs-active low` for most devices)
* Try reducing frequency with `--frequency 100k`
### Garbled Data
* Verify SPI mode (CPOL/CPHA) matches the device
* Check bit order (MSB vs LSB first)
* Ensure word size matches the device protocol
### CS Pin Not Working (LabJack T7)
* LabJack T7 uses manual GPIO-based CS control
* The driver automatically asserts/deasserts CS via GPIO writes
* Verify the `cs_pin` in the net configuration matches your wiring
***
## Notes
* Default SPI net can be set with `lager defaults add --spi-net NETNAME`
* LabJack T7 operates at \~450 kHz regardless of requested frequency due to hardware limitations
* SPI is full-duplex: data is always sent and received simultaneously
* Use `--keep-cs` for multi-part transactions that require CS to stay asserted
* Configuration set via `config` persists across subsequent `transfer`/`read`/`write` commands
## See Also
* [I2C](/source/reference/cli/i2c) -- I2C bus communication (the other common serial protocol)
* [Python SPI API](/source/reference/python/spi) -- Automate SPI operations in Python scripts
* [Glossary](/source/getting-started/glossary) -- Definitions of SPI, I2C, and other terms
# SSH
Source: https://docs.lagerdata.com/source/reference/cli/ssh
SSH into a Lager Box
Open an interactive SSH session to a Lagerbox, or run a single command on it
and return.
## Syntax
```bash theme={null}
lager ssh [OPTIONS] [COMMAND]...
```
## Options
| Option | Description |
| ----------- | --------------------------- |
| `--box BOX` | Lagerbox name or IP address |
## Arguments
| Argument | Description |
| --------- | ------------------------------------------------------------------------------- |
| `COMMAND` | Optional command to run on the box. If omitted, an interactive shell is opened. |
***
## Usage
```bash theme={null}
# SSH to specific Lager Box (interactive shell)
lager ssh --box my-lager-box
# SSH to default Lager Box
lager ssh
```
### Run a command on the box
With a `COMMAND`, `lager ssh` behaves like `ssh user@host `: it runs
the command on the box, streams its output back, and exits with the command's
exit code — no interactive shell. This is handy for scripting and one-off
checks.
```bash theme={null}
# Run a single command and return
lager ssh --box lab-lager-box -- cat /etc/lager/version
# Flags pass through; use `--` to separate them from lager's own options
lager ssh --box lab-lager-box -- ls -la /etc/lager
# Inspect the running containers
lager ssh --box lab-lager-box -- sudo docker ps
```
Use `--` to separate lager's options from the remote command whenever the
command contains its own dashed flags, so they aren't parsed as `lager`
options. The remote command's exit code is propagated as `lager ssh`'s exit
code, so it composes cleanly in scripts.
***
## How It Works
The command:
1. Resolves the Lager Box name to IP address
2. Looks up the SSH username (default: `lagerdata`)
3. Opens an interactive SSH session
***
## Username Resolution
SSH usernames are resolved in order:
1. Username stored with box configuration (`lager boxes add --user`)
2. Default username: `lagerdata`
To use a different username for a Lager Box:
```bash theme={null}
# Configure username when adding box
lager boxes add --name pi-lager-box --ip --user pi
# Or edit existing box
lager boxes edit --name pi-lager-box --user pi
```
***
## SSH Key Setup
For passwordless access, set up SSH keys:
```bash theme={null}
# Generate key if needed
ssh-keygen -t ed25519
# Copy to Lager Box
ssh-copy-id lagerdata@
```
***
## Examples
```bash theme={null}
# Quick interactive SSH access
lager ssh --box my-lager-box
# One-off commands (no interactive shell)
lager ssh --box my-lager-box -- cat /etc/lager/version
lager ssh --box my-lager-box -- sudo docker ps
lager ssh --box my-lager-box -- sudo ufw status
```
***
## Common Tasks via SSH
Each of these can be run as a one-liner with `lager ssh --box -- `,
or interactively after `lager ssh --box `.
### Check Container Status
```bash theme={null}
lager ssh --box my-lager-box -- sudo docker ps -a
lager ssh --box my-lager-box -- sudo docker logs controller --tail 100
```
### View Lager Box Version
```bash theme={null}
lager ssh --box my-lager-box -- cat /etc/lager/version
```
### Check Disk Space
```bash theme={null}
lager ssh --box my-lager-box -- df -h
```
### View Firewall Status
```bash theme={null}
lager ssh --box my-lager-box -- sudo ufw status verbose
```
***
## Notes
* With no `COMMAND`, opens a fully interactive shell session (exit with `exit` or Ctrl+D)
* With a `COMMAND`, runs it on the box and exits with the command's exit code
* The session runs as a child process so Lager's cleanup hooks still fire
* Default Lager Box is used if `--box` not specified
# SSH Setup
Source: https://docs.lagerdata.com/source/reference/cli/ssh-setup
Set up passwordless SSH from this machine to a Lager Box
Install this machine's Lager SSH key on a Lagerbox so that `lager` commands and
`lager ssh` work without a password. Run it once per box, enter the box password
when prompted, and subsequent commands authenticate with the key.
This wraps the `ssh-keygen` / `ssh-copy-id` dance into a single command, so a
`Permission denied (publickey,password)` error can be fixed without knowing the
key path or the `ssh-copy-id` incantation by hand.
Introduced in **lager 0.27.1** as `lager authorize`; renamed to
`lager ssh-setup` because the old name read like authentication once
`lager login` (gateway sign-in) arrived. The old spelling still works
with a deprecation warning.
## Syntax
```bash theme={null}
lager ssh-setup [OPTIONS]
```
## Options
| Option | Description |
| ----------- | ------------------------------------------------------------- |
| `--box BOX` | Lagerbox name or IP address (uses the default box if omitted) |
***
## Usage
```bash theme={null}
# Authorize a specific Lager Box
lager ssh-setup --box my-lager-box
# Authorize the default Lager Box
lager ssh-setup
```
You are prompted for the box password **once** (by `ssh-copy-id`). After the key
is installed, no further password prompts appear for that box.
***
## How It Works
1. **Resolves** the box name to an IP and looks up its SSH user (see
[SSH username resolution](/source/reference/cli/ssh#username-resolution)).
2. **Generates** the key pair `~/.ssh/lager_box` (and `lager_box.pub`) if it does
not already exist.
3. **Skips early if already authorized** — if key authentication already works for
the box, it reports success and changes nothing (the command is idempotent).
4. **Copies** the public key to the box with `ssh-copy-id` (one password prompt).
5. **Verifies** that passwordless key authentication now works, reporting a clear
error if it does not.
The key lives at `~/.ssh/lager_box`. Because that filename is not one of SSH's
default identities, `lager ssh` passes `-i ~/.ssh/lager_box` explicitly when the
key exists (since lager 0.28.1), so authorized boxes connect without a password.
***
## Examples
```bash theme={null}
# First-time setup for a new box
lager boxes add --name my-lager-box --ip 100.x.y.z
lager ssh-setup --box my-lager-box
# Re-running against an already-authorized box is safe (no-op)
lager ssh-setup --box my-lager-box
# my-lager-box is already authorized — no password needed.
```
***
## Troubleshooting
| Issue | Cause | Fix |
| ------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ |
| `ssh-copy-id was not found` | OpenSSH client tools are not installed | Install the OpenSSH client, or append the key manually (see below) |
| `ssh-copy-id ... failed` | Wrong password, or the box rejected the connection | Confirm the box user and password with your admin, then retry |
| Key copied but auth still fails | The box's `sshd_config` may disallow `publickey` auth | Test with `ssh -i ~/.ssh/lager_box @` and check `sshd_config` |
If `ssh-copy-id` is unavailable, append the public key to the box manually:
```bash theme={null}
cat ~/.ssh/lager_box.pub | ssh @ 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'
```
***
## See Also
* [SSH](/source/reference/cli/ssh) — open an interactive shell on a Lager Box
* [Boxes](/source/reference/cli/boxes) — register box names, IPs, and SSH users
# Power Supply
Source: https://docs.lagerdata.com/source/reference/cli/supply
Control power supply on box
Control and monitor power supply Nets through the Lager CLI. Supports multi-vendor bench power supplies with voltage/current setting, protection thresholds, real-time monitoring via TUI, and concurrent access.
## Syntax
```bash theme={null}
lager supply [OPTIONS] NET_NAME COMMAND [ARGS]...
```
When invoked without a subcommand, lists all available power supply nets on the box:
```bash theme={null}
lager supply --box my-lager-box
```
```
Name Net Type Instrument Channel Address
================================================================
PSU_CH1 power-supply Rigol_DP832 CH1 USB0::0x1AB1::0x0E11::DP8...
PSU_CH2 power-supply Rigol_DP832 CH2 USB0::0x1AB1::0x0E11::DP8...
DUT_POWER power-supply Keysight_E36312A 1 USB0::0x2A8D::0x1602::...
```
## Global Options
| Option | Description |
| ------------ | --------------------------- |
| `--box TEXT` | Lagerbox name or IP address |
| `--help` | Show help message and exit |
## Commands
| Command | Description |
| ----------- | --------------------------------------------------------------------- |
| `voltage` | Set or read output voltage with optional protection thresholds |
| `current` | Set or read output current with optional protection thresholds |
| `enable` | Enable power output (requires confirmation) |
| `disable` | Disable power output (requires confirmation) |
| `state` | Show current power state including measurements and protection status |
| `clear-ovp` | Clear over-voltage protection fault |
| `clear-ocp` | Clear over-current protection fault |
| `set` | Set power supply mode |
| `tui` | Launch interactive terminal UI for real-time monitoring and control |
## CLI Validation Ranges
The CLI enforces conservative upper bounds before sending commands to hardware:
| Parameter | Maximum | Notes |
| --------- | ------- | -------------------------------- |
| Voltage | 100.0 V | Most bench supplies are 30-60 V |
| Current | 30.0 A | Typical bench supply limit |
| OVP | 110.0 V | Can be slightly above max output |
| OCP | 33.0 A | Can be slightly above max output |
All values must be positive. OVP must be greater than or equal to the voltage setpoint.
## Command Reference
### `voltage`
Set or read output voltage with optional protection thresholds.
```bash theme={null}
lager supply NET_NAME voltage [VALUE] [OPTIONS]
```
**Arguments:**
* `VALUE` - Voltage in volts. Omit to read the current voltage setting.
**Options:**
| Option | Type | Description |
| ------------- | ------ | ------------------------------------------------------ |
| `--ovp FLOAT` | Volts | Over-voltage protection threshold (must be >= voltage) |
| `--ocp FLOAT` | Amps | Over-current protection threshold |
| `--yes` | Flag | Apply without confirmation prompt |
| `--box TEXT` | String | Lagerbox name or IP |
When VALUE is provided, the CLI prompts for confirmation unless `--yes` is passed:
```
Set voltage to 3.3 V? [y/N]:
```
**Examples:**
```bash theme={null}
# Read current voltage
lager supply PSU voltage
# Set voltage to 3.3V (will prompt for confirmation)
lager supply PSU voltage 3.3
# Set voltage with automatic confirmation
lager supply PSU voltage 3.3 --yes
# Set voltage with OVP and OCP thresholds
lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes
```
### `current`
Set or read output current with optional protection thresholds.
```bash theme={null}
lager supply NET_NAME current [VALUE] [OPTIONS]
```
**Arguments:**
* `VALUE` - Current in amps. Omit to read the current limit setting.
**Options:**
| Option | Type | Description |
| ------------- | ------ | ------------------------------------------------------ |
| `--ovp FLOAT` | Volts | Over-voltage protection threshold |
| `--ocp FLOAT` | Amps | Over-current protection threshold (must be >= current) |
| `--yes` | Flag | Apply without confirmation prompt |
| `--box TEXT` | String | Lagerbox name or IP |
When VALUE is provided, the CLI prompts for confirmation unless `--yes` is passed:
```
Set current to 1.0 A? [y/N]:
```
**Examples:**
```bash theme={null}
# Read current limit
lager supply PSU current
# Set current limit to 1A
lager supply PSU current 1.0 --yes
# Set current with protection thresholds
lager supply PSU current 1.0 --ocp 1.2 --ovp 5.0 --yes
```
### `enable`
Enable power output to device. Requires confirmation to prevent accidental power-on.
```bash theme={null}
lager supply NET_NAME enable [OPTIONS]
```
**Options:**
| Option | Description |
| ------------ | ---------------------------------- |
| `--yes` | Enable without confirmation prompt |
| `--box TEXT` | Lagerbox name or IP |
```
Enable Net? [y/N]:
```
### `disable`
Disable power output. Requires confirmation to prevent accidental power-off.
```bash theme={null}
lager supply NET_NAME disable [OPTIONS]
```
**Options:**
| Option | Description |
| ------------ | ----------------------------------- |
| `--yes` | Disable without confirmation prompt |
| `--box TEXT` | Lagerbox name or IP |
```
Disable Net? [y/N]:
```
### `state`
Show comprehensive power supply state including measurements, setpoints, and protection status.
```bash theme={null}
lager supply NET_NAME state [--box TEXT]
```
**Example output (Rigol DP800):**
```
Channel: CH1
Enabled: ON
Mode: CV
Voltage: 3.3000
Current: 0.1520
Power: 0.5016
OCP Limit: 1.0000
OCP Tripped: NO
OVP Limit: 3.6000
OVP Tripped: NO
```
Fields:
* **Channel** - Active channel on multi-channel supplies
* **Enabled** - Output ON or OFF
* **Mode** - CV (constant voltage) or CC (constant current)
* **Voltage/Current/Power** - Live measurements (4 decimal places)
* **OCP/OVP Limit** - Protection thresholds
* **OCP/OVP Tripped** - Whether protection has triggered (color-coded: green=NO, red=YES)
### `clear-ovp`
Clear over-voltage protection fault. Use after an OVP trip to reset the protection and allow the output to be re-enabled.
```bash theme={null}
lager supply NET_NAME clear-ovp [--box TEXT]
```
### `clear-ocp`
Clear over-current protection fault. Use after an OCP trip to reset the protection and allow the output to be re-enabled.
```bash theme={null}
lager supply NET_NAME clear-ocp [--box TEXT]
```
### `set`
Set power supply mode. The available modes depend on the hardware.
```bash theme={null}
lager supply NET_NAME set [--box TEXT]
```
### `tui`
Launch an interactive terminal UI for real-time power supply monitoring and control. The TUI provides live-updating measurements, inline command entry, and keyboard shortcuts.
```bash theme={null}
lager supply NET_NAME tui [--box TEXT]
```
Requires the `textual` Python package (`pip install textual`).
**TUI display:**
* Live voltage, current, and power measurements (updated every second)
* Output status (ON/OFF) with color coding
* Mode indicator (CV/CC) with color coding
* Protection thresholds and trip status
* Hardware maximum ratings
* Scrollable command log
**TUI commands** (enter at the prompt):
| Command | Description |
| --------------------- | ------------------------- |
| `voltage [VALUE]` | Set or read voltage |
| `current [VALUE]` | Set or read current limit |
| `ocp [VALUE]` | Set or read OCP threshold |
| `ovp [VALUE]` | Set or read OVP threshold |
| `enable` | Enable output |
| `disable` | Disable output |
| `state` | Display current state |
| `clear-ocp` | Clear OCP trip |
| `clear-ovp` | Clear OVP trip |
| `help` | Show available commands |
| `clear` | Clear the command log |
| `q` / `quit` / `exit` | Exit the TUI |
**Keyboard shortcuts:**
| Key | Action |
| -------------- | ------------------------ |
| `q` | Quit |
| `Ctrl+C` | Quit |
| `r` | Refresh display |
| Up/Down arrows | Navigate command history |
**Concurrent access:** While the TUI is running, other `lager supply` CLI commands (e.g., `lager supply PSU voltage 3.3 --yes`) will automatically route through the TUI's WebSocket connection on port 9000, sharing the USB instrument connection. If the TUI is not running, commands use direct USB access.
## OVP / OCP Protection
Over-voltage protection (OVP) and over-current protection (OCP) thresholds protect your device under test from damage.
**Setting thresholds:**
```bash theme={null}
# Set OVP and OCP when setting voltage
lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes
# Set OCP when setting current
lager supply PSU current 1.0 --ocp 1.2 --yes
```
**Validation rules:**
* All values must be positive
* OVP must be >= the voltage setpoint
* OCP must be >= the current setpoint
* Values are validated against CLI maximum limits before being sent to hardware
**When protection trips:**
1. The supply output is disabled automatically
2. `state` shows the tripped status in red
3. Clear the fault with `clear-ovp` or `clear-ocp`
4. Re-enable the output with `enable`
**Automatic OVP management (Rigol):** When setting a new voltage that would exceed the current OVP limit, the driver temporarily raises OVP by 10% headroom, sets the new voltage, then restores the desired OVP. This prevents false trips during voltage changes.
## Supported Hardware
| Manufacturer | Model Series | Channels | Specs | Notes |
| ------------ | -------------- | -------- | -------------------------- | ---------------------- |
| Rigol | DP832 / DP832A | 3 | Ch1-2: 30V/3A, Ch3: 5V/3A | Most common |
| Rigol | DP821 | 2 | Ch1: 60V/1A, Ch2: 8V/10A | |
| Rigol | DP811 / DP811A | 1 | 20V/10A or 40V/5A (range) | |
| Keithley | 2281S | 1 | 20V/6A/120W | Battery simulator mode |
| Keysight | E36200 series | 2 | E36233A: 30V/20A per ch | Dual output |
| Keysight | E36300 series | 3 | E36311A/12A/13A | Triple output |
| EA | PSI/EL series | 1 | PSB 10080-60, PSB 10060-60 | Two-quadrant |
Multi-channel supplies use the channel number configured in the net record. Each channel is typically configured as a separate net.
## Default Net
Set a default power supply net to avoid specifying the name each time:
```bash theme={null}
lager defaults add --supply-net PSU
```
Then commands can omit the net name:
```bash theme={null}
lager supply voltage 3.3 --yes
lager supply state
```
## Examples
```bash theme={null}
# List all power supply nets
lager supply --box my-lager-box
# Set voltage with protection
lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes
# Set current limit
lager supply PSU current 1.0 --ocp 1.2 --yes
# Enable output (skip confirmation)
lager supply PSU enable --yes
# Check state
lager supply PSU state
# Disable output
lager supply PSU disable --yes
# Clear protection faults
lager supply PSU clear-ovp
lager supply PSU clear-ocp
# Launch interactive TUI
lager supply PSU tui
```
### Scripting Example
```bash theme={null}
#!/bin/bash
# Power cycle a device under test
BOX="my-lager-box"
NET="DUT_POWER"
lager supply $NET voltage 3.3 --ovp 3.6 --ocp 0.5 --yes --box $BOX
lager supply $NET enable --yes --box $BOX
sleep 2
# Run tests while powered
lager python test_script.py --box $BOX
# Power down
lager supply $NET disable --yes --box $BOX
```
## Troubleshooting
| Error | Cause | Fix |
| ------------------------ | ---------------------------------------- | ----------------------------------------------- |
| "Resource busy" | TUI is using the supply's USB connection | Close the TUI (press `q`), then retry |
| "No route to host" | Box unreachable | Check VPN/Tailscale: `lager hello --box ` |
| Connection refused | Box service not running | Verify box is online: `lager hello --box ` |
| OVP/OCP validation error | Protection threshold below setpoint | Set OVP >= voltage, OCP >= current |
| "exceeds maximum limit" | Value above CLI safety limits | Check equipment specs; CLI max is 100V / 30A |
## Notes
* All `voltage` and `current` set operations require confirmation (use `--yes` to skip)
* `enable` and `disable` also require confirmation
* Net names refer to names assigned when setting up your testbed with `lager nets`
* The TUI connects via WebSocket (port 9000) for real-time updates
* Protection thresholds help prevent damage to your device under test
* Multi-channel supplies: each channel is configured as a separate net
## See Also
* [Battery Simulation](/source/reference/cli/battery) -- Control battery simulators (Keithley 2281S)
* [Electronic Load](/source/reference/cli/eload) -- Programmable electronic loads
* [Watt Meter](/source/reference/cli/watt) -- Power measurement with Yocto-Watt and Joulescope
* [Python Supply API](/source/reference/python/supply) -- Automate power supply control in Python scripts
# Thermocouple
Source: https://docs.lagerdata.com/source/reference/cli/tc
Read thermocouple temperature measurements
Read thermocouple temperature values from thermocouple nets through the Lager CLI for temperature measurement and monitoring.
## Syntax
```bash theme={null}
lager thermocouple [NETNAME] [OPTIONS]
```
## Arguments
| Argument | Description |
| --------- | -------------------------------------------------- |
| `NETNAME` | Thermocouple net name (optional if default is set) |
## Options
| Option | Description |
| ------------ | --------------------------- |
| `--box TEXT` | Lagerbox name or IP address |
| `--help` | Show help message and exit |
***
## Usage
```bash theme={null}
# Read temperature from thermocouple net
lager thermocouple TEMP_SENSOR --box my-lager-box
# List available thermocouple nets (when no net specified)
lager thermocouple --box my-lager-box
# Using default net
lager thermocouple
```
***
## Output
Returns temperature in degrees Celsius:
```bash theme={null}
$ lager thermocouple TEMP_SENSOR --box my-lager-box
23.5
```
***
## Supported Hardware
| Device | Type | Range |
| --------------- | ---------------------- | ----------------- |
| Phidget TMP1101 | 4-channel thermocouple | -200°C to +1300°C |
***
## Examples
```bash theme={null}
# Read temperature from sensor
lager thermocouple TEMP_SENSOR --box my-lager-box
# Monitor multiple thermocouples
lager thermocouple TC1 --box my-lager-box
lager thermocouple TC2 --box my-lager-box
lager thermocouple TC3 --box my-lager-box
# Use in shell script
TEMP=$(lager thermocouple OVEN_TC --box my-lager-box)
if (( $(echo "$TEMP > 100" | bc -l) )); then
echo "Temperature exceeded 100°C!"
fi
```
***
## Creating Thermocouple Nets
```bash theme={null}
# Create a thermocouple net
lager nets add TEMP_SENSOR thermocouple 0 PHIDGET_SERIAL --box my-lager-box
```
Where:
* `TEMP_SENSOR` - Net name
* `thermocouple` - Net type
* `0` - Channel number (0-3 for 4-channel Phidget)
* `PHIDGET_SERIAL` - Phidget device serial number
***
## Notes
* Net names refer to names assigned when setting up your testbed
* Results are returned in degrees Celsius (°C)
* Only works with nets of type `thermocouple`
* If no net is specified and no default is set, lists available thermocouple nets
* Default net can be set with `lager defaults add --thermocouple-net`
# Terminal
Source: https://docs.lagerdata.com/source/reference/cli/terminal
Interactive REPL for running Lager commands
Launch an interactive terminal with tab completion, command history, and auto-suggestions for running Lager CLI commands.
## Syntax
```bash theme={null}
lager terminal
```
The terminal also launches automatically when you run `lager` with no subcommand.
## Features
| Feature | Description |
| -------------------- | -------------------------------------------------------- |
| Tab completion | Auto-complete commands, subcommands, and flags |
| Command history | Persistent history stored in `~/.lager_terminal_history` |
| Auto-suggest | History-based suggestions as you type |
| Arrow key navigation | Browse previous commands with up/down arrows |
| Color-coded output | Green for success, red for errors |
| Execution timing | Shows duration for each command |
## Built-in Commands
These commands are available inside the REPL in addition to all `lager` commands:
| Command | Description |
| -------------- | -------------------------------- |
| `help`, `?` | Show available commands and help |
| `clear` | Clear the screen |
| `exit`, `quit` | Exit the terminal |
## Keyboard Shortcuts
| Shortcut | Action |
| ----------- | --------------------------- |
| `Tab` | Auto-complete current input |
| `Up / Down` | Navigate command history |
| `Ctrl+R` | Search command history |
| `Ctrl+C` | Cancel current input |
| `Ctrl+D` | Exit terminal |
## Usage
All Lager commands are available inside the terminal without the `lager` prefix:
```bash theme={null}
# Launch the terminal
lager terminal
# Inside the REPL:
> hello --box my-lager-box
> supply voltage 3.3 --yes
> adc read VCC
> status --box my-lager-box
> debug flash --hexfile firmware.hex
> ?
> exit
```
The terminal automatically prepends `lager` to each command and executes it through the CLI, so authentication, box resolution, and all standard behavior works as expected.
## Blocked Commands
Interactive commands that require their own terminal session are blocked inside the REPL:
* Commands with `tui` subcommands (e.g., `supply tui`, `battery tui`)
* Interactive UART sessions
The terminal warns you and suggests running these from a regular shell instead.
## Dependencies
The terminal requires `prompt_toolkit` and `rich`. If these are not installed, the CLI falls back to showing normal help output:
```bash theme={null}
pip install prompt_toolkit rich
```
## Notes
* Running `lager` with no arguments launches the terminal automatically
* Command history persists across sessions in `~/.lager_terminal_history`
* Exit codes from each command are shown with a check mark or X indicator
* The welcome screen adapts to your terminal width
# UART
Source: https://docs.lagerdata.com/source/reference/cli/uart
Connect to UART serial ports
Connect to UART serial ports on Lagerboxes for serial communication with devices.
## Syntax
```bash theme={null}
lager uart [NETNAME] [OPTIONS]
```
## Arguments
| Argument | Description |
| --------- | ------------------------------------------ |
| `NETNAME` | UART net name (optional if default is set) |
## Options
| Option | Description |
| ---------------------------- | --------------------------------------- |
| `--box BOX` | Lagerbox name or IP address |
| `--baudrate RATE` | Baudrate (e.g., 9600, 115200) |
| `--bytesize SIZE` | Data bits (5, 6, 7, 8) |
| `--parity MODE` | Parity: none, even, odd, mark, space |
| `--stopbits BITS` | Stop bits (1, 1.5, 2) |
| `--xonxoff` / `--no-xonxoff` | Software flow control |
| `--rtscts` / `--no-rtscts` | Hardware flow control (RTS/CTS) |
| `--dsrdtr` / `--no-dsrdtr` | Hardware flow control (DSR/DTR) |
| `-i` / `--interactive` | Enable input mode for typing |
| `--opost` / `--no-opost` | Convert \n to \r\n on output |
| `--line-ending MODE` | Line ending: lf, crlf, cr (default: lf) |
***
## Usage
### Read-Only Mode (Default)
```bash theme={null}
# Monitor serial output
lager uart SERIAL1 --box my-lager-box
# With specific baudrate
lager uart SERIAL1 --baudrate 115200
```
### Interactive Mode
```bash theme={null}
# Type commands and see responses
lager uart SERIAL1 --interactive
# With specific settings
lager uart SERIAL1 -i --baudrate 9600 --parity none
```
### Get Serial Port Info
```bash theme={null}
# Show device path for a UART net
lager uart SERIAL1 serial-port
```
***
## Serial Configuration
### Baudrate
Common baudrates:
* 9600 (default for many devices)
* 19200
* 38400
* 57600
* 115200 (common for embedded development)
* 230400
* 460800
* 921600
### Data Format
| Setting | Options | Default |
| -------- | ---------------------------- | ------- |
| Bytesize | 5, 6, 7, 8 | 8 |
| Parity | none, even, odd, mark, space | none |
| Stopbits | 1, 1.5, 2 | 1 |
### Flow Control
| Type | Options | Description |
| -------- | ----------- | ------------------- |
| Software | `--xonxoff` | XON/XOFF characters |
| Hardware | `--rtscts` | RTS/CTS pins |
| Hardware | `--dsrdtr` | DSR/DTR pins |
Flow control types cannot be combined.
***
## Line Endings
| Mode | Sequence | Use Case |
| ------ | -------- | -------------- |
| `lf` | \n | Unix/Linux |
| `crlf` | \r\n | Windows |
| `cr` | \r | Legacy systems |
Use `--opost` to automatically convert line endings on output.
***
## Supported USB Serial Adapters
The following USB-to-serial adapters are automatically detected and supported:
| Adapter | VID:PID | Description |
| --------------------- | --------- | --------------------------- |
| Prolific USB-Serial | 067b:23a3 | Common USB-Serial adapter |
| Silicon Labs CP210x | 10c4:ea60 | Popular embedded dev boards |
| FTDI FT232R | 0403:6001 | Single-channel USB-Serial |
| FTDI FT4232H | 0403:6011 | Quad-channel USB-Serial |
| ESP32 USB JTAG/Serial | 303a:1001 | Built-in ESP32-S3/C3 USB |
These adapters are automatically recognized by `lager instruments` and can be configured as UART nets.
***
## Device Path Support
UART nets can reference devices two ways:
**USB Serial Number** (preferred):
```
Net configured with USB serial number
Automatically resolves to correct /dev/ttyUSBx
```
**Direct Device Path** (fallback):
```bash theme={null}
# When adapter has no serial number
lager uart /dev/ttyUSB0 --baudrate 115200
```
***
## Examples
```bash theme={null}
# Basic serial monitor
lager uart DEBUG_UART --box my-lager-box
# Interactive shell with common embedded settings
lager uart CONSOLE -i --baudrate 115200
# Legacy device with specific format
lager uart LEGACY --baudrate 9600 --bytesize 7 --parity even --stopbits 2
# With software flow control
lager uart MODEM --xonxoff
# Windows-style line endings
lager uart TERMINAL --line-ending crlf --opost
```
***
## WebSocket Connection
The UART command uses WebSocket for communication:
* Provides real-time bidirectional data
* Supports both read-only and interactive modes
* Automatically reconnects on connection loss
***
## Troubleshooting
### Device Not Found
```bash theme={null}
# List available instruments to find UART devices
lager instruments --box my-lager-box
# Check if USB serial adapter is connected
ssh lagerdata@my-lager-box 'ls -la /dev/ttyUSB*'
```
### Permission Denied
```bash theme={null}
# Ensure udev rules are installed
lager update --box my-lager-box --yes
```
### No Output
* Check baudrate matches device
* Verify TX/RX connections
* Try interactive mode to test input
* Check flow control settings
***
## Notes
* Interactive mode requires a TTY terminal
* USB serial numbers are truncated for display
* Default net can be set with `lager defaults add --uart-net`
* Connection retry logic handles temporary disconnections
## See Also
* [Python UART API](/source/reference/python/uart) -- Access UART nets from Python scripts
* [Python Serial API](/source/reference/python/serial) -- Native pyserial support for advanced serial use cases
# Uninstall
Source: https://docs.lagerdata.com/source/reference/cli/uninstall
Remove Lager box code from a box
Remove the Lager box software, Docker containers, and supporting files from a box.
## Syntax
```bash theme={null}
lager uninstall [OPTIONS]
```
## Options
| Option | Type | Default | Description |
| ---------------------- | ------ | ----------- | ----------------------------------------------------------------------------------------- |
| `--box TEXT` | string | | Box name (uses stored IP and username from `.lager` config) |
| `--ip TEXT` | string | | Target box IP address |
| `--user TEXT` | string | `lagerdata` | SSH username |
| `--keep-config` | flag | | Preserve `/etc/lager` directory (saved nets, box ID, etc.) |
| `--keep-docker-images` | flag | | Remove containers only, keep Docker images |
| `--all` | flag | | Remove everything including udev rules, sudoers, third-party tools, and legacy deploy key |
| `--yes` | flag | | Skip confirmation prompts |
| `--dry-run` | flag | | Show what would be removed without making changes |
| `--help` | | | Show help message and exit |
Either `--box` or `--ip` is required.
## What Gets Removed
### Default Removal
| Component | Description |
| ---------------------- | -------------------------------------------------------------- |
| Docker containers | Stops and removes the `lager` container |
| Docker images | Removes images and build cache (unless `--keep-docker-images`) |
| `~/box` directory | Box code and services |
| `/etc/lager` directory | Saved nets, box ID, version (unless `--keep-config`) |
### With `--all`
In addition to the above:
| Component | Description |
| ----------------- | ---------------------------------------------------------------------------------- |
| Udev rules | `/etc/udev/rules.d/lager-*.rules` |
| Sudoers config | `/etc/sudoers.d/lagerdata-udev` |
| `~/third_party` | J-Link, custom binaries |
| Legacy deploy key | `~/.ssh/lager_deploy_key*` and SSH config entry (from pre-open-source deployments) |
## Examples
```bash theme={null}
# Basic uninstall
lager uninstall --ip 192.168.1.100
# Uninstall but keep saved nets
lager uninstall --ip 192.168.1.100 --keep-config
# Uninstall but keep Docker images for faster reinstall
lager uninstall --ip 192.168.1.100 --keep-docker-images
# Complete cleanup
lager uninstall --ip 192.168.1.100 --all
# Non-interactive uninstall of a stored box
lager uninstall --box my-lager-box --yes
```
## Uninstall Flow
1. **Resolve target** - Looks up box IP from `--box` name or uses `--ip` directly
2. **Verify SSH** - Tests key-based authentication, falls back to password if needed
3. **Show summary** - Lists what will be removed based on flags
4. **Confirm** - Requires explicit confirmation (unless `--yes`)
5. **Remove step by step:**
* Stop and remove Docker containers
* Clean Docker images and build cache
* Remove `~/box` directory
* Remove `/etc/lager` directory
* Remove additional components (if `--all`)
6. **Report** - Confirms completion and suggests `--all` if not used
## Notes
* Each removal step continues even if a previous step fails, so partial uninstalls are possible
* Use `--keep-config` if you plan to reinstall and want to preserve your net configuration
* Use `--keep-docker-images` for a faster reinstall since images won't need to be rebuilt
* After uninstalling, use `lager install` to redeploy
# Update
Source: https://docs.lagerdata.com/source/reference/cli/update
Update Lager Box code from GitHub repository
Update Lager Box software on Lagerboxes with comprehensive progress tracking and automatic configuration.
## Syntax
```bash theme={null}
lager update [OPTIONS]
```
## Options
| Option | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `--box BOX` | Lagerbox name or IP address |
| `--all` | Update all saved boxes that need updating |
| `--version VERSION` | Release tag, semver pin, or branch to update to (default: main). See [Version pinning](#version-pinning). |
| `--yes` | Skip confirmation prompt |
| `--check` | Dry run: report what would change without modifying the box |
| `--skip-restart` | Skip container restart after update |
| `--check-jlink` | Check for J-Link and offer to install if missing |
| `--force` | Force fresh Docker build by removing cached image |
| `--verbose` / `-v` | Show detailed output (default shows progress bar) |
Either `--box` or `--all` is required. They cannot be used together.
## Version pinning
Since **lager 0.22.0**, a semver value passed to `--version` — with or without a
leading `v` (e.g. `0.26.0` or `v0.26.0`), including common pre-release suffixes
(`-rc1`, `-beta2`, `-alpha`, `-preview`) — resolves to the release **tag**
`vX.Y.Z`. Release tags are the single source of truth for a pinned version.
```bash theme={null}
# All three pin the same release tag (v0.26.0)
lager update --box my-lager-box --version v0.26.0 --yes
lager update --box my-lager-box --version 0.26.0 --yes
lager update --box my-lager-box --version v0.27.0-rc1 --yes
```
Any other value (`main`, `staging`, or a feature branch name) resolves to
`origin/` as before.
Per-release **version branches** (bare `X.Y.Z` branches) are deprecated in favour
of tags. A value like `0.26.0` now resolves to the tag `v0.26.0`, not a branch of
the same name. See `RELEASE_PROCESS.md` in the repository.
## Usage
### Basic Update
```bash theme={null}
# Update to latest main branch
lager update --box my-lager-box --yes
# Update to staging branch
lager update --box my-lager-box --version staging --yes
# Update with verbose output
lager update --box my-lager-box --yes --verbose
# Force fresh Docker build (use for major code changes)
lager update --box my-lager-box --force --yes
```
### Update All Boxes
The `--all` flag queries every saved box's version and updates only those that are behind the current CLI version.
```bash theme={null}
# Update all boxes that need updating
lager update --all --yes
# Preview which boxes need updating (without --yes, shows confirmation)
lager update --all
```
**How `--all` works:**
1. Queries each saved box's `/cli-version` endpoint
2. Compares box version against the current CLI version
3. Builds a list of boxes that need updating (version older than CLI or unknown)
4. Skips boxes that are already current, newer, or unreachable
5. Updates each box sequentially
6. Displays a summary of successes and failures
**Example output:**
```
CLI version: 0.3.22
Checking box versions...
my-lager-box 0.3.20 will update
staging-box 0.3.22 current
pi-box 0.3.19 will update
offline-box --- unreachable (skipped)
2 box(es) need updating. Proceed? [Y/n]: y
Updating my-lager-box...
[OK] Updated to 0.3.22
Updating pi-box...
[OK] Updated to 0.3.22
Update complete (45.2s total)
Successful: 2
Failed: 0
```
### Skip Container Restart
```bash theme={null}
# Update code but don't restart containers
lager update --box my-lager-box --skip-restart --yes
# Manually restart later
ssh lagerdata@my-lager-box 'cd ~/box && ./start_box.sh'
```
### J-Link Installation Check
```bash theme={null}
# Check if J-Link is installed, offer to install if missing
lager update --box my-lager-box --check-jlink --yes
```
***
## Update Process
The update command performs the following steps (shown in progress bar):
1. **SSH Connection** - Establish secure connection to Lager Box
2. **Git Repository Check** - Validate lager repository exists
3. **Git Pull** - Pull latest code from specified branch
4. **Udev Rules** - Install USB instrument access rules
5. **Docker Build** - Build updated containers (no-cache)
6. **Firewall Setup** - Configure UFW if needed
7. **Customer Binaries** - Set up custom binaries directory
8. **J-Link Check** - Verify debug probe software (optional)
9. **Version Update** - Record version in `/etc/lager/version`
10. **Container Restart** - Start updated containers
11. **Status Verification** - Confirm containers are running
***
## Version Tracking
The Lager Box tracks its current version in `/etc/lager/version`. The
`lager boxes` listing queries each configured box and shows its current version
in the `version` column:
```bash theme={null}
# List all boxes with their current versions
lager boxes
```
***
## SSH Key Authentication
The update command automatically detects SSH key configuration:
* **With SSH keys**: Updates proceed without password prompts
* **Without SSH keys**: Prompts for password (interactive mode)
To set up SSH keys:
```bash theme={null}
ssh-copy-id lagerdata@
```
***
## Firewall Auto-Configuration
If UFW is not configured, the update command will:
1. Detect missing firewall rules
2. Offer to install and configure UFW
3. Apply secure defaults (VPN-only access to Lager ports)
```bash theme={null}
# Manual firewall configuration
cli/deployment/security/secure_box_firewall.sh
```
***
## Examples
```bash theme={null}
# Standard update workflow
lager update --box my-lager-box --version main --yes
# Update all boxes at once
lager update --all --yes
# Update all boxes to staging branch
lager update --all --version staging --yes
# Force fresh build on a specific box
lager update --box my-lager-box --force --yes
# Verbose update for troubleshooting
lager update --box my-lager-box --verbose
```
***
## Troubleshooting
### Update Fails at Git Pull
```bash theme={null}
# Check the remote URL (should be HTTPS, not SSH)
ssh lagerdata@ 'cd ~/box && git remote get-url origin'
# If it shows git@github.com:..., switch to HTTPS:
ssh lagerdata@ 'cd ~/box && git remote set-url origin https://github.com/lagerdata/lager.git'
```
### Container Build Fails
```bash theme={null}
# SSH in and check Docker status
ssh lagerdata@
docker ps -a
docker logs controller
```
### Firewall Issues
```bash theme={null}
# Check firewall status
ssh lagerdata@ 'sudo ufw status verbose'
# Re-run firewall setup
cli/deployment/security/secure_box_firewall.sh
```
***
## Notes
* Progress bar is disabled when `--verbose` is used
* Container builds use `--no-cache` to ensure fresh builds
* Version defaults to `main` if not specified
* Update automatically verifies container health after restart
* `--all` checks versions automatically and only updates outdated boxes
* `--force` removes cached Docker images before building (useful after major code changes)
* `--box` and `--all` are mutually exclusive
# USB
Source: https://docs.lagerdata.com/source/reference/cli/usb
Control USB hub port power
Control programmable USB hub ports through the Lager CLI for power management and device connectivity.
## Syntax
```bash theme={null}
lager usb [OPTIONS] [NET_NAME] [COMMAND]
```
## Global Options
| Option | Description |
| ------------ | --------------------------- |
| `--box TEXT` | Lagerbox name or IP address |
| `--help` | Show help message and exit |
## Arguments
| Argument | Description |
| ---------- | ----------------------------------------------- |
| `NET_NAME` | USB net name (optional - lists nets if omitted) |
| `COMMAND` | Power command: `enable`, `disable`, or `toggle` |
## Commands
| Command | Description |
| --------- | -------------------------------- |
| `enable` | Enable (power on) the USB port |
| `disable` | Disable (power off) the USB port |
| `toggle` | Toggle the current power state |
***
## Usage
### List USB Nets
When invoked without a net name, lists all USB nets on the box:
```bash theme={null}
lager usb --box my-lager-box
```
**Output:**
```
Name Net Type Instrument Channel Address
USB1 usb Acroname_Hub 0 USB::123456
USB2 usb Acroname_Hub 1 USB::123456
CAM_USB usb YKUSH 0 USB::789012
```
### Control USB Port Power
```bash theme={null}
lager usb NET_NAME COMMAND [--box BOX]
```
**Examples:**
```bash theme={null}
# Enable USB port
lager usb USB1 enable --box my-lager-box
# Disable USB port
lager usb USB1 disable --box my-lager-box
# Toggle USB port state
lager usb USB1 toggle --box my-lager-box
```
***
## Examples
```bash theme={null}
# List all USB nets
lager usb --box my-lager-box
# Power on a USB port for a camera
lager usb CAM_USB enable --box my-lager-box
# Power off USB port to reset a device
lager usb USB1 disable --box my-lager-box
# Toggle power state (useful for power cycling)
lager usb USB1 toggle --box my-lager-box
# Power cycle a device (disable then enable)
lager usb USB1 disable --box my-lager-box
sleep 2
lager usb USB1 enable --box my-lager-box
```
***
## Supported Hardware
| Manufacturer | Model | Description |
| ------------ | --------- | ------------------------ |
| Acroname | USBHub3+ | Programmable USB 3.0 hub |
| Acroname | USBHub2x4 | 4-port programmable hub |
| YKUSH | YKUSH3 | USB switchable hub |
***
## Notes
* Net names (e.g., `USB1`, `CAM_USB`) refer to USB ports configured on your testbed
* Commands are case-insensitive (`enable`, `ENABLE`, and `Enable` all work)
* Useful for power cycling USB devices during testing
* USB hubs must be connected to the box and configured as instruments
* Default net can be set with `lager defaults add --usb-net`
* Create USB nets with `lager nets add usb `
# Watt Meter
Source: https://docs.lagerdata.com/source/reference/cli/watt
Read power, current, and voltage from a watt meter
Read power, current, and voltage measurements from watt meter Nets through the Lager CLI. Supports Yocto-Watt, Joulescope JS220, and Nordic PPK2 hardware.
## Syntax
```bash theme={null}
lager watt [NET_NAME] [COMMAND] [OPTIONS]
```
With no `COMMAND`, `lager watt NET_NAME` reads power (watts). The `current`, `voltage`, and `all` subcommands read the other quantities (Joulescope JS220 and Nordic PPK2 only).
## Commands
| Command | Description |
| ------------------ | ----------------------------------------- |
| *(none)* / `power` | Read power in watts |
| `current` | Read current in amps |
| `voltage` | Read voltage in volts |
| `all` | Read current, voltage, and power together |
## Options
These options are available on each read subcommand (`power`/`current`/`voltage`/`all`). `--box` is also accepted directly after `lager watt NET_NAME` for the default power read.
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `--box BOX` | Lagerbox name or IP address |
| `-d, --duration FLOAT` | Averaging window in seconds (default `0.1`). Longer windows average more samples for a lower-noise, higher-resolution reading. |
| `--json` | Emit a machine-readable JSON object instead of formatted text |
| `--help` | Show help message and exit |
## Arguments
| Argument | Description |
| ---------- | --------------------------------------------------------------------------------------------------------- |
| `NET_NAME` | Name of the watt meter net to read (optional if a default is set). Must appear **before** the subcommand. |
## Usage
```bash theme={null}
lager watt NET_NAME [--box BOX] # power
lager watt NET_NAME current [--box BOX] # current
lager watt NET_NAME voltage [--box BOX] # voltage
lager watt NET_NAME all [--box BOX] # current + voltage + power
```
If `NET_NAME` is omitted and no default is set, `lager watt` lists all available watt meter nets on the box.
## Output
Readings are formatted with an SI prefix so small magnitudes stay readable (for example a 52.34 µW load is shown as `52.340 µW` rather than rounding to `0.000 W`):
```
Power 'POWER_METER': 52.340 µW
Current 'POWER_METER': 12.000 mA
Voltage 'POWER_METER': 3.300 V
```
`all` prints all three quantities:
```
Measurements 'POWER_METER' (0.1s):
Current: 12.000 mA
Voltage: 3.300 V
Power: 39.600 mW
```
With `--json`, output is a single JSON object (units are base SI — amps, volts, watts):
```bash theme={null}
lager watt POWER_METER all --json --box my-lager-box
{"netname": "POWER_METER", "current": 0.012, "voltage": 3.3, "power": 0.0396, "duration_s": 0.1}
```
The default reading timeout is 30 seconds (scaled up for long `--duration` windows). If no reading is received within that time, the command exits with an error suggesting the device may be disconnected or experiencing USB issues.
### Increasing resolution
Two levers improve a power/current reading:
* **Display** — output is SI-scaled automatically, so sub-milliwatt and sub-milliamp readings are shown in µ/n units instead of rounding to zero. For values too small for even the nano prefix, the reading is shown in scientific notation (e.g. `3.000e-13 W`) rather than rounding to `0.000` — a nonzero reading is never lost.
* **Averaging window** — pass `--duration` to average over a longer capture. A longer window reduces noise on the mean, giving a steadier, higher-effective-resolution value:
```bash theme={null}
lager watt POWER_METER current --duration 1.0 --box my-lager-box
```
### Long averaging windows
`--duration` also works for long windows — e.g. the average current over a minute:
```bash theme={null}
lager watt POWER_METER current --duration 60 --box my-lager-box
```
On a Joulescope JS220, windows longer than \~10 s are measured with the instrument's **on-device charge accumulator** (average current = Δcharge ÷ Δt) rather than by buffering raw samples. This is **gapless** (it captures every transient, not just the sampled fraction), uses constant memory, and scales to arbitrarily long windows (a minute, ten minutes, longer). Short windows continue to use direct sampling.
## Supported Hardware
| Manufacturer | Model | Identification | Features |
| -------------------- | ---------- | ----------------------- | ------------------------------------------------------- |
| Yoctopuce | Yocto-Watt | USB VID:PID `24e0:002a` | Real-time power measurement |
| Joulescope | JS220 | USB VID:PID `16d0:10ba` | High-precision power, voltage, and current measurement |
| Nordic Semiconductor | PPK2 | USB VID:PID `1915:c00a` | Current, voltage, and power measurement via source mode |
### Hardware Feature Comparison
| Feature | Yocto-Watt | Joulescope JS220 | Nordic PPK2 |
| ------------------------------------- | ------------------ | ------------------- | ------------------------------- |
| Power reading (`power`) | Yes | Yes | Yes |
| Voltage reading (`voltage`) | No | Yes | Yes (configured source voltage) |
| Current reading (`current`) | No | Yes | Yes |
| Combined reading (`all`) | No | Yes | Yes |
| Configurable averaging (`--duration`) | No (instantaneous) | Yes | Yes |
| Device selection | Channel-based | Serial number-based | Serial number-based |
The `current`, `voltage`, and `all` subcommands require a Joulescope JS220 or Nordic PPK2. On a Yocto-Watt (power only) they exit with a clear "not supported" message — use `lager watt NET_NAME` for power instead. The same readings are also available through the [Python API](/reference/python/watt).
### Instrument Name Matching
The backend driver is selected based on the instrument name in the net configuration:
| Pattern | Driver |
| ------------------------------------------------------ | ---------------- |
| Contains `joulescope` or `js220` (case-insensitive) | Joulescope JS220 |
| Contains `ppk2`, `ppk`, or `nordic` (case-insensitive) | Nordic PPK2 |
| All other watt meter instruments | Yocto-Watt |
## Default Net
To avoid specifying the net name each time:
```bash theme={null}
lager defaults add --watt-meter-net POWER_METER
```
Then:
```bash theme={null}
lager watt
```
## Examples
```bash theme={null}
# Read power from watt meter
lager watt POWER_METER --box my-lager-box
# Read current / voltage (Joulescope JS220 or Nordic PPK2)
lager watt POWER_METER current --box my-lager-box
lager watt POWER_METER voltage --box my-lager-box
# Read current, voltage, and power together
lager watt POWER_METER all --box my-lager-box
# Average over 1 second for a lower-noise reading
lager watt POWER_METER current --duration 1.0 --box my-lager-box
# Machine-readable output for scripts
lager watt POWER_METER all --json --box my-lager-box
# Read using default net
lager watt
# List available watt meter nets
lager watt --box my-lager-box
```
## Scripting Examples
### Power Threshold Check (JSON)
```bash theme={null}
#!/bin/bash
# Verify power consumption is within limits using JSON output
POWER=$(lager watt POWER all --json --box my-lager-box | python3 -c 'import sys,json; print(json.load(sys.stdin)["power"])')
if (( $(echo "$POWER > 10" | bc -l) )); then
echo "FAIL: Power consumption too high: ${POWER}W"
exit 1
fi
echo "PASS: Power within limits (${POWER}W)"
```
### Current Profiling (JSON)
```bash theme={null}
#!/bin/bash
# Sample current over time
BOX="my-lager-box"
NET="POWER"
echo "timestamp,current_a"
for i in $(seq 1 10); do
CURRENT=$(lager watt $NET current --json --box $BOX | python3 -c 'import sys,json; print(json.load(sys.stdin)["current"])')
echo "$(date +%s),$CURRENT"
sleep 1
done
```
## Troubleshooting
| Error | Cause | Fix |
| ------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------- |
| `does not support reading current/voltage` | Net is backed by a Yocto-Watt (power only) | Use `lager watt NET_NAME` for power, or move the net to a Joulescope/PPK2 |
| Timeout | Device disconnected or USB issue | Check USB connection; replug device |
| Connection refused | Box service not running | Check box: `lager hello --box ` |
| Device not found | Watt meter not detected | Verify device is connected: `lager instruments --box ` |
## Notes
* Readings are SI-scaled (W/mW/µW/nW, A/mA/µA/nA, V/mV) with 3 significant decimal places; values too small for the nano prefix fall back to scientific notation instead of rounding to `0.000`. `--json` emits base SI units (W, A, V).
* `--duration` sets the averaging window; the Joulescope JS220 and Nordic PPK2 honor it, while the Yocto-Watt returns an instantaneous value and ignores it. On the JS220, windows longer than \~10 s are measured gaplessly with the on-device charge accumulator (constant memory, any length).
* The Nordic PPK2 operates in source mode (supplies a configurable voltage 0.8–5V and measures current); its `voltage` reading is the configured source voltage.
* The `current`, `voltage`, and `all` subcommands must follow the net name: `lager watt NET_NAME current`.
* Net names refer to names assigned when setting up your testbed.
* Use `lager nets` to see available watt meter nets, and `lager instruments --box ` to verify the device is detected.
```
```
# Webcam
Source: https://docs.lagerdata.com/source/reference/cli/webcam
Manage webcam streams on boxes
Control webcam streaming for visual monitoring of devices.
## Syntax
```bash theme={null}
lager webcam [NETNAME] COMMAND [OPTIONS]
```
## Commands
| Command | Description |
| ----------- | -------------------------------- |
| `start` | Start webcam stream |
| `stop` | Stop webcam stream |
| `url` | Print URLs of all active streams |
| `start-all` | Start all webcam streams |
| `stop-all` | Stop all webcam streams |
## Options
| Option | Description |
| ----------- | --------------------------- |
| `--box BOX` | Lagerbox name or IP address |
***
## Command Reference
### `start`
Start a webcam stream.
```bash theme={null}
lager webcam CAM1 start --box my-lager-box
```
### `stop`
Stop a webcam stream.
```bash theme={null}
lager webcam CAM1 stop --box my-lager-box
```
### `url`
Print URLs of all active webcam streams.
```bash theme={null}
lager webcam url --box my-lager-box
```
Output:
```
Active webcam streams:
CAM1: http://:8081/stream
CAM2: http://:8082/stream
```
### `start-all`
Start all configured webcam streams.
```bash theme={null}
lager webcam start-all --box my-lager-box
```
### `stop-all`
Stop all webcam streams.
```bash theme={null}
lager webcam stop-all --box my-lager-box
```
***
## Usage
### Basic Workflow
```bash theme={null}
# Start a webcam
lager webcam CAM1 start --box my-lager-box
# Get the stream URL
lager webcam url --box my-lager-box
# Open in browser
# http://:8081/stream
# Stop when done
lager webcam CAM1 stop --box my-lager-box
```
### Multiple Cameras
```bash theme={null}
# Start all cameras
lager webcam start-all --box my-lager-box
# View all URLs
lager webcam url --box my-lager-box
# Stop all cameras
lager webcam stop-all --box my-lager-box
```
***
## Stream Format
Webcam streams are provided as:
* **HTTP MJPEG streams** for browser viewing
* **Per-webcam ports** (8081, 8082, etc.)
* **Device path reporting** for debugging
***
## Use Cases
### Visual Inspection
Monitor physical state of device during testing:
```bash theme={null}
lager webcam BENCH_CAM start
# Run tests while monitoring
lager webcam BENCH_CAM stop
```
### Remote Debugging
View LED states, display outputs, or physical connections:
```bash theme={null}
lager webcam url --box my-lager-box
# Open stream in browser
```
### Documentation
Capture visual evidence of test results:
```bash theme={null}
lager webcam BOARD_VIEW start
# Capture screenshots from stream
lager webcam BOARD_VIEW stop
```
***
## Examples
```bash theme={null}
# Set default webcam
lager defaults add --webcam-net MAIN_CAM
# Quick start/stop
lager webcam start
lager webcam stop
# Check status
lager webcam url
```
***
## Notes
* Webcams must be connected via USB to the box
* Each webcam uses a unique port (8081+)
* Streams are accessible via box IP address
* Default net can be set with `lager defaults add --webcam-net`
* USB webcams should be compatible with V4L2
# WiFi
Source: https://docs.lagerdata.com/source/reference/cli/wifi
Manage WiFi network settings on a Lager Box
View, connect to, and manage WiFi networks on a Lager Box.
## Syntax
```bash theme={null}
lager wifi COMMAND [OPTIONS]
```
## Commands
| Command | Description |
| ------------------- | ------------------------------------------ |
| `status` | Get the current WiFi status of the box |
| `access-points` | List WiFi access points visible to the box |
| `connect` | Connect the box to a WiFi network |
| `delete-connection` | Delete a saved network from the box |
***
## `lager wifi status`
Get the current WiFi connection status of the box.
```bash theme={null}
lager wifi status [--box BOX]
```
### Options
| Option | Description |
| ------------ | --------------------------- |
| `--box TEXT` | Lagerbox name or IP address |
### Examples
```bash theme={null}
lager wifi status --box my-lager-box
```
***
## `lager wifi access-points`
Scan for and list WiFi access points visible to the box.
```bash theme={null}
lager wifi access-points [--box BOX] [--interface IFACE]
```
### Options
| Option | Default | Description |
| ------------------ | ------- | -------------------------------------- |
| `--box TEXT` | | Lagerbox name or IP address |
| `--interface TEXT` | `wlan0` | Wireless interface to use for scanning |
### Examples
```bash theme={null}
# Scan using default interface
lager wifi access-points --box my-lager-box
# Scan using a specific interface
lager wifi access-points --box my-lager-box --interface wlan1
```
***
## `lager wifi connect`
Connect the box to a WiFi network.
```bash theme={null}
lager wifi connect [--box BOX] --ssid SSID [--password PASS] [--interface IFACE]
```
### Options
| Option | Default | Description |
| ------------------ | ------------ | ------------------------------------------------- |
| `--box TEXT` | | Lagerbox name or IP address |
| `--ssid TEXT` | *(required)* | SSID of the network to connect to |
| `--password TEXT` | *(empty)* | Password for the network (omit for open networks) |
| `--interface TEXT` | `wlan0` | Wireless interface to use |
### Validation
* SSID must be 1-32 printable characters (IEEE 802.11 limit)
* WPA/WPA2 passwords must be 8-63 characters
### Examples
```bash theme={null}
# Connect to a WPA2 network
lager wifi connect --box my-lager-box --ssid MyNetwork --password mypassword
# Connect to an open network
lager wifi connect --box my-lager-box --ssid OpenNetwork
# Connect using a specific interface
lager wifi connect --box my-lager-box --ssid MyNetwork --password mypassword --interface wlan1
```
***
## `lager wifi delete-connection`
Delete a saved WiFi network from the box. This removes the network configuration so the box will no longer auto-connect to it.
```bash theme={null}
lager wifi delete-connection SSID [--box BOX] [--yes]
```
### Arguments
| Argument | Description |
| -------- | ----------------------------- |
| `SSID` | Name of the network to delete |
### Options
| Option | Description |
| ------------ | ---------------------------- |
| `--box TEXT` | Lagerbox name or IP address |
| `--yes` | Skip the confirmation prompt |
### Examples
```bash theme={null}
# Delete with confirmation prompt
lager wifi delete-connection MyNetwork --box my-lager-box
# Delete without prompting
lager wifi delete-connection MyNetwork --box my-lager-box --yes
```
Deleting a WiFi connection may disconnect the box from the network. An ethernet connection will be required to bring the box back online if it was connected via WiFi only.
***
## Notes
* WiFi commands execute on the Lager Box itself, not on your local machine
* The box must be reachable (via ethernet or an existing WiFi connection) to run these commands
* Common wireless interface names: `wlan0`, `wlan1`, `wlp2s0`, `wlp3s0`
* Use `lager hello --box ` to verify connectivity before managing WiFi settings
# Authoring DUT Context
Source: https://docs.lagerdata.com/source/reference/mcp/dut-context
Give AI agents system-level understanding of your device under test and its schematics
An AI agent can read a netlist, but a netlist alone doesn't tell it *what the
box is for* or *what each wire means*. Knowing `uart1` is a UART is not the same
as knowing it's the DUT's debug CLI. **DUT context** is the narrative you author
once so agents reason about your bench at the level of *systems*, not loose
wires.
DUT context lives in `/etc/lager/bench.json` and is surfaced to agents through
the MCP resources `lager://dut/overview.md` and `lager://dut/context`, and the
`discover_dut()` and `cite_schematic()` tools.
## The two things to author
### 1. Per-net purpose
Each net carries a single-sentence **purpose** plus optional **notes**. Set them
in the Net Manager TUI:
```bash theme={null}
lager nets tui --box my-lager-box
```
Select a net, open its details, and fill in:
* **Purpose** — *"DUT debug CLI over UART; primary command/response channel."*
* **Notes** (optional) — gotchas, jumper positions, scope probe points.
* **Tags** (optional) — short keywords the planning tools match on, e.g.
`flash`, `boot-critical`.
`purpose` and `notes` are prose for the agent to read; `tags` are keywords the
planning tools score against (a tag matching a test goal is the strongest
relevance signal). You can also set these without the TUI:
```bash theme={null}
lager nets describe uart1 \
--purpose "DUT debug CLI over UART" \
--notes "PA9/PA10; 115200 8N1" \
--tag cli --tag boot-critical \
--box my-lager-box
```
### 2. DUT-wide context
The DUT context describes the board as a whole: its purpose, MCU, key
peripherals, subsystems, and references to documents. Author it with the
[`lager dut`](/source/reference/cli/dut) command group.
```bash theme={null}
# View current context
lager dut show --box my-lager-box
# Edit the whole DUT block in $EDITOR
lager dut edit --box my-lager-box
```
A fully authored DUT context looks like this in `bench.json`:
```json theme={null}
{
"dut_context": {
"name": "main",
"purpose": "Power-regression rig for FeatureA boards",
"mcu": "STM32H7",
"key_peripherals": ["QSPI flash", "PMIC"],
"summary": "STM32H7-based DUT used to validate the power tree under fault injection.",
"schematic_refs": [
{"title": "Main schematic", "kind": "schematic", "repo_path": "docs/sch.pdf"}
],
"datasheet_refs": [
{"title": "STM32H7 RM", "kind": "datasheet", "url": "https://...", "pages": "150-200"}
],
"subsystems": [
{
"name": "Flash subsystem",
"summary": "QSPI flash",
"nets": ["flash_cs", "flash_clk"],
"doc_refs": [
{"title": "Flash sheet", "kind": "schematic", "repo_path": "docs/sch.pdf", "pages": "3"}
]
},
{"name": "Power tree", "summary": "PMIC + LDOs", "nets": ["psu1"]}
]
}
}
```
**Subsystems** group related nets (Power tree, Flash subsystem, Debug, ...) so
the agent reasons about functional blocks. The agent can ask for one net and
learn which subsystem it belongs to and which schematic sheet covers it.
## Attaching schematics and datasheets
The Lager Box is **not** a document store. It records *pointers* to your
documents; the agent fetches and analyses them with its own (vision-capable)
tools. This keeps the box lean and lets the agent use the best tool for reading
a PDF or board image.
Attach a pointer without hand-editing JSON:
```bash theme={null}
lager dut add-doc --kind schematic \
--title "Main board" --repo-path docs/sch.pdf --pages 3-5 --box my-lager-box
lager dut add-doc --kind datasheet \
--title "STM32H7 reference manual" --url "https://..." --pages 150-200 --box my-lager-box
```
A document reference (`DocRef`) has:
| Field | Meaning |
| ----------- | ------------------------------------------------------------------------------- |
| `title` | Human label. |
| `kind` | `schematic`, `layout`, `datasheet`, `firmware`, `manual`, `errata`, or `other`. |
| `url` | External URL (any URL the agent can fetch). |
| `repo_path` | Path relative to your test project (synced to the box on `lager python`). |
| `pages` | Optional page/sheet hint, e.g. `"3-5"` or `"POWER sheet"`. |
| `notes` | Optional free-form note. |
You must supply at least one of `--url` or `--repo-path`.
### URL vs. repo-path: which to use
| Situation | Recommended | Why |
| ----------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------- |
| Automated / CI / headless agent | `--repo-path` | The file is synced with your project; no network, no auth, fully deterministic. |
| Publicly fetchable doc | `--url` | Any agent with a web-fetch tool can pull it. |
| Private doc (Google Doc, Confluence, SSO) | `--repo-path` **or** a Drive connector | The box never authenticates; a login-walled URL returns an auth page, not content. |
For Google Docs, prefer an **export** URL over the editor URL — the `/edit` URL
returns the JS app, not the content:
```
https://docs.google.com/document/d//export?format=pdf
```
Either share it "anyone with the link," give your agent a Google Drive
connector/MCP server that holds the credentials, or export it into your repo and
use `--repo-path`.
## How the agent uses it
Once authored, the context drives the whole agent loop:
1. The agent reads `lager://dut/overview.md` and learns: *"power-regression rig,
STM32H7, flash + power-tree subsystems, schematic at `docs/sch.pdf`."*
2. `plan_firmware_test("flash driver", "exercise QSPI")` returns a plan already
scoped to the flash subsystem, with a pointer to schematic page 3.
3. `cite_schematic("flash_cs")` returns just the refs for that net:
```json theme={null}
{
"net": "flash_cs",
"net_purpose": "SPI flash chip-select",
"subsystem": "Flash subsystem",
"subsystem_doc_refs": [
{"title": "Flash sheet", "repo_path": "docs/sch.pdf", "pages": "3"}
]
}
```
The agent opens `docs/sch.pdf` at page 3 with its own file tools — no scanning
the whole PDF.
## Applying changes
The MCP server watches `/etc/lager/bench.json`, `/etc/lager/saved_nets.json`,
and `/etc/lager/box_id` and **auto-reloads when any of them changes on disk**.
So after `lager dut edit`, `lager dut add-doc`, or `lager nets describe`,
agents see the new context on their next `discover_dut()`, `discover_bench()`, or
`lager://dut/overview.md` request — no manual step required.
If you want to force a reload immediately (e.g. to confirm a change took), a
connected agent can still call the `box_manage` tool with `action="reload"`, or
you can restart the box service.
# MCP Server Overview
Source: https://docs.lagerdata.com/source/reference/mcp/overview
How AI agents discover hardware and plan tests on a Lager Box via the Model Context Protocol
Every Lager Box runs an **MCP (Model Context Protocol) server** that lets an AI
agent understand the bench, understand the device under test (DUT), and plan
hardware-in-the-loop tests. It runs on-box and is reachable over the box's local
IP.
The MCP server is **read-only**: it describes the bench and DUT but never drives
hardware or runs code. The agent executes tests over a separate channel — the
`lager` CLI.
```
MCP-compatible AI agent
| MCP (streamable-http via box IP) ← discovery + planning (read-only)
v
Lager MCP Server (on-box, port 8100)
| reads /etc/lager bench config (nets, DUT context, instruments)
v
Bench / DUT metadata
AI agent ── lager python path/to/test.py --box ──▶ Hardware
(execution happens over the CLI, not MCP)
```
## Connecting an agent
Point any MCP-compatible client at the box:
```json theme={null}
{
"mcpServers": {
"lager": {
"url": "http://:8100/mcp"
}
}
}
```
The MCP server is for **discovery and planning** only — it does not run code or
drive hardware. To **execute** a test, the agent writes a Python file locally and
runs it with `lager python path/to/test.py --box `, which syncs the
project to the box and runs it with full project context. Pass the box's **IP
address** to `--box` — the same IP you connected to the MCP server on. Local box
names are just client-side aliases, so the IP is the only identifier both sides
can rely on. To make this concrete, `discover_bench()` echoes the address you
actually connected on as `box_address` and hands back a ready-to-run
`lager python … --box ` command.
## What the agent sees
The server exposes two kinds of things: **resources** (read-only context the
agent reads) and **tools** (callable functions).
### Resources
| Resource | What it gives the agent |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lager://dut/overview.md` | **Read this first.** A narrative briefing: what the box tests, the DUT MCU/peripherals, subsystems, and which documents to fetch. |
| `lager://dut/context` | The full DUT context as structured JSON. |
| `lager://bench/identity` | Box ID, hostname, version, and a per-DUT summary (purpose, MCU). |
| `lager://bench/netlist` | Every net with type, roles, instrument, and metadata. |
| `lager://bench/interfaces` | Protocol interfaces (SPI, I2C, UART) and their nets. |
| `lager://guide/overview` | What Lager is and what a "net" is. |
| `lager://guide/workflow` | The recommended orient → discover → plan → write → run loop. |
| `lager://guide/rtt-defmt` | The core firmware-log workflow: streaming RTT and decoding `defmt`. Covers the `dbg.session()` scope, the reconnect-aware RTT reader and self-healing `reset()`/`read_memory()` (so you don't write flash/reset workarounds), and the DA1469x post-flash reconnect exception. |
| `lager://reference/{net_type}` | The full API reference for one net type as JSON (e.g. `lager://reference/Debug`) — methods, gotchas, and a runnable example snippet. |
| `lager://guide/api-quick-reference` | Compact `lager.Net` API cheat sheet by net type. |
| `lager://guide/docs` | Links to the full hosted docs (`docs.lagerdata.com`) and the `llms.txt` page index for anything not covered on-box. |
### Tools
| Tool | Purpose |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `discover_dut()` | One call for orientation: DUT purpose, MCU, peripherals, subsystems, and document references. |
| `discover_bench(net_name?)` | Enumerate hardware — nets, plus instruments with their channels, capabilities, and authored specs/ranges. With a net name, return that net's full metadata, capabilities, parent subsystem, and relevant document references (and, if the net is unknown, the list of available net names). |
| `cite_schematic(net_name)` | Return just the schematic/datasheet references and page hints relevant to one net. |
| `plan_firmware_test(firmware_description, test_goals)` | Generate a phased test plan, scoped to relevant nets, with DUT context and document references attached. |
| `assess_suitability(test_type)` | Check whether the bench can run a given test type. |
| `get_test_example(query)` | Find runnable example scripts by net type, pattern, or keyword. |
| `box_manage(action)` | `health` check or `reload` the bench config from disk. |
The tool surface is intentionally read-only. There are no tools that drive
hardware (set a voltage, toggle a GPIO, flash firmware) or mutate the box. All
of that lives in the test script the agent writes and runs with `lager python`,
or in dedicated [CLI commands](/source/reference/cli).
### Prompts
The server also registers a few **prompts** — slash-command-style entry points
that steer a client (e.g. Cursor) through the discover → plan → write → run
workflow. They don't do work themselves; each returns an instruction the agent
follows using the tools above.
| Prompt | What it does |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `write_lager_test(what_to_test)` | Walks the agent through discovering the bench/DUT, planning, writing a test, and running it over the CLI. |
| `explore_bench()` | Orients on what the box is, what it tests, and what it can run. |
| `assess_test_feasibility(test_description)` | Checks whether the bench has the capabilities for a described test. |
## The recommended agent workflow
Read `lager://dut/overview.md` (or call `discover_dut()`) to learn what the
box tests, the MCU and peripherals, the subsystems, and which documents to
fetch.
Call `discover_bench()` to enumerate nets, instruments, and capabilities.
Call `discover_bench(net_name)` for detail on a specific net, including its
subsystem and the schematic sheet it lives on.
Call `plan_firmware_test(...)` to get a phased plan with API references and
document pointers per step.
Author a Python test file using `from lager import Net, NetType`. Identify
the box by the IP address you connected to the MCP server on — local box
names are arbitrary client-side aliases. `--box` accepts a raw IP, so no
registration is needed: run `lager python path/to/test.py --box `.
The runnable can also be a **folder** (entrypoint `main.py`), which syncs
and imports everything in it — handy for shipping reusable helper modules:
`lager python path/to/test_dir --box `. (Optionally,
`lager boxes add --name --ip ` registers a friendly alias.)
Review the CLI output, adjust the script, and re-run it with `lager python`.
## Where context comes from
The quality of everything above depends on the metadata you author once, at
bench setup:
* **Per-net `purpose`** — set in the Net Manager TUI (`lager nets tui`). One
sentence describing what each wire does on the DUT.
* **DUT context** — set with [`lager dut`](/source/reference/cli/dut):
the box's purpose, MCU, subsystems, and references to schematics and
datasheets.
See [Authoring DUT Context](/source/reference/mcp/dut-context) for the full
guide.
# ADC
Source: https://docs.lagerdata.com/source/reference/python/adc
Read analog-to-digital converter values
Read analog voltage values from ADC pins. Supports LabJack T7 and MCC USB-202 hardware.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| --------- | ------------------- |
| `input()` | Read analog voltage |
## Method Reference
### `Net.get(name, type=NetType.ADC)`
Get an ADC net by name.
```python theme={null}
from lager import Net, NetType
adc = Net.get('SENSOR', type=NetType.ADC)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------- |
| `name` | `str` | Name of the ADC net |
| `type` | `NetType` | Must be `NetType.ADC` |
**Returns:** ADC Net instance
### `input()`
Read the analog voltage.
```python theme={null}
voltage = adc.input()
print(f"Voltage: {voltage}V")
```
**Returns:** `float` - Voltage in volts
## Examples
### Single Reading
```python theme={null}
from lager import Net, NetType
sensor = Net.get('TEMP_SENSOR', type=NetType.ADC)
voltage = sensor.input()
print(f"Sensor voltage: {voltage:.3f}V")
```
### Continuous Monitoring
```python theme={null}
from lager import Net, NetType
import time
battery = Net.get('BATTERY_SENSE', type=NetType.ADC)
for i in range(10):
voltage = battery.input()
print(f"Battery: {voltage:.2f}V")
time.sleep(1)
```
### Data Logging
```python theme={null}
from lager import Net, NetType
import time
import csv
sensor = Net.get('CURRENT_SENSE', type=NetType.ADC)
with open('readings.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['time', 'voltage'])
start = time.time()
for i in range(100):
elapsed = time.time() - start
voltage = sensor.input()
writer.writerow([elapsed, voltage])
time.sleep(0.1)
```
### Multiple Sensors
```python theme={null}
from lager import Net, NetType
sensors = {
'TEMP': Net.get('TEMP_SENSOR', type=NetType.ADC),
'CURRENT': Net.get('CURRENT_SENSE', type=NetType.ADC),
'BATTERY': Net.get('BATTERY_SENSE', type=NetType.ADC),
}
for name, sensor in sensors.items():
voltage = sensor.input()
print(f"{name}: {voltage:.3f}V")
```
## Supported Hardware
| Hardware | Channels | Range | Resolution |
| ----------- | --------------- | ------- | --------------------- |
| LabJack T7 | 14 (AIN0-AIN13) | +/-10 V | 16-bit (\~0.3 mV/LSB) |
| MCC USB-202 | 8 (CH0-CH7) | +/-10 V | 12-bit (\~4.9 mV/LSB) |
Both devices support bipolar measurement (positive and negative voltages).
### Pin Naming
**LabJack T7:**
| Pin Input | Channel |
| ------------------ | ---------- |
| `0`-`13` | AIN0-AIN13 |
| `"AIN0"`-`"AIN13"` | AIN0-AIN13 |
**MCC USB-202:**
| Pin Input | Channel |
| --------------- | -------------------------- |
| `0`-`7` | CH0-CH7 |
| `"CH0"`-`"CH7"` | CH0-CH7 (case-insensitive) |
## Notes
* ADC nets work directly without `enable()`/`disable()` calls
* Returns voltage as a float in volts
* Both devices have a +/-10 V input range (bipolar)
* LabJack T7 shares a connection handle with DAC, GPIO, and SPI operations
* USB-202 opens and closes the connection on each read
* Net names must match those configured on the Lager Box
# Robot Arm
Source: https://docs.lagerdata.com/source/reference/python/arm
Control robotic arms for automated positioning and manipulation
Control robotic arms for automated pick-and-place operations, positioning, and test fixture manipulation.
## Import
```python theme={null}
from lager import Net, NetType
# For exception handling
from lager import InvalidNetError
```
## Methods
| Method | Description |
| --------------------- | ---------------------------------------- |
| `position()` | Get current arm position (x, y, z) |
| `move_to()` | Move to absolute coordinates |
| `move_relative()` | Move relative to current position |
| `go_home()` | Return arm to home position |
| `enable_motor()` | Enable arm motors |
| `disable_motor()` | Disable arm motors |
| `save_position()` | Save current position to memory |
| `get_full_position()` | Get full position including joint angles |
| `sliding_rail_init()` | Initialize the sliding rail |
## Method Reference
### Getting an Arm Net
Get a robotic arm net instance by name and type.
```python theme={null}
from lager import Net, NetType
# Get the arm net
arm = Net.get('arm1', type=NetType.Arm)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------------------- |
| `name` | `str` | Net name (configured in Lager system) |
| `type` | `NetType` | Must be `NetType.Arm` |
**Returns:** Arm net instance with all methods listed below
### `position()`
Get the current arm position.
```python theme={null}
x, y, z = arm.position()
print(f"Position: X={x}, Y={y}, Z={z}")
```
**Returns:** `tuple[float, float, float]` - (x, y, z) coordinates in mm
### `move_to(x, y, z, timeout=15.0)`
Move the arm to absolute coordinates with blocking wait.
```python theme={null}
# Move to specific position
arm.move_to(100, 200, 50)
# Move with longer timeout
arm.move_to(100, 200, 50, timeout=10.0)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ------- | -------------------------------------------- |
| `x` | `float` | Target X coordinate (mm) |
| `y` | `float` | Target Y coordinate (mm) |
| `z` | `float` | Target Z coordinate (mm) |
| `timeout` | `float` | Maximum wait time in seconds (default: 15.0) |
**Raises:** `MovementTimeoutError` if position not reached within timeout
### `move_relative(dx=0, dy=0, dz=0, timeout=15.0)`
Move relative to current position.
```python theme={null}
# Move 10mm in X direction
new_x, new_y, new_z = arm.move_relative(dx=10)
# Move diagonally
new_x, new_y, new_z = arm.move_relative(dx=10, dy=10, dz=-5)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ------- | --------------------------------- |
| `dx` | `float` | Relative X movement (mm) |
| `dy` | `float` | Relative Y movement (mm) |
| `dz` | `float` | Relative Z movement (mm) |
| `timeout` | `float` | Maximum wait time (default: 15.0) |
**Returns:** `tuple[float, float, float]` - New (x, y, z) position after move
### `go_home()`
Return the arm to its home position (X=0, Y=300, Z=0).
```python theme={null}
arm.go_home()
```
### `enable_motor()`
Enable the arm motors.
```python theme={null}
arm.enable_motor()
```
### `disable_motor()`
Disable the arm motors (arm will be loose).
```python theme={null}
arm.disable_motor()
```
### `save_position()`
Save the current position to the arm's internal memory.
```python theme={null}
arm.save_position()
```
### `get_full_position()`
Get the full position including joint angles.
```python theme={null}
x, y, z, e, a, b, c = arm.get_full_position()
print(f"Cartesian: ({x}, {y}, {z})")
print(f"Joint angles: A={a}, B={b}, C={c}")
```
**Returns:** `tuple[float, ...]` - (x, y, z, e, a, b, c) where a, b, c are joint angles
## End Effector Methods
### Soft Gripper
```python theme={null}
arm.soft_gripper_pick() # Activate gripper to pick
arm.soft_gripper_place() # Release gripper to place
arm.soft_gripper_neutral() # Return to neutral state
arm.soft_gripper_stop() # Stop gripper action
```
### Air Picker
```python theme={null}
arm.air_picker_pick() # Activate vacuum to pick
arm.air_picker_place() # Release vacuum to place
arm.air_picker_neutral() # Return to neutral state
arm.air_picker_stop() # Stop vacuum action
```
### Laser Module
```python theme={null}
arm.laser_on(value=0) # Turn laser on with power level
arm.laser_off() # Turn laser off
```
## Conveyor Belt Methods
```python theme={null}
arm.conveyor_belt_forward(speed=0) # Move belt forward (speed 0-100)
arm.conveyor_belt_backward(speed=0) # Move belt backward (speed 0-100)
arm.conveyor_belt_stop() # Stop belt
```
## Sliding Rail Methods
```python theme={null}
arm.sliding_rail_init() # Initialize the sliding rail
```
## Utility Methods
### `delay_ms(value)` / `delay_s(value)`
Add delay to the arm's command queue.
```python theme={null}
arm.delay_ms(500) # 500ms delay
arm.delay_s(2) # 2 second delay
```
### `set_acceleration(acceleration, travel_acceleration, retract_acceleration=60)`
Configure acceleration settings.
```python theme={null}
arm.set_acceleration(100, 100, 60)
```
### `set_module_type(module_type)`
Set the attached module type.
```python theme={null}
# 0=PEN, 1=LASER, 2=PNEUMATIC, 3=3D
arm.set_module_type(0) # Set to PEN module
```
### `get_module_type()`
Get the currently detected module type.
```python theme={null}
module = arm.get_module_type()
print(f"Module: {module}") # 'PEN', 'LASER', 'PUMP', or '3D'
```
## Examples
### Basic Movement
```python theme={null}
from lager import Net, NetType
# Get the arm net
arm = Net.get('arm1', type=NetType.Arm)
# Go to home position
arm.go_home()
# Get current position
x, y, z = arm.position()
print(f"Home position: ({x}, {y}, {z})")
# Move to test position
arm.move_to(100, 250, 0)
# Move up
arm.move_relative(dz=50)
# Return home
arm.go_home()
```
### Pick and Place
```python theme={null}
from lager import Net, NetType
# Get the arm net
arm = Net.get('arm1', type=NetType.Arm)
arm.go_home()
# Move to pick position
arm.move_to(100, 200, 50) # Above target
arm.move_relative(dz=-30) # Lower to pick height
# Pick up object
arm.soft_gripper_pick()
arm.delay_ms(500)
# Lift
arm.move_relative(dz=30)
# Move to place position
arm.move_to(150, 200, 50)
arm.move_relative(dz=-30)
# Place object
arm.soft_gripper_place()
arm.delay_ms(500)
# Return home
arm.move_relative(dz=30)
arm.go_home()
```
### Automated Test Positioning
```python theme={null}
from lager import Net, NetType
# Test positions for probe points
PROBE_POSITIONS = [
(100, 200, -10), # Test point 1
(120, 200, -10), # Test point 2
(140, 200, -10), # Test point 3
]
# Get arm and probe nets
arm = Net.get('arm1', type=NetType.Arm)
adc = Net.get('PROBE', type=NetType.ADC)
arm.go_home()
results = []
for i, (x, y, z) in enumerate(PROBE_POSITIONS):
# Move above position
arm.move_to(x, y, z + 20)
# Lower probe
arm.move_to(x, y, z)
arm.delay_ms(200) # Settle time
# Take measurement
voltage = adc.input()
results.append((i, voltage))
print(f"Point {i}: {voltage}V")
# Lift
arm.move_relative(dz=20)
arm.go_home()
```
### Error Handling
```python theme={null}
from lager import Net, NetType
from lager import InvalidNetError
try:
arm = Net.get('arm1', type=NetType.Arm)
arm.move_to(100, 200, 50, timeout=3.0)
except InvalidNetError as e:
print(f"Arm net not found: {e}")
except RuntimeError as e:
print(f"Movement failed: {e}")
except Exception as e:
print(f"Error: {e}")
```
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | ------ | ------------------------------------------------ |
| Rotrics | Dexarm | 4-axis, multiple end effectors, conveyor support |
## Exceptions
| Exception | Module | Description |
| ---------------------- | ------------------- | ---------------------------------------- |
| `MovementTimeoutError` | `lager.arm.arm_net` | Arm didn't reach target position in time |
| `RuntimeError` | builtin | Arm device not found or cannot be opened |
## Notes
* Always use the context manager (`with` statement) to ensure proper cleanup
* The arm must be homed before accurate movements
* Movement timeout errors may indicate obstructions or out-of-bounds coordinates
* Joint angles (a, b, c) are in the arm's internal coordinate system
* Default tolerance for position verification is 0.5mm
* The arm auto-detects via USB VID/PID (0x0483:0x5740)
# Battery Simulation
Source: https://docs.lagerdata.com/source/reference/python/battery
Control battery simulation nets
Simulate battery behavior to test your DUT's response to various charge levels, voltages, and protection events.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
The Net-based API provides methods that can either get or set values. When called without a value parameter, methods read and print the current value. When called with a value, they set it.
| Method | Description |
| ---------------------- | --------------------------------------------------- |
| `mode(mode_type)` | Set or read simulation mode ('static' or 'dynamic') |
| `set_mode_battery()` | Initialize battery simulation mode |
| `soc(value)` | Set or read state of charge (0-100%) |
| `voc(value)` | Set or read open-circuit voltage |
| `voltage_full(value)` | Set or read full charge voltage |
| `voltage_empty(value)` | Set or read empty battery voltage |
| `capacity(value)` | Set or read battery capacity (Ah) |
| `current_limit(value)` | Set or read current limit (A) |
| `ovp(value)` | Set or read over-voltage protection threshold |
| `ocp(value)` | Set or read over-current protection threshold |
| `model(partnumber)` | Set or read battery model |
| `enable()` | Enable battery simulation output |
| `disable()` | Disable battery simulation output |
| `clear_ovp()` | Clear over-voltage protection fault |
| `clear_ocp()` | Clear over-current protection fault |
| `print_state()` | Print comprehensive battery state |
| `terminal_voltage()` | Read terminal voltage (returns float) |
| `current()` | Read current (returns float) |
| `esr()` | Read ESR (returns float) |
## Method Reference
### `Net.get(name, type=NetType.Battery)`
Get a battery simulation net by name.
```python theme={null}
from lager import Net, NetType
batt = Net.get('BATT', type=NetType.Battery)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------- |
| `name` | `str` | Name of the battery net |
| `type` | `NetType` | Must be `NetType.Battery` |
**Returns:** Battery simulation Net instance
### `set_mode_battery()`
Initialize the instrument for battery simulation mode.
```python theme={null}
batt.set_mode_battery()
```
### `mode(mode_type=None)`
Set or read the battery simulation mode.
```python theme={null}
# Set mode
batt.mode('static') # Fixed parameters
batt.mode('dynamic') # Parameters evolve based on battery model
# Read mode (prints current mode)
batt.mode()
```
**Parameters:**
| Parameter | Type | Description |
| ----------- | --------------- | --------------------------------------------------- |
| `mode_type` | `str` or `None` | 'static' or 'dynamic'. If None, reads current mode. |
### `soc(value=None)`
Set or read the state of charge.
```python theme={null}
# Set SOC
batt.soc(80) # Set to 80%
# Read SOC (prints current value)
batt.soc()
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----------------- | ---------------------------------------------------------------------------------- |
| `value` | `float` or `None` | State of charge (0-100). Rounded to nearest integer. If None, reads current value. |
### `voc(value=None)`
Set or read the open-circuit voltage.
```python theme={null}
# Set VOC
batt.voc(3.7) # Set to 3.7V
# Read VOC (prints current value)
batt.voc()
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----------------- | ----------------------------------------------- |
| `value` | `float` or `None` | Voltage in volts. If None, reads current value. |
### `voltage_full(value=None)` / `voltage_empty(value=None)`
Set or read the full/empty battery voltages.
```python theme={null}
# Set voltages
batt.voltage_full(4.2) # Full charge at 4.2V
batt.voltage_empty(3.0) # Empty at 3.0V
# Read voltages
batt.voltage_full()
batt.voltage_empty()
```
### `capacity(value=None)`
Set or read the battery capacity. Must be greater than 0. The instrument may clamp the value to its supported range and will print a warning if the applied value differs.
```python theme={null}
# Set capacity
batt.capacity(2.5) # 2.5 Ah
# Read capacity
batt.capacity()
```
### `current_limit(value=None)`
Set or read the maximum charge/discharge current. Range: 0.001 A to 6.0 A (Keithley 2281S).
```python theme={null}
# Set current limit
batt.current_limit(1.5) # 1.5A max
# Read current limit
batt.current_limit()
```
### `ovp(value=None)` / `ocp(value=None)`
Set or read protection thresholds.
```python theme={null}
# Set protection thresholds
batt.ovp(4.4) # Over-voltage protection at 4.4V
batt.ocp(2.0) # Over-current protection at 2.0A
# Read thresholds
batt.ovp()
batt.ocp()
```
### `model(partnumber=None)`
Set or read the battery model.
```python theme={null}
# Set model to discharge (always available)
batt.model('discharge')
# Or use pre-configured battery models (if available)
batt.model('18650') # Requires model saved in slot 1
batt.model('liion') # Requires model saved in slot 1
batt.model('nimh') # Requires model saved in slot 2
# Read current model
batt.model()
```
**Keithley 2281S Battery Models:**
The Keithley 2281S stores battery models in memory slots (0-9):
| Model Alias | Slot | Availability |
| -------------------- | ---- | ---------------------------------------------------- |
| `'discharge'` | 0 | Always available (basic constant-voltage simulation) |
| `'18650'`, `'liion'` | 1 | Requires pre-saved model |
| `'nimh'` | 2 | Requires pre-saved model |
| `'nicd'` | 3 | Requires pre-saved model |
| `'lead-acid'` | 4 | Requires pre-saved model |
**Note:** If a slot is empty, you'll get an error suggesting to use 'discharge' or save a model via the instrument front panel. Use `'discharge'` for basic battery simulation that works on all instruments.
### `enable()` / `disable()`
Enable or disable battery simulation output.
```python theme={null}
batt.enable() # Enable output
batt.disable() # Disable output
```
### `clear_ovp()` / `clear_ocp()`
Clear protection faults.
```python theme={null}
batt.clear_ovp() # Clear over-voltage fault
batt.clear_ocp() # Clear over-current fault
```
### `print_state()`
Print comprehensive battery simulator state.
```python theme={null}
batt.print_state()
# Prints: terminal voltage, current, ESR, SOC, VOC, capacity, protection status
```
### `terminal_voltage()` / `current()` / `esr()`
Read measurements (return values, don't print).
```python theme={null}
v = batt.terminal_voltage() # Returns terminal voltage in volts
i = batt.current() # Returns current in amps
r = batt.esr() # Returns ESR in ohms
print(f"Voltage: {v}V, Current: {i}A, ESR: {r} ohms")
```
**Returns:** `float` - Measurement value
## Examples
### Basic Battery Simulation
```python theme={null}
from lager import Net, NetType
# Get battery net
batt = Net.get('BATT', type=NetType.Battery)
# Initialize and configure
batt.set_mode_battery()
batt.mode('static')
batt.model('discharge') # Use discharge mode (always available)
batt.voc(3.7)
batt.capacity(2.5)
# Enable output
batt.enable()
# Read state
batt.print_state()
print(f"Terminal voltage: {batt.terminal_voltage()}V")
# Disable when done
batt.disable()
```
### Simulate Battery Discharge
```python theme={null}
from lager import Net, NetType
import time
batt = Net.get('BATT', type=NetType.Battery)
batt.set_mode_battery()
# Configure battery parameters
batt.mode('static')
batt.model('discharge')
batt.voc(4.2)
batt.voltage_full(4.2)
batt.voltage_empty(3.0)
batt.capacity(3.0)
batt.soc(100) # Start fully charged
batt.enable()
# Simulate discharge by stepping SOC
for soc_level in [100, 75, 50, 25, 10]:
batt.soc(soc_level)
time.sleep(0.5)
v = batt.terminal_voltage()
print(f"SOC: {soc_level}%, Terminal: {v:.2f}V")
batt.disable()
```
### With Protection Monitoring
```python theme={null}
from lager import Net, NetType
batt = Net.get('BATT', type=NetType.Battery)
batt.set_mode_battery()
# Configure with protection
batt.model('discharge')
batt.voc(3.7)
batt.capacity(2.0)
batt.ovp(4.3) # Over-voltage at 4.3V
batt.ocp(2.0) # Over-current at 2.0A
batt.enable()
# Monitor
print(f"Voltage: {batt.terminal_voltage():.2f}V")
print(f"Current: {batt.current():.3f}A")
# Clear any faults if needed
batt.clear_ovp()
batt.clear_ocp()
batt.disable()
```
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | ----- | ------------------------------------ |
| Keithley | 2281S | Battery simulation, dynamic modeling |
## Notes
* Call `set_mode_battery()` before using other battery methods
* Methods like `soc()`, `voc()`, etc. can get or set values depending on whether a value is passed
* `soc()` values are rounded to the nearest integer before being sent to the instrument
* Use `mode('static')` for fixed parameters, `mode('dynamic')` for evolving behavior
* Always call `disable()` when finished
* `terminal_voltage()`, `current()`, and `esr()` return values (for use in code)
* `print_state()` prints values (for debugging)
* Protection thresholds help prevent damage to your DUT
* Keithley 2281S limits: 0-20 V output, 0.001-6.0 A current, capacity must be > 0
* OVP range: 0-60 V; OCP range: 0.001-6.0 A
# Custom Binaries
Source: https://docs.lagerdata.com/source/reference/python/binaries
Execute custom binaries on the Lager Box
Execute custom binaries that have been uploaded to the Lager Box via the CLI. This is useful for running customer-specific tools, device interaction utilities, or third-party command-line applications.
## Import
```python theme={null}
from lager.binaries import run_custom_binary, get_binary_path, list_binaries, BinaryNotFoundError
```
## Functions
| Function | Description |
| --------------------- | ---------------------------------------- |
| `run_custom_binary()` | Execute a custom binary with arguments |
| `get_binary_path()` | Get the full filesystem path to a binary |
| `list_binaries()` | List all available custom binaries |
## Exception Classes
| Exception | Description |
| --------------------- | ------------------------------------------ |
| `BinaryNotFoundError` | Binary does not exist or is not executable |
## Function Reference
### `run_custom_binary(binary_name, *args, **kwargs)`
Execute a custom binary that was uploaded via `lager binaries add`.
```python theme={null}
from lager.binaries import run_custom_binary
# Simple usage
result = run_custom_binary('rt_newtmgr', 'image', 'list')
print(result.stdout)
# With timeout
result = run_custom_binary('slow_tool', '--verbose', timeout=60)
# Check return code
if result.returncode != 0:
print(f"Error: {result.stderr}")
```
**Parameters:**
| Parameter | Type | Default | Description |
| ---------------- | ---------------- | ------- | ----------------------------------------------------- |
| `binary_name` | `str` | - | Name of the binary to run (e.g., 'rt\_newtmgr') |
| `*args` | `str` | - | Arguments to pass to the binary |
| `timeout` | `int` | `30` | Maximum time in seconds to wait (None for no timeout) |
| `capture_output` | `bool` | `True` | Capture stdout/stderr |
| `text` | `bool` | `True` | Return stdout/stderr as strings instead of bytes |
| `check` | `bool` | `False` | Raise `CalledProcessError` if return code is non-zero |
| `cwd` | `str` | `None` | Working directory for the process |
| `env` | `dict` | `None` | Environment variables (inherits from parent if None) |
| `input` | `str` or `bytes` | `None` | Input to send to stdin |
**Returns:** `subprocess.CompletedProcess` with attributes:
* `returncode` - Exit code of the process
* `stdout` - Captured standard output (if `capture_output=True`)
* `stderr` - Captured standard error (if `capture_output=True`)
**Raises:**
* `BinaryNotFoundError` - If the binary doesn't exist or isn't executable
* `subprocess.TimeoutExpired` - If the process times out
* `subprocess.CalledProcessError` - If `check=True` and return code is non-zero
### `get_binary_path(binary_name)`
Get the full filesystem path to a custom binary.
```python theme={null}
from lager.binaries import get_binary_path
path = get_binary_path('rt_newtmgr')
print(f"Binary located at: {path}")
# Output: /home/www-data/customer-binaries/rt_newtmgr
```
**Parameters:**
| Parameter | Type | Description |
| ------------- | ----- | --------------------------------- |
| `binary_name` | `str` | Name of the binary (without path) |
**Returns:** `str` - Full path to the binary
**Raises:** `BinaryNotFoundError` - If the binary doesn't exist or isn't executable
### `list_binaries()`
List all available custom binaries on the Lager Box.
```python theme={null}
from lager.binaries import list_binaries
binaries = list_binaries()
print("Available binaries:")
for name in binaries:
print(f" - {name}")
```
**Returns:** `list[str]` - Sorted list of binary names
## Examples
### Run Device Interaction Tool
```python theme={null}
from lager.binaries import run_custom_binary, BinaryNotFoundError
try:
# Run rt_newtmgr to list images on device
result = run_custom_binary('rt_newtmgr', 'image', 'list', '-c', '/dev/ttyUSB0')
if result.returncode == 0:
print("Images on device:")
print(result.stdout)
else:
print(f"Error: {result.stderr}")
except BinaryNotFoundError as e:
print(f"Binary not found: {e}")
```
### Run with Custom Environment
```python theme={null}
from lager.binaries import run_custom_binary
# Pass environment variables to the binary
result = run_custom_binary(
'my_tool',
'--config', 'test.json',
env={'DEBUG': '1', 'LOG_LEVEL': 'verbose'},
timeout=60
)
print(result.stdout)
```
### Check Available Binaries
```python theme={null}
from lager.binaries import list_binaries, run_custom_binary
# List what's available
binaries = list_binaries()
if not binaries:
print("No custom binaries installed.")
print("Use 'lager binaries add --box ' to upload binaries.")
else:
print(f"Found {len(binaries)} custom binaries:")
for name in binaries:
print(f" - {name}")
```
### Error Handling
```python theme={null}
from lager.binaries import run_custom_binary, BinaryNotFoundError
import subprocess
try:
result = run_custom_binary('my_tool', '--version', check=True, timeout=10)
print(f"Version: {result.stdout.strip()}")
except BinaryNotFoundError as e:
print(f"Binary not available: {e}")
except subprocess.TimeoutExpired:
print("Command timed out")
except subprocess.CalledProcessError as e:
print(f"Command failed with exit code {e.returncode}")
print(f"stderr: {e.stderr}")
```
### Integration with Test Script
```python theme={null}
from lager.binaries import run_custom_binary
from lager import Net, NetType
import time
def flash_device_firmware():
"""Flash firmware using custom programming tool."""
# Power on the device
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.enable()
time.sleep(1)
# Run custom flashing tool
result = run_custom_binary(
'device_flasher',
'--port', '/dev/ttyUSB0',
'--firmware', '/tmp/firmware.bin',
'--verify',
timeout=120
)
if result.returncode == 0:
print("Firmware flashed successfully!")
return True
else:
print(f"Flash failed: {result.stderr}")
return False
def run_device_tests():
"""Run device-specific test tool."""
result = run_custom_binary(
'device_test_runner',
'--all',
'--json-output',
timeout=300
)
if result.returncode == 0:
import json
results = json.loads(result.stdout)
return results
else:
raise RuntimeError(f"Tests failed: {result.stderr}")
```
## Uploading Binaries
Custom binaries are uploaded to the Lager Box using the CLI:
```bash theme={null}
# Upload a binary to the Lager Box
lager binaries add ./my_tool --box my-lager-box
# List binaries on the Lager Box
lager binaries list --box my-lager-box
# Remove a binary
lager binaries remove my_tool --box my-lager-box
```
## Binary Location
On the Lager Box, custom binaries are stored at:
* **Host path:** `/home/lagerdata/third_party/customer-binaries/`
* **Container path:** `/home/www-data/customer-binaries/`
The directory is mounted into the container, so binaries are immediately available after upload without rebuilding.
## Notes
* Binaries must be Linux x86\_64 compatible (matching Lager Box architecture)
* Binary names cannot contain path separators (`/`, `\`) or `..`
* Binaries must be executable (set automatically during upload)
* Default timeout is 30 seconds; use `timeout=None` for no limit
* Use `capture_output=False` for interactive or streaming output
* Environment variables are inherited from parent process unless `env` is specified
# Bluetooth Low Energy
Source: https://docs.lagerdata.com/source/reference/python/ble
BLE device scanning, connection, and GATT operations
Communicate with Bluetooth Low Energy (BLE) devices for scanning, connecting, reading/writing characteristics, and subscribing to notifications.
## Import
```python theme={null}
from lager.ble import Client, Central, noop_handler, notify_handler, waiter
```
## Classes
| Class | Description |
| --------- | -------------------------------------------------------- |
| `Central` | BLE central role for scanning and initiating connections |
| `Client` | BLE client for GATT operations on a connected device |
## Functions
| Function | Description |
| ------------------ | -------------------------------- |
| `noop_handler()` | No-op notification handler |
| `notify_handler()` | Event-based notification handler |
| `waiter()` | Async wait helper |
## Central Class
The `Central` class provides BLE scanning and connection initiation.
### `Central(loop=None)`
Create a BLE central instance.
```python theme={null}
from lager.ble import Central
central = Central()
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ------------------- | ----------------------------------- |
| `loop` | `asyncio.EventLoop` | Event loop (optional, uses default) |
### `scan(scan_time=5.0, name=None, address=None)`
Scan for nearby BLE devices.
```python theme={null}
central = Central()
# Scan for all devices
devices = central.scan(scan_time=5.0)
for device in devices:
print(f"{device.name}: {device.address}")
# Scan for specific device name
devices = central.scan(name="MyDevice")
# Scan for specific MAC address
devices = central.scan(address="AA:BB:CC:DD:EE:FF")
```
**Parameters:**
| Parameter | Type | Description |
| ----------- | ------- | --------------------------------------- |
| `scan_time` | `float` | Scan duration in seconds (default: 5.0) |
| `name` | `str` | Filter by device name (optional) |
| `address` | `str` | Filter by MAC address (optional) |
**Returns:** `list` - List of discovered BLE devices
### `connect(address)`
Connect to a BLE device by address.
```python theme={null}
central = Central()
client = central.connect("AA:BB:CC:DD:EE:FF")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | ------------------------- |
| `address` | `str` | MAC address of the device |
**Returns:** `Client` - Connected BLE client
### `pair(address)`
Pair with a BLE device.
```python theme={null}
central = Central()
client = central.pair("AA:BB:CC:DD:EE:FF")
```
## Client Class
The `Client` class provides GATT operations on a connected BLE device.
### Creating a Client
```python theme={null}
from lager.ble import Client, Central
from bleak import BleakClient
import asyncio
# Method 1: Using Central
central = Central()
client = central.connect("AA:BB:CC:DD:EE:FF")
# Method 2: Direct creation with context manager
loop = asyncio.get_event_loop()
with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client:
# Use client
pass
```
### `connect()`
Establish connection to the BLE device.
```python theme={null}
client.connect()
```
### `disconnect()`
Disconnect from the BLE device.
```python theme={null}
client.disconnect()
```
### `pair()`
Pair with the connected device.
```python theme={null}
client.pair()
```
### `get_services()`
Discover and retrieve all GATT services.
```python theme={null}
services = client.get_services()
for service in services:
print(f"Service: {service.uuid}")
for char in service.characteristics:
print(f" Characteristic: {char.uuid}")
```
**Returns:** `BleakGATTServiceCollection` - Collection of discovered services
### `has_characteristic(uuid)`
Check if a characteristic exists on the device.
```python theme={null}
if client.has_characteristic("00002a19-0000-1000-8000-00805f9b34fb"):
print("Battery level characteristic found")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | ------------------- |
| `uuid` | `str` | Characteristic UUID |
**Returns:** `bool` - True if characteristic exists
### `read_gatt_char(char_specifier)`
Read a characteristic value.
```python theme={null}
# Read by UUID
data = client.read_gatt_char("00002a19-0000-1000-8000-00805f9b34fb")
print(f"Battery level: {data[0]}%")
# Read by handle
data = client.read_gatt_char(0x0012)
```
**Parameters:**
| Parameter | Type | Description |
| ---------------- | -------------- | ---------------------------- |
| `char_specifier` | `str` or `int` | UUID string or handle number |
**Returns:** `bytearray` - Characteristic value
### `write_gatt_char(char_specifier, data)`
Write a value to a characteristic.
```python theme={null}
# Write bytes
client.write_gatt_char("characteristic-uuid", b'\x01\x02\x03')
# Write string
client.write_gatt_char("characteristic-uuid", "hello".encode('utf-8'))
```
**Parameters:**
| Parameter | Type | Description |
| ---------------- | -------------- | ---------------------------- |
| `char_specifier` | `str` or `int` | UUID string or handle number |
| `data` | `bytes` | Data to write |
### `start_notify(char_specifier, callback=noop_handler, max_messages=None, timeout=None)`
Subscribe to characteristic notifications.
```python theme={null}
from lager.ble import noop_handler
# Simple notification subscription
def my_callback(handle, data):
print(f"Received: {data.hex()}")
timed_out, messages = client.start_notify(
"characteristic-uuid",
callback=my_callback,
max_messages=10,
timeout=30.0
)
if timed_out:
print("Timed out waiting for notifications")
else:
print(f"Received {len(messages)} messages")
```
**Parameters:**
| Parameter | Type | Description |
| ---------------- | -------------- | --------------------------------------- |
| `char_specifier` | `str` or `int` | Characteristic UUID or handle |
| `callback` | `callable` | Function called for each notification |
| `max_messages` | `int` | Stop after receiving this many messages |
| `timeout` | `float` | Timeout in seconds |
**Returns:** `tuple[bool, list]` - (timed\_out, messages) where timed\_out is True if timeout occurred
### `stop_notify(char_specifier)`
Unsubscribe from characteristic notifications.
```python theme={null}
client.stop_notify("characteristic-uuid")
```
### `sleep(timeout)`
Sleep for a duration (async-safe).
```python theme={null}
client.sleep(1.0) # Sleep for 1 second
```
## Helper Functions
### `noop_handler(handle, data)`
A no-op notification handler.
```python theme={null}
from lager.ble import noop_handler
# Use when you only care about collecting messages
timed_out, messages = client.start_notify(
"uuid",
callback=noop_handler,
max_messages=5
)
```
### `notify_handler(evt, messages, callback, max_messages, handle, data)`
Internal notification handler that collects messages and signals completion.
### `waiter(event, timeout)`
Async wait helper for notification events.
## Examples
### Scan for Devices
```python theme={null}
from lager.ble import Central
central = Central()
# Discover all nearby BLE devices
print("Scanning for BLE devices...")
devices = central.scan(scan_time=10.0)
for device in devices:
name = device.name or "Unknown"
print(f" {name}: {device.address}")
```
### Connect and Read Characteristic
```python theme={null}
from lager.ble import Client, Central
from bleak import BleakClient
import asyncio
# Standard BLE UUIDs
BATTERY_SERVICE = "0000180f-0000-1000-8000-00805f9b34fb"
BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb"
loop = asyncio.get_event_loop()
with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client:
# Check if battery service exists
if client.has_characteristic(BATTERY_LEVEL):
data = client.read_gatt_char(BATTERY_LEVEL)
print(f"Battery level: {data[0]}%")
```
### Subscribe to Notifications
```python theme={null}
from lager.ble import Client, Central
from bleak import BleakClient
import asyncio
NOTIFY_UUID = "your-characteristic-uuid"
def handle_notification(handle, data):
print(f"Notification from {handle}: {data.hex()}")
loop = asyncio.get_event_loop()
with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client:
# Subscribe and wait for 10 messages or 30 seconds
timed_out, messages = client.start_notify(
NOTIFY_UUID,
callback=handle_notification,
max_messages=10,
timeout=30.0
)
if timed_out:
print(f"Timeout - received {len(messages)} messages")
else:
print(f"Received all {len(messages)} messages")
# Process collected messages
for msg in messages:
print(f" {msg.hex()}")
client.stop_notify(NOTIFY_UUID)
```
### Write Command and Read Response
```python theme={null}
from lager.ble import Client
from bleak import BleakClient
import asyncio
WRITE_UUID = "write-characteristic-uuid"
READ_UUID = "read-characteristic-uuid"
loop = asyncio.get_event_loop()
with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client:
# Send command
command = b'\x01\x02\x03'
client.write_gatt_char(WRITE_UUID, command)
# Wait for processing
client.sleep(0.1)
# Read response
response = client.read_gatt_char(READ_UUID)
print(f"Response: {response.hex()}")
```
### Device Firmware Version Check
```python theme={null}
from lager.ble import Client, Central
from bleak import BleakClient
import asyncio
# Standard Device Information Service UUIDs
DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb"
FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb"
MANUFACTURER_NAME = "00002a29-0000-1000-8000-00805f9b34fb"
def check_device_info(address):
loop = asyncio.get_event_loop()
with Client(BleakClient(address), loop=loop) as client:
# Read manufacturer
if client.has_characteristic(MANUFACTURER_NAME):
data = client.read_gatt_char(MANUFACTURER_NAME)
print(f"Manufacturer: {data.decode('utf-8')}")
# Read firmware version
if client.has_characteristic(FIRMWARE_REVISION):
data = client.read_gatt_char(FIRMWARE_REVISION)
print(f"Firmware: {data.decode('utf-8')}")
# First scan to find device
central = Central()
devices = central.scan(name="MyDevice")
if devices:
check_device_info(devices[0].address)
```
### BLE Production Test
```python theme={null}
from lager.ble import Client, Central
from bleak import BleakClient
import asyncio
DEVICE_NAME = "DUT_BLE"
TEST_CHAR = "test-characteristic-uuid"
def ble_production_test():
central = Central()
loop = asyncio.get_event_loop()
# Step 1: Scan for DUT
print("Scanning for DUT...")
devices = central.scan(name=DEVICE_NAME, scan_time=10.0)
if not devices:
print("FAIL: DUT not found")
return False
address = devices[0].address
print(f"Found DUT at {address}")
# Step 2: Connect and test
try:
with Client(BleakClient(address), loop=loop) as client:
# Test read
data = client.read_gatt_char(TEST_CHAR)
if len(data) == 0:
print("FAIL: Empty response")
return False
# Test write
client.write_gatt_char(TEST_CHAR, b'\x55')
client.sleep(0.1)
# Verify write
data = client.read_gatt_char(TEST_CHAR)
if data[0] != 0x55:
print("FAIL: Write verification failed")
return False
print("PASS: BLE test complete")
return True
except Exception as e:
print(f"FAIL: {e}")
return False
# Run test
ble_production_test()
```
## Hardware Requirements
| Requirement | Description |
| ------------ | ---------------------------------------- |
| BLE Hardware | Bluetooth 4.0+ adapter on Lager Box |
| Permissions | May require root/sudo for BLE operations |
## Dependencies
The BLE module uses [Bleak](https://bleak.readthedocs.io/) as the underlying BLE library, which provides cross-platform BLE support.
## Notes
* BLE operations are synchronous wrappers around async Bleak operations
* The Client class supports context manager (`with` statement) for automatic cleanup
* Notification callbacks receive `(handle, data)` parameters
* Use `max_messages` and `timeout` together to control notification collection
* MAC addresses are typically in format `AA:BB:CC:DD:EE:FF`
* Some BLE operations may require pairing before they work
* Signal strength (RSSI) is available on scanned device objects
# Breakpoints
Source: https://docs.lagerdata.com/source/reference/python/breakpoints
Pause a running lager python script to inspect the bench, then continue
Pause a `lager python` script mid-run so you can inspect the bench with ad-hoc `lager`
commands (or a live Python prompt), then resume where it left off. Useful for a long test
that reaches a known trouble spot, or for checking on a device in an unknown state without
killing and restarting the run.
Introduced in **lager 0.21.0**.
## Import
```python theme={null}
from lager import pause
```
## `pause(label=None, *, timeout=None, interactive=False)`
Blocks the script at the call site until it is resumed (or the timeout elapses).
| Argument | Default | Description |
| ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | `None` | A short note shown in the pause banner and console — e.g. the reason for the breakpoint. |
| `timeout` | `None` | Seconds to wait before auto-resuming. `None` uses the `LAGER_BREAKPOINT_TIMEOUT` env var, or **300 s** if unset. `0` waits indefinitely (until you resume). |
| `interactive` | `False` | When `True`, also exposes a Python console attached to the paused script (see [Interactive console](#interactive-console)). |
```python theme={null}
from lager import pause
pause("check the DUT before the final step")
```
When the line runs, the script prints a banner and stops:
```
=== lager breakpoint "check the DUT before the final step" at test.py:14 (id 7f3a…e9)
resume: press Enter here, or `lager python --continue 7f3a…e9 --box mybox`
inspect: `lager python --console 7f3a…e9 --box mybox`
auto-resume in 300s
```
`pause()` is a safe no-op when it can't pause — when the script isn't running under
`lager python` (no breakpoint context), or when [breakpoints are disabled](#disabling-breakpoints).
It never raises.
## Resuming
A paused script can be resumed three ways:
1. **Press Enter** in the terminal running the script (the foreground `lager python` session).
2. **`lager python --continue --box `** — from any terminal, anywhere. Use the `id`
from the banner. Handy when the script is detached or you're already in another terminal.
3. **Auto-resume** — after the timeout (default 300 s) the script continues on its own and logs
that it did. This keeps an unattended or forgotten breakpoint from hanging a run.
### Controlling the auto-resume timeout
The default is **300 seconds**. Override it per breakpoint, per run, or disable it entirely:
```python theme={null}
pause("inspect", timeout=1800) # wait up to 30 minutes
pause("inspect", timeout=0) # wait forever — never auto-resume
```
```bash theme={null}
# whole run, via the existing --env flag (applies to pause() calls with no explicit timeout=)
lager python test.py --box mybox --env LAGER_BREAKPOINT_TIMEOUT=1800
```
Resolution order is **`timeout=` argument → `LAGER_BREAKPOINT_TIMEOUT` env → 300 s default**, so an
explicit `timeout=` in the script wins over the env var.
`lager python --timeout` is a different setting — the script's total runtime limit, capped at
300 s on the box — and it will terminate the whole run when it elapses, paused or not. Leave it
at its default (`0`, unlimited) when using long breakpoint pauses.
## Interactive console
With `pause(interactive=True)`, the breakpoint also opens a Python console **running inside the
paused script's process**, seeded with the variables in scope at the pause:
```python theme={null}
readings = read_adcs()
pause("inspect bench", interactive=True)
```
Connect to it from another terminal:
```bash theme={null}
lager python --console --box mybox
```
```
Connected to interactive console (Ctrl+D to disconnect)
>>> readings
{'adc1': -10.6032, 'adc2': -10.6031, 'adc3': -10.6032}
>>> readings['adc1'] * 1000
-10603.2
>>> read_adcs()
{'adc1': -10.6032, 'adc2': -10.6032, 'adc3': -10.6032}
```
You can read any variable, evaluate expressions, and call functions the script defines.
`Ctrl+D` disconnects (the script stays paused).
The console is for **inspection**: it operates on a snapshot of the script's namespace, so
changes you make in the console do **not** carry back into the running script when it resumes.
## Inspecting hardware while paused
Because a paused script holds no box-wide lock, you can run normal `lager` commands against the
bench from another terminal while it waits:
```bash theme={null}
lager supply supply2 state --box mybox # power supply
lager battery battery1 state --box mybox # battery
lager adc adc4 --box mybox # an ADC the script isn't using
```
Two hardware rules to keep in mind, both a consequence of USB instruments allowing only one
owner at a time:
* **A device the script itself has open is claimed by the paused process.** Reading it from a
second terminal returns a "device busy / claimed by another process" error. Read it through the
**`--console`** instead — that runs in the same process and shares the open handle.
* **One net per physical instrument per process.** Two nets on the same instrument can't both be
open at once in a single script (e.g. the two channels of one Rigol DP821, `supply2`/`supply3`,
or the dual-role Keithley 2281S, `supply1`/`battery1`). Read them from separate terminals, or
one at a time.
## Built-in `breakpoint()`
Calling Python's built-in `breakpoint()` in a `lager python` script triggers the same interactive
pause as `lager.pause()`:
```python theme={null}
breakpoint() # same as pause()
breakpoint("check DUT") # same as pause("check DUT")
```
## Disabling breakpoints
Set `LAGER_BREAKPOINTS` to an off value to turn every `pause()` (and `breakpoint()`) into a no-op
— useful for a clean, non-interactive run of a script that has breakpoints left in it:
```bash theme={null}
lager python test.py --box mybox --env LAGER_BREAKPOINTS=off
```
Accepts `off`, `0`, `false`, or `no` (case-insensitive).
## Full example
`test.py`:
```python theme={null}
import time
from lager import Net, NetType, pause
adc_nets = ["adc1", "adc2", "adc3"]
def read_adcs():
return {n: round(float(Net.get(n, type=NetType.ADC).input()), 4) for n in adc_nets}
print("Running test...")
for step in range(1, 4):
print(f" step {step}/3 ...")
time.sleep(1)
readings = read_adcs()
print(f"sensor readings: {readings}")
pause("inspect bench before final step", interactive=True)
print("Resuming - running final step.")
print("Done.")
```
Run it (terminal 1):
```bash theme={null}
lager python test.py --box mybox
```
```
Running test...
step 1/3 ...
step 2/3 ...
step 3/3 ...
sensor readings: {'adc1': -10.6032, 'adc2': -10.6031, 'adc3': -10.6032}
=== lager breakpoint "inspect bench before final step" at test.py:18 (id 7f3a…e9)
resume: press Enter here, or `lager python --continue 7f3a…e9 --box mybox`
inspect: `lager python --console 7f3a…e9 --box mybox`
auto-resume in 300s
```
While it's paused, check the bench (terminal 2):
```bash theme={null}
lager supply supply2 state --box mybox # a shared instrument — reads fine
lager python --console 7f3a…e9 --box mybox # then: readings / read_adcs()
```
Press **Enter** in terminal 1 (or run `lager python --continue 7f3a…e9 --box mybox`) and the
script finishes:
```
=== resumed
Resuming - running final step.
Done.
```
# DAC
Source: https://docs.lagerdata.com/source/reference/python/dac
Control digital-to-analog converter outputs
Set analog voltage outputs using DAC pins. Supports LabJack T7 and MCC USB-202 hardware.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ----------------- | ------------------------------ |
| `output(voltage)` | Set analog output voltage |
| `get_voltage()` | Read configured output voltage |
| `input()` | Alias for `get_voltage()` |
## Method Reference
### `Net.get(name, type=NetType.DAC)`
Get a DAC net by name.
```python theme={null}
from lager import Net, NetType
dac = Net.get('VREF', type=NetType.DAC)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------- |
| `name` | `str` | Name of the DAC net |
| `type` | `NetType` | Must be `NetType.DAC` |
**Returns:** DAC Net instance
### `output(voltage)`
Set the analog output voltage.
```python theme={null}
dac.output(2.5) # Set to 2.5V
```
| Parameter | Type | Description |
| --------- | ------- | ----------------------- |
| `voltage` | `float` | Output voltage in volts |
### `get_voltage()`
Read the currently configured output voltage.
```python theme={null}
v = dac.get_voltage()
print(f"DAC set to: {v}V")
```
**Returns:** `float` - Configured voltage in volts
### `input()`
Alias for `get_voltage()`. Returns the currently configured output voltage.
```python theme={null}
v = dac.input()
print(f"DAC output: {v}V")
```
**Returns:** `float` - Configured voltage in volts
## Examples
### Set Reference Voltage
```python theme={null}
from lager import Net, NetType
vref = Net.get('VREF', type=NetType.DAC)
vref.output(1.8) # Set to 1.8V
```
### Generate Ramp Signal
```python theme={null}
from lager import Net, NetType
import time
signal = Net.get('SIGNAL_OUT', type=NetType.DAC)
# Ramp from 0 to 5V in 0.5V steps
for v in range(0, 51, 5):
voltage = v / 10.0
signal.output(voltage)
print(f"Output: {voltage}V")
time.sleep(0.1)
```
### Voltage Sweep Test
```python theme={null}
from lager import Net, NetType
import time
control = Net.get('CONTROL', type=NetType.DAC)
sensor = Net.get('RESPONSE', type=NetType.ADC)
# Sweep control voltage and measure response
for mv in range(0, 3301, 100):
voltage = mv / 1000.0
control.output(voltage)
time.sleep(0.1) # Settling time
response = sensor.input()
print(f"Control: {voltage:.2f}V, Response: {response:.3f}V")
```
### Set and Verify
```python theme={null}
from lager import Net, NetType
dac = Net.get('ANALOG_OUT', type=NetType.DAC)
# Set output
dac.output(3.3)
# Read back to verify
readback = dac.get_voltage()
print(f"Set: 3.3V, Readback: {readback}V")
```
## Supported Hardware
| Hardware | Channels | Range |
| ----------- | ----------- | ------ |
| LabJack T7 | DAC0-DAC1 | 0-10 V |
| MCC USB-202 | AOUT0-AOUT1 | 0-5 V |
### Pin Naming
**LabJack T7:**
| Pin Input | Channel |
| ----------------- | --------- |
| `0`-`1` | DAC0-DAC1 |
| `"DAC0"`-`"DAC1"` | DAC0-DAC1 |
**MCC USB-202:**
| Pin Input | Channel |
| ------------------- | ----------- |
| `0`-`1` | AOUT0-AOUT1 |
| `"DAC0"`-`"DAC1"` | AOUT0-AOUT1 |
| `"AOUT0"`-`"AOUT1"` | AOUT0-AOUT1 |
## Notes
* DAC nets work directly without `enable()`/`disable()` calls
* LabJack T7 output range: 0-10 V
* USB-202 output range: 0-5 V
* `input()` is an alias for `get_voltage()` and returns the configured output value
* Output values are maintained until changed or power cycle
* Net names must match those configured on the Lager Box
# Debug
Source: https://docs.lagerdata.com/source/reference/python/debug
Embedded debug operations for J-Link probes
Control embedded debug operations including device connection, firmware flashing, reset, and memory access using J-Link debug probes.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
The Net-based API provides methods for embedded debugging operations.
| Method | Description |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `connect(speed, transport, *, script, force, ignore_if_connected)` | Connect to target device (optional per-connect J-Link script override) |
| `disconnect()` | Disconnect from target |
| `reset(halt)` | Reset the device |
| `flash(firmware_path)` | Flash firmware to device |
| `erase()` | Perform full chip erase |
| `read_memory(address, length)` | Read memory from device |
| `status()` | Get connection status |
| `rtt(channel, search_addr, search_size, chunk_size)` | Create RTT session for bidirectional communication (raw bytes) |
| `rtt_defmt(elf, channel)` | RTT session decoded through `defmt-print` (yields log lines) |
| `session(...)` | Scoped session: connect on entry, guaranteed teardown on exit |
## Method Reference
### `Net.get(name, type=NetType.Debug)`
Get a debug net by name.
```python theme={null}
from lager import Net, NetType
dbg = Net.get('DUT', type=NetType.Debug)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ----------------------- |
| `name` | `str` | Name of the debug net |
| `type` | `NetType` | Must be `NetType.Debug` |
**Returns:** Debug Net instance
**Note:** The debug net must be configured with the target device name stored in the `channel` field (e.g., 'NRF52840\_XXAA', 'R7FA0E107').
### `connect(speed=None, transport=None, *, script=None, force=False, ignore_if_connected=False)`
Connect to the target device (start the gdbserver for this probe). The backend
(J-Link or OpenOCD) is chosen automatically from the probe.
```python theme={null}
# Connect with default settings (4000 kHz, SWD)
dbg.connect()
# Connect with custom speed
dbg.connect(speed='adaptive')
# Connect with JTAG
dbg.connect(transport='JTAG')
# Connect with a per-connect J-Link script override (box path or base64 blob)
dbg.connect(script='/home/lagerdata/probes/my_target.JLinkScript')
```
**Parameters:**
| Parameter | Type | Default | Description |
| --------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `speed` | `str` | `'4000'` | Interface speed in kHz (e.g., '4000') or 'adaptive' |
| `transport` | `str` | `'SWD'` | Transport protocol ('SWD' or 'JTAG') |
| `script` | `str` | `None` | **J-Link only** — a per-connect `.JLinkScript` override: a path on the box **or** a base64-encoded script blob. It is copied to the shared script temp path so subsequent `flash()` / `reset()` / `read_memory()` calls pick it up immediately. The OpenOCD backend ignores this. |
| `force` | `bool` | `False` | Stop any gdbserver already running for this probe and start fresh. |
| `ignore_if_connected` | `bool` | `False` | If a gdbserver is already running for this probe, return its status without touching it. |
**Returns:** `dict` - Status dictionary with connection information
A `script` override is only adopted by a *relaunch* of the gdbserver. If a server
is already running, pass `force=True` to restart it with the new script;
`ignore_if_connected=True` returns early without relaunching (though the script
file is still repointed for subsequent Commander operations). Invalid input — a
missing path that isn't valid base64, or an empty string — is silently ignored and
the net's previously materialised script stays in effect. **Concurrency caveat:**
two debug nets connecting with different scripts share one temp path and can clobber
each other.
### `disconnect()`
Disconnect from the target device.
```python theme={null}
dbg.disconnect()
```
**Returns:** `dict` - Status dictionary
### `reset(halt=False)`
Reset the device.
```python theme={null}
# Reset and continue execution
output = dbg.reset(halt=False)
print(output)
# Reset and halt for debugging
output = dbg.reset(halt=True)
print(output)
```
**Parameters:**
| Parameter | Type | Default | Description |
| --------- | ------ | ------- | -------------------- |
| `halt` | `bool` | `False` | Halt CPU after reset |
**Returns:** `str` - Combined output from reset operation
**Self-heal (both backends):** Right after a `flash()` there is a short window where the debug server isn't reachable yet — for J-Link the restarted GDB server's PID isn't observable, for OpenOCD a transient daemon/RPC fault — and a bare call would raise. `reset()` now retries with bounded backoff on both the J-Link and OpenOCD backends, and only (re)starts a server when one is genuinely down. It never tears down a server that is already running, so an attached RTT session is left intact, and callers no longer need their own retry wrappers.
**DA1469x exception:** On DA1469x, `flash()` deliberately leaves the server down (it ends in a software reset rather than a server restart) and the documented flow is an explicit, halt-aware reconnect. So on DA1469x the self-heal retries but never auto-starts a server — a genuinely-down server still surfaces the original error, exactly as before, instead of silently coming up unhalted (which can yield garbage QSPI-XIP reads).
### `flash(firmware_path)`
Flash firmware to the device.
```python theme={null}
# Flash a hex file
output = dbg.flash('/path/to/firmware.hex')
print(output)
# Flash a binary file (address 0x00000000 assumed)
output = dbg.flash('/path/to/firmware.bin')
print(output)
# Flash an ELF file
output = dbg.flash('/path/to/firmware.elf')
print(output)
```
**Parameters:**
| Parameter | Type | Description |
| --------------- | ----- | ------------------------------------------- |
| `firmware_path` | `str` | Path to firmware file (.hex, .bin, or .elf) |
**Returns:** `str` - Combined output from flash operation
**Note:** For .bin files, flash address defaults to 0x00000000.
### `erase()`
Perform full chip erase. This erases ALL flash memory including protection settings.
```python theme={null}
# Full chip erase
output = dbg.erase()
print(output)
```
**Returns:** `str` - Combined output from erase operation
### `read_memory(address, length)`
Read memory from the target device.
```python theme={null}
# Read 256 bytes starting at address 0x20000000
data = dbg.read_memory(0x20000000, 256)
print(f"Read {len(data)} bytes")
print(data.hex())
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | ----------------------- |
| `address` | `int` | Starting memory address |
| `length` | `int` | Number of bytes to read |
**Returns:** `bytes` - Memory data
**Self-heal:** like `reset()`, `read_memory()` retries with bounded backoff across the brief post-`flash()` settling window on both backends and only reconnects when no server is running, never disturbing a live session. `erase()` behaves the same way. The same DA1469x exception applies — no server is auto-started, so a post-flash DA1469x read raises clearly rather than returning unhalted-XIP garbage.
### `status()`
Get the current connection status.
```python theme={null}
status = dbg.status()
print(f"Connected: {status.get('connected', False)}")
```
**Returns:** `dict` - Status dictionary with connection information
### `session(speed=None, transport=None, connect=True, ignore_if_connected=True, disconnect_on_exit=True)`
Scoped debug session. Connects on entry and guarantees teardown on exit, so the safe flash → attach-RTT → reset ordering is encoded once instead of being rediscovered in every script. The `with` target is the net itself, so the full surface (`flash`, `rtt_defmt`, `reset`, `read_memory`, …) is available inside the block.
```python theme={null}
with dbg.session() as s:
s.flash('build/app.hex') # built-in stop->flash->restart handoff
with s.rtt_defmt(elf='build/app.elf') as logs:
s.reset(halt=False) # reader re-attaches across the reset blip
for line in logs:
if 'boot ok' in line:
break
# GDB server is torn down here (disconnect_on_exit=True)
```
**Parameters:**
| Parameter | Type | Default | Description |
| --------------------- | --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `speed` | `str` or `None` | `None` | Forwarded to `connect()` |
| `transport` | `str` or `None` | `None` | Forwarded to `connect()` |
| `connect` | `bool` | `True` | Connect on entry. Set `False` to attach to a server you manage yourself |
| `ignore_if_connected` | `bool` | `True` | Reuse a running server instead of raising (regression-safe: never restarts a live server) |
| `disconnect_on_exit` | `bool` | `True` | Stop the GDB server on exit. Set `False` to leave it running for later commands |
**Returns:** a context manager yielding the debug net.
**Why it pairs with RTT:** the in-process RTT reader is reconnect-aware (see below), so a `flash()` or `reset()` inside the session that bounces the GDB server doesn't kill a log stream opened in the same block.
### `rtt(channel=0, search_addr=None, search_size=None, chunk_size=None)`
Create an RTT (Real-Time Transfer) session for bidirectional communication with the target device.
```python theme={null}
# Open RTT session on default channel (0)
with dbg.rtt() as rtt:
# Read debug output
data = rtt.read_some(timeout=1.0)
if data:
print(data.decode('utf-8'))
# Send commands to device
rtt.write(b'test_command\n')
# Use different RTT channel
with dbg.rtt(channel=1) as rtt:
data = rtt.read_some(timeout=2.0)
# Specify RAM search region for RTT control block
with dbg.rtt(search_addr=0x20000000, search_size=0x10000) as rtt:
data = rtt.read_some(timeout=1.0)
```
**Parameters:**
| Parameter | Type | Default | Description |
| ------------- | --------------- | ------- | ---------------------------------------------- |
| `channel` | `int` | `0` | RTT channel number (typically 0-15) |
| `search_addr` | `int` or `None` | `None` | RAM start address for RTT control block search |
| `search_size` | `int` or `None` | `None` | Size of RAM region to search in bytes |
| `chunk_size` | `int` or `None` | `None` | Size of each read chunk in bytes |
**Returns:** RTT context manager with methods:
* `read_some(timeout)` - Read available data with timeout (returns bytes or None)
* `write(data)` - Write data to target (accepts bytes or str)
**Note:** Debug connection must be active before using RTT. Call `connect()` first.
**Reconnect-aware (both backends):** a J-Link `flash()` (and `reset()` via its Commander grab) briefly frees the probe's USB and restarts the GDB server on the *same* ports, dropping the RTT socket. The reader transparently re-attaches to the same RTT telnet port instead of going silent, so a long-lived `read_some()` / `rtt_defmt()` loop keeps producing across a flash. The OpenOCD reader is reconnect-aware too — OpenOCD keeps its daemon up across an ordinary flash so the socket rarely drops, but if it does (daemon force-restart or rtt-server bounce) the reader re-runs the `rtt setup` / `rtt server start` and re-attaches. In both cases reconnection is bounded (default 30 s) and only re-attaches once the server/daemon is actually back up — it never *starts* one — so a flash that deliberately leaves the server down (e.g. DA1469x) won't spin forever, and the reader can't disturb a DA1469x left intentionally down. Pass `reconnect=False` for the legacy one-shot behavior.
## Examples
### Flash Firmware and Reset
```python theme={null}
from lager import Net, NetType
# Get debug net
dbg = Net.get('DUT', type=NetType.Debug)
# Connect to target
status = dbg.connect()
print(f"Connected: {status}")
# Flash firmware
output = dbg.flash('/etc/lager/firmware/app.hex')
print(output)
# Reset and run
output = dbg.reset(halt=False)
print(output)
# Disconnect
dbg.disconnect()
```
### Chip Erase Before Programming
```python theme={null}
from lager import Net, NetType
# Get debug net
dbg = Net.get('DUT', type=NetType.Debug)
# Connect to target
status = dbg.connect()
print(f"Connected: {status}")
# Erase entire chip first (ensures clean state)
print("Erasing chip...")
output = dbg.erase()
print(output)
# Flash new firmware
output = dbg.flash('/etc/lager/firmware/app.hex')
print(output)
# Disconnect
dbg.disconnect()
```
### Read Memory
```python theme={null}
from lager import Net, NetType
dbg = Net.get('DUT', type=NetType.Debug)
# Connect to target
dbg.connect()
# Read 256 bytes from RAM
data = dbg.read_memory(0x20000000, 256)
print(f"Read {len(data)} bytes")
print(data.hex())
# Disconnect
dbg.disconnect()
```
## CLI Commands (Recommended)
For most use cases, the CLI provides a simpler interface:
```bash theme={null}
# Start GDB server (connect to target)
lager debug gdbserver --box
# Flash firmware
lager debug flash --hex firmware.hex --box
# Reset device
lager debug reset --box
# Erase flash
lager debug erase --box
# Read memory
lager debug memrd 0x20000000 256 --box
# Disconnect
lager debug disconnect --box
# Check status
lager debug status --box
```
See the [CLI Debug Reference](/source/reference/cli/debug) for full CLI documentation.
## RTT Streaming
SEGGER Real-Time Transfer (RTT) enables high-speed bidirectional communication with embedded devices during debugging (faster than UART, no timing impact).
```python theme={null}
from lager import Net, NetType
# Connect debug probe first
debug = Net.get('debug1', type=NetType.Debug)
debug.connect()
# Open RTT session for reading debug output
with debug.rtt() as rtt:
# Read debug output from MCU
data = rtt.read_some(timeout=1.0)
if data:
print(data.decode('utf-8'))
# Can also write commands to MCU
rtt.write(b'start_test\n')
```
**RTT Methods:**
| Method | Description |
| -------------------- | -------------------------------------------------------- |
| `read_some(timeout)` | Read available data with timeout (returns bytes or None) |
| `write(data)` | Write data to RTT (accepts bytes or str) |
`rtt().read_some()` returns **raw, still-encoded** bytes. Firmware that logs with [defmt](https://defmt.ferrous-systems.com/) (the de-facto standard for embedded Rust) emits a compressed binary format — calling `.decode('utf-8')` on it yields garbage. For defmt firmware, use `rtt_defmt()` below or the CLI pipe, both of which decode through `defmt-print`.
### Decoding defmt logs with `rtt_defmt()`
`rtt_defmt(elf, channel=0)` opens an RTT session and pipes it through `defmt-print` (preinstalled on the Lager Box), yielding **decoded log lines** instead of raw bytes. The `elf` must be the exact firmware flashed on the target — defmt needs its symbol metadata to decode.
```python theme={null}
from lager import Net, NetType
import time
dbg = Net.get('debug1', type=NetType.Debug)
dbg.connect(ignore_if_connected=True) # reuse a running gdbserver if one is up
dbg.flash('build/app.elf') # skip if already flashed; same ELF you decode against
dbg.reset() # restart to capture boot logs
# Capture a bounded ~10s window of decoded logs
with dbg.rtt_defmt(elf='build/app.elf', channel=0) as logs:
deadline = time.time() + 10
while time.time() < deadline:
line = logs.read_line(timeout=1.0) # decoded str, or None
if line:
print(line)
assert 'panic' not in line.lower(), f"firmware panicked: {line}"
```
`rtt_defmt()` returns a context manager exposing:
| Method | Description |
| ------------------------------- | ----------------------------------------------------------------- |
| `read_line(timeout=None)` | Next decoded log line as `str`, or `None` on timeout / stream end |
| iteration (`for line in logs:`) | Yield decoded lines until the stream ends |
| `write(data)` | Send bytes or `str` to the target's RTT down-channel |
Like the CLI pipe, the RTT stream never ends on its own — bound your read loop with a time budget or line count, then exit the `with` block.
#### Driving the firmware while decoding its logs
`write()` makes a decoding session bi-directional, so a script can send a command and assert on the decoded response. Decoding is one-way — `defmt-print` only sees the up-channel — so writes bypass it and go straight to the target. This is the only way to do both at once: the RTT telnet port accepts a single client, so you cannot open a raw `rtt()` alongside a `rtt_defmt()`.
```python theme={null}
with dbg.rtt_defmt(elf='build/app.elf') as logs:
logs.write(b'self_test\n') # command the firmware
deadline = time.time() + 5
while time.time() < deadline:
line = logs.read_line(timeout=1.0)
if line and 'self_test: pass' in line:
break
else:
raise AssertionError('firmware never reported a passing self-test')
```
This requires the firmware to declare an RTT **down** buffer on the channel you opened. `defmt-rtt` alone only sets up the up buffer — with no down buffer the target silently discards whatever you write, which looks like a host-side failure and is not one.
**Parameters:**
| Parameter | Type | Default | Description |
| ----------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `elf` | `str` | required | Path to the firmware ELF flashed on the DUT (relative paths resolve against the script's working dir on the box) |
| `channel` | `int` | `0` | RTT channel number |
| `defmt_print_bin` | `str` or `None` | `None` | Override the `defmt-print` binary (path or name on PATH) |
| `read_timeout` | `float` | `0.5` | Poll interval (seconds) for the internal RTT read loop |
**CLI Alternative:** For interactive tailing, pipe the CLI directly: `lager debug gdbserver --box --rtt 2>/dev/null | defmt-print -e build/app.elf`. See the [CLI Debug Reference](/source/reference/cli/debug#decoding-defmt-logs). Use `rtt_defmt()` when you need to assert on log content inside a test script; use the pipe when you just want to watch logs.
## Supported Devices
J-Link supports a wide range of ARM Cortex-M and other microcontrollers. Common device names:
| Manufacturer | Device Name | Description |
| ------------ | ------------------ | ------------------------ |
| Nordic | `NRF52840_XXAA` | nRF52840 |
| Nordic | `NRF52833_XXAA` | nRF52833 |
| Nordic | `NRF5340_XXAA_APP` | nRF5340 Application Core |
| Renesas | `R7FA0E107` | RA0E1 Series |
| Renesas | `R7FA2L1` | RA2L1 Series |
| STMicro | `STM32F103C8` | STM32F1 Series |
| STMicro | `STM32F407VG` | STM32F4 Series |
| STMicro | `STM32L476RG` | STM32L4 Series |
For a complete list, see [SEGGER's supported devices](https://www.segger.com/supported-devices/jlink/).
## Supported Hardware
| Debug Probe | Features |
| ----------- | ------------------------------------- |
| J-Link | JTAG/SWD debugging, flash programming |
| CMSIS-DAP | SWD debugging (via pyOCD backend) |
| ST-Link | SWD debugging (via pyOCD backend) |
## Notes
* Debug nets must be configured with the target device name in the `channel` field
* The CLI (`lager debug`) is recommended for most use cases
* Python Net API is intended for advanced automation scripts running on the Lager Box
* Always call `disconnect()` when finished to release the debug probe
* Use `erase()` to perform a full chip erase and clear protection settings
* RTT requires an active debug connection (see RTT Streaming section above)
# Electronic Load
Source: https://docs.lagerdata.com/source/reference/python/eload
Control electronic load nets
Control electronic loads to sink current in various modes for testing power systems.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
The Net-based API provides methods that can either get or set values. When called without a value parameter, methods read and return the current value. When called with a value, they set it.
| Method | Description |
| -------------------- | ---------------------------------------- |
| `mode(mode_type)` | Set or read operation mode (CC/CV/CR/CW) |
| `current(value)` | Set or read constant current (A) |
| `voltage(value)` | Set or read constant voltage (V) |
| `resistance(value)` | Set or read constant resistance (ohms) |
| `power(value)` | Set or read constant power (W) |
| `enable()` | Enable electronic load input |
| `disable()` | Disable electronic load input |
| `print_state()` | Print comprehensive state |
| `measured_voltage()` | Read measured voltage (returns float) |
| `measured_current()` | Read measured current (returns float) |
| `measured_power()` | Read measured power (returns float) |
## Method Reference
### `Net.get(name, type=NetType.ELoad)`
Get an electronic load net by name.
```python theme={null}
from lager import Net, NetType
eload = Net.get('LOAD', type=NetType.ELoad)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------------- |
| `name` | `str` | Name of the electronic load net |
| `type` | `NetType` | Must be `NetType.ELoad` |
**Returns:** Electronic load Net instance
### `mode(mode_type=None)`
Set or read the operation mode.
```python theme={null}
# Set mode
eload.mode('CC') # Constant Current
eload.mode('CV') # Constant Voltage
eload.mode('CR') # Constant Resistance
eload.mode('CW') # Constant Power (also 'CP')
# Read current mode
current_mode = eload.mode()
print(f"Mode: {current_mode}")
```
**Parameters:**
| Parameter | Type | Description |
| ----------- | --------------- | ------------------------------------------------------------ |
| `mode_type` | `str` or `None` | 'CC', 'CV', 'CR', or 'CW'/'CP'. If None, reads current mode. |
**Returns:** Current mode string if `mode_type` is None
### `current(value=None)`
Set or read the constant current setting.
```python theme={null}
# Set CC mode current
eload.mode('CC')
eload.current(0.5) # Set to 500mA
# Read current setting
i = eload.current()
print(f"Current setting: {i}A")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----------------- | ------------------------------------------------ |
| `value` | `float` or `None` | Current in amps. If None, reads current setting. |
**Returns:** Current setting in amps if `value` is None
### `voltage(value=None)`
Set or read the constant voltage setting.
```python theme={null}
# Set CV mode voltage
eload.mode('CV')
eload.voltage(5.0) # Set to 5V
# Read voltage setting
v = eload.voltage()
print(f"Voltage setting: {v}V")
```
### `resistance(value=None)`
Set or read the constant resistance setting. Values below 0.02 ohms are at the minimum range limit of the instrument and may be clamped. A warning is printed if the applied value differs from the requested value.
```python theme={null}
# Set CR mode resistance
eload.mode('CR')
eload.resistance(100.0) # Set to 100 ohms
# Read resistance setting
r = eload.resistance()
print(f"Resistance setting: {r} ohms")
```
### `power(value=None)`
Set or read the constant power setting.
```python theme={null}
# Set CW mode power
eload.mode('CW')
eload.power(5.0) # Set to 5W
# Read power setting
p = eload.power()
print(f"Power setting: {p}W")
```
### `enable()` / `disable()`
Enable or disable the electronic load input.
```python theme={null}
eload.enable() # Start sinking current
eload.disable() # Stop sinking current
```
### `print_state()`
Print comprehensive electronic load state.
```python theme={null}
eload.print_state()
# Prints: mode, current/voltage/resistance/power settings, measured values, input state
```
### `measured_voltage()` / `measured_current()` / `measured_power()`
Read actual measured values (return floats, for use in code).
```python theme={null}
v = eload.measured_voltage() # Returns measured voltage
i = eload.measured_current() # Returns measured current
p = eload.measured_power() # Returns measured power
print(f"V={v:.2f}V, I={i:.3f}A, P={p:.3f}W")
```
**Returns:** `float` - Measured value
## Load Modes
| Mode | Code | Description |
| ------------------- | ------------ | ------------------------------------------- |
| Constant Current | `CC` | Sinks a fixed current regardless of voltage |
| Constant Voltage | `CV` | Maintains a fixed voltage across the load |
| Constant Resistance | `CR` | Behaves as a fixed resistance |
| Constant Power | `CW` or `CP` | Dissipates a fixed power |
## Examples
### Constant Current Test
```python theme={null}
from lager import Net, NetType
eload = Net.get('LOAD', type=NetType.ELoad)
# Configure constant current mode at 500mA
eload.mode('CC')
eload.current(0.5)
eload.enable()
# Read measurements
print(f"Voltage: {eload.measured_voltage():.2f}V")
print(f"Current: {eload.measured_current():.3f}A")
print(f"Power: {eload.measured_power():.3f}W")
eload.disable()
```
### Battery Discharge Test
```python theme={null}
from lager import Net, NetType
import time
eload = Net.get('LOAD', type=NetType.ELoad)
# Configure constant resistance load
eload.mode('CR')
eload.resistance(10.0)
eload.enable()
# Monitor discharge
for i in range(30):
v = eload.measured_voltage()
i_curr = eload.measured_current()
print(f"V={v:.2f}V, I={i_curr:.3f}A")
if v < 3.0:
print("Discharge cutoff reached")
break
time.sleep(1)
eload.disable()
```
### Power Efficiency Test
```python theme={null}
from lager import Net, NetType
# Input power supply
psu = Net.get('INPUT', type=NetType.PowerSupply)
psu.voltage(12.0)
psu.current(2.0)
psu.enable()
# Output load
eload = Net.get('OUTPUT', type=NetType.ELoad)
eload.mode('CC')
eload.current(0.5)
eload.enable()
# Measure efficiency
p_in = 12.0 * psu.measured_current() # Input power
p_out = eload.measured_power() # Output power
efficiency = (p_out / p_in) * 100 if p_in > 0 else 0
print(f"Input: {p_in:.2f}W")
print(f"Output: {p_out:.2f}W")
print(f"Efficiency: {efficiency:.1f}%")
eload.disable()
psu.disable()
```
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | ---------------------- | ----------------- |
| Rigol | DL3021 (DL3000 series) | CC/CV/CR/CP modes |
## Notes
* Use `mode()` to set the operation mode before setting the corresponding value
* Methods like `current()`, `voltage()`, `resistance()`, `power()` can get or set values
* `measured_voltage()`, `measured_current()`, `measured_power()` return actual measurements
* `print_state()` prints state (for debugging), measurement methods return values (for code)
* Always call `enable()` to start sinking current
* Always call `disable()` when finished
* Resistance values below 0.02 ohms may be clamped by the instrument
# Energy Analyzer
Source: https://docs.lagerdata.com/source/reference/python/energy
Integrate energy and charge, and compute power statistics using an energy-analyzer net
Integrate energy and charge over time, or compute current/voltage/power statistics, from an energy-analyzer net. Requires the `energy-analyzer` net type.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ----------------------- | -------------------------------------------------------- |
| `read_energy(duration)` | Integrate energy and charge over `duration` seconds |
| `read_stats(duration)` | Compute mean/min/max/std for current, voltage, and power |
## Method Reference
### `Net.get(name, type=NetType.EnergyAnalyzer)`
Get an energy analyzer net by name.
```python theme={null}
from lager import Net, NetType
power = Net.get('POWER_METER', type=NetType.EnergyAnalyzer)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | -------------------------------- |
| `name` | `str` | Name of the energy-analyzer net |
| `type` | `NetType` | Must be `NetType.EnergyAnalyzer` |
**Returns:** Energy analyzer Net instance
### `read_energy(duration)`
Integrate current and power over `duration` seconds.
```python theme={null}
result = power.read_energy(10.0)
print(f"Energy: {result['energy_wh'] * 1000:.3f} mWh")
print(f"Charge: {result['charge_ah'] * 1000:.3f} mAh")
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ------- | ----------------------------- |
| `duration` | `float` | Integration window in seconds |
**Returns:** `dict` with keys:
| Key | Type | Description |
| -------------- | ------- | --------------------------- |
| `"energy_j"` | `float` | Energy in joules |
| `"energy_wh"` | `float` | Energy in watt-hours |
| `"charge_c"` | `float` | Charge in coulombs |
| `"charge_ah"` | `float` | Charge in amp-hours |
| `"duration_s"` | `float` | Duration that was requested |
### `read_stats(duration)`
Compute mean, minimum, maximum, and standard deviation for current, voltage, and power over `duration` seconds.
```python theme={null}
stats = power.read_stats(1.0)
print(f"Current: {stats['current']['mean'] * 1e6:.1f} uA (mean)")
print(f"Voltage: {stats['voltage']['mean']:.3f} V (mean)")
print(f"Power: {stats['power']['mean'] * 1000:.3f} mW (mean)")
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ------- | ----------------------------- |
| `duration` | `float` | Measurement window in seconds |
**Returns:** `dict` with keys:
| Key | Type | Description |
| -------------- | ------- | --------------------------- |
| `"current"` | `dict` | Current statistics in amps |
| `"voltage"` | `dict` | Voltage statistics in volts |
| `"power"` | `dict` | Power statistics in watts |
| `"duration_s"` | `float` | Duration that was requested |
Each statistics sub-dict contains:
| Key | Type | Description |
| -------- | ------- | ------------------ |
| `"mean"` | `float` | Mean value |
| `"min"` | `float` | Minimum value |
| `"max"` | `float` | Maximum value |
| `"std"` | `float` | Standard deviation |
## Examples
### Energy Budget Check
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer)
result = power.read_energy(10.0)
energy_mwh = result['energy_wh'] * 1000
charge_mah = result['charge_ah'] * 1000
print(f"Energy: {energy_mwh:.3f} mWh")
print(f"Charge: {charge_mah:.3f} mAh")
if energy_mwh > 50:
print("FAIL: Energy consumption exceeds budget")
else:
print("PASS: Energy within budget")
```
### Sleep Current Verification
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer)
# Measure over 5 seconds for accuracy
stats = power.read_stats(5.0)
sleep_ua = stats['current']['mean'] * 1e6
supply_v = stats['voltage']['mean']
print(f"Sleep current: {sleep_ua:.1f} uA")
print(f"Supply voltage: {supply_v:.3f} V")
if sleep_ua < 100:
print("PASS: Sleep current below 100 uA")
else:
print(f"FAIL: Sleep current {sleep_ua:.1f} uA exceeds 100 uA limit")
```
### Battery Life Estimation
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer)
# Measure average current draw over a representative workload
stats = power.read_stats(30.0)
avg_current_ma = stats['current']['mean'] * 1000
avg_voltage_v = stats['voltage']['mean']
# Estimate from a 1000 mAh battery
battery_mah = 1000
hours = battery_mah / avg_current_ma if avg_current_ma > 0 else float('inf')
print(f"Average current: {avg_current_ma:.3f} mA")
print(f"Average voltage: {avg_voltage_v:.3f} V")
print(f"Estimated battery life: {hours:.1f} hours")
```
### Startup Energy Capture
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer)
# Capture energy consumed during device startup (2 seconds)
result = power.read_energy(2.0)
print(f"Startup energy: {result['energy_j'] * 1000:.2f} mJ")
print(f"Startup charge: {result['charge_c'] * 1000:.2f} mC")
```
### Current Spike Analysis
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer)
# Short window to capture peak current
stats = power.read_stats(1.0)
peak_ma = stats['current']['max'] * 1000
avg_ma = stats['current']['mean'] * 1000
std_ma = stats['current']['std'] * 1000
print(f"Peak current: {peak_ma:.3f} mA")
print(f"Average current: {avg_ma:.3f} mA")
print(f"Std deviation: {std_ma:.3f} mA")
# Flag if peak is more than 10x average (unexpected spike)
if peak_ma > avg_ma * 10:
print("WARNING: Unexpected current spike detected")
```
## Supported Hardware
| Manufacturer | Model | Net Type | USB VID:PID |
| -------------------- | ----- | ----------------- | ----------- |
| Joulescope | JS220 | `energy-analyzer` | `16d0:10ba` |
| Nordic Semiconductor | PPK2 | `energy-analyzer` | `1915:c00a` |
For instantaneous power readings (also available on Yocto-Watt), see [Watt Meter](/source/reference/python/watt).
## Notes
* The Joulescope JS220 samples at high frequency; longer durations yield more accurate statistics
* The Nordic PPK2 operates in source mode (supplies a configurable voltage 0.8–5V and measures current); voltage readings reflect the configured value
* `read_energy()` and `read_stats()` block for the full `duration` before returning
* The same physical JS220 device is shared between `WattMeter` and `EnergyAnalyzer` net roles — opening both net types for the same device is safe
* Use `lager instruments --box ` to verify the JS220 is detected before running scripts
# GPIO
Source: https://docs.lagerdata.com/source/reference/python/gpio
Control digital input/output pins
Control digital input/output pins for digital signaling and control.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ---------------------------- | ------------------------------------ |
| `input()` | Read digital pin state |
| `output(level)` | Set digital pin state |
| `wait_for_level(level, ...)` | Wait for pin to reach a target level |
## Method Reference
### `Net.get(name, type=NetType.GPIO)`
Get a GPIO net by name.
```python theme={null}
from lager import Net, NetType
pin = Net.get('LED', type=NetType.GPIO)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ---------------------- |
| `name` | `str` | Name of the GPIO net |
| `type` | `NetType` | Must be `NetType.GPIO` |
**Returns:** GPIO Net instance
### `input()`
Read the digital state of the pin.
```python theme={null}
from lager import Net, NetType
pin = Net.get('BUTTON', type=NetType.GPIO)
state = pin.input()
if state:
print("Pin is HIGH")
else:
print("Pin is LOW")
```
**Returns:** `int` - 0 for LOW, 1 for HIGH
### `output(level)`
Set the digital state of the pin.
```python theme={null}
from lager import Net, NetType
pin = Net.get('LED', type=NetType.GPIO)
# Set HIGH
pin.output(1)
# Set LOW
pin.output(0)
```
| Parameter | Type | Description |
| --------- | -------------- | --------------------------------------------- |
| `level` | `int` or `str` | 0/1, "low"/"high", "off"/"on", "true"/"false" |
### `wait_for_level(level, timeout=None, ...)`
Wait for the pin to reach a target level. Blocks until the pin reads the specified level or the timeout expires.
The LabJack T7 uses hardware streaming at up to 20 kHz for fast edge detection. Other hardware uses software polling.
```python theme={null}
from lager import Net, NetType
pin = Net.get('INTERRUPT', type=NetType.GPIO)
# Wait for pin to go HIGH (block forever)
elapsed = pin.wait_for_level(1)
print(f"Pin went HIGH after {elapsed:.3f}s")
# Wait with timeout
try:
elapsed = pin.wait_for_level(0, timeout=5.0)
print(f"Pin went LOW after {elapsed:.3f}s")
except TimeoutError:
print("Timed out waiting for pin")
```
**Parameters:**
| Parameter | Type | Default | Description |
| ---------------- | ----------------- | -------- | -------------------------------------------------------------- |
| `level` | `int` or `str` | required | Target level: 0/1, "low"/"high", "off"/"on" |
| `timeout` | `float` or `None` | `None` | Maximum seconds to wait. `None` = wait forever. |
| `scan_rate` | `int` | `20000` | Sample rate in Hz (LabJack T7 only) |
| `scans_per_read` | `int` | `2` | Batch size per read (LabJack T7 only; lower = faster reaction) |
| `poll_interval` | `float` | `0.01` | Seconds between polls (non-LabJack hardware only) |
**Returns:** `float` - Elapsed time in seconds until the level was detected
**Raises:** `TimeoutError` if the timeout expires before the target level is reached
**Hardware-specific behavior:**
| Hardware | Method | Default Rate |
| ---------- | --------------------------------------- | ----------------------- |
| LabJack T7 | Hardware streaming (`ljm.eStreamStart`) | 20,000 Hz |
| USB-202 | Software polling | 100 Hz (10 ms interval) |
## Examples
### Read Button State
```python theme={null}
from lager import Net, NetType
button = Net.get('BUTTON', type=NetType.GPIO)
state = button.input()
if state:
print("Button pressed")
else:
print("Button released")
```
### Control LED
```python theme={null}
from lager import Net, NetType
led = Net.get('LED', type=NetType.GPIO)
# Turn on
led.output(1)
# Turn off
led.output(0)
```
### Wait for Interrupt
```python theme={null}
from lager import Net, NetType
irq_pin = Net.get('INTERRUPT', type=NetType.GPIO)
# Wait for interrupt (rising edge)
try:
elapsed = irq_pin.wait_for_level(1, timeout=10.0)
print(f"Interrupt detected after {elapsed:.3f}s")
except TimeoutError:
print("No interrupt within 10 seconds")
```
### Button-Controlled LED
```python theme={null}
from lager import Net, NetType
import time
button = Net.get('BUTTON', type=NetType.GPIO)
led = Net.get('LED', type=NetType.GPIO)
print("Press Ctrl+C to exit")
while True:
try:
if button.input():
led.output(1)
else:
led.output(0)
time.sleep(0.1)
except KeyboardInterrupt:
led.output(0)
break
```
### Toggle Output
```python theme={null}
from lager import Net, NetType
import time
output_pin = Net.get('SIGNAL', type=NetType.GPIO)
# Generate 10 pulses
for i in range(10):
output_pin.output(1)
time.sleep(0.5)
output_pin.output(0)
time.sleep(0.5)
```
## Supported Hardware
| Hardware | Pins | Logic Level |
| ----------- | ------------------------------- | ----------- |
| LabJack T7 | FIO0-FIO7, EIO0-EIO7, CIO0-CIO3 | 3.3V |
| MCC USB-202 | DIO0-DIO7 | 5V TTL |
### Pin Naming
**LabJack T7:**
| Pin Input | Channel |
| ----------------- | --------- |
| `0`-`7` | FIO0-FIO7 |
| `8`-`15` | EIO0-EIO7 |
| `16`-`19` | CIO0-CIO3 |
| `"FIO0"`-`"FIO7"` | FIO0-FIO7 |
**MCC USB-202:**
| Pin Input | Channel |
| ----------------- | --------- |
| `0`-`7` | DIO0-DIO7 |
| `"DIO0"`-`"DIO7"` | DIO0-DIO7 |
## Notes
* GPIO nets work directly without `enable()`/`disable()` calls
* `input()` returns 0 or 1
* `output()` accepts integers (0/1) or strings ("high"/"low", "on"/"off", "true"/"false")
* `wait_for_level()` blocks the calling thread until the target level is detected
* LabJack T7 `wait_for_level()` uses hardware streaming for sub-millisecond detection
* Net names must match those configured on the Lager Box
# I2C
Source: https://docs.lagerdata.com/source/reference/python/i2c
Communicate with I2C devices on the bus
Read, write, and scan I2C (Inter-Integrated Circuit) devices connected to a Lager Box.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| -------------- | -------------------------------------------------------- |
| `config()` | Configure I2C bus parameters |
| `scan()` | Scan bus for connected devices |
| `read()` | Read bytes from a device |
| `write()` | Write bytes to a device |
| `write_read()` | Write then read in a single transaction (repeated start) |
| `get_config()` | Get raw net configuration |
## Method Reference
### `Net.get(name, type=NetType.I2C)`
Get an I2C net by name.
```python theme={null}
from lager import Net, NetType
i2c = Net.get('MY_I2C_NET', type=NetType.I2C)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------- |
| `name` | `str` | Name of the I2C net |
| `type` | `NetType` | Must be `NetType.I2C` |
**Returns:** I2C Net instance
### `config(frequency_hz, pull_ups)`
Configure I2C bus parameters. Only explicitly-provided parameters are changed; omitted parameters retain their stored values.
```python theme={null}
i2c.config(frequency_hz=400_000)
i2c.config(frequency_hz=100_000, pull_ups=True)
```
| Parameter | Type | Description |
| -------------- | ---------------- | --------------------------------------------------------------------------- |
| `frequency_hz` | `int` or `None` | Clock frequency in Hz (e.g., 100000, 400000). `None` keeps stored value |
| `pull_ups` | `bool` or `None` | Enable/disable internal pull-ups (Aardvark only). `None` keeps stored value |
### `scan(start_addr, end_addr)`
Scan the I2C bus for connected devices.
```python theme={null}
devices = i2c.scan()
print(f"Found: {[hex(a) for a in devices]}")
```
| Parameter | Type | Description |
| ------------ | ----- | --------------------------------------------- |
| `start_addr` | `int` | First 7-bit address to probe (default `0x08`) |
| `end_addr` | `int` | Last 7-bit address to probe (default `0x77`) |
**Returns:** `list[int]` - List of 7-bit addresses that responded with ACK
### `read(address, num_bytes, output_format, overrides)`
Read bytes from an I2C device.
```python theme={null}
data = i2c.read(address=0x48, num_bytes=2)
temp = (data[0] << 8) | data[1]
```
| Parameter | Type | Description |
| --------------- | ---------------- | ------------------------------------------------------------ |
| `address` | `int` | 7-bit device address (`0x00`-`0x7F`) |
| `num_bytes` | `int` | Number of bytes to read |
| `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` |
| `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) |
**Returns:** `list[int]` - Received bytes as integers (when `output_format="list"`)
### `write(address, data, overrides)`
Write bytes to an I2C device.
```python theme={null}
i2c.write(address=0x48, data=[0x0A, 0x03])
```
| Parameter | Type | Description |
| ----------- | ---------------- | ------------------------------------------------------------ |
| `address` | `int` | 7-bit device address (`0x00`-`0x7F`) |
| `data` | `list[int]` | Bytes to write |
| `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) |
### `write_read(address, data, num_bytes, output_format, overrides)`
Write then read in a single I2C transaction using a repeated start condition. This is the standard pattern for reading device registers.
```python theme={null}
# Read 2-byte temperature register at address 0x00
temp_bytes = i2c.write_read(address=0x48, data=[0x00], num_bytes=2)
temperature = (temp_bytes[0] << 8) | temp_bytes[1]
```
| Parameter | Type | Description |
| --------------- | ---------------- | ------------------------------------------------------------ |
| `address` | `int` | 7-bit device address (`0x00`-`0x7F`) |
| `data` | `list[int]` | Bytes to write before reading (typically a register address) |
| `num_bytes` | `int` | Number of bytes to read after writing |
| `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` |
| `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) |
**Returns:** `list[int]` - Received bytes as integers (when `output_format="list"`)
### `get_config()`
Get the raw net configuration dictionary.
```python theme={null}
cfg = i2c.get_config()
print(cfg['name'])
print(cfg['params'])
```
**Returns:** `dict` - Full net configuration including name, role, instrument, and params
## Output Formats
The `output_format` parameter on `read()` and `write_read()` controls how data is returned:
| Format | Return Type | Example |
| --------- | ----------- | -------------------------- |
| `"list"` | `list[int]` | `[72, 118, 153]` |
| `"hex"` | `str` | `"48 76 99"` |
| `"bytes"` | `str` | `"72 118 153"` |
| `"json"` | `dict` | `{"data": [72, 118, 153]}` |
## Examples
### Basic Device Read
```python theme={null}
from lager import Net, NetType
i2c = Net.get('my_i2c', type=NetType.I2C)
# Scan for devices
devices = i2c.scan()
print(f"Found devices at: {[hex(a) for a in devices]}")
# Read 2 bytes from device at 0x48
data = i2c.read(address=0x48, num_bytes=2)
print(f"Data: {data}")
```
### Register Read/Write
```python theme={null}
from lager import Net, NetType
i2c = Net.get('my_i2c', type=NetType.I2C)
# Write configuration register
i2c.write(address=0x48, data=[0x01, 0x60, 0xA0])
# Read temperature register (write register addr, then read 2 bytes)
temp_bytes = i2c.write_read(address=0x48, data=[0x00], num_bytes=2)
raw = (temp_bytes[0] << 8) | temp_bytes[1]
celsius = raw / 256.0
print(f"Temperature: {celsius:.1f} C")
```
### Bus Configuration
```python theme={null}
from lager import Net, NetType
i2c = Net.get('my_i2c', type=NetType.I2C)
# Configure for 400 kHz Fast Mode with pull-ups
i2c.config(frequency_hz=400_000, pull_ups=True)
# Scan a specific address range
devices = i2c.scan(start_addr=0x20, end_addr=0x7F)
for addr in devices:
print(f" 0x{addr:02x}")
```
### Multi-Device Setup
```python theme={null}
from lager import Net, NetType
i2c = Net.get('sensor_bus', type=NetType.I2C)
i2c.config(frequency_hz=100_000)
# Read from multiple sensors on the same bus
TEMP_SENSOR = 0x48
PRESSURE_SENSOR = 0x76
# Temperature (TMP102)
temp_raw = i2c.write_read(address=TEMP_SENSOR, data=[0x00], num_bytes=2)
temp_c = ((temp_raw[0] << 4) | (temp_raw[1] >> 4)) * 0.0625
print(f"Temperature: {temp_c:.1f} C")
# Pressure (BMP280) - read chip ID register
chip_id = i2c.write_read(address=PRESSURE_SENSOR, data=[0xD0], num_bytes=1)
print(f"BMP280 chip ID: 0x{chip_id[0]:02x}")
```
### Per-Call Configuration Override
```python theme={null}
from lager import Net, NetType
i2c = Net.get('my_i2c', type=NetType.I2C)
i2c.config(frequency_hz=100_000)
# Most devices use standard mode
data = i2c.read(address=0x48, num_bytes=2)
# One device needs fast mode for this transaction
data = i2c.read(address=0x50, num_bytes=256,
overrides={"frequency_hz": 400_000})
```
## Supported Hardware
| Adapter | Description |
| ---------------- | ---------------------------------------------- |
| LabJack T7 | Uses GPIO pins (FIO/EIO) for SDA and SCL |
| Aardvark I2C/SPI | Dedicated USB I2C adapter with pull-up support |
## Notes
* Net must be configured as `NetType.I2C`
* Addresses are 7-bit format (`0x00`-`0x7F`), not left-shifted
* `pull_ups` only works on the Aardvark adapter; ignored on LabJack T7
* `write_read()` uses a repeated start condition for atomic register reads
* Default scan range (`0x08`-`0x77`) skips reserved addresses
* Configuration changes persist to `saved_nets.json` for subsequent commands
* LabJack T7 runs at approximately 450 kHz regardless of requested frequency due to hardware limitations
# Logic Analyzer (Preview)
Source: https://docs.lagerdata.com/source/reference/python/logic
Digital signal capture and protocol decoding
Capture and analyze digital signals using the logic analyzer functionality of mixed-signal oscilloscopes.
**Not Yet Available:** The Logic Analyzer Python API for Rigol MSO5000 series is currently under development.
The Net-based API and associated methods are documented for preview purposes only. The underlying device
implementation is not yet complete. Attempting to use logic analyzer nets will result in a "method not found"
error. Check back in a future release for full functionality.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ------------------------------------------------- | --------------------------------- |
| `enable()` | Enable the logic channel display |
| `disable()` | Disable the logic channel display |
| `start_capture()` | Start continuous acquisition |
| `stop_capture()` | Stop acquisition |
| `start_single_capture()` | Start single-shot capture |
| `force_trigger()` | Force a trigger event |
| `set_signal_threshold()` | Set logic level threshold voltage |
| `display_position()` | Set channel display position |
| `size_large()` / `size_medium()` / `size_small()` | Set display size |
## Method Reference
### `Net.get(name, type=NetType.Logic)`
Get a logic analyzer net by name.
```python theme={null}
from lager import Net, NetType
logic = Net.get('SPI_CLK', type=NetType.Logic)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ----------------------- |
| `name` | `str` | Name of the logic net |
| `type` | `NetType` | Must be `NetType.Logic` |
**Returns:** Logic analyzer Net instance
### `enable()`
Enable the logic channel display.
```python theme={null}
logic.enable()
```
### `disable()`
Disable the logic channel display.
```python theme={null}
logic.disable()
```
### `start_capture()`
Start continuous waveform acquisition.
```python theme={null}
logic.start_capture()
```
### `stop_capture()`
Stop waveform acquisition.
```python theme={null}
logic.stop_capture()
```
### `start_single_capture()`
Start single-shot capture (captures one triggered event).
```python theme={null}
logic.start_single_capture()
```
### `force_trigger()`
Force a trigger event immediately.
```python theme={null}
logic.force_trigger()
```
### `set_signal_threshold(voltage)`
Set the logic level threshold voltage.
```python theme={null}
logic.set_signal_threshold(1.65) # 1.65V for 3.3V CMOS
logic.set_signal_threshold(2.5) # 2.5V for 5V TTL
```
| Parameter | Type | Description |
| --------- | ------- | -------------------------- |
| `voltage` | `float` | Threshold voltage in volts |
**Note:** Channels 0-7 share one threshold, channels 8-15 share another.
### `display_position(position)`
Set the channel display position.
```python theme={null}
logic.display_position(100) # Set vertical position
```
| Parameter | Type | Description |
| ---------- | ----- | ----------------------- |
| `position` | `int` | Vertical position value |
### `size_large()` / `size_medium()` / `size_small()`
Set the display size for enabled channels.
```python theme={null}
logic.size_large() # Maximum visibility
logic.size_medium() # Balanced
logic.size_small() # Compact view
```
## Trigger Settings
Logic analyzer nets support advanced triggering through `trigger_settings`:
### Edge Trigger
```python theme={null}
logic = Net.get('SPI_CLK', type=NetType.Logic)
# Set edge trigger on this channel
logic.trigger_settings.edge.set_source(logic)
logic.trigger_settings.edge.set_slope_rising()
logic.trigger_settings.set_mode_normal()
```
**Edge trigger methods:**
| Method | Description |
| -------------------------- | ------------------------- |
| `edge.set_source(net)` | Set trigger source net |
| `edge.set_slope_rising()` | Trigger on rising edge |
| `edge.set_slope_falling()` | Trigger on falling edge |
| `edge.set_slope_both()` | Trigger on either edge |
| `edge.get_slope()` | Get current slope setting |
### Pulse Trigger
```python theme={null}
logic = Net.get('PULSE_SIG', type=NetType.Logic)
# Trigger on pulse width > 1ms
logic.trigger_settings.pulse.set_source(logic)
logic.trigger_settings.pulse.set_trigger_on_pulse_greater_than_width(0.001)
# Trigger on pulse width < 100us
logic.trigger_settings.pulse.set_trigger_on_pulse_less_than_width(0.0001)
```
### Protocol Triggers
#### UART Trigger
```python theme={null}
logic = Net.get('UART_TX', type=NetType.Logic)
logic.trigger_settings.uart.set_source(logic)
logic.trigger_settings.uart.set_uart_params(baud=115200, bits=8, parity=None, stopbits=1)
# Trigger on start bit
logic.trigger_settings.uart.set_trigger_on_start()
# Trigger on specific data
logic.trigger_settings.uart.set_trigger_on_data(data=0x55)
# Trigger on frame error
logic.trigger_settings.uart.set_trigger_on_frame_error()
```
#### I2C Trigger
```python theme={null}
scl = Net.get('I2C_SCL', type=NetType.Logic)
sda = Net.get('I2C_SDA', type=NetType.Logic)
scl.trigger_settings.i2c.set_source(net_scl=scl, net_sda=sda)
# Trigger on start condition
scl.trigger_settings.i2c.set_trigger_on_start()
# Trigger on specific address
scl.trigger_settings.i2c.set_trigger_on_address(bits=7, address=0x48)
# Trigger on NACK
scl.trigger_settings.i2c.set_trigger_on_nack()
```
#### SPI Trigger
```python theme={null}
clk = Net.get('SPI_CLK', type=NetType.Logic)
mosi = Net.get('SPI_MOSI', type=NetType.Logic)
cs = Net.get('SPI_CS', type=NetType.Logic)
clk.trigger_settings.spi.set_source(net_sck=clk, net_mosi_miso=mosi, net_cs=cs)
clk.trigger_settings.spi.set_clk_edge_positive()
# Trigger on specific data
clk.trigger_settings.spi.set_trigger_data(bits=8, data=0xAA)
# Trigger on CS
clk.trigger_settings.spi.set_trigger_on_cs_low()
```
#### CAN Trigger
```python theme={null}
can = Net.get('CAN_RX', type=NetType.Logic)
can.trigger_settings.can.set_source(can)
can.trigger_settings.can.set_baud(500000)
# Trigger on start of frame
can.trigger_settings.can.set_trigger_on_sof()
# Trigger on error frame
can.trigger_settings.can.set_trigger_on_error_frame()
```
## Measurements
Logic analyzer nets support digital timing measurements:
```python theme={null}
logic = Net.get('CLK', type=NetType.Logic)
# Frequency and period
freq = logic.measurement.frequency()
period = logic.measurement.period()
# Pulse measurements
pos_width = logic.measurement.pulse_width_positive()
neg_width = logic.measurement.pulse_width_negative()
pos_duty = logic.measurement.duty_cycle_positive()
neg_duty = logic.measurement.duty_cycle_negative()
# Rise/fall times
rise = logic.measurement.rise_time()
fall = logic.measurement.fall_time()
# Edge counts
pos_edges = logic.measurement.positive_edge_count()
neg_edges = logic.measurement.negative_edge_count()
```
## Bus Decoding
For protocol analysis, create bus decoders using multiple logic channels:
### UART Bus
```python theme={null}
from lager.nets.mappers.rigol_mso5000 import BusUART_RigolMSO5000FunctionMapper
tx = Net.get('UART_TX', type=NetType.Logic)
rx = Net.get('UART_RX', type=NetType.Logic)
bus = BusUART_RigolMSO5000FunctionMapper(tx=tx, rx=rx)
bus.set_baud(115200)
bus.set_data_bits(8)
bus.set_parity_none()
bus.set_stop_bits(1)
bus.enable()
bus.show_table()
```
### I2C Bus
```python theme={null}
from lager.nets.mappers.rigol_mso5000 import BusI2C_RigolMSO5000FunctionMapper
scl = Net.get('I2C_SCL', type=NetType.Logic)
sda = Net.get('I2C_SDA', type=NetType.Logic)
bus = BusI2C_RigolMSO5000FunctionMapper(scl=scl, sda=sda)
bus.set_signal_threshold(sda=1.5, scl=1.5)
bus.enable()
bus.show_table()
```
### SPI Bus
```python theme={null}
from lager.nets.mappers.rigol_mso5000 import BusSPI_RigolMSO5000FunctionMapper
clk = Net.get('SPI_CLK', type=NetType.Logic)
mosi = Net.get('SPI_MOSI', type=NetType.Logic)
miso = Net.get('SPI_MISO', type=NetType.Logic)
cs = Net.get('SPI_CS', type=NetType.Logic)
bus = BusSPI_RigolMSO5000FunctionMapper(clk=clk, mosi=mosi, miso=miso, cs=cs)
bus.set_sck_phase_rising_edge()
bus.set_data_width(8)
bus.set_endianness_msb()
bus.enable()
bus.show_table()
```
### CAN Bus
```python theme={null}
from lager.nets.mappers.rigol_mso5000 import BusCAN_RigolMSO5000FunctionMapper
can_net = Net.get('CAN_RX', type=NetType.Logic)
bus = BusCAN_RigolMSO5000FunctionMapper(can=can_net)
bus.set_baud(500000)
bus.set_signal_type_rx()
bus.set_signal_threshold(2.0)
bus.enable()
bus.show_table()
```
## Examples
### Basic Digital Signal Capture
```python theme={null}
from lager import Net, NetType
import time
# Get logic channel
clk = Net.get('SYS_CLK', type=NetType.Logic)
# Configure
clk.enable()
clk.set_signal_threshold(1.65) # 3.3V logic
clk.size_medium()
# Set trigger
clk.trigger_settings.edge.set_source(clk)
clk.trigger_settings.edge.set_slope_rising()
clk.trigger_settings.set_mode_normal()
# Capture
clk.start_capture()
time.sleep(1)
# Measure
freq = clk.measurement.frequency()
print(f"Clock frequency: {freq / 1e6:.3f} MHz")
clk.stop_capture()
clk.disable()
```
### SPI Communication Test
```python theme={null}
from lager import Net, NetType
from lager.nets.mappers.rigol_mso5000 import BusSPI_RigolMSO5000FunctionMapper
import time
# Get SPI signals
clk = Net.get('SPI_CLK', type=NetType.Logic)
mosi = Net.get('SPI_MOSI', type=NetType.Logic)
miso = Net.get('SPI_MISO', type=NetType.Logic)
cs = Net.get('SPI_CS', type=NetType.Logic)
# Enable channels
for net in [clk, mosi, miso, cs]:
net.enable()
net.set_signal_threshold(1.65)
# Create bus decoder
bus = BusSPI_RigolMSO5000FunctionMapper(clk=clk, mosi=mosi, miso=miso, cs=cs)
bus.set_sck_phase_rising_edge()
bus.set_data_width(8)
bus.enable()
bus.show_table()
# Trigger on CS going low
clk.trigger_settings.spi.set_trigger_on_cs_low()
clk.trigger_settings.set_mode_single()
# Start capture
clk.start_single_capture()
# Wait for trigger or timeout
time.sleep(5)
# View decoded data in table
print("SPI transaction captured - check scope display")
# Clean up
bus.disable()
for net in [clk, mosi, miso, cs]:
net.disable()
```
### I2C Address Scanner
```python theme={null}
from lager import Net, NetType
import time
scl = Net.get('I2C_SCL', type=NetType.Logic)
sda = Net.get('I2C_SDA', type=NetType.Logic)
scl.enable()
sda.enable()
scl.set_signal_threshold(1.65)
sda.set_signal_threshold(1.65)
# Configure I2C trigger
scl.trigger_settings.i2c.set_source(net_scl=scl, net_sda=sda)
scl.trigger_settings.i2c.set_scl_trigger_level(1.65)
scl.trigger_settings.i2c.set_sda_trigger_level(1.65)
# Trigger on start condition to capture all traffic
scl.trigger_settings.i2c.set_trigger_on_start()
scl.trigger_settings.set_mode_normal()
scl.start_capture()
print("Monitoring I2C bus - trigger on start condition")
# Let it run and capture activity
time.sleep(10)
scl.stop_capture()
scl.disable()
sda.disable()
```
### Protocol Timing Verification
```python theme={null}
from lager import Net, NetType
# Test UART timing
uart_tx = Net.get('UART_TX', type=NetType.Logic)
uart_tx.enable()
uart_tx.set_signal_threshold(1.65)
uart_tx.start_capture()
# Measure bit timing
period = uart_tx.measurement.period()
if period:
measured_baud = 1.0 / period
expected_baud = 115200
error_pct = abs(measured_baud - expected_baud) / expected_baud * 100
print(f"Measured baud: {measured_baud:.0f}")
print(f"Expected baud: {expected_baud}")
print(f"Error: {error_pct:.2f}%")
if error_pct < 3:
print("PASS: Baud rate within tolerance")
else:
print("FAIL: Baud rate out of tolerance")
uart_tx.stop_capture()
uart_tx.disable()
```
## Digital Channels
| Channel | Range |
| ------- | ------------------------ |
| D0-D7 | Pod 1 (shared threshold) |
| D8-D15 | Pod 2 (shared threshold) |
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | -------------- | ------------------------------------ |
| Rigol | MSO5000 series | 16 digital channels, protocol decode |
## Notes
* Logic channels are numbered D0-D15
* Channels D0-D7 share one threshold voltage, D8-D15 share another
* Protocol decoding requires enabling bus analysis mode
* Use `NetType.Logic` for digital channels, `NetType.Analog` for analog
* Bus decoders work with both Logic and Analog nets as sources
* The trigger can use any combination of analog and digital channels
# Net
Source: https://docs.lagerdata.com/source/reference/python/net
Core Net class for managing hardware connections
The `Net` class is the primary abstraction for interacting with hardware instruments. It provides a unified interface for controlling different types of hardware including power supplies, oscilloscopes, GPIO, ADC, DAC, and more.
## Import
```python theme={null}
from lager import Net, NetType
# For exception handling
from lager import InvalidNetError, SetupFunctionRequiredError
```
## Class Methods
| Method | Description |
| ----------------------------- | --------------------------------------------------------- |
| `Net.get()` | Create a Net instance for the specified net name and type |
| `Net.list_saved()` | List all nets configured on the Lager Box |
| `Net.list_all_from_env()` | List nets from environment (legacy) |
| `Net.get_local_nets()` | Get all local net configurations |
| `Net.save_local_nets()` | Save multiple net configurations |
| `Net.save_local_net()` | Save a single net configuration |
| `Net.delete_local_net()` | Delete a net configuration |
| `Net.delete_all_local_nets()` | Delete all net configurations |
| `Net.rename_local_net()` | Rename a net configuration |
| `Net.filter_nets()` | Filter nets by name and/or role |
## Instance Methods
| Method | Description |
| ----------- | -------------------------------------------- |
| `enable()` | Enable the net and connect to hardware |
| `disable()` | Disable the net and disconnect from hardware |
## Method Reference
### `Net.get(name, type, *, setup_function=None, teardown_function=None)`
Create a Net instance for the specified net name and type.
```python theme={null}
# Get a power supply net
psu = Net.get('VDD', type=NetType.PowerSupply)
# Get a GPIO net
led = Net.get('LED', type=NetType.GPIO)
# Get an analog oscilloscope net
scope = Net.get('PROBE', type=NetType.Analog)
```
**Parameters:**
| Parameter | Type | Description |
| ------------------- | ---------- | --------------------------------------------------------- |
| `name` | `str` | Name of the net to create |
| `type` | `NetType` | Type of net (e.g., `NetType.PowerSupply`, `NetType.GPIO`) |
| `setup_function` | `callable` | Optional function called when net is enabled |
| `teardown_function` | `callable` | Optional function called when net is disabled |
**Returns:** Net instance appropriate for the specified type
### `Net.list_saved()`
List all nets configured on the Lager Box.
```python theme={null}
nets = Net.list_saved()
for net in nets:
print(f"{net['name']}: {net['role']}")
```
**Returns:** `list[dict]` - List of net configurations with keys:
* `name` (str) - Net name
* `role` (str) - Net type/role
* `channel` (int) - Hardware channel number
* `instrument` (str) - Associated instrument type
### `Net.list_all_from_env()`
List nets from the LAGER\_MUXES environment variable (legacy behavior).
```python theme={null}
nets = Net.list_all_from_env()
for net in nets:
print(f"{net['name']}: {net['role']} on channel {net['channel']}")
```
**Returns:** `list[dict]` - List of net information
### `Net.save_local_net(data)`
Save a net configuration to the Lager Box.
```python theme={null}
Net.save_local_net({
'name': 'VDD',
'role': 'power-supply',
'channel': 1,
'instrument': 'rigol_dp800',
'address': '192.168.1.100'
})
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ------ | ---------------------------- |
| `data` | `dict` | Net configuration dictionary |
### `Net.delete_local_net(name, role=None)`
Delete a net configuration from the Lager Box.
```python theme={null}
# Delete by name only
Net.delete_local_net('VDD')
# Delete by name and role
Net.delete_local_net('VDD', role='power-supply')
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | ---------------------- |
| `name` | `str` | Name of net to delete |
| `role` | `str` | Optional role to match |
**Returns:** `bool` - True if net was deleted
### `Net.rename_local_net(old_name, new_name)`
Rename a net configuration.
```python theme={null}
Net.rename_local_net('OLD_NAME', 'NEW_NAME')
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ----- | ---------------- |
| `old_name` | `str` | Current net name |
| `new_name` | `str` | New net name |
**Returns:** `bool` - True if net was renamed
### `Net.get_local_nets()`
Get all local net configurations.
```python theme={null}
nets = Net.get_local_nets()
for net in nets:
print(f"{net['name']}: {net['role']}")
```
**Returns:** `list[dict]` - List of net configuration dictionaries
### `Net.save_local_nets(nets)`
Save multiple net configurations at once.
```python theme={null}
Net.save_local_nets([
{'name': 'VDD', 'role': 'power-supply', 'channel': 1, 'instrument': 'rigol_dp800', 'address': '192.168.1.100'},
{'name': 'GND', 'role': 'power-supply', 'channel': 2, 'instrument': 'rigol_dp800', 'address': '192.168.1.100'}
])
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ------------ | -------------------------------------- |
| `nets` | `list[dict]` | List of net configuration dictionaries |
### `Net.delete_all_local_nets()`
Delete all net configurations from the Lager Box.
```python theme={null}
Net.delete_all_local_nets()
```
**Returns:** `bool` - True if nets were deleted
### `Net.filter_nets(all_nets, name, role=None)`
Filter a list of nets by name and optionally by role.
```python theme={null}
all_nets = Net.get_local_nets()
# Find all nets named 'VDD'
vdd_nets = Net.filter_nets(all_nets, 'VDD')
# Find 'VDD' with specific role
psu_nets = Net.filter_nets(all_nets, 'VDD', role='power-supply')
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ------------ | ---------------------- |
| `all_nets` | `list[dict]` | List of nets to search |
| `name` | `str` | Net name to match |
| `role` | `str` | Optional role to match |
**Returns:** `list[dict]` - Matching nets
### `enable()`
Enable the net and connect to hardware.
```python theme={null}
scope = Net.get('PROBE', type=NetType.Analog)
scope.enable() # Connect to oscilloscope
```
**Behavior by net type:**
* **Analog**: Connects to multiplexer and enables oscilloscope channel
* **Logic**: Enables logic analyzer channel
* **Battery**: Enables battery simulation output
* **PowerSupply**: Enables power supply output
* **ELoad**: Enables electronic load
### `disable(teardown=True)`
Disable the net and disconnect from hardware.
```python theme={null}
scope.disable() # Disconnect and run teardown
scope.disable(teardown=False) # Disconnect without teardown
```
**Parameters:**
| Parameter | Type | Default | Description |
| ---------- | ------ | ------- | --------------------------------- |
| `teardown` | `bool` | `True` | Whether to call teardown function |
## NetType Enum
Available net types:
| NetType | Role String | Description |
| ----------------------- | ----------------- | -------------------------------------------- |
| `NetType.Analog` | `analog` | Oscilloscope analog input |
| `NetType.Logic` | `logic` | Logic analyzer input |
| `NetType.Waveform` | `waveform` | Waveform generator |
| `NetType.Battery` | `battery` | Battery simulator |
| `NetType.PowerSupply` | `power-supply` | Power supply |
| `NetType.ELoad` | `eload` | Electronic load |
| `NetType.GPIO` | `gpio` | Digital I/O |
| `NetType.ADC` | `adc` | Analog-to-digital converter |
| `NetType.DAC` | `dac` | Digital-to-analog converter |
| `NetType.Thermocouple` | `thermocouple` | Temperature sensor |
| `NetType.WattMeter` | `watt-meter` | Power meter |
| `NetType.UART` | `uart` | Serial communication |
| `NetType.Debug` | `debug` | Debug probe |
| `NetType.Arm` | `arm` | Robotic arm |
| `NetType.Usb` | `usb` | USB device |
| `NetType.Rotation` | `rotation` | Rotary encoder |
| `NetType.Wifi` | `wifi` | WiFi module |
| `NetType.Actuate` | `actuate` | Actuator control |
| `NetType.PowerSupply2Q` | `power-supply-2q` | Two-quadrant power supply (solar simulation) |
## Properties
### `name`
Get the net name.
```python theme={null}
print(net.name) # 'VDD'
```
### `type`
Get the net type.
```python theme={null}
print(net.type) # NetType.PowerSupply
```
## Examples
### List and Use Nets
```python theme={null}
from lager import Net, NetType
# List all available nets
nets = Net.list_saved()
print("Available nets:")
for net in nets:
print(f" {net['name']}: {net['role']}")
# Get and use a specific net
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.enable()
```
### Simple Nets (GPIO, ADC, DAC)
```python theme={null}
from lager import Net, NetType
# GPIO - no enable/disable needed
button = Net.get('BUTTON', type=NetType.GPIO)
state = button.input()
led = Net.get('LED', type=NetType.GPIO)
led.output(1)
# ADC - no enable/disable needed
sensor = Net.get('SENSOR', type=NetType.ADC)
voltage = sensor.input()
# DAC - no enable/disable needed
vref = Net.get('VREF', type=NetType.DAC)
vref.output(2.5)
```
### Complex Nets (Power, Scope)
```python theme={null}
from lager import Net, NetType
# Power supply - requires enable/disable
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.set_current(0.5)
psu.enable()
# ... use the power supply ...
psu.disable()
# Oscilloscope - requires enable/disable
scope = Net.get('PROBE', type=NetType.Analog)
scope.enable()
freq = scope.measurement.frequency()
scope.disable()
```
### Manage Net Configuration
```python theme={null}
from lager import Net
# Save a new net
Net.save_local_net({
'name': 'NEW_PSU',
'role': 'power-supply',
'channel': 2,
'instrument': 'rigol_dp800',
'address': '192.168.1.100'
})
# Rename a net
Net.rename_local_net('NEW_PSU', 'MAIN_POWER')
# Delete a net
Net.delete_local_net('MAIN_POWER')
```
## Notes
* Simple nets (GPIO, ADC, DAC, Thermocouple) work directly without `enable()`/`disable()` calls
* Complex nets (Analog, Logic, PowerSupply, Battery, ELoad) require `enable()` before use
* Always call `disable()` when finished with complex nets to properly release hardware
* Net names must match those configured on the Lager Box
* Use `Net.list_saved()` to see all available nets
# Python SDK Overview
Source: https://docs.lagerdata.com/source/reference/python/overview
Introduction to the Lager Python SDK for test automation and hardware control
The Lager Python SDK provides a powerful, object-oriented interface for controlling hardware and automating tests on your Device Under Test (DUT). It enables programmatic control of power supplies, sensors, debug probes, and more.
## Import
```python theme={null}
from lager import Net, NetType # Convenience import for Net and NetType
```
## Core Classes
| Class | Description | Import |
| ------------------------ | -------------------------------------------- | ---------------------------------------------- |
| [`binaries`](./binaries) | Execute custom binaries | `from lager.binaries import run_custom_binary` |
| [`Net`](./net) | Core class for managing hardware connections | `from lager import Net, NetType` |
| [`Central`](./ble) | BLE scanning and connection | `from lager.ble import Central, Client` |
## Net Types
The SDK supports various net types for different hardware:
| NetType | Description | Hardware |
| ------------------------ | ------------------------------- | ------------------------------- |
| `NetType.PowerSupply` | Programmable power supply | Rigol DP800, Keithley, Keysight |
| `NetType.PowerSupply2Q` | Two-quadrant supply (solar sim) | EA PSI/EL series |
| `NetType.Battery` | Battery simulator | Keithley 2281S |
| `NetType.ELoad` | Electronic load | Rigol DL3000 |
| `NetType.Analog` | Oscilloscope analog input | Rigol MSO5000 |
| `NetType.Logic` | Logic analyzer input | Rigol MSO5000 |
| `NetType.Waveform` | Waveform generator | Rigol MSO5000 |
| `NetType.GPIO` | Digital I/O | LabJack T7, MCC USB-202 |
| `NetType.ADC` | Analog-to-digital converter | LabJack T7, MCC USB-202 |
| `NetType.DAC` | Digital-to-analog converter | LabJack T7, MCC USB-202 |
| `NetType.Thermocouple` | Temperature sensor | Phidget |
| `NetType.Rotation` | Rotary encoder | Phidget |
| `NetType.WattMeter` | Power meter | Yocto-Watt, Joulescope JS220 |
| `NetType.UART` | Serial communication | USB Serial |
| `NetType.Debug` | Debug probe | J-Link, pyOCD |
| `NetType.Arm` | Robotic arm | Rotrics Dexarm |
| `NetType.Usb` | USB port control | Acroname, YKUSH |
| `NetType.Wifi` | WiFi module | Lager Box WiFi |
| `NetType.Actuate` | Actuator control | Dexarm actuator |
| `NetType.SPI` | SPI bus communication | Aardvark, FT232H, LabJack T7 |
| `NetType.I2C` | I2C bus communication | Aardvark, FT232H, LabJack T7 |
| `NetType.EnergyAnalyzer` | Energy integration measurement | Joulescope JS220 |
| `NetType.Webcam` | Video streaming | USB Webcam |
## Quick Start
### List Available Nets
```python theme={null}
from lager import Net
nets = Net.list_saved()
for net in nets:
print(f"{net['name']}: {net['role']}")
```
### Control a Power Supply
```python theme={null}
from lager import Net, NetType
# Get the power supply net
psu = Net.get('VDD', type=NetType.PowerSupply)
# Configure and enable
psu.set_voltage(3.3)
psu.set_current(0.5)
psu.enable()
# Read measurements
print(f"Voltage: {psu.voltage()}V")
print(f"Current: {psu.current()}A")
# Disable when done
psu.disable()
```
### Read an ADC
```python theme={null}
from lager import Net, NetType
adc = Net.get('SENSOR', type=NetType.ADC)
voltage = adc.input()
print(f"Voltage: {voltage}V")
```
### Control GPIO
```python theme={null}
from lager import Net, NetType
# Read input
button = Net.get('BUTTON', type=NetType.GPIO)
state = button.input()
# Set output
led = Net.get('LED', type=NetType.GPIO)
led.output(1) # HIGH
led.output(0) # LOW
```
### Control Debug Probe
```python theme={null}
from lager import Net, NetType
# Get the debug net
debug = Net.get('jlink1', type=NetType.Debug)
# Connect and flash firmware
debug.connect()
debug.flash('firmware.hex')
debug.reset()
```
### Control USB Hub
```python theme={null}
from lager import Net, NetType
# Get the USB net
usb = Net.get('SENSOR_USB', type=NetType.Usb)
# Power control
usb.enable() # Power on
usb.disable() # Power off
usb.toggle() # Toggle state
```
## Complete Example
```python theme={null}
from lager import Net, NetType
import time
# 1. Flash firmware
debug = Net.get('jlink1', type=NetType.Debug)
debug.connect()
debug.flash('firmware.hex')
debug.reset()
print("Firmware flashed")
# 2. Power on USB peripheral
usb = Net.get('SENSOR_USB', type=NetType.Usb)
usb.enable()
print("USB sensor powered on")
# 3. Enable main power
main_power = Net.get("VDD_MAIN", type=NetType.PowerSupply)
main_power.set_voltage(3.3)
main_power.enable()
print("Main power enabled")
# 4. Read sensor
sensor = Net.get("TEMP_SENSE", type=NetType.ADC)
temperature = sensor.input()
print(f"Temperature: {temperature}V")
# 5. Clean up
main_power.disable()
usb.disable()
print("Test complete")
```
## API Reference
### Core Classes
| Page | Description |
| ----------------------------- | -------------------------------------------------------- |
| [Custom Binaries](./binaries) | Execute custom binaries on the Lager Box |
| [Net](./net) | Hardware net management and core operations |
| [Debug](./debug) | Device flashing, reset, and debug control (includes RTT) |
| [USB](./usb) | USB device power control |
### Power & Simulation
| Page | Description |
| -------------------------- | --------------------------------- |
| [Power Supply](./supply) | Programmable power supply control |
| [Battery](./battery) | Battery simulation and testing |
| [Solar](./solar) | Solar panel simulation |
| [Electronic Load](./eload) | Electronic load control |
| [Watt Meter](./watt) | Power measurement |
### Measurement
| Page | Description |
| ------------------------- | ---------------------------------------------- |
| [Scope](./scope) | Oscilloscope waveform capture and measurements |
| [Logic Analyzer](./logic) | Digital signal capture and protocol decode |
| [ADC](./adc) | Analog-to-digital conversion |
| [Thermocouple](./tc) | Temperature measurement |
### I/O & Communication
| Page | Description |
| ------------------ | ---------------------------------- |
| [GPIO](./gpio) | Digital input/output control |
| [DAC](./dac) | Digital-to-analog conversion |
| [I2C](./i2c) | I2C bus communication |
| [SPI](./spi) | SPI bus communication |
| [UART](./uart) | UART net serial communication |
| [Serial](./serial) | Native pyserial support |
| [BLE](./ble) | Bluetooth Low Energy communication |
| [WiFi](./wifi) | WiFi configuration |
### Utilities
| Page | Description |
| ------------------ | ---------------------------------- |
| [Robot Arm](./arm) | Robotic arm control |
| [Webcam](./webcam) | Webcam streaming and video capture |
## Error Handling
```python theme={null}
from lager import Net, NetType, InvalidNetError
try:
net = Net.get('INVALID_NET', NetType.Analog)
except InvalidNetError as e:
print(f"Net not found: {e}")
except Exception as e:
print(f"Error: {e}")
```
## Notes
* Always call `disable()` when finished with power-related nets
* Simple nets (GPIO, ADC, DAC) don't require `enable()`/`disable()` calls
* Complex nets (PowerSupply, Battery, Analog) require `enable()` before use
* Net names must match those configured in the Lager system
* Use try/except blocks for robust error handling
## Demo Script
For a more comprehensive example combining robot arm control, USB hub power cycling, debug probe flashing, power supply management, and ADC measurement in a single automated workflow, see the [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py).
# Oscilloscope
Source: https://docs.lagerdata.com/source/reference/python/scope
Python SDK for oscilloscope control
The oscilloscope module provides Python interfaces for waveform capture, triggering, and measurements.
## Overview
Use the scope module to control oscilloscopes for analog signal capture, triggering on specific events, and automated measurements.
## Import
```python theme={null}
from lager import Net, NetType
```
## Usage
```python theme={null}
from lager import Net, NetType
# Get scope net
scope = Net.get('ANALOG1', type=NetType.Analog)
# Enable the channel
scope.enable()
# Start capture
scope.start_capture()
# Take measurements
freq = scope.measurement.frequency()
period = scope.measurement.period()
# Stop and disable
scope.stop_capture()
scope.disable()
```
## Methods
### Channel Control
#### `enable()`
Enable the oscilloscope channel.
```python theme={null}
scope.enable()
```
#### `disable()`
Disable the oscilloscope channel.
```python theme={null}
scope.disable()
```
### Capture Control
#### `start_capture()`
Start continuous waveform capture.
```python theme={null}
scope.start_capture()
```
#### `start_single_capture()`
Start single-shot capture (captures one triggered event).
```python theme={null}
scope.start_single_capture()
```
#### `stop_capture()`
Stop waveform capture.
```python theme={null}
scope.stop_capture()
```
### Measurements
Access comprehensive measurements through the `measurement` attribute:
#### Voltage Measurements
```python theme={null}
# Basic voltage
vmax = scope.measurement.voltage_max() # Maximum voltage
vmin = scope.measurement.voltage_min() # Minimum voltage
vpp = scope.measurement.voltage_peak_to_peak() # Peak-to-peak
vavg = scope.measurement.voltage_average() # Average voltage
vrms = scope.measurement.voltage_rms() # RMS voltage
# Waveform characteristics
vtop = scope.measurement.voltage_flat_top() # Flat top voltage
vbase = scope.measurement.voltage_flat_base() # Flat base voltage
vamp = scope.measurement.voltage_flat_amplitude() # Amplitude
# Thresholds
vupper = scope.measurement.voltage_threshold_upper()
vlower = scope.measurement.voltage_threshold_lower()
vmid = scope.measurement.voltage_threshold_mid()
# Signal quality
overshoot = scope.measurement.voltage_overshoot()
preshoot = scope.measurement.voltage_preshoot()
```
#### Timing Measurements
```python theme={null}
# Frequency and period
freq = scope.measurement.frequency()
period = scope.measurement.period()
# Rise and fall times
rise = scope.measurement.rise_time()
fall = scope.measurement.fall_time()
# Pulse widths
pos_width = scope.measurement.pulse_width_positive()
neg_width = scope.measurement.pulse_width_negative()
# Duty cycles
pos_duty = scope.measurement.duty_cycle_positive()
neg_duty = scope.measurement.duty_cycle_negative()
# Time at voltage extremes
t_vmax = scope.measurement.time_at_voltage_max()
t_vmin = scope.measurement.time_at_voltage_min()
# Slew rates
pos_slew = scope.measurement.positive_slew_rate()
neg_slew = scope.measurement.negative_slew_rate()
```
#### Counting Measurements
```python theme={null}
# Edge counts
pos_edges = scope.measurement.positive_edge_count()
neg_edges = scope.measurement.negative_edge_count()
# Pulse counts
pos_pulses = scope.measurement.positive_pulse_count()
neg_pulses = scope.measurement.negative_pulse_count()
```
#### Area Measurements
```python theme={null}
area = scope.measurement.waveform_area()
period_area = scope.measurement.waveform_period_area()
```
#### Statistical Measurements
```python theme={null}
variance = scope.measurement.variance()
pvrms = scope.measurement.voltage_rms_period() # Period RMS voltage
```
#### Delay and Phase Measurements
```python theme={null}
# Delay measurements (between channels)
rr_delay = scope.measurement.delay_rising_rising_edge()
rf_delay = scope.measurement.delay_rising_falling_edge()
fr_delay = scope.measurement.delay_falling_rising_edge()
ff_delay = scope.measurement.delay_falling_falling_edge()
# Phase measurements
rr_phase = scope.measurement.phase_rising_rising_edge()
rf_phase = scope.measurement.phase_rising_falling_edge()
fr_phase = scope.measurement.phase_falling_rising_edge()
ff_phase = scope.measurement.phase_falling_falling_edge()
```
#### Measurement Options
Most measurements accept optional parameters:
```python theme={null}
# Keep measurement displayed on scope
freq = scope.measurement.frequency(display=True)
# Enable cursor measurement mode
vpp = scope.measurement.voltage_peak_to_peak(measurement_cursor=True)
```
## Streaming (PicoScope)
For PicoScope devices, streaming capabilities are available:
### `stream_start(channel, volts_per_div, time_per_div, trigger_level, trigger_slope, capture_mode, coupling)`
Start streaming acquisition.
**Parameters:**
* `channel` (str): Channel to enable - `"A"`, `"B"`, `"1"`, `"2"`
* `volts_per_div` (float): Vertical scale
* `time_per_div` (float): Horizontal scale in seconds
* `trigger_level` (float): Trigger level in volts
* `trigger_slope` (str): `"rising"`, `"falling"`, `"either"`
* `capture_mode` (str): `"auto"`, `"normal"`, `"single"`
* `coupling` (str): `"dc"`, `"ac"`
```python theme={null}
scope.stream_start(
channel="A",
volts_per_div=1.0,
time_per_div=0.001,
trigger_level=0.5,
trigger_slope="rising",
capture_mode="auto",
coupling="dc"
)
```
### `stream_stop()`
Stop streaming acquisition.
```python theme={null}
scope.stream_stop()
```
### `stream_capture(output, duration, samples)`
Capture data to file.
**Parameters:**
* `output` (str): Output file path
* `duration` (float): Capture duration in seconds
* `samples` (int): Number of samples (optional)
```python theme={null}
scope.stream_capture(
output="waveform.csv",
duration=5.0
)
```
## Complete Example
```python theme={null}
from lager import Net, NetType
import time
def measure_pwm_signal():
"""Measure PWM signal characteristics."""
# Get scope net
pwm_net = Net.get('PWM_OUTPUT', type=NetType.Analog)
try:
# Enable channel
pwm_net.enable()
# Configure trigger
pwm_net.trigger_settings.set_mode_normal()
pwm_net.trigger_settings.set_coupling_DC()
pwm_net.trigger_settings.edge.set_source(pwm_net)
pwm_net.trigger_settings.edge.set_slope_rising()
pwm_net.trigger_settings.edge.set_level(1.65) # 50% of 3.3V
# Start capture
pwm_net.start_capture()
time.sleep(0.5) # Wait for stable capture
# Take measurements
frequency = pwm_net.measurement.frequency()
period = pwm_net.measurement.period()
print(f"PWM Frequency: {frequency:.2f} Hz")
print(f"PWM Period: {period*1000:.3f} ms")
# Calculate duty cycle from pulse width if available
# ...
finally:
pwm_net.stop_capture()
pwm_net.disable()
if __name__ == "__main__":
measure_pwm_signal()
```
### Trace Settings
Configure vertical and horizontal scale through `trace_settings`:
```python theme={null}
# Vertical scale (V/div)
scope.trace_settings.set_volts_per_div(1.0)
volts = scope.trace_settings.get_volts_per_div()
# Vertical offset
scope.trace_settings.set_volt_offset(0.5)
offset = scope.trace_settings.get_volt_offset()
# Horizontal scale (s/div)
scope.trace_settings.set_time_per_div(0.001) # 1ms/div
time_scale = scope.trace_settings.get_time_per_div()
# Horizontal offset
scope.trace_settings.set_time_offset(0.0)
time_offset = scope.trace_settings.get_time_offset()
```
### Advanced Trigger Settings
Access advanced trigger configuration through `trigger_settings`:
```python theme={null}
# Trigger mode
scope.trigger_settings.set_mode_auto()
scope.trigger_settings.set_mode_normal()
scope.trigger_settings.set_mode_single()
mode = scope.trigger_settings.get_mode()
# Trigger coupling
scope.trigger_settings.set_coupling_DC()
scope.trigger_settings.set_coupling_AC()
scope.trigger_settings.set_coupling_low_freq_reject()
scope.trigger_settings.set_coupling_high_freq_reject()
coupling = scope.trigger_settings.get_coupling()
# Edge trigger settings
scope.trigger_settings.edge.set_source(scope)
scope.trigger_settings.edge.set_slope_rising()
scope.trigger_settings.edge.set_slope_falling()
scope.trigger_settings.edge.set_slope_both()
scope.trigger_settings.edge.set_level(1.65)
# Get status
status = scope.trigger_settings.get_status()
```
### Cursor Control
Access cursor functions through the `cursor` attribute:
```python theme={null}
# Set cursor positions
scope.cursor.set_a(x=100, y=50)
scope.cursor.set_b(x=200, y=50)
# Get cursor positions
ax, ay = scope.cursor.get_a()
bx, by = scope.cursor.get_b()
# Move cursors relatively
scope.cursor.move_a(x_del=10, y_del=5)
scope.cursor.move_b(x_del=-10, y_del=0)
# Read cursor measurements
x_delta = scope.cursor.x_delta() # Time difference
y_delta = scope.cursor.y_delta() # Voltage difference
inv_x = scope.cursor.frequency() # Frequency
# Get individual values
ax_val = scope.cursor.a_x()
ay_val = scope.cursor.a_y()
bx_val = scope.cursor.b_x()
by_val = scope.cursor.b_y()
# Hide cursor
scope.cursor.hide()
```
## Supported Hardware
| Manufacturer | Model Series | Features |
| ------------ | ------------ | -------------------------------------------- |
| Rigol | MSO5000 | Multi-channel, mixed-signal, protocol decode |
| PicoScope | Various | Streaming support |
## Notes
* Use `NetType.Analog` for oscilloscope channels (1-4)
* Use `NetType.Logic` for digital channels (D0-D15) on MSO scopes
* Streaming features are only available on PicoScope devices
* Configure trigger before starting capture for reliable measurements
* Measurement methods return `float` on success, or `None` if the measurement is invalid (e.g., no signal, no trigger, wrong channel). On Rigol hardware, the instrument returns 9.9E+37 for invalid measurements, which is automatically converted to `None`.
* For protocol triggering (UART, I2C, SPI, CAN), see the [Logic Analyzer](./logic) documentation
# Serial
Source: https://docs.lagerdata.com/source/reference/python/serial
Native pyserial support for serial communication
Native `pyserial` support for serial communication with your DUT.
## Import
```python theme={null}
import serial
```
## Methods
| Method | Description |
| -------------- | ------------------------ |
| `Serial()` | Create serial connection |
| `readline()` | Read a line |
| `read()` | Read specified bytes |
| `read_until()` | Read until delimiter |
| `write()` | Write data |
| `open()` | Open connection |
| `close()` | Close connection |
| `is_open` | Check connection state |
## Method Reference
### `serial.Serial(port, baudrate, **kwargs)`
Create a serial connection.
```python theme={null}
import serial
ser = serial.Serial('/dev/ttyUSB1', 115200)
# With additional parameters
ser = serial.Serial(
port='/dev/ttyUSB1',
baudrate=115200,
timeout=60,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE
)
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ------- | --------------------------------------------------- |
| `port` | `str` | Serial port path (e.g., `/dev/ttyUSB1`) |
| `baudrate` | `int` | Baud rate in bits per second |
| `timeout` | `float` | Read timeout in seconds |
| `bytesize` | `int` | Data bits (5, 6, 7, or 8) |
| `parity` | `str` | Parity (`PARITY_NONE`, `PARITY_EVEN`, `PARITY_ODD`) |
| `stopbits` | `int` | Stop bits (1, 1.5, or 2) |
**Returns:** `Serial` object
### `readline()`
Read a line from the serial port.
```python theme={null}
line = ser.readline()
print(f"Received: {line.decode('utf-8').strip()}")
```
**Returns:** `bytes` - The received line
### `read(size)`
Read a specified number of bytes.
```python theme={null}
data = ser.read(10)
```
| Parameter | Type | Description |
| --------- | ----- | ----------------------- |
| `size` | `int` | Number of bytes to read |
**Returns:** `bytes` - The received data
### `read_until(expected=b'\n', size=None)`
Read until expected sequence is found.
```python theme={null}
line = ser.read_until(b'\n')
```
| Parameter | Type | Description |
| ---------- | ------- | ---------------------- |
| `expected` | `bytes` | Sequence to read until |
| `size` | `int` | Maximum bytes to read |
**Returns:** `bytes` - Data up to expected sequence
### `write(data)`
Write data to the serial port.
```python theme={null}
ser.write(b'AT+VER\r\n')
```
| Parameter | Type | Description |
| --------- | ------- | ------------- |
| `data` | `bytes` | Data to write |
**Returns:** `int` - Number of bytes written
### `close()`
Close the serial connection.
```python theme={null}
ser.close()
```
### `is_open`
Check if connection is open.
```python theme={null}
if ser.is_open:
print("Connected")
```
**Returns:** `bool` - True if connection is open
## Examples
### Basic Communication
```python theme={null}
import serial
import time
ser = serial.Serial('/dev/ttyUSB1', 115200, timeout=60)
# Send command
ser.write(b'AT+VER\r\n')
time.sleep(0.1)
# Read response
response = ser.readline()
print(f"Response: {response.decode('utf-8').strip()}")
ser.close()
```
### Interactive Session
```python theme={null}
import serial
def interactive_session(port, baudrate=115200):
ser = serial.Serial(port, baudrate, timeout=1)
print(f"Connected to {port}")
while True:
try:
# Read any available data
if ser.in_waiting:
data = ser.read(ser.in_waiting)
print(data.decode('utf-8'), end='')
# Get user input
command = input()
if command.lower() == 'quit':
break
ser.write(f"{command}\r\n".encode('utf-8'))
except KeyboardInterrupt:
break
ser.close()
```
### Data Logging
```python theme={null}
import serial
import time
from datetime import datetime
def log_serial(port, log_file, duration=60):
ser = serial.Serial(port, 115200, timeout=1)
with open(log_file, 'w') as f:
start = time.time()
while time.time() - start < duration:
if ser.in_waiting:
data = ser.read(ser.in_waiting)
timestamp = datetime.now().isoformat()
f.write(f"[{timestamp}] {data.decode('utf-8')}")
f.flush()
time.sleep(0.1)
ser.close()
```
### Command/Response Pattern
```python theme={null}
import serial
import time
class SerialDevice:
def __init__(self, port, baudrate=115200):
self.ser = serial.Serial(port, baudrate, timeout=60)
def send_command(self, command, timeout=5):
self.ser.reset_input_buffer()
self.ser.write(f"{command}\r\n".encode('utf-8'))
response = ""
start = time.time()
while time.time() - start < timeout:
if self.ser.in_waiting:
line = self.ser.readline().decode('utf-8').strip()
response += line + "\n"
time.sleep(0.1)
return response
def close(self):
self.ser.close()
# Usage
device = SerialDevice('/dev/ttyUSB1')
response = device.send_command('AT+VER')
print(response)
device.close()
```
## Hardware Integration
| Connection | Description |
| ------------ | ------------------------------------------ |
| Raw UART | Direct TX/RX line connections |
| USB CDC | Virtual serial over USB |
| Flow Control | Hardware (RTS/CTS) and software (XON/XOFF) |
## Notes
* Lager supports native `pyserial` for serial communication
* Serial ports are designated during Lager setup configuration
* Supports both raw UART and USB CDC connections
* Default baud rate is 115200
* Always handle `SerialException` errors appropriately
* Use `decode('utf-8')` for string conversion
# Solar Simulation
Source: https://docs.lagerdata.com/source/reference/python/solar
Control solar panel simulation nets
**Coming Soon:** The Solar Simulator Python API for EA PSI/EL series two-quadrant power supplies is currently under development. The Net-based API (`Net.get('solar1', type=NetType.PowerSupply2Q)`) and associated methods are documented for preview purposes, but full testing and validation are pending hardware availability. Check back in a future release for production-ready functionality.
Simulate solar panel characteristics for testing solar-powered devices.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ------------------- | --------------------------------------- |
| `enable()` | Connect and start solar simulation mode |
| `disable()` | Disconnect and stop solar simulation |
| `irradiance(value)` | Set or read irradiance (W/m²) |
| `mpp_current()` | Read maximum power point current |
| `mpp_voltage()` | Read maximum power point voltage |
| `voc()` | Read open-circuit voltage |
| `temperature()` | Read simulated cell temperature |
| `resistance(value)` | Set or read panel resistance |
Solar methods return string values from the instrument. Convert to float if needed for calculations.
## Method Reference
### `Net.get(name, type=NetType.PowerSupply2Q)`
Get a solar simulation net by name.
```python theme={null}
from lager import Net, NetType
solar = Net.get('SOLAR', type=NetType.PowerSupply2Q)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------------- |
| `name` | `str` | Name of the solar net |
| `type` | `NetType` | Must be `NetType.PowerSupply2Q` |
**Returns:** Solar simulation Net instance
### `enable()`
Connect to the instrument and start solar simulation mode.
```python theme={null}
from lager import Net, NetType
solar = Net.get('SOLAR', type=NetType.PowerSupply2Q)
solar.enable()
```
### `disable()`
Disconnect from the instrument and stop solar simulation.
```python theme={null}
solar.disable()
```
### `irradiance(value=None)`
Set or read the irradiance level.
```python theme={null}
# Set irradiance
solar.irradiance(1000) # Standard test condition (1000 W/m²)
# Read current irradiance
irr = solar.irradiance()
print(f"Irradiance: {irr}") # Returns string
```
| Parameter | Type | Description |
| --------- | ----------------- | ---------------------------------------------------------- |
| `value` | `float` or `None` | Irradiance in W/m² (0-1500). If None, reads current value. |
**Returns:** `str` - Current irradiance value
### `voc()`
Read the open-circuit voltage.
```python theme={null}
voc_str = solar.voc()
print(f"Voc: {voc_str}")
voc = float(voc_str) # Convert to float for calculations
```
**Returns:** `str` - Voltage value
### `mpp_voltage()`
Read the maximum power point voltage.
```python theme={null}
v_mpp = solar.mpp_voltage()
print(f"MPP voltage: {v_mpp}")
```
**Returns:** `str` - Voltage value
### `mpp_current()`
Read the maximum power point current.
```python theme={null}
i_mpp = solar.mpp_current()
print(f"MPP current: {i_mpp}")
```
**Returns:** `str` - Current value
### `resistance(value=None)`
Set or read the dynamic panel resistance.
```python theme={null}
# Set resistance
solar.resistance(5.0)
# Read resistance
r = solar.resistance()
print(f"Resistance: {r}")
```
| Parameter | Type | Description |
| --------- | ----------------- | ------------------------------------------------- |
| `value` | `float` or `None` | Resistance in ohms. If None, reads current value. |
**Returns:** `str` - Resistance value
### `temperature()`
Read the simulated cell temperature.
```python theme={null}
temp = solar.temperature()
print(f"Cell temp: {temp}")
```
**Returns:** `str` - Temperature value
## Examples
### Basic Solar Simulation
```python theme={null}
from lager import Net, NetType
import time
solar = Net.get('SOLAR_INPUT', type=NetType.PowerSupply2Q)
# Start simulation
solar.enable()
# Set standard test conditions (1000 W/m²)
solar.irradiance(1000)
time.sleep(1)
# Read panel characteristics (returns strings)
voc = solar.voc()
v_mpp = solar.mpp_voltage()
i_mpp = solar.mpp_current()
print(f"Voc: {voc}")
print(f"MPP: {v_mpp}V @ {i_mpp}A")
# Calculate max power (convert to float first)
v = float(v_mpp)
i = float(i_mpp)
print(f"Max Power: {v * i:.2f}W")
# Clean up
solar.disable()
```
### Test Multiple Irradiance Levels
```python theme={null}
from lager import Net, NetType
import time
solar = Net.get('SOLAR', type=NetType.PowerSupply2Q)
solar.enable()
conditions = [200, 500, 800, 1000, 1200]
for irr in conditions:
solar.irradiance(irr)
time.sleep(1)
# Read and convert values
voc = float(solar.voc())
v_mpp = float(solar.mpp_voltage())
i_mpp = float(solar.mpp_current())
print(f"Irradiance: {irr} W/m²")
print(f" Voc: {voc:.2f}V")
print(f" MPP: {v_mpp:.2f}V @ {i_mpp:.3f}A")
print(f" Power: {v_mpp * i_mpp:.2f}W")
print()
solar.disable()
```
### MPPT Tracking Test
```python theme={null}
from lager import Net, NetType
import time
solar = Net.get('SOLAR', type=NetType.PowerSupply2Q)
dut_current = Net.get('DUT_CURRENT', type=NetType.ADC)
solar.enable()
solar.irradiance(1000)
# Monitor MPPT tracking
for i in range(30):
i_mpp = float(solar.mpp_current())
actual = dut_current.input()
efficiency = (actual / i_mpp) * 100 if i_mpp > 0 else 0
print(f"Target: {i_mpp:.3f}A, Actual: {actual:.3f}A, Eff: {efficiency:.1f}%")
time.sleep(1)
solar.disable()
```
## Supported Hardware
| Manufacturer | Model | Features |
| ------------ | ------------- | --------------------------- |
| EA | PSI/EL series | Two-quadrant, PV simulation |
| EA | PSB 10060-60 | Bidirectional |
| EA | PSB 10080-60 | Bidirectional |
## Notes
* Solar simulation requires bidirectional (two-quadrant) power supplies
* Standard Test Conditions (STC): 1000 W/m², 25°C, AM1.5
* The I-V curve is automatically generated based on irradiance
* Call `enable()` before using solar-specific methods
* Always call `disable()` when finished
* **Return values are strings** - convert to float for calculations
# SPI
Source: https://docs.lagerdata.com/source/reference/python/spi
Communicate with SPI devices over the serial peripheral interface
Perform full-duplex SPI (Serial Peripheral Interface) communication with devices connected to a Lager Box.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| -------------- | ------------------------------------------- |
| `config()` | Configure SPI bus parameters |
| `read()` | Read words from a device (sends fill bytes) |
| `read_write()` | Simultaneous full-duplex read and write |
| `transfer()` | Transfer with automatic padding/truncation |
| `write()` | Write words to a device (discards response) |
| `get_config()` | Get raw net configuration |
## Method Reference
### `Net.get(name, type=NetType.SPI)`
Get an SPI net by name.
```python theme={null}
from lager import Net, NetType
spi = Net.get('MY_SPI_NET', type=NetType.SPI)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------- |
| `name` | `str` | Name of the SPI net |
| `type` | `NetType` | Must be `NetType.SPI` |
**Returns:** SPI Net instance
### `config(mode, bit_order, frequency_hz, word_size, cs_active, cs_mode)`
Configure SPI bus parameters. Only explicitly-provided parameters are changed; omitted parameters retain their stored values.
```python theme={null}
spi.config(mode=0, frequency_hz=1_000_000)
spi.config(mode=3, bit_order="lsb", word_size=16)
spi.config(cs_mode="manual")
```
| Parameter | Type | Description |
| -------------- | --------------- | -------------------------------------------------------- |
| `mode` | `int` or `None` | SPI mode 0-3 (see SPI Modes table below) |
| `bit_order` | `str` or `None` | `"msb"` (most significant bit first) or `"lsb"` |
| `frequency_hz` | `int` or `None` | Clock frequency in Hz |
| `word_size` | `int` or `None` | Bits per word: `8`, `16`, or `32` |
| `cs_active` | `str` or `None` | Chip select polarity: `"low"` or `"high"` |
| `cs_mode` | `str` or `None` | `"auto"` (hardware CS) or `"manual"` (user-managed GPIO) |
#### SPI Modes
| Mode | CPOL | CPHA | Clock Idle | Sample Edge |
| ---- | ---- | ---- | ---------- | ----------- |
| 0 | 0 | 0 | Low | Rising |
| 1 | 0 | 1 | Low | Falling |
| 2 | 1 | 0 | High | Falling |
| 3 | 1 | 1 | High | Rising |
### `read(n_words, fill, keep_cs, output_format)`
Read data from an SPI device. Sends fill bytes while receiving data (full duplex).
```python theme={null}
data = spi.read(n_words=4)
data = spi.read(n_words=4, fill=0x00)
```
| Parameter | Type | Description |
| --------------- | ------ | --------------------------------------------------- |
| `n_words` | `int` | Number of words to read |
| `fill` | `int` | Fill value sent while reading (default `0xFF`) |
| `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) |
| `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` |
**Returns:** `list[int]` - Received words as integers (when `output_format="list"`)
### `read_write(data, keep_cs, output_format)`
Perform simultaneous full-duplex SPI read and write. Sends data while simultaneously receiving the response.
```python theme={null}
# Send JEDEC Read ID command and read 3 response bytes
response = spi.read_write([0x9F, 0x00, 0x00, 0x00])
manufacturer_id = response[1]
device_id = (response[2] << 8) | response[3]
```
| Parameter | Type | Description |
| --------------- | ----------- | --------------------------------------------------- |
| `data` | `list[int]` | Words to transmit |
| `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) |
| `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` |
**Returns:** `list[int]` - Received words (same length as transmitted data)
### `transfer(n_words, data, fill, keep_cs, output_format)`
Perform SPI transfer with automatic padding or truncation. If data is shorter than `n_words`, it is padded with the fill value. If longer, it is truncated.
```python theme={null}
# Send 1-byte command, read 3 response bytes (4 total)
response = spi.transfer(n_words=4, data=[0x9F])
# data [0x9F] is padded to [0x9F, 0xFF, 0xFF, 0xFF]
```
| Parameter | Type | Description |
| --------------- | --------------------- | --------------------------------------------------- |
| `n_words` | `int` | Total number of words to transfer |
| `data` | `list[int]` or `None` | Words to transmit (padded/truncated to `n_words`) |
| `fill` | `int` | Fill value for padding (default `0xFF`) |
| `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) |
| `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` |
**Returns:** `list[int]` - Received words
### `write(data, keep_cs)`
Write data to an SPI device, discarding the response. Convenience method for write-only operations.
```python theme={null}
# Send Write Enable command
spi.write([0x06])
# Send Page Program with address and data
spi.write([0x02, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF])
```
| Parameter | Type | Description |
| --------- | ----------- | ------------------------------------------------- |
| `data` | `list[int]` | Words to transmit |
| `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) |
### `get_config()`
Get the raw net configuration dictionary.
```python theme={null}
cfg = spi.get_config()
print(cfg['name'])
print(cfg['params'])
```
**Returns:** `dict` - Full net configuration including name, role, instrument, and params
## Output Formats
The `output_format` parameter controls how data is returned. Hex formatting is word-size-aware:
| Format | Return Type | 8-bit Example | 16-bit Example |
| --------- | ----------- | ---------------------- | ------------------- |
| `"list"` | `list[int]` | `[222, 173]` | `[57005]` |
| `"hex"` | `str` | `"de ad"` | `"dead"` |
| `"bytes"` | `str` | `"222 173"` | `"57005"` |
| `"json"` | `dict` | `{"data": [222, 173]}` | `{"data": [57005]}` |
## Examples
### Read SPI Flash JEDEC ID
```python theme={null}
from lager import Net, NetType
spi = Net.get('flash_spi', type=NetType.SPI)
spi.config(mode=0, frequency_hz=1_000_000)
# JEDEC Read ID: send 0x9F, read 3 response bytes
response = spi.read_write([0x9F, 0x00, 0x00, 0x00])
print(f"Manufacturer: 0x{response[1]:02x}")
print(f"Device ID: 0x{(response[2] << 8) | response[3]:04x}")
```
### Read Flash Memory
```python theme={null}
from lager import Net, NetType
spi = Net.get('flash_spi', type=NetType.SPI)
spi.config(mode=0, frequency_hz=1_000_000)
# Read 32 bytes starting at address 0x001000
# Command: 0x03 (Read), followed by 3-byte address
response = spi.transfer(
n_words=4 + 32,
data=[0x03, 0x00, 0x10, 0x00],
)
# First 4 bytes are command echo; data starts at index 4
data = response[4:]
print(f"Read {len(data)} bytes: {' '.join(f'{b:02x}' for b in data)}")
```
### Multi-Part Transaction with keep\_cs
```python theme={null}
from lager import Net, NetType
spi = Net.get('flash_spi', type=NetType.SPI)
# Part 1: Send address with CS held low
spi.write([0x03, 0x00, 0x10, 0x00], keep_cs=True)
# Part 2: Read data while CS is still asserted
data = spi.read(n_words=32, keep_cs=False)
print(f"Read {len(data)} bytes")
```
### Write to SPI Flash
```python theme={null}
from lager import Net, NetType
spi = Net.get('flash_spi', type=NetType.SPI)
spi.config(mode=0, frequency_hz=1_000_000)
# Step 1: Write Enable
spi.write([0x06])
# Step 2: Page Program at address 0x001000
payload = [0xDE, 0xAD, 0xBE, 0xEF]
spi.write([0x02, 0x00, 0x10, 0x00] + payload)
# Step 3: Wait for write to complete (poll status register)
import time
while True:
status = spi.read_write([0x05, 0x00])
if not (status[1] & 0x01): # WIP bit cleared
break
time.sleep(0.01)
print("Write complete")
```
### 16-bit Word Mode
```python theme={null}
from lager import Net, NetType
spi = Net.get('dac_spi', type=NetType.SPI)
spi.config(mode=1, word_size=16, frequency_hz=500_000)
# Send 16-bit DAC command (channel A, gain 1x, active, value 0x0800)
spi.write([0x3800])
# Read back 16-bit status register
status = spi.read_write([0x0000])
print(f"Status: 0x{status[0]:04x}")
```
## Supported Hardware
| Adapter | Description |
| ---------------- | ---------------------------------------------------- |
| LabJack T7 | Uses GPIO pins (FIO/EIO) for CLK, MOSI, MISO, and CS |
| Aardvark I2C/SPI | Dedicated USB SPI adapter with GPIO bit-bang |
## Notes
* Net must be configured as `NetType.SPI`
* All SPI operations are full duplex; data is sent and received simultaneously
* `write()` performs a full-duplex transfer but discards the received data
* `keep_cs=True` holds the chip select line asserted between calls for multi-part transactions
* `transfer()` pads short data arrays with the fill value or truncates long arrays to `n_words`
* LabJack T7 supports up to 56 bytes per transaction and a maximum of approximately 800 kHz
* Aardvark uses GPIO bit-bang mode; actual speed is limited by USB round-trip time regardless of `frequency_hz`
* Configuration changes persist to `saved_nets.json` for subsequent commands
* LSB-first mode (`bit_order="lsb"`) uses software bit reversal on LabJack T7
# Power Supply
Source: https://docs.lagerdata.com/source/reference/python/supply
Control programmable power supply nets
Control programmable power supplies to set voltage, current, and protection thresholds for your DUT.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ------------------ | ---------------------------------------------- |
| `set_voltage()` | Set output voltage |
| `set_current()` | Set output current limit |
| `voltage()` | Read measured voltage |
| `current()` | Read measured current |
| `power()` | Read measured power |
| `enable()` | Enable power output |
| `disable()` | Disable power output |
| `set_ovp()` | Set over-voltage protection threshold |
| `set_ocp()` | Set over-current protection threshold |
| `get_ovp_limit()` | Get over-voltage protection limit |
| `get_ocp_limit()` | Get over-current protection limit |
| `is_ovp()` | Check if OVP fault is active |
| `is_ocp()` | Check if OCP fault is active |
| `clear_ovp()` | Clear over-voltage protection fault |
| `clear_ocp()` | Clear over-current protection fault |
| `state()` | Print comprehensive power state |
| `get_full_state()` | Print extended state with setpoints and limits |
## Method Reference
### `Net.get(name, type=NetType.PowerSupply)`
Get a power supply net by name.
```python theme={null}
from lager import Net, NetType
psu = Net.get('VDD', type=NetType.PowerSupply)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ----------------------------- |
| `name` | `str` | Name of the power supply net |
| `type` | `NetType` | Must be `NetType.PowerSupply` |
**Returns:** Power supply Net instance
### `set_voltage(value)`
Set the output voltage.
```python theme={null}
psu.set_voltage(3.3) # Set to 3.3V
```
| Parameter | Type | Description |
| --------- | ------- | ----------------------- |
| `value` | `float` | Target voltage in volts |
### `set_current(value)`
Set the output current limit.
```python theme={null}
psu.set_current(0.5) # Set limit to 0.5A
```
| Parameter | Type | Description |
| --------- | ------- | --------------------- |
| `value` | `float` | Current limit in amps |
### `voltage()`
Read the measured output voltage.
```python theme={null}
v = psu.voltage()
print(f"Voltage: {v}V")
```
**Returns:** `float` - Measured voltage in volts
### `current()`
Read the measured output current.
```python theme={null}
i = psu.current()
print(f"Current: {i}A")
```
**Returns:** `float` - Measured current in amps
### `power()`
Read the measured output power.
```python theme={null}
p = psu.power()
print(f"Power: {p}W")
```
**Returns:** `float` - Measured power in watts
### `enable()`
Enable the power output.
```python theme={null}
psu.enable()
```
### `disable()`
Disable the power output.
```python theme={null}
psu.disable()
```
### `set_ovp(limit)`
Set over-voltage protection threshold. OVP must be greater than or equal to the configured voltage. When the measured voltage exceeds this threshold, the output is automatically disabled.
```python theme={null}
psu.set_ovp(3.6) # Trip at 3.6V
```
| Parameter | Type | Description |
| --------- | ------- | ---------------------- |
| `limit` | `float` | OVP threshold in volts |
### `set_ocp(limit)`
Set over-current protection threshold. When the measured current exceeds this threshold, the output is automatically disabled.
```python theme={null}
psu.set_ocp(1.0) # Trip at 1.0A
```
| Parameter | Type | Description |
| --------- | ------- | --------------------- |
| `limit` | `float` | OCP threshold in amps |
### `get_ovp_limit()`
Get the configured OVP limit.
```python theme={null}
ovp = psu.get_ovp_limit()
print(f"OVP limit: {ovp}V")
```
**Returns:** `float` - OVP threshold in volts
### `get_ocp_limit()`
Get the configured OCP limit.
```python theme={null}
ocp = psu.get_ocp_limit()
print(f"OCP limit: {ocp}A")
```
**Returns:** `float` - OCP threshold in amps
### `is_ovp()`
Check if an over-voltage fault is active.
```python theme={null}
if psu.is_ovp():
print("OVP fault detected!")
```
**Returns:** `bool` - True if OVP fault is active
### `is_ocp()`
Check if an over-current fault is active.
```python theme={null}
if psu.is_ocp():
print("OCP fault detected!")
```
**Returns:** `bool` - True if OCP fault is active
### `clear_ovp()`
Clear over-voltage protection fault.
```python theme={null}
psu.clear_ovp()
```
### `clear_ocp()`
Clear over-current protection fault.
```python theme={null}
psu.clear_ocp()
```
### `state()`
Print comprehensive power supply state including channel, enabled status, mode (CV/CC), measured voltage/current/power, and protection status.
```python theme={null}
psu.state()
```
**Example output:**
```
Channel: CH1
Enabled: ON
Mode: CV
Voltage: 3.3000
Current: 0.1520
Power: 0.5016
OCP Limit: 1.0000
OCP Tripped: NO
OVP Limit: 3.6000
OVP Tripped: NO
```
### `get_full_state()`
Print extended state including all measurements, configured setpoints, protection limits, and hardware maximum ratings.
```python theme={null}
psu.get_full_state()
```
**Example output:**
```
Channel: CH1
Enabled: ON
Mode: CV
Voltage: 3.3000
Current: 0.1520
Power: 0.5016
Voltage_Set: 3.3000
Current_Set: 1.0000
OCP Limit: 1.0000
OCP Tripped: NO
OVP Limit: 3.6000
OVP Tripped: NO
Voltage_Max: 30.0000
Current_Max: 3.0000
```
Additional fields beyond `state()`:
* **Voltage\_Set / Current\_Set** - Configured setpoints
* **Voltage\_Max / Current\_Max** - Hardware channel ratings
## Examples
### Basic Power Control
```python theme={null}
from lager import Net, NetType
# Get power supply net
psu = Net.get('VDD', type=NetType.PowerSupply)
# Configure output
psu.set_voltage(3.3)
psu.set_current(0.5)
# Enable output
psu.enable()
# Read measurements
print(f"Voltage: {psu.voltage():.2f}V")
print(f"Current: {psu.current():.3f}A")
print(f"Power: {psu.power():.3f}W")
# Disable when done
psu.disable()
```
### With Protection Thresholds
```python theme={null}
from lager import Net, NetType
import time
psu = Net.get('VDD', type=NetType.PowerSupply)
# Configure voltage/current
psu.set_voltage(5.0)
psu.set_current(0.5)
# Set protection thresholds
psu.set_ovp(5.5) # Trip at 5.5V
psu.set_ocp(0.6) # Trip at 0.6A
# Enable output
psu.enable()
print("Power enabled")
# Monitor for faults
time.sleep(1)
if psu.is_ocp():
print("OCP fault! Clearing...")
psu.clear_ocp()
if psu.is_ovp():
print("OVP fault! Clearing...")
psu.clear_ovp()
# Clean up
psu.disable()
```
### Monitor Power Consumption
```python theme={null}
from lager import Net, NetType
import time
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.set_current(1.0)
psu.enable()
# Log power consumption
for sample in range(10):
v = psu.voltage()
i = psu.current()
p = psu.power()
print(f"V={v:.2f}V, I={i:.3f}A, P={p:.3f}W")
time.sleep(1)
psu.disable()
```
### Full State Inspection
```python theme={null}
from lager import Net, NetType
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.set_ovp(3.6)
psu.set_ocp(0.5)
psu.enable()
# Print comprehensive state
psu.get_full_state()
psu.disable()
```
## Supported Hardware
| Manufacturer | Model Series | Channels | Features |
| ------------ | -------------- | -------- | ----------------------------------- |
| Rigol | DP832 / DP832A | 3 | Ch1-2: 30V/3A, Ch3: 5V/3A |
| Rigol | DP821 | 2 | Ch1: 60V/1A, Ch2: 8V/10A |
| Rigol | DP811 / DP811A | 1 | 20V/10A or 40V/5A |
| Keithley | 2281S | 1 | 20V/6A/120W, battery simulator mode |
| Keysight | E36200 series | 2 | E36233A: 30V/20A per channel |
| Keysight | E36300 series | 3 | E36311A/12A/13A |
| EA | PSI/EL series | 1 | Two-quadrant operation |
## Notes
* Net must be configured as `NetType.PowerSupply`
* Call `enable()` to turn on the output after setting voltage/current
* Always call `disable()` when finished
* Protection faults automatically disable output; use `clear_ovp()` or `clear_ocp()` after addressing the fault
* OVP must be >= the voltage setpoint; setting a lower OVP will raise an error
* Voltage and current limits depend on hardware capabilities
* Multi-channel supplies: each channel is configured as a separate net
* `state()` and `get_full_state()` print to stdout; use `voltage()`, `current()`, `power()` to get values in code
# Thermocouple
Source: https://docs.lagerdata.com/source/reference/python/tc
Read temperature from thermocouple sensors
Read temperature measurements from thermocouple sensors.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| -------- | ---------------------- |
| `read()` | Read temperature in °C |
## Method Reference
### `Net.get(name, type=NetType.Thermocouple)`
Get a thermocouple net by name.
```python theme={null}
from lager import Net, NetType
tc = Net.get('TEMP_SENSOR', type=NetType.Thermocouple)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------------ |
| `name` | `str` | Name of the thermocouple net |
| `type` | `NetType` | Must be `NetType.Thermocouple` |
**Returns:** Thermocouple Net instance
### `read()`
Read the temperature from the thermocouple.
```python theme={null}
temp = tc.read()
print(f"Temperature: {temp}°C")
```
**Returns:** `float` - Temperature in degrees Celsius
## Examples
### Single Reading
```python theme={null}
from lager import Net, NetType
probe = Net.get('OVEN_PROBE', type=NetType.Thermocouple)
temp = probe.read()
print(f"Temperature: {temp:.1f}°C")
```
### Continuous Monitoring
```python theme={null}
from lager import Net, NetType
import time
tc = Net.get('FURNACE_TC', type=NetType.Thermocouple)
print("Monitoring temperature. Press Ctrl+C to exit.")
while True:
try:
temp = tc.read()
print(f"Temperature: {temp:.2f}°C")
time.sleep(1)
except KeyboardInterrupt:
print("Monitoring stopped.")
break
```
### Temperature Logging
```python theme={null}
from lager import Net, NetType
import time
import csv
tc = Net.get('DUT_TEMP', type=NetType.Thermocouple)
with open('temp_log.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['time', 'temperature'])
start = time.time()
for i in range(100):
elapsed = time.time() - start
temp = tc.read()
writer.writerow([elapsed, temp])
time.sleep(1)
print("Logging complete")
```
### Thermal Protection
```python theme={null}
from lager import Net, NetType
import time
tc = Net.get('BOARD_TEMP', type=NetType.Thermocouple)
psu = Net.get('VDD', type=NetType.PowerSupply)
MAX_TEMP = 85.0 # °C
psu.set_voltage(3.3)
psu.enable()
while True:
temp = tc.read()
print(f"Board temp: {temp:.1f}°C")
if temp > MAX_TEMP:
print("OVER TEMPERATURE! Shutting down.")
psu.disable()
break
time.sleep(1)
```
## Hardware Integration
| Hardware | Features |
| -------- | ----------------------------- |
| Phidget | K-type thermocouple interface |
## Notes
* Thermocouple nets work directly without `enable()`/`disable()` calls
* Temperature is returned in degrees Celsius
* Reading rate depends on thermocouple hardware
* Net names must match those configured on the Lager Box
# UART Net
Source: https://docs.lagerdata.com/source/reference/python/uart
High-level UART serial communication through Lager nets
Access UART serial ports through the Lager net abstraction for simplified device path resolution and connection management.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ---------------- | ------------------------------------------- |
| `get_path()` | Get the device path (e.g., `/dev/ttyUSB0`) |
| `connect()` | Connect and return a pyserial Serial object |
| `get_baudrate()` | Get the configured baudrate |
| `get_config()` | Get the raw net configuration |
## Method Reference
### `Net.get(name, type=NetType.UART)`
Get a UART net by name.
```python theme={null}
from lager import Net, NetType
uart = Net.get('DUT_SERIAL', type=NetType.UART)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ---------------------- |
| `name` | `str` | Name of the UART net |
| `type` | `NetType` | Must be `NetType.UART` |
**Returns:** `UARTNet` instance
### `get_path()`
Get the device path for the UART net.
```python theme={null}
uart = Net.get('DUT_SERIAL', type=NetType.UART)
device_path = uart.get_path()
print(f"Device: {device_path}") # e.g., "/dev/ttyUSB0"
```
**Returns:** `str` - Device path like `/dev/ttyUSB0`
**Raises:** `FileNotFoundError` if the UART device is not connected
### `connect(**overrides)`
Connect to the UART serial port with pyserial.
```python theme={null}
uart = Net.get('DUT_SERIAL', type=NetType.UART)
# Connect with default settings
ser = uart.connect()
# Connect with custom baudrate
ser = uart.connect(baudrate=9600, timeout=1.0)
# Use the pyserial connection
ser.write(b'AT\r\n')
response = ser.readline()
```
**Parameters:**
| Parameter | Type | Description |
| ---------- | ------- | ----------------------------------------- |
| `baudrate` | `int` | Baud rate (default from config or 115200) |
| `timeout` | `float` | Read timeout in seconds |
| `bytesize` | `int` | Data bits (5, 6, 7, or 8) |
| `parity` | `str` | Parity (`'N'`, `'E'`, `'O'`) |
| `stopbits` | `float` | Stop bits (1, 1.5, or 2) |
**Returns:** `serial.Serial` - Connected pyserial object
### `get_baudrate()`
Get the configured baudrate for this net.
```python theme={null}
uart = Net.get('DUT_SERIAL', type=NetType.UART)
baudrate = uart.get_baudrate()
print(f"Baudrate: {baudrate}")
```
**Returns:** `int` - Configured baudrate (default: 115200)
### `get_config()`
Get the raw net configuration dictionary.
```python theme={null}
uart = Net.get('DUT_SERIAL', type=NetType.UART)
config = uart.get_config()
print(config)
```
**Returns:** `dict` - Configuration dictionary
## Properties
| Property | Type | Description |
| ------------ | ------ | ------------------------------------------- |
| `name` | `str` | Net name |
| `usb_serial` | `str` | USB serial number for device identification |
| `channel` | `str` | Channel/port number |
| `params` | `dict` | Serial parameters (baudrate, etc.) |
## Examples
### Basic UART Communication
```python theme={null}
from lager import Net, NetType
# Get the UART net
uart = Net.get('DUT_SERIAL', type=NetType.UART)
# Connect with default settings
ser = uart.connect()
# Send command
ser.write(b'AT\r\n')
# Read response
response = ser.readline()
print(f"Response: {response.decode('utf-8').strip()}")
# Clean up
ser.close()
```
### Using Device Path Directly
```python theme={null}
from lager import Net, NetType
import serial
# Get the UART net
uart = Net.get('DUT_SERIAL', type=NetType.UART)
# Get device path for manual pyserial usage
device_path = uart.get_path()
print(f"Using device: {device_path}")
# Create your own serial connection
ser = serial.Serial(
port=device_path,
baudrate=115200,
timeout=5,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE
)
# Use the connection
ser.write(b'Hello\r\n')
ser.close()
```
### Interactive Console
```python theme={null}
from lager import Net, NetType
uart = Net.get('DUT_SERIAL', type=NetType.UART)
ser = uart.connect(baudrate=115200, timeout=0.1)
print("Interactive console (Ctrl+C to exit)")
print("-" * 40)
try:
while True:
# Read any available data
if ser.in_waiting:
data = ser.read(ser.in_waiting)
print(data.decode('utf-8', errors='ignore'), end='')
# Get user input (non-blocking would need additional handling)
# This is a simple example
except KeyboardInterrupt:
print("\nExiting...")
finally:
ser.close()
```
### Command/Response Pattern
```python theme={null}
from lager import Net, NetType
import time
def send_command(ser, command, timeout=2.0):
"""Send command and wait for response."""
ser.reset_input_buffer()
ser.write(f"{command}\r\n".encode('utf-8'))
response = ""
start = time.time()
while time.time() - start < timeout:
if ser.in_waiting:
data = ser.read(ser.in_waiting)
response += data.decode('utf-8', errors='ignore')
# Check for response terminator
if '\n' in response:
break
time.sleep(0.01)
return response.strip()
# Usage
uart = Net.get('DUT_SERIAL', type=NetType.UART)
ser = uart.connect(timeout=1.0)
# Query firmware version
version = send_command(ser, "AT+VER")
print(f"Firmware: {version}")
# Query status
status = send_command(ser, "AT+STATUS")
print(f"Status: {status}")
ser.close()
```
### Production Test with UART
```python theme={null}
from lager import Net, NetType
import time
def uart_loopback_test(net_name):
"""Test UART by sending data and verifying echo."""
uart = Net.get(net_name, type=NetType.UART)
ser = uart.connect(baudrate=115200, timeout=1.0)
test_data = b"LOOPBACK_TEST_12345"
try:
# Clear buffers
ser.reset_input_buffer()
ser.reset_output_buffer()
# Send test data
ser.write(test_data)
time.sleep(0.1)
# Read response
response = ser.read(len(test_data))
# Verify
if response == test_data:
print(f"PASS: Loopback test successful")
return True
else:
print(f"FAIL: Expected {test_data}, got {response}")
return False
finally:
ser.close()
# Run test
uart_loopback_test('DUT_SERIAL')
```
### Multi-Net UART Test
```python theme={null}
from lager import Net, NetType
# Test multiple UART nets
UART_NETS = ['UART1', 'UART2', 'DEBUG_SERIAL']
results = {}
for net_name in UART_NETS:
try:
uart = Net.get(net_name, type=NetType.UART)
path = uart.get_path()
baudrate = uart.get_baudrate()
ser = uart.connect()
ser.write(b'AT\r\n')
response = ser.readline()
ser.close()
results[net_name] = {
'path': path,
'baudrate': baudrate,
'status': 'OK' if response else 'NO_RESPONSE'
}
except Exception as e:
results[net_name] = {'status': 'ERROR', 'error': str(e)}
# Print results
for name, result in results.items():
print(f"{name}: {result}")
```
## UART vs Serial Module
The Lager Python SDK provides two ways to work with serial communication:
| Feature | UART Net (`NetType.UART`) | Serial (`pyserial`) |
| -------------------- | ------------------------------- | ------------------- |
| **Device Discovery** | Automatic via USB serial number | Manual device path |
| **Configuration** | Stored in Lager config | Manual in code |
| **Integration** | Full Lager net system | Standalone |
| **Best For** | Production tests, multi-device | Quick prototyping |
Use **UART Net** when:
* Device paths may change between reboots
* You need to identify devices by USB serial number
* You're using Lager's net configuration system
* Running automated production tests
Use **raw pyserial** when:
* You know the exact device path
* You need maximum flexibility
* You're doing quick debugging
## Hardware Integration
| Hardware | Description |
| ------------------- | --------------------------------- |
| USB-Serial Adapters | FTDI, CP2102, CH340, etc. |
| UART Bridges | Multi-port USB-UART converters |
| Built-in UART | Native UART on Lager Box hardware |
## Notes
* UART nets resolve device paths using USB serial numbers for consistent identification
* The `connect()` method returns a standard pyserial `Serial` object
* Default baudrate is 115200 if not specified in configuration
* Device paths are cached after first resolution
* Use `get_path()` if you need the raw device path for other tools
* Serial parameters from net configuration can be overridden in `connect()`
# USB Control
Source: https://docs.lagerdata.com/source/reference/python/usb
Control USB device power state
Control the power state of USB devices and ports on your testbed using USB hubs with per-port power control.
## Import
```python theme={null}
from lager import Net, NetType
# For exception handling
from lager import (
USBBackendError,
LibraryMissingError,
DeviceNotFoundError,
PortStateError
)
```
## Methods
| Method | Description |
| ----------- | ------------------------------------------------------------------------- |
| `enable()` | Enable (power on) USB port |
| `disable()` | Disable (power off) USB port |
| `toggle()` | Toggle USB port power state; returns the resulting state (`True`=enabled) |
| `state()` | Read the current power state without changing it (`True`=enabled) |
## Exception Classes
| Exception | Description |
| --------------------- | --------------------------------- |
| `USBBackendError` | Base class for USB hub errors |
| `LibraryMissingError` | Required vendor SDK not installed |
| `DeviceNotFoundError` | USB hub not found |
| `PortStateError` | Error changing port state |
## Method Reference
### `Net.get(name, type=NetType.Usb)`
Get a USB net by name.
```python theme={null}
from lager import Net, NetType
usb = Net.get('DUT_USB', type=NetType.Usb)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------- |
| `name` | `str` | Name of the USB net |
| `type` | `NetType` | Must be `NetType.Usb` |
**Returns:** USB Net instance
### `enable()`
Enable (power on) the USB port.
```python theme={null}
from lager import Net, NetType
usb = Net.get('CAMERA_USB', type=NetType.Usb)
usb.enable()
print("USB port powered on")
```
### `disable()`
Disable (power off) the USB port.
```python theme={null}
from lager import Net, NetType
usb = Net.get('CAMERA_USB', type=NetType.Usb)
usb.disable()
print("USB port powered off")
```
### `toggle()`
Toggle the power state of the USB port. Returns the resulting state
(`True` if now enabled, `False` if now disabled).
```python theme={null}
from lager import Net, NetType
usb = Net.get('SENSOR_USB', type=NetType.Usb)
now_on = usb.toggle() # On -> Off or Off -> On
```
### `state()`
Read the current power state of the USB port **without changing it**. Returns
`True` if the port is currently enabled (powered on), `False` if disabled. The
value is read live from the hub, so it always reflects the real port state.
```python theme={null}
from lager import Net, NetType
usb = Net.get('SENSOR_USB', type=NetType.Usb)
if not usb.state():
usb.enable() # only power on if it isn't already
```
## Examples
### Basic Power Control
```python theme={null}
from lager import Net, NetType
# Get USB net
usb = Net.get('CAMERA', type=NetType.Usb)
# Power on USB device
usb.enable()
print("Camera powered on")
# Power off USB device
usb.disable()
print("Camera powered off")
```
### Power Cycle Device
```python theme={null}
from lager import Net, NetType
import time
def power_cycle(net_name, delay=2):
"""Power cycle a USB device."""
usb = Net.get(net_name, type=NetType.Usb)
print(f"Power cycling {net_name}...")
usb.disable()
time.sleep(delay)
usb.enable()
print(f"{net_name} restarted")
power_cycle('DUT_USB')
```
### Error Handling
```python theme={null}
from lager import Net, NetType
from lager import (
USBBackendError,
LibraryMissingError,
DeviceNotFoundError,
PortStateError
)
try:
usb = Net.get('SENSOR_USB', type=NetType.Usb)
usb.enable()
print("Sensor powered on")
except LibraryMissingError:
print("USB hub SDK not installed")
except DeviceNotFoundError:
print("USB hub not found - check connection")
except PortStateError as e:
print(f"Port error: {e}")
except USBBackendError as e:
print(f"USB error: {e}")
```
### Automated Test Setup
```python theme={null}
from lager import Net, NetType
import time
def setup_test():
"""Power on all USB peripherals for testing."""
usb_devices = ['PROGRAMMER', 'SENSOR', 'DEBUGGER']
for name in usb_devices:
try:
usb = Net.get(name, type=NetType.Usb)
usb.enable()
print(f"{name} powered on")
except Exception as e:
print(f"Warning: {name} - {e}")
time.sleep(1) # Wait for USB enumeration
# Enable main power
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.set_voltage(3.3)
psu.enable()
return True
def teardown_test():
"""Power off all USB peripherals."""
# Disable main power first
psu = Net.get('VDD', type=NetType.PowerSupply)
psu.disable()
# Power off USB peripherals
for name in ['PROGRAMMER', 'SENSOR', 'DEBUGGER']:
try:
usb = Net.get(name, type=NetType.Usb)
usb.disable()
except Exception:
pass
```
### USB Device Reset
```python theme={null}
from lager import Net, NetType
import time
def reset_usb_device(net_name, reset_time=2, settle_time=3):
"""
Reset a USB device by power cycling.
Args:
net_name: USB net name
reset_time: Time to keep power off (seconds)
settle_time: Time to wait after power on (seconds)
"""
usb = Net.get(net_name, type=NetType.Usb)
print(f"Resetting {net_name}...")
# Power off
usb.disable()
print(f" Power off for {reset_time}s")
time.sleep(reset_time)
# Power on
usb.enable()
print(f" Power on, waiting {settle_time}s for enumeration")
time.sleep(settle_time)
print(f" {net_name} reset complete")
# Usage
reset_usb_device('DUT_USB', reset_time=2, settle_time=5)
```
### Toggle for Quick State Change
```python theme={null}
from lager import Net, NetType
import time
# Get USB net
usb = Net.get('LED_USB', type=NetType.Usb)
# Quick on/off cycle using toggle
for i in range(5):
usb.toggle()
time.sleep(0.5)
```
## Supported Hardware
| Hardware | Features |
| -------------------------- | ------------------------------------------------- |
| Acroname BrainStem USB Hub | Individual port power control, current monitoring |
| YKUSH USB Hub | Per-port power switching |
## Notes
* USB nets must be configured on the Lager Box with hub serial number and port mapping
* Power state changes take effect immediately
* Allow time for USB enumeration after powering on (\~1-3 seconds)
* Power cycling can be useful for device reset/recovery
* The `toggle()` function is useful for quick state changes
* Use exception handling for robust error recovery
# Watt Meter
Source: https://docs.lagerdata.com/source/reference/python/watt
Read power consumption
Measure power consumption from watt meter nets. Supports Yocto-Watt, Joulescope JS220, and Nordic PPK2 hardware.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Hardware | Description |
| ---------------- | ----------------------------- | ------------------------------------------------------ |
| `read()` | All | Read power in watts |
| `read_current()` | Joulescope JS220, Nordic PPK2 | Read current in amps |
| `read_voltage()` | Joulescope JS220, Nordic PPK2 | Read voltage in volts |
| `read_all()` | Joulescope JS220, Nordic PPK2 | Read current, voltage, and power in a single operation |
## Method Reference
### `Net.get(name, type=NetType.WattMeter)`
Get a watt meter net by name.
```python theme={null}
from lager import Net, NetType
power = Net.get('POWER_METER', type=NetType.WattMeter)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | --------------------------- |
| `name` | `str` | Name of the watt meter net |
| `type` | `NetType` | Must be `NetType.WattMeter` |
**Returns:** Watt meter Net instance
### `read()`
Read the current power consumption. Available on all watt meter hardware.
```python theme={null}
watts = power.read()
print(f"Power: {watts}W")
```
**Returns:** `float` - Power in watts
### `read_current()`
Read the current in amps. **Joulescope JS220 and Nordic PPK2.**
```python theme={null}
amps = power.read_current()
print(f"Current: {amps}A")
```
**Returns:** `float` - Current in amps
### `read_voltage()`
Read the voltage in volts. **Joulescope JS220 and Nordic PPK2.**
```python theme={null}
volts = power.read_voltage()
print(f"Voltage: {volts}V")
```
**Returns:** `float` - Voltage in volts
### `read_all()`
Read current, voltage, and power in a single atomic measurement. **Joulescope JS220 and Nordic PPK2.** This is more efficient than calling `read_current()`, `read_voltage()`, and `read()` separately, as all values come from the same sample window.
```python theme={null}
measurements = power.read_all()
print(f"Current: {measurements['current']}A")
print(f"Voltage: {measurements['voltage']}V")
print(f"Power: {measurements['power']}W")
```
**Returns:** `dict` with keys:
| Key | Type | Description |
| ----------- | ------- | ---------------- |
| `"current"` | `float` | Current in amps |
| `"voltage"` | `float` | Voltage in volts |
| `"power"` | `float` | Power in watts |
## Examples
### Basic Power Reading
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.WattMeter)
watts = power.read()
print(f"Power consumption: {watts:.3f}W")
```
### Joulescope Full Measurement
```python theme={null}
from lager import Net, NetType
power = Net.get('DEVICE_POWER', type=NetType.WattMeter)
# Read all measurements at once (Joulescope JS220 and Nordic PPK2)
data = power.read_all()
print(f"Voltage: {data['voltage']:.3f}V")
print(f"Current: {data['current']:.6f}A")
print(f"Power: {data['power']:.3f}W")
```
### Power Profiling
```python theme={null}
from lager import Net, NetType
import time
power = Net.get('DEVICE_POWER', type=NetType.WattMeter)
measurements = []
# Take 60 seconds of measurements
for i in range(60):
watts = power.read()
measurements.append(watts)
print(f"[{i:3d}s] {watts:.3f}W")
time.sleep(1)
# Statistics
avg = sum(measurements) / len(measurements)
max_p = max(measurements)
min_p = min(measurements)
print(f"\nAverage: {avg:.3f}W")
print(f"Maximum: {max_p:.3f}W")
print(f"Minimum: {min_p:.3f}W")
```
### Battery Life Estimation
```python theme={null}
from lager import Net, NetType
import time
power = Net.get('POWER', type=NetType.WattMeter)
# Average over multiple readings
readings = []
for _ in range(10):
readings.append(power.read())
time.sleep(0.1)
avg_power = sum(readings) / len(readings)
# Estimate battery life (1000mAh @ 3.7V = 3.7Wh)
battery_wh = 3.7
hours = battery_wh / avg_power if avg_power > 0 else float('inf')
print(f"Average power: {avg_power:.3f}W")
print(f"Est. battery life: {hours:.1f} hours")
```
### Power Limit Verification
```python theme={null}
from lager import Net, NetType
import time
def verify_power_limits(min_watts, max_watts):
power = Net.get('DEVICE_POWER', type=NetType.WattMeter)
# Average multiple readings
readings = []
for _ in range(10):
readings.append(power.read())
time.sleep(0.1)
avg = sum(readings) / len(readings)
if min_watts <= avg <= max_watts:
print(f"PASS: {avg:.3f}W in range [{min_watts}, {max_watts}]")
return True
else:
print(f"FAIL: {avg:.3f}W outside range [{min_watts}, {max_watts}]")
return False
# Test
verify_power_limits(0.1, 5.0)
```
### Sleep Current Verification (Joulescope)
```python theme={null}
from lager import Net, NetType
import time
power = Net.get('DEVICE_POWER', type=NetType.WattMeter)
# Put device in sleep mode (external trigger)
# set_device_sleep_mode()
time.sleep(2) # Wait for stabilization
# Use read_all() for atomic measurement (Joulescope JS220)
data = power.read_all()
sleep_current_ua = data['current'] * 1e6
print(f"Sleep voltage: {data['voltage']:.3f}V")
print(f"Sleep current: {sleep_current_ua:.1f}uA")
print(f"Sleep power: {data['power']:.6f}W")
if sleep_current_ua < 100:
print("PASS: Sleep current below 100uA")
else:
print("FAIL: Sleep current exceeds limit")
```
## Supported Hardware
| Manufacturer | Model | Measurement | Features |
| -------------------- | ---------- | ----------------------- | ------------------------------------------- |
| Yoctopuce | Yocto-Watt | Power only | Instantaneous reading |
| Joulescope | JS220 | Power, voltage, current | 0.1s averaged, atomic multi-measurement |
| Nordic Semiconductor | PPK2 | Power, voltage, current | 0.3s averaged, source mode constant voltage |
### Hardware Feature Comparison
| Feature | Yocto-Watt | Joulescope JS220 | Nordic PPK2 |
| ------------------ | ------------- | ------------------- | ------------------- |
| `read()` (power) | Yes | Yes | Yes |
| `read_current()` | No | Yes | Yes |
| `read_voltage()` | No | Yes | Yes |
| `read_all()` | No | Yes | Yes |
| Measurement method | Instantaneous | 0.1s averaged | 0.3s averaged |
| Device selection | Channel-based | Serial number-based | Serial number-based |
## Notes
* Power is returned in watts (W)
* Joulescope JS220 averages measurements over 0.1 seconds for higher precision
* Nordic PPK2 averages measurements over 0.3 seconds; operates in source mode (supplies a configurable voltage 0.8–5V), so voltage readings reflect the configured value, not an independent measurement
* `read_current()`, `read_voltage()`, and `read_all()` are available on the Joulescope JS220 and Nordic PPK2; calling them on a Yocto-Watt will raise an error
* For current on Yocto-Watt: calculate from power and known voltage (I = P / V)
* Use multiple readings and averaging for stability
* Allow settling time after device state changes
# Webcam
Source: https://docs.lagerdata.com/source/reference/python/webcam
Webcam streaming and video capture for visual inspection
Stream video from webcams attached to the Lager Box for visual inspection, automated vision testing, and remote monitoring.
## Import
```python theme={null}
from lager import Net, NetType
```
## Methods
| Method | Description |
| ------------------ | ----------------------------------- |
| `start(box_ip)` | Start a webcam stream |
| `stop()` | Stop the webcam stream |
| `get_info(box_ip)` | Get info about the stream |
| `get_url(box_ip)` | Get just the URL for the stream |
| `is_active()` | Check if stream is currently active |
## Method Reference
### `Net.get(name, type=NetType.Webcam)`
Get a webcam net by name.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
```
**Parameters:**
| Parameter | Type | Description |
| --------- | --------- | ------------------------ |
| `name` | `str` | Name of the webcam net |
| `type` | `NetType` | Must be `NetType.Webcam` |
**Returns:** Webcam Net instance
### `start(box_ip)`
Start a webcam video stream.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
result = webcam.start(box_ip='')
print(f"Stream URL: {result['url']}")
print(f"Port: {result['port']}")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | --------------------------------------- |
| `box_ip` | `str` | Lager Box IP address for URL generation |
**Returns:** `dict` with keys:
* `url` - Full stream URL (e.g., `http://:8081/`)
* `port` - Port number for the stream
* `already_running` - Boolean indicating if stream was already active
**Raises:** `RuntimeError` if device is already in use or not found
### `stop()`
Stop the webcam stream.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
stopped = webcam.stop()
if stopped:
print("Stream stopped")
else:
print("Stream was not running")
```
**Returns:** `bool` - True if stopped successfully, False if not running
### `get_info(box_ip)`
Get information about the stream.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
info = webcam.get_info(box_ip='')
if info:
print(f"URL: {info['url']}")
print(f"Port: {info['port']}")
print(f"Device: {info['video_device']}")
else:
print("Stream not active")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | -------------------- |
| `box_ip` | `str` | Lager Box IP address |
**Returns:** `dict` or `None` - Stream info dict or None if not running
### `get_url(box_ip)`
Get just the URL for the stream.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
url = webcam.get_url(box_ip='')
if url:
print(f"Stream at: {url}")
```
**Parameters:**
| Parameter | Type | Description |
| --------- | ----- | -------------------- |
| `box_ip` | `str` | Lager Box IP address |
**Returns:** `str` or `None` - Stream URL or None if not running
### `is_active()`
Check if the stream is currently active.
```python theme={null}
from lager import Net, NetType
webcam = Net.get('camera1', type=NetType.Webcam)
if webcam.is_active():
print("Stream is running")
else:
print("Stream is stopped")
```
**Returns:** `bool` - True if stream is running, False otherwise
## Examples
### Start Multiple Cameras
```python theme={null}
from lager import Net, NetType
# Lager Box IP
BOX_IP = ''
# Start multiple camera streams
cameras = ['overview', 'microscope', 'solder_station']
for name in cameras:
try:
webcam = Net.get(name, type=NetType.Webcam)
result = webcam.start(BOX_IP)
print(f"{name}: {result['url']}")
except RuntimeError as e:
print(f"{name}: Failed - {e}")
```
### Stream Management
```python theme={null}
from lager import Net, NetType
BOX_IP = ''
def start_camera(name):
"""Start a camera stream."""
try:
webcam = Net.get(name, type=NetType.Webcam)
result = webcam.start(BOX_IP)
if result['already_running']:
print(f"Stream '{name}' was already running at {result['url']}")
else:
print(f"Started '{name}' at {result['url']}")
except RuntimeError as e:
print(f"Failed to start '{name}': {e}")
def stop_camera(name):
"""Stop a camera stream."""
webcam = Net.get(name, type=NetType.Webcam)
if webcam.stop():
print(f"Stopped '{name}'")
else:
print(f"Stream '{name}' was not running")
def check_camera(name):
"""Check camera status."""
webcam = Net.get(name, type=NetType.Webcam)
if webcam.is_active():
info = webcam.get_info(BOX_IP)
print(f"{name}: Running at {info['url']}")
else:
print(f"{name}: Stopped")
# Usage
start_camera('main')
check_camera('main')
stop_camera('main')
```
### Visual Inspection Test
```python theme={null}
from lager import Net, NetType
import time
BOX_IP = ''
def visual_inspection_test(camera_name, inspection_callback):
"""
Start camera stream and wait for operator inspection.
Args:
camera_name: Webcam net name
inspection_callback: Function to handle the stream URL
Returns:
bool: True if inspection passed
"""
# Start stream
webcam = Net.get(camera_name, type=NetType.Webcam)
result = webcam.start(BOX_IP)
stream_url = result['url']
print(f"Visual inspection stream: {stream_url}")
# Notify external system (could open browser, send to UI, etc.)
inspection_callback(stream_url)
# Wait for inspection (in real usage, this would wait for operator input)
print("Waiting for visual inspection...")
time.sleep(10) # Placeholder
# Stop stream
webcam.stop()
# Return result (would come from operator in real usage)
return True
# Usage
def handle_url(url):
print(f"Open in browser: {url}")
result = visual_inspection_test('inspection_cam', handle_url)
print(f"Inspection result: {'PASS' if result else 'FAIL'}")
```
### Camera Discovery and Testing
```python theme={null}
from lager import Net, NetType
import os
BOX_IP = ''
def discover_cameras():
"""Find all available video devices."""
cameras = []
for i in range(10): # Check video0 through video9
device = f'/dev/video{i}'
if os.path.exists(device):
cameras.append(device)
return cameras
def test_camera(net_name):
"""Test if a camera net works."""
try:
webcam = Net.get(net_name, type=NetType.Webcam)
result = webcam.start(BOX_IP)
print(f"{net_name}: OK - {result['url']}")
webcam.stop()
return True
except RuntimeError as e:
print(f"{net_name}: FAIL - {e}")
return False
# Discover and test all cameras
print("Discovering cameras...")
devices = discover_cameras()
print(f"Found {len(devices)} video devices")
# Test configured nets (assumes you have webcam nets configured)
test_camera('camera1')
test_camera('camera2')
```
### Already Running Detection
```python theme={null}
from lager import Net, NetType
BOX_IP = ''
webcam = Net.get('camera1', type=NetType.Webcam)
# Start stream first time
result1 = webcam.start(BOX_IP)
print(f"First start: already_running = {result1['already_running']}")
# Try to start again - should detect it's already running
result2 = webcam.start(BOX_IP)
print(f"Second start: already_running = {result2['already_running']}")
# Check if active
print(f"Is active: {webcam.is_active()}")
# Cleanup
webcam.stop()
```
## Web Interface
Each stream provides a web interface at its URL with:
* Live MJPEG video stream
* Zoom controls (+, -, Reset)
* FPS display
* Sidebar with links to other active streams
### API Endpoints
| Endpoint | Method | Description |
| ----------------- | ------ | --------------------------- |
| `/` | GET | HTML page with video viewer |
| `/stream` | GET | Raw MJPEG video stream |
| `/api/zoom` | GET | Get current zoom level |
| `/api/zoom/in` | POST | Increase zoom |
| `/api/zoom/out` | POST | Decrease zoom |
| `/api/zoom/reset` | POST | Reset zoom to 1.0x |
| `/api/fps` | GET | Get current FPS |
| `/api/streams` | GET | List all active streams |
| `/test` | GET | Health check endpoint |
## Hardware Requirements
| Requirement | Description |
| ------------- | -------------------------- |
| USB Webcams | UVC-compatible cameras |
| Video Devices | `/dev/video*` device files |
| OpenCV | Required for video capture |
## Notes
* Webcam nets must be configured on the Lager Box with video device path
* Streams run on ports starting from 8081
* Each stream uses a separate port, automatically allocated
* Streams persist until explicitly stopped or the process dies
* Dead stream processes are automatically cleaned up
* Only one stream can use a video device at a time
* Default resolution is 640x480 at 30 FPS
* JPEG quality is set to 80 for bandwidth/quality balance
* Streams are accessible via HTTP from any network the Lager Box is on
* Zoom is digital (crop and scale), not optical
# WiFi
Source: https://docs.lagerdata.com/source/reference/python/wifi
WiFi network management
Manage WiFi network connections on Lager Boxes. WiFi operations are box-level functions — they manage the box's own wireless interface, not a test net on a PCB.
## Import
```python theme={null}
from lager.protocols.wifi import scan_wifi, connect_to_wifi, get_wifi_status, disconnect_wifi
```
## Function Reference
| Function | Description |
| -------------------------------------------- | ---------------------------------- |
| `scan_wifi(interface)` | Scan for available WiFi networks |
| `connect_to_wifi(ssid, password, interface)` | Connect to a WiFi network |
| `get_wifi_status()` | Get current WiFi connection status |
| `disconnect_wifi(interface)` | Disconnect from WiFi network |
### `scan_wifi(interface='wlan0')`
Scan for available WiFi networks.
```python theme={null}
from lager.protocols.wifi import scan_wifi
result = scan_wifi()
networks = result.get('access_points', [])
for network in networks:
print(f"{network['ssid']}: {network['strength']}%")
```
**Parameters:**
| Parameter | Type | Default | Description |
| ----------- | ----- | --------- | ---------------------------- |
| `interface` | `str` | `'wlan0'` | Network interface to scan on |
**Returns:** `dict` with key `access_points` containing a list of network dicts, each with:
* `ssid` - Network name
* `strength` - Signal strength as percentage (0-100)
* `security` - Security type ('Open' or 'Secured')
### `connect_to_wifi(ssid, password, interface='wlan0')`
Connect to a WiFi network.
```python theme={null}
from lager.protocols.wifi import connect_to_wifi
result = connect_to_wifi('MyNetwork', 'secret123')
if result['success']:
print(f"Connected: {result['message']}")
else:
print(f"Failed: {result['error']}")
```
**Parameters:**
| Parameter | Type | Default | Description |
| ----------- | ----- | --------- | ------------------------------------------------- |
| `ssid` | `str` | | Network name |
| `password` | `str` | | Network password (empty string for open networks) |
| `interface` | `str` | `'wlan0'` | Network interface to use |
**Returns:** `dict` with keys:
* `success` - Boolean indicating connection success
* `message` - Success message (when `success` is True)
* `error` - Error message (when `success` is False)
### `get_wifi_status()`
Get current WiFi connection status for all interfaces.
```python theme={null}
from lager.protocols.wifi import get_wifi_status
interfaces = get_wifi_status()
for name, info in interfaces.items():
print(f"{name}: {info['state']} - {info['ssid']}")
```
**Returns:** `dict` keyed by interface name, each value containing:
* `interface` - Interface name
* `ssid` - Connected network name or 'Not Connected'
* `state` - 'Connected' or 'Disconnected'
### `disconnect_wifi(interface='wlan0')`
Disconnect from the current WiFi network.
```python theme={null}
from lager.protocols.wifi import disconnect_wifi
result = disconnect_wifi()
if result['success']:
print(f"Disconnected: {result['message']}")
else:
print(f"Failed: {result['error']}")
```
**Parameters:**
| Parameter | Type | Default | Description |
| ----------- | ----- | --------- | ------------------------------- |
| `interface` | `str` | `'wlan0'` | Network interface to disconnect |
**Returns:** `dict` with keys:
* `success` - Boolean indicating disconnect success
* `message` - Success message (when `success` is True)
* `error` - Error message (when `success` is False)
## Router Internet Access Control
The `Wifi` net type controls internet access via an Asus router's parental control feature. This is a separate concern from box-level WiFi management — it blocks/unblocks a device's internet access by MAC address.
```python theme={null}
from lager import Net, NetType
# Requires a wifi net configured with router credentials
wifi = Net.get('wifi1', type=NetType.Wifi)
wifi.disable() # Block internet access (parental control)
wifi.enable() # Restore internet access
```
## Examples
### Scan and Connect
```python theme={null}
from lager.protocols.wifi import scan_wifi, connect_to_wifi, get_wifi_status
import time
# Scan for networks
result = scan_wifi()
networks = result.get('access_points', [])
# Find target network
for network in networks:
if network['ssid'] == 'TestNetwork':
print(f"Found: {network['strength']}% signal")
break
# Connect
result = connect_to_wifi('TestNetwork', 'password123')
if result['success']:
print(f"Connection successful: {result['message']}")
else:
print(f"Connection failed: {result['error']}")
# Wait for connection to stabilize
time.sleep(5)
# Verify status
interfaces = get_wifi_status()
for name, info in interfaces.items():
if info['state'] == 'Connected':
print(f"Connected to {info['ssid']} on {name}")
```
### Network Verification Test
```python theme={null}
from lager.protocols.wifi import scan_wifi
def verify_network_visible(expected_ssid):
result = scan_wifi()
networks = result.get('access_points', [])
ssids = [n['ssid'] for n in networks]
if expected_ssid in ssids:
print(f"PASS: {expected_ssid} is visible")
return True
else:
print(f"FAIL: {expected_ssid} not found")
return False
```
### Signal Strength Test
```python theme={null}
from lager.protocols.wifi import scan_wifi
def check_signal_strength(ssid, min_strength=50):
"""Check if signal strength meets minimum threshold (0-100%)"""
result = scan_wifi()
networks = result.get('access_points', [])
for network in networks:
if network['ssid'] == ssid:
strength = network['strength']
if strength >= min_strength:
print(f"PASS: {ssid} signal {strength}%")
return True
else:
print(f"FAIL: {ssid} signal {strength}% below {min_strength}%")
return False
print(f"FAIL: {ssid} not found")
return False
```
### Connection Test
```python theme={null}
from lager.protocols.wifi import connect_to_wifi, get_wifi_status
import time
def test_wifi_connection(ssid, password):
result = connect_to_wifi(ssid, password)
if not result['success']:
print(f"FAIL: Connection error - {result['error']}")
return False
time.sleep(5)
interfaces = get_wifi_status()
for name, info in interfaces.items():
if info['state'] == 'Connected' and info['ssid'] == ssid:
print(f"PASS: Connected to {ssid}")
return True
print(f"FAIL: Not connected to {ssid}")
return False
```
## Hardware Requirements
| Requirement | Description |
| ------------------ | ------------------------- |
| WiFi Hardware | USB adapter or built-in |
| Permissions | Root/sudo access required |
| Supported Security | WPA2, WPA3, Open |
## Notes
* Lager Box must have WiFi hardware
* Root/sudo access required for most operations
* WPA2/WPA3 networks supported
* Open networks require empty password string (`''`)
* Interface defaults to 'wlan0'
* `get_wifi_status()` takes no parameters and returns all interfaces
* Router management (enable/disable) requires Asus router with parental control and a configured wifi net
# Authentication
Source: https://docs.lagerdata.com/source/reference/rust/auth
Using the Rust crate with boxes behind an authenticating gateway
Plain (ungated) boxes need none of this: no header is sent and none of
this code runs. This page applies to deployments that place an
authenticating reverse proxy (a *gateway*) in front of a box, which
rejects unauthenticated traffic with 401 + an `X-Gateway-Auth-Url` header
— the same contract the Lager CLI speaks.
The crate handles gated boxes transparently, in two modes.
## CLI session reuse (zero config)
If you've run `lager login ` on the machine, the crate picks up
that session automatically:
* Reads the CLI's token store (`~/.lager_gateway_auth`, overridable via
`LAGER_GATEWAY_AUTH_FILE`).
* Attaches `Authorization: Bearer` to every request — including
debug-service traffic and UART Socket.IO handshakes.
* Refreshes expired access tokens transparently.
* Learns which auth server fronts a box from the gateway's discovery
header on first contact, and retries the denied request within the same
call.
No code changes needed — `LagerBox::from_env()` just works:
```sh theme={null}
lager login https://auth.example.com
LAGER_BOX_HOST=192.168.1.42 cargo test
```
## Pinned token (CI)
On machines with no CLI login (CI runners), supply a token directly:
```rust theme={null}
let lager = lager::LagerBox::builder("192.168.1.42")
.bearer_token(std::env::var("MY_CI_TOKEN").unwrap())
.build()?;
```
or set the `LAGER_GATEWAY_TOKEN` environment variable — no code change:
```yaml theme={null}
env:
LAGER_BOX_HOST: ${{ vars.LAGER_BOX_HOST }}
LAGER_GATEWAY_TOKEN: ${{ secrets.LAGER_GATEWAY_TOKEN }}
```
A pinned token is attached verbatim to every request and is never
refreshed or written to the token store. If the gateway rejects it, the
call fails immediately.
## Errors
When a gateway asks for auth and no usable credential exists, calls fail
with `Error::AuthRequired`, which names the auth server to log into:
| Gateway response | Crate behavior |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| 401 (no/expired credential) | Resolve or refresh a token and retry once; otherwise `Error::AuthRequired` with the `lager login ` fix |
| 403 (no access grant for this box) | `Error::Box { status: 403, .. }` — ask your admin for access |
| 503 (gateway can't reach its auth server) | `Error::Box { status: 503, .. }` — retry shortly |
## Environment variables
| Variable | Meaning |
| ------------------------- | --------------------------------------------------------------------- |
| `LAGER_GATEWAY_TOKEN` | Pinned bearer token for every request (CI). |
| `LAGER_GATEWAY_AUTH_FILE` | Overrides the CLI token store path (default `~/.lager_gateway_auth`). |
The full client/gateway contract (discovery header, auth server endpoints,
store schema, retry semantics) is specified in the monorepo at
[`docs/reference/gateway-auth-contract.md`](https://github.com/lagerdata/lager/blob/main/docs/reference/gateway-auth-contract.md).
# Debug Probes & UART
Source: https://docs.lagerdata.com/source/reference/rust/debug-and-uart
Flash firmware, stream RTT logs, and drive serial from Rust tests
The two workflows embedded developers usually evaluate first: driving a
debug probe (flash / erase / reset / memory reads / RTT) and streaming
UART. Both are fully supported by the crate.
## Flashing and memory access
`DebugNet` drives a J-Link/OpenOCD debug probe through the box's debug
service (port 8765, published on the box host):
```rust theme={null}
let lager = lager::LagerBox::from_env()?;
let debug = lager.debug("debug1");
debug.connect()?;
debug.erase()?;
debug.flash("firmware.hex")?; // .hex/.elf/.bin inferred from extension
debug.reset(false)?; // false = run after reset
let head = debug.read_memory(0x0800_0000, 16)?;
println!("vector table: {head:02x?}");
```
`flash_bin(path, address)` places a raw binary at an explicit address, and
`flash_bytes` uploads an in-memory image — useful when your test builds
firmware variants on the fly. `info()` and `status()` report probe and
target state.
## RTT log streaming
`rtt()` returns a blocking byte stream implementing `std::io::Read`, so
asserting on target output is plain std I/O:
```rust theme={null}
use std::io::{BufRead, BufReader};
let mut lines = BufReader::new(debug.rtt()?).lines();
assert!(lines.next().transpose()?.unwrap().contains("boot ok"));
```
## Tunneled debug service
If the debug service is reached through an SSH tunnel rather than directly,
point the crate at it:
```rust theme={null}
let lager = lager::LagerBox::builder("192.168.1.42")
.debug_service_url("http://127.0.0.1:8765")
.build()?;
```
or set the `LAGER_DEBUG_SERVICE_URL` environment variable.
## Streaming UART
Enable the `uart` feature:
```toml theme={null}
lager = { package = "lager-net", version = "0.2", features = ["uart"] }
```
A `Uart` session streams over the box's Socket.IO `/uart` namespace.
`wait_for` accumulates output until a needle appears (bytes after the
needle stay buffered for the next read), which makes boot assertions
one-liners:
```rust theme={null}
use std::time::Duration;
let mut uart = lager.uart("DUT_CONSOLE")?;
// Reboot the DUT and assert on its boot banner.
debug.reset(false)?;
let banner = uart.wait_for(b"boot ok", Duration::from_secs(10))?;
println!("{}", String::from_utf8_lossy(&banner));
// Talk to the DUT's shell.
uart.write_str("version\r\n")?;
let reply = uart.read(Duration::from_secs(2))?;
uart.stop()?;
```
The session reports adapter re-enumeration (hub power-cycle, DUT reflash)
via `last_status()` — `"reconnecting"` / `"reconnected"` — and keeps
streaming across it, so power-cycling tests don't need to reopen the port.
# Net Types
Source: https://docs.lagerdata.com/source/reference/rust/net-types
Every net handle and box-level capability the Rust crate exposes
A `LagerBox` hands out lightweight, cloneable handles keyed by net name.
Constructing a handle does no I/O — the first method call does — so handles
are cheap to create in test setup.
Method-level documentation for every handle lives on
[docs.rs/lager-net](https://docs.rs/lager-net); the tables below map each
net type to its constructor and highlights.
## Instrument nets
| Handle | Constructor | Highlights |
| --------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [`Supply`](https://docs.rs/lager-net/latest/lager/nets/supply/) | `lager.supply(name)` | `set_voltage`, `set_current`, `enable`/`disable`, OCP/OVP, `state()` |
| [`Battery`](https://docs.rs/lager-net/latest/lager/nets/battery/) | `lager.battery(name)` | `set_soc`, `set_voc`, model/capacity/mode, `state()` |
| [`Eload`](https://docs.rs/lager-net/latest/lager/nets/eload/) | `lager.eload(name)` | `set(EloadMode::Cc, 0.5)`, `setpoint`, `state()` |
| [`Solar`](https://docs.rs/lager-net/latest/lager/nets/solar/) | `lager.solar(name)` | `set`/`stop` PV simulation, `irradiance`, `voc`, `mpp_voltage`/`mpp_current`, `resistance`, `temperature` |
| [`Gpio`](https://docs.rs/lager-net/latest/lager/nets/gpio/) | `lager.gpio(name)` | `input`, `output`, `toggle`, `wait_for_level` (hardware-timed) |
| [`Adc`](https://docs.rs/lager-net/latest/lager/nets/adc/) / [`Dac`](https://docs.rs/lager-net/latest/lager/nets/dac/) | `lager.adc(name)` / `lager.dac(name)` | `read()`; `set(volts)` |
| [`Thermocouple`](https://docs.rs/lager-net/latest/lager/nets/thermocouple/) | `lager.thermocouple(name)` | `read()` in °C |
| [`WattMeter`](https://docs.rs/lager-net/latest/lager/nets/watt/) | `lager.watt_meter(name)` | `power/current/voltage/all(duration)` |
| [`EnergyAnalyzer`](https://docs.rs/lager-net/latest/lager/nets/energy/) | `lager.energy_analyzer(name)` | `read_energy`, `read_stats` |
| [`Spi`](https://docs.rs/lager-net/latest/lager/nets/spi/) | `lager.spi(name)` | `configure`, `read`, `write`, `read_write`, `transfer` |
| [`I2c`](https://docs.rs/lager-net/latest/lager/nets/i2c/) | `lager.i2c(name)` | `configure`, `scan`, `read`, `write`, `write_read` |
| [`UsbPort`](https://docs.rs/lager-net/latest/lager/nets/usb/) | `lager.usb(name)` | `enable`/`disable`/`toggle`/`state` |
| [`Arm`](https://docs.rs/lager-net/latest/lager/nets/arm/) | `lager.arm(name)` | `position`, `move_to`/`move_by`, `go_home`, motor enable/disable, `set_acceleration` |
| [`Webcam`](https://docs.rs/lager-net/latest/lager/nets/webcam/) | `lager.webcam(name)` | `start`/`stop` MJPEG stream, `url`, `status` |
| [`Router`](https://docs.rs/lager-net/latest/lager/nets/router/) | `lager.router(name)` | `system_info`, interfaces/clients/leases, `block_internet`, generic `command(action, params)` |
| [`DebugNet`](https://docs.rs/lager-net/latest/lager/nets/debug/) | `lager.debug(name)` | `connect`, `flash`, `erase`, `reset`, `read_memory`, `info`/`status`, `rtt` (blocking) |
| [`Uart`](https://docs.rs/lager-net/latest/lager/nets/uart/) | `lager.uart(name)?` *(feature `uart`)* | streaming `read`, `write`, `wait_for(b"boot ok", ...)` |
See **[Debug probes & UART](/source/reference/rust/debug-and-uart)** for
worked examples of the last two.
## Box-level capabilities
These drive the box's own hardware and take no net name:
| Handle | Constructor | Highlights |
| ------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------- |
| [`Ble`](https://docs.rs/lager-net/latest/lager/nets/ble/) | `lager.ble()` | `scan`/`scan_named`, `info`/`connect` (GATT enumeration), `disconnect` |
| [`Wifi`](https://docs.rs/lager-net/latest/lager/nets/wifi/) | `lager.wifi()` | `status`, `scan`, `connect(ssid, password)`, `delete` |
| [`Blufi`](https://docs.rs/lager-net/latest/lager/nets/blufi/) | `lager.blufi()` | `scan`, `connect`, `provision(device, ssid, password)`, `wifi_scan`, `status`, `version` |
## Discovery and box health
```rust theme={null}
let lager = lager::LagerBox::from_env()?;
// All nets configured on the box.
for net in lager.nets()? {
println!("{} ({})", net.name, net.role);
}
// Box health and capability probing.
lager.health()?;
let status = lager.status()?;
assert!(status.capabilities.net_command, "box image too old — run: lager box update");
```
`status().capabilities` also reports `net_command_roles` (the newer
arm/webcam/router roles), `ble_command`, `wifi_command`, and
`blufi_command`, so a suite can skip tests a box image doesn't support
instead of failing on them.
## Not yet available
`Scope` (oscilloscope / logic analyzer) is a documented stub whose methods
return `Error::NotSupportedByBox` until the box exposes those workflows
over its HTTP API. `Rotation`/`Actuate` nets and net CRUD are out of the
crate's scope for now — see the crate's
[`MISSING_ENDPOINTS.md`](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md).
# Rust SDK Overview
Source: https://docs.lagerdata.com/source/reference/rust/overview
Write your entire hardware-in-the-loop test suite in Rust and run it with cargo test
The Lager Rust crate gives embedded developers first-class access to Lager
nets, so a hardware-in-the-loop (HIL) test suite can live next to your
firmware and run with `cargo test` — no Python required.
The crate is a pure HTTP/JSON client of the Lager Box API: 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.
The package publishes on crates.io as
[**`lager-net`**](https://crates.io/crates/lager-net) (the bare `lager`
name is taken by an unrelated crate), but the library target is named
`lager`, so your code reads `use lager::LagerBox;`. Full API reference
lives on [docs.rs/lager-net](https://docs.rs/lager-net).
## Quickstart
Add the crate to your firmware project's dev-dependencies:
```toml theme={null}
# Cargo.toml
[dev-dependencies]
lager = { package = "lager-net", version = "0.2" }
```
Write a test:
```rust theme={null}
// 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()
}
```
Run it:
```sh theme={null}
LAGER_BOX_HOST=192.168.1.42 cargo test
```
`LagerBox::connect("hostname-or-ip")` also works, with an optional
`host:port` or full URL.
## Features
| Feature | Default | What you get |
| ---------- | ------- | ------------------------------------------------------------------------------------------------ |
| `blocking` | yes | `LagerBox` on [`ureq`](https://crates.io/crates/ureq) — tiny dependency tree, no tokio |
| `async` | no | `AsyncLagerBox` on [`reqwest`](https://crates.io/crates/reqwest)/tokio; same methods, `.await`ed |
| `uart` | no | `Uart` streaming sessions over the box's Socket.IO `/uart` namespace |
Both clients execute the exact same request builders and response parsers,
so the two transports cannot drift apart.
```toml theme={null}
lager = { package = "lager-net", version = "0.2", features = ["async"] }
```
## Errors
Everything returns `lager::Result` with a single `Error` enum:
| Variant | Meaning |
| ------------------------- | -------------------------------------------------------------- |
| `Connection` | Box unreachable (network/Tailscale/box offline) |
| `Timeout` | The box stalled past the (already widened) budget |
| `Box { status, message }` | The box refused or the hardware failed |
| `UnsupportedByBox` | HTTP 501: the box image predates this endpoint; update the box |
| `AuthRequired` | The box's gateway wants a bearer token and none is available |
| `NotSupportedByBox` | The net type is a documented stub (see the note below) |
## Requirements
* A Lager Box with software new enough to serve `POST /net/command` —
check `lager.status()?.capabilities.net_command`, or run
`lager box update`.
* Rust 1.75+.
Oscilloscope / logic-analyzer workflows are not exposed on the box HTTP
API yet, so `Scope` ships as a documented stub whose methods return
`Error::NotSupportedByBox`. Support lands when the box API does; the
endpoint sketch is tracked in the crate's
[`MISSING_ENDPOINTS.md`](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md).
## Next steps
* **[Net types](/source/reference/rust/net-types)** — every handle the crate exposes
* **[Testing guide](/source/reference/rust/testing)** — structuring a `cargo test` HIL suite, parallelism, CI
* **[Debug probes & UART](/source/reference/rust/debug-and-uart)** — flashing, RTT, and streaming serial
* **[Authentication](/source/reference/rust/auth)** — boxes behind an authenticating gateway
# Testing with cargo test
Source: https://docs.lagerdata.com/source/reference/rust/testing
Structuring a Rust hardware-in-the-loop suite, parallelism, and CI
The crate is designed so your HIL suite is just ordinary Rust integration
tests: files under `tests/`, run by `cargo test`, living in the same
repository as your firmware.
## Structure
Point tests at a box with the `LAGER_BOX_HOST` environment variable and
construct the client with `LagerBox::from_env()`:
```rust theme={null}
// tests/power.rs
use lager::LagerBox;
#[test]
fn dut_draws_less_than_100ma_idle() -> lager::Result<()> {
let lager = LagerBox::from_env()?;
let supply = lager.supply("DUT_POWER");
supply.set_voltage(3.3)?;
supply.enable()?;
let current = supply.state()?.current.expect("supply reports current");
assert!(current < 0.1, "idle draw {current} A");
supply.disable()
}
```
```sh theme={null}
LAGER_BOX_HOST=192.168.1.42 cargo test
```
Group tests by net or by DUT feature — one file per concern (`boot.rs`,
`power.rs`, `sensors.rs`) keeps `cargo test ` filtering useful.
## Parallel tests and instrument safety
`cargo test` runs tests on multiple threads by default. That is safe at the
instrument level: 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 tests 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 (one test's
`disable()` is visible to another test reading the same supply). Either
partition nets across tests, or serialize:
```sh theme={null}
cargo test -- --test-threads=1
```
## Timeouts
Timeout budgets mirror the Lager CLI: quick commands use 10 s, and
operations that block on the box for a caller-controlled duration
(watt/energy integration windows, `wait_for_level`) widen or drop the
client timeout automatically, so a healthy long measurement is never
aborted mid-flight.
## Hermetic tests vs. hardware tests
Tests that always need real hardware should be marked `#[ignore]` so a
plain `cargo test` (on a laptop with no box, or in a PR check) stays green:
```rust theme={null}
#[test]
#[ignore = "requires a Lager box"]
fn flash_and_boot() -> lager::Result<()> {
// ...
}
```
Then opt in explicitly where a box is available:
```sh theme={null}
LAGER_BOX_HOST=192.168.1.42 cargo test -- --ignored
```
This is the convention the crate itself uses: its own suite is hermetic
(`cargo test` runs against a mock box), and its hardware smoke tests run
with `cargo test --test hardware -- --ignored`.
## CI
A minimal GitHub Actions job, assuming the runner can reach the box (e.g.
a self-hosted runner on the lab network or a Tailscale-connected runner):
```yaml theme={null}
jobs:
hil:
runs-on: [self-hosted, lab]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run HIL suite
env:
LAGER_BOX_HOST: ${{ vars.LAGER_BOX_HOST }}
# Only needed for boxes behind an authenticating gateway:
LAGER_GATEWAY_TOKEN: ${{ secrets.LAGER_GATEWAY_TOKEN }}
run: cargo test -- --ignored
```
For gateway-fronted boxes, see
**[Authentication](/source/reference/rust/auth)** for how the token is
attached and refreshed.
# Version 0.10.0
Source: https://docs.lagerdata.com/source/release-notes/v0.10.0
March 17, 2026
## Features
* **`lager router` command group** — manage routers as Lager nets
* **`lager router add-net`** — register a router (MikroTik hAP or compatible) as a net on a Lager Box
* **`lager router connect`** — verify connectivity to a router net
* **`lager router interfaces`** / **`lager router wireless-interfaces`** — inspect network and wireless interfaces
* **`lager router wireless-clients`** — list currently connected wireless clients
* **`lager router dhcp-leases`** — list devices that have received IP addresses from the router
* **`lager router system-info`** — query router CPU, memory, and uptime
* **`lager router reboot`** — reboot a router net
* **`lager router enable-interface`** / **`lager router disable-interface`** — toggle wireless interfaces on/off
* **`lager router block-internet`** — drop all forwarded traffic for network isolation testing
* **`lager router reset`** — restore a router to a clean baseline (removes test firewall rules, bandwidth limits, and access list entries; optionally re-applies a baseline SSID and WPA2 password)
* **`lager router run`** — make arbitrary REST API GET calls against the router
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.10.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.10.0/)
# Version 0.11.0
Source: https://docs.lagerdata.com/source/release-notes/v0.11.0
March 18, 2026
## Features
* **Nordic PPK2 instrument support** — The Nordic Semiconductor Power Profiler Kit II is now a supported watt-meter and energy-analyzer instrument, alongside the Joulescope JS220 and Yocto-Watt
* `lager watt --box ` reads instantaneous power (watts) from a PPK2
* `lager energy read --box --duration ` integrates energy (J, Wh) and charge (C, Ah) over a configurable duration
* `lager energy stats --box --duration ` computes mean/min/max/std statistics for current, voltage, and power
* PPK2 devices are auto-detected by `lager instruments` and can be added with `lager nets add-all`
* Full Python API via `Net.get(name, type=NetType.WattMeter)` and `Net.get(name, type=NetType.EnergyAnalyzer)`
## Bug Fixes
* Fixed webcam MJPEG stream returning 404 for dashboard `/stream/{netName}` requests
* Fixed `Net.get()` not resolving instrument location when saved net config uses `address` instead of `location`
## Improvements
* `lager energy` command now uses consistent argument order: `lager energy --options`
* Cleaner formatted output for energy read and stats commands
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.11.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.11.0/)
# Version 0.12.0
Source: https://docs.lagerdata.com/source/release-notes/v0.12.0
March 20, 2026
> Historical note: `lager boxes connect` was later removed from open-source Lager once downstream control planes became the canonical box bootstrap and orchestration layer.
## Features
* **Command-in-progress lock** — When a `lager` command is running on a Lager Box, all other commands are automatically blocked with a clear error message, including from the same user. Locks auto-expire after 30 minutes to handle crashed CLI processes
* **User lock (`lager boxes lock/unlock`)** — Explicitly lock a Lager Box so only you can run commands on it. Other users see a lock error until you unlock. The user who locked it can still run commands
* `--force-command` global flag to bypass command-in-progress locks
* `lager boxes` list now shows "locked by" and "busy" columns when applicable
* `lager python --kill`, `--kill-all`, and `--reattach` skip lock checks so you can always manage running processes
## Improvements
* Hardcoded control plane URL for the historical `lager boxes connect` flow, removing the `--url` flag
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.12.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.12.0/)
# Version 0.13.0
Source: https://docs.lagerdata.com/source/release-notes/v0.13.0
March 20, 2026
> Historical note: `boxes connect` was a temporary migration seam. Current open-source Lager no longer exposes that command; downstream control planes own box install/bootstrap.
## Features
* `--force-command` flag is now available on all subcommands that target a Lager Box (not just as a global flag). Place it anywhere after the subcommand name: `lager python script.py --box lab-box --force-command`
* `--force-command` added to `hello`, `install`, `uninstall`, and the historical `boxes connect` command
## Improvements
* `lager python --detach` now keeps the command lock until the detached process finishes on the Lager Box, preventing others from accidentally interfering with running scripts. The lock is automatically released when the script completes
* All commands that acquire locks now automatically support `--force-command` via shared decorator
## Bug Fixes
* Updated locking documentation to reflect current behavior (detach keeps lock, local flag syntax, correct `lager boxes` output format)
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.13.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.13.0/)
# Version 0.13.2
Source: https://docs.lagerdata.com/source/release-notes/v0.13.2
March 21, 2026
## Improvements
* Updated `.gitignore` to exclude local AI assistant configuration directories
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.13.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.13.2/)
# Version 0.13.3
Source: https://docs.lagerdata.com/source/release-notes/v0.13.3
March 21, 2026
## Bug Fixes
* **`lager python --detach` now holds command lock**: Detached Python scripts now correctly keep the Lager Box marked as busy while the script runs. Previously, the command lock was released immediately after detaching, allowing other commands (e.g. `lager hello`) to run against a busy box. The lock is automatically released when the detached process finishes or is killed.
## Installation
```bash theme={null}
pip install lager-cli==0.13.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.13.3/)
# Version 0.13.4
Source: https://docs.lagerdata.com/source/release-notes/v0.13.4
March 23, 2026
## Bug Fixes
* Removed the automatic command-in-progress lock (ephemeral lock) that fired on every CLI command. The feature had multiple corner cases — supply commands never released the lock, long-running commands blocked all other commands on the same box, and detached processes left stale locks
* Removed `--force-command` flag from all commands (no longer needed)
## Improvements
* User lock (`lager boxes lock` / `lager boxes unlock`) is unchanged and remains the recommended way to reserve a Lager Box
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.13.4
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.13.4/)
# Version 0.14.0
Source: https://docs.lagerdata.com/source/release-notes/v0.14.0
March 24, 2026
## Features
* `lager install-wheel path/to/wheel --box X` installs a local Python wheel file on a Lager Box. Automatically uninstalls any previously installed version of the package before installing, so the version number does not need to be bumped on every rebuild. The package name is parsed from the wheel filename per the wheel specification.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.14.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.14.0/)
# Version 0.14.1
Source: https://docs.lagerdata.com/source/release-notes/v0.14.1
March 24, 2026
## Bug Fixes
* `lager update --version v0.14.0` (and any version tag) now works correctly. Previously, version tags were incorrectly resolved as remote branch refs (`origin/v0.14.0`), causing the update to fail. Tags are now resolved directly.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.14.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.14.1/)
# Version 0.14.2
Source: https://docs.lagerdata.com/source/release-notes/v0.14.2
March 30, 2026
## Bug Fixes
* `lager debug erase` and `lager debug flash` now correctly pass the JLinkScript to J-Link during the connect step. Previously only `gdbserver` passed the script, causing erase/flash to fail on MCUs that require a JLinkScript to load the correct flash algorithm (e.g. DA1469x with external QSPI flash)
* For DA1469x targets, erase now uses address-range erase instead of chip erase, and no longer halts after erase when flashing
* Fixed crash when running RTT after flashing
* Improved J-Link process management: stale PID files are now cleaned up, and JLinkGDBServer is stopped before chip erase operations
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.14.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.14.2/)
# Version 0.14.3
Source: https://docs.lagerdata.com/source/release-notes/v0.14.3
March 31, 2026
## Bug Fixes
* Supply net current limit no longer gets automatically reset to 1A on TUI startup or any CLI command
* OVP value now correctly displays in `lager supply state` output
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.14.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.14.3/)
# Version 0.14.4
Source: https://docs.lagerdata.com/source/release-notes/v0.14.4
March 31, 2026
## Changes
* `lager debug flash` now erases flash by default before programming, ensuring a clean boot state. Use `--no-erase` to skip. The `--erase` flag is retained for backwards compatibility.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.14.4
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.14.4/)
# Version 0.15.0
Source: https://docs.lagerdata.com/source/release-notes/v0.15.0
April 2, 2026
## Features
* `lager boxes lock` now accepts a `--user` flag to lock as a specific username, useful when running inside a Docker container where the effective user would otherwise be `root`
## Improvements
* `lager boxes` now shows a warning when any Lager Box is locked as `root`, with instructions to use `--user` or `lager defaults add --user`
* `LAGER_USER` environment variable is now the highest-priority source when determining the lager user for lock operations (before `~/.lager` config and the OS username)
* Lock output and error messages now display the user's email address when available. External tools that lock boxes using the `::` lock format will have the email extracted and shown rather than the raw lock string
* `lager update` SSH operations now use `StrictHostKeyChecking=accept-new` to avoid host-key prompts on first connection to a new Lager Box
* `lager update` Docker rebuild step now correctly passes the explicit SSH key file when one is in use
* `lager update` stop/remove step now targets the `lager` and `pigpio` containers by name instead of stopping all running containers
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.15.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.15.0/)
# Version 0.15.1
Source: https://docs.lagerdata.com/source/release-notes/v0.15.1
April 7, 2026
## Bug Fixes
* DA1469x post-flash reset now uses GDB-based reset instead of J-Link Commander register writes, fixing unreliable behavior on DA1469x targets after flashing
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.15.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.15.1/)
# Version 0.15.2
Source: https://docs.lagerdata.com/source/release-notes/v0.15.2
April 8, 2026
## New Features
* `lager install --version` now accepts a release tag (e.g. `v0.15.0`) in addition to a git branch, so you can install a box at a pinned version directly: `lager install --ip --version v0.15.0`. This replaces the old `--branch` flag, which only accepted branches.
## Bug Fixes
* Reverted DA1469x post-flash reset to use J-Link Commander register writes (restores 0.15.0 behavior). The GDB-based reset introduced in 0.15.1 caused regressions on DA1469x targets.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.15.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.15.2/)
# Version 0.16.0
Source: https://docs.lagerdata.com/source/release-notes/v0.16.0
April 13, 2026
## Features
* **Lager MCP server** — a Model Context Protocol server now runs on the Lager Box on port 8100 (FastMCP, streamable-http), allowing AI agents to discover a bench and understand how its nets are wired to the DUT. The server has moved from the CLI to the box and is started automatically inside the Docker container.
* **Net metadata** — nets now support `description`, `dut_connection`, `test_hints`, and `tags` fields. New `lager nets` CLI commands and TUI flows let you edit this metadata interactively so AI agents (and humans) can reason about what each net is for.
* **Capability graph and heuristic engine** — a new engine maps test types to the nets available on a bench, giving agents a principled way to pick the right instrument for a given task.
* **Auto-generated MCP API reference** — the MCP API reference is now generated from driver introspection at Docker image build time. The build fails fast if a driver is renamed, so the agent-facing surface stays in sync with the code.
## Improvements
* Every MCP tool call is wired through an `@audited` decorator that records the call via `audit.log_tool_call`, giving downstream control planes a consistent audit trail.
* `quick_io` writes now pass through a `preflight_check` that enforces voltage, current, and dangerous-action constraints before touching hardware.
* The `bench.json` parser is now defensive: a single malformed entry no longer breaks `discover_bench`.
* MCP errors no longer return raw tracebacks to agents. `NetType()` inputs are validated against the enum.
* `plan_firmware_test` uses a regex-based pattern split instead of the previous unsafe `get_pattern` split.
* New integration test `test_agent_loop` plus unit tests for the bench loader, capability graph, heuristic engine, safety preflight, and MCP schemas.
## Security
* The `run_lager` MCP passthrough tool is now gated behind the `LAGER_MCP_ALLOW_RUN_LAGER` environment flag and is **off by default**. Operators must opt in explicitly before agents can invoke arbitrary `lager` commands on a box.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.0/)
# Version 0.16.1
Source: https://docs.lagerdata.com/source/release-notes/v0.16.1
April 13, 2026
## Bug Fixes
* **`bench_loader` null-value crash** — the MCP bench loader no longer crashes when `bench.json` or `saved_nets.json` contains explicit `null` values for list or dict fields such as `test_hints`, `tags`, `aliases`, `params`, `net_overrides`, `dut_slots`, `interfaces`, or `channels`. Previously, `dict.get(key, default)` only substituted the default when the key was absent, so a literal `"test_hints": null` would return `None` and break downstream iteration. All affected sites now coerce an explicit `null` to the same empty default as an absent key. Regression tests added in `test/mcp/unit/test_bench_loader.py::TestNullTolerance`.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.1/)
# Version 0.16.10
Source: https://docs.lagerdata.com/source/release-notes/v0.16.10
May 1, 2026
## Bug Fixes
* **`lager debug connect` no longer hides the real Segger error behind an `AttributeError` when J-Link cannot reach the target.** When J-Link's multi-speed retry loop in `box/lager/debug/api.py:connect_jlink` exhausted without ever reaching the target, `status['logfile']` could be set to `None` instead of being absent — so `status.get('logfile', 'No log available')` returned `None`, which was then passed into `clean_logfile_content` and crashed with `AttributeError: 'NoneType' object has no attribute 'replace'`. The crash masked the real Segger "Connecting to target failed" message that operators need to see in the dashboard. Two changes: `connect_jlink` now uses `status.get('logfile') or 'No log available'` so the literal fallback fires for both missing and `None` values, and `clean_logfile_content` itself returns `''` when given `None` as defense in depth for any future caller.
## Internal
* Bumped seven transitive Rust dependencies in `box/oscilloscope-daemon/Cargo.lock` (`quinn-proto`, `rustls-webpki`, `time`, `bytes`, `tracing-subscriber`, `rand` 0.8 and 0.9 lines) to clear ten Dependabot security advisories on the daemon's QUIC/TLS stack. Lockfile-only change; the daemon binary is built and deployed separately from the lager-cli pip package, so this has no runtime effect on existing boxes until the daemon is rebuilt. Verified with a full release build + libps2000 link on a Picoscope-equipped box.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.10
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.10/)
# Version 0.16.2
Source: https://docs.lagerdata.com/source/release-notes/v0.16.2
April 17, 2026
## Bug Fixes
* **Keysight E36313A USB PID** — corrected the USB product ID for the Keysight E36313A power supply (`2a8d:1202`) in the `SUPPORTED_USB` tables used by the box's USB scanner and the CLI's `query_instruments` path. The PID was previously a placeholder (`????`), so the instrument was not recognized on plug-in. A matching udev rule was added so PyVISA can open the device directly via libusb (`MODE=0666`, with the `usbtmc` driver unbound on `bind` to prevent "Resource busy" errors).
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.2/)
# Version 0.16.3
Source: https://docs.lagerdata.com/source/release-notes/v0.16.3
April 24, 2026
## Improvements
* **`user` column in `lager boxes`** — the table output for `lager boxes` (and `lager boxes list`) now includes a `user` column between `ip` and `version`. Lager Boxes added with `--user` show the configured SSH username; Lager Boxes added without `--user` show the default (`lagerdata`). Makes it easy to see, at a glance, which Lager Boxes are configured for a non-default SSH user.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.3/)
# Version 0.16.4
Source: https://docs.lagerdata.com/source/release-notes/v0.16.4
April 27, 2026
## Bug Fixes
* **`/instruments/list` returning empty from worker threads** — when the box's HTTP server handled `/instruments/list` on a `ThreadingHTTPServer` worker, the USB scan's `signal.SIGALRM`-based timeout raised "signal only works in main thread of the main interpreter". The error was silently swallowed and the endpoint returned `[]`, so connected devices (e.g. LabJack T7) appeared to be missing. The scanner now falls back to a no-timeout direct call when it isn't on the main thread; the underlying serial and sysfs reads already have their own I/O timeouts. The CLI path was not affected because it runs `query_instruments.py` as a subprocess.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.4
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.4/)
# Version 0.16.5
Source: https://docs.lagerdata.com/source/release-notes/v0.16.5
April 27, 2026
## Bug Fixes
* **Keysight E36xxx supplies reporting `Enabled: OFF` after `enable`** — running `lager supply state` (or any other read-only supply command) immediately after a successful `lager supply enable` would report the output as `OFF` on Keysight E36200/E36300 series supplies. The `KeysightE36000` driver constructor was unconditionally calling `disable_output()` as a "safe default" on every connect, so each fresh CLI invocation silently turned the output off before running its query. The disable is now gated behind the explicit `reset=True` flag, so constructing a driver for a read (or for `enable`) no longer mutates output state.
* **EA PSB supplies briefly dropping output on re-enable** — `lager supply enable` on an EA PSB 10060-60 / 10080-60 caused a brief (\~500ms) output drop when the output was already on. `EA.enable()` always ran `_clear_latched_events()` first, which writes `OUTPut OFF` and waits 200ms before turning the output back on. `enable()` is now idempotent: if `OUTPut?` reports the output is already on, the call returns immediately without toggling. The off→on path that needs latched-protection clearing is unchanged.
* **`lager supply tui` closing silently on Rigol DP821** — the supply TUI was closing after \~5 seconds with no visible error whenever a direct supply command (e.g. `state`) had been run beforehand. The WebSocket supply monitor on the box was opening its own pyvisa session, conflicting with the cached VISA session held by `hardware_service.py` on port 8080. Instruments that don't tolerate concurrent USB sessions (Rigol DP821 reproduces this) hung silently and the TUI's 15-second wait for `supply_driver_ready` always timed out. The monitor thread now releases the cached handle (via `localhost:8080/cache/clear`) before opening its own session. As a defensive bonus, `get_channel_limits()` and the session-store block now emit a visible `error` event on any failure during init, and the CLI captures the TUI's exit reason and prints it red to stderr after Textual's alt-screen tears down — so failures no longer disappear with the screen restore.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.5
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.5/)
# Version 0.16.6
Source: https://docs.lagerdata.com/source/release-notes/v0.16.6
April 27, 2026
## Bug Fixes
* **`lager battery tui` now works for the first time.** The OLD WebSocket battery monitor crashed at module load with `ImportError: cannot import name '_resolve_net_and_driver' from 'lager.power.battery.dispatcher'` — that symbol existed at module level only in the supply dispatcher, never the battery one. Nobody had reported the bug because nobody had tested the battery TUI before. Incidentally fixed by the VISA-ownership unification below; the battery monitor now also emits a `battery_driver_ready` event mirroring `supply_driver_ready` for client symmetry.
* **Concurrent SCPI access on the same instrument now serializes correctly.** Two `/invoke` requests against the same cached driver in `box/lager/hardware_service.py` could race on the SCPI bus and produce `Query INTERRUPTED` pyvisa errors. Added a per-`(device_name, address)` lock that wraps every driver call. Multi-channel devices (e.g., Rigol DP821) correctly share one lock since they share one VISA session.
## Improvements
* **VISA session ownership unified.** The supply and battery WebSocket monitors no longer open their own pyvisa sessions in monitor threads. They now route every driver call through `hardware_service.py:/invoke` via the existing `Device` HTTP proxy. `hardware_service.py` (port 8080) is the sole owner of pyvisa sessions per `(device_name, address)`. The v0.16.5 `POST /cache/clear` band-aid is removed — this is the architectural fix that replaces it. Net effect: TUIs are more robust, no longer trip on stale cached sessions, and `Query INTERRUPTED` errors during simultaneous TUI + CLI activity are gone.
* **Battery handlers consolidated.** \~670 lines of duplicate battery-handler code in `box/lager/box_http_server.py` (parallel to the modular `box/lager/http_handlers/battery.py`) were deleted. `box_http_server.py` now imports and registers the modular versions, matching what was already done for supply.
* **Test hygiene.** Two unit tests in `test/unit/cli/test_performance_improvements.py` had been silently failing since the `.lager` config format was migrated to JSON-only. Tempfiles now use `{"LAGER": {...}}` JSON; full unit suite is back to 141/141 passing.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.6
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.6/)
# Version 0.16.7
Source: https://docs.lagerdata.com/source/release-notes/v0.16.7
April 28, 2026
## Bug Fixes
* **`lager uart ` no longer returns `404 — UART net not found`.** The v0.16.6 battery-handler consolidation (commit `f277402`) deleted the two-line UART handler registration in `box/lager/box_http_server.py` as collateral damage. The imports stayed in place, so the file still parsed cleanly; the `/uart/nets/list` Flask route just was never registered, so every UART CLI command 404'd. Restored the `register_uart_routes(app)` / `register_uart_socketio(socketio)` calls alongside the supply and battery registrations.
* **`lager supply state` (and other one-shot supply/battery commands) no longer fail with `[Errno 16] Resource busy` immediately after exiting the TUI.** Root cause: `/supply/command` and `/battery/command` returned 404 when no active WebSocket session was found, forcing the CLI's `_run_backend` into a direct-pyvisa subprocess fallback (`cli/impl/power/supply.py` → dispatcher) that opened its own pyvisa session against the same USB device `hardware_service.py` was still caching. Both endpoints now build a transient `Device` proxy via `resolve_net_proxy()` when no active WS session exists, routing through `hardware_service.py:/invoke` like the WS monitor already does. There is now exactly one pyvisa session per `(device_name, address)` regardless of TUI lifecycle. This completes v0.16.6's "VISA session ownership unified" promise.
* **Concurrent TUI + CLI access on the same supply no longer cascades `Resource busy` errors across subsequent commands.** Previously, a single transient kernel-level USB-claim collision (the kind that can momentarily occur when two pyvisa-issued USB transfers overlap on the same device) would be mis-classified as a stale pyvisa session: `_is_visa_session_error()` matched the substring `'resource'` inside `'Resource busy'`, the retry path then popped the live cache entry and called `module.create_device()` on the same address, and the new open hit `Resource busy` again because the original session was still alive in the same process. The result was that an isolated USB-busy error turned into a chain of failures across every following command. Removed `'resource'` from `_VISA_SESSION_ERROR_KEYWORDS` in `box/lager/hardware_service.py`; retry now fires only for genuine stale-session signals (`'session'`, `'closed'`, `'invalid'`). An isolated USB-busy collision is still possible on heavily-contended USB transfers but is now returned to the caller cleanly without disturbing the cache, so the next command immediately succeeds.
## Known Limitations
* **Keithley 2281S dual-role nets must be used one at a time.** When the same physical Keithley 2281S has both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`) configured, `box/lager/hardware_service.py` caches them under two different keys (`("keithley", address)` vs `("keithley_battery", address)`) and tries to open two pyvisa sessions on the same USB device — the second hits `[Errno 16] Resource busy`. Workaround: configure only the role you need on the Keithley 2281S, or restart the box's lager container between switching roles. The proper fix (shared pyvisa Resource between supply and battery driver instances, or a merged dual-role driver class) is targeted for v0.16.8.
* **Concurrent battery TUI + CLI on the Keithley 2281S can surface `[Errno 16] Resource busy`.** Running `lager battery tui` in one terminal while running `lager battery state` (or any other one-shot battery CLI command against the same net) in another terminal can fail with `Resource busy`, even when only the battery role is configured on the Keithley (so this is distinct from the dual-role limitation above). v0.16.7's Bug-B retry-classification fix prevents this from cascading across subsequent commands but does not eliminate the initial collision; the underlying contention appears to live in the Keithley pyvisa session itself rather than in `hardware_service.py`'s lock. Workaround: do not invoke battery CLI commands while a battery TUI is open against the Keithley 2281S — close the TUI first, or run TUI-only or CLI-only on the Keithley battery net. Root-cause investigation tracked for v0.16.8.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.7
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.7/)
# Version 0.16.8
Source: https://docs.lagerdata.com/source/release-notes/v0.16.8
April 28, 2026
## Features
* **SEGGER J-Link Flasher PRO support** — the J-Link Flasher PRO (USB `1366:0105`) is now recognized as a supported `debug` instrument. Plugging one into a Lager Box and running `lager instruments` will list it as `J-Link_Flasher_Pro`. Both the box-side scanner (`box/lager/http_handlers/usb_scanner.py`) and the CLI-side scanner (`cli/impl/query_instruments.py`) were updated.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.8
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.8/)
# Version 0.16.9
Source: https://docs.lagerdata.com/source/release-notes/v0.16.9
April 29, 2026
## Bug Fixes
* **Keithley 2281S dual-role nets now switch cleanly between supply and battery roles.** When the same physical Keithley 2281S is configured with both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`), the box now opens exactly one pyvisa session per VISA address and both driver classes wrap that one session — instead of each driver opening its own session and the second hitting `[Errno 16] Resource busy`. Implemented as a process-wide shared-resource cache (`_visa_resources` keyed by VISA address) in `box/lager/hardware_service.py`, plus a `raw_resource=` factory kwarg on `box/lager/power/supply/keithley.py:create_device` and `box/lager/power/battery/keithley.py:create_device`. Both Keithley driver constructors track an `_owns_resource` flag so `close()` does not release the underlying USB claim while the sibling driver still needs it. SCPI serialization moved to a per-address lock (was per `(device_name, address)` cache key) so a supply command followed by a battery command against the same Keithley serialize correctly on the USB bus. This resolves the **sequential** half of the dual-role known limitation in v0.16.7 — a script can now alternate `lager supply ` and `lager battery ` commands against the same Keithley without restarting the box service. The instrument's two operating modes (Power Supply via `:ENTR:FUNC POW`, Battery Simulator via `:ENTR:FUNC BATT`) remain mutually exclusive in firmware, so genuinely *concurrent* supply + battery operation against one Keithley is still not supported by the hardware itself; see Known Limitations.
* **Concurrent battery TUI + CLI on the Keithley 2281S no longer fails with `[Errno 16] Resource busy`.** The stale-VISA-session retry path in `box/lager/hardware_service.py:/invoke` now calls `_close_device(old_device, cache_key)` before invoking `module.create_device(net_info)`. Previously the popped driver instance stayed alive in the Python process and kept libusb's USB claim, so the recreated session's `pyvisa.ResourceManager().open_resource(addr)` failed with `Resource busy` — surfaced as `Could not open instrument at ...`. Closing the old session before opening a new one fixes this for any driver whose retry path fires; for Keithley shared-resource drivers the underlying pyvisa session is also reopened so both supply and battery drivers get a fresh handle. This resolves the concurrent battery TUI + CLI known limitation documented in v0.16.7.
* **Keithley 2281S supply commands no longer crash with `TypeError`.** `box/lager/http_handlers/supply.py` is modeled on multi-channel drivers (Rigol DP800) and calls supply-driver methods with a `channel=` kwarg or positional channel. The Keithley 2281S supply driver follows the `SupplyNet` abstract (no `channel` parameter — the 2281S is single-channel), so the very first call hit `TypeError: Keithley2281S.output_is_enabled() got an unexpected keyword argument 'channel'`. The handler treated that as a hardware failure and triggered `/cache/clear`, which tore down the shared pyvisa session this release had just opened for dual-role mode. `Keithley2281S.output_is_enabled` now accepts (and ignores) a `channel=None` kwarg, and six new public OCP/OVP wrapper methods (`set_overcurrent_protection_value`, `enable_overcurrent_protection`, `set_overvoltage_protection_value`, `enable_overvoltage_protection`, `clear_overcurrent_protection_trip`, `clear_overvoltage_protection_trip`) delegate to the existing private `_set_ocp` / `_set_ovp` and public `clear_ocp` / `clear_ovp` methods so the supply handler can call them without `AttributeError`. No new SCPI logic — the wrappers exist purely so a single-channel driver can satisfy the multi-channel calling convention used elsewhere.
* **`lager battery state` no longer collides with the shared pyvisa session.** The battery CLI sends `action='print_state'` (matching the dispatcher function name), but `/battery/command` previously only recognized `action='state'` (matching the supply handler). The mismatched action returned HTTP 400, the CLI's `_run_backend` fell through to the python:5000 dispatcher path, and that subprocess opened a *second* pyvisa session against the same Keithley — colliding with the shared session that hardware\_service had just opened in the previous `lager supply` command and surfacing as `Could not open instrument at USB0::...: failed to set configuration [Errno 16] Resource busy`. `/battery/command` now accepts both `'state'` and `'print_state'`, keeping the CLI on the WebSocket → hardware\_service path so the shared pyvisa session this release introduces is actually reused for sequential supply→battery CLI workflows.
* **`lager python` no longer wipes hardware\_service's cache on every script exit.** `cli/commands/development/python.py` was POSTing `/cache/clear` on script normal exit, Ctrl+C, and BrokenPipeError — a v0.16.5 band-aid that pre-dates Phase 2's per-address shared session. With v0.16.9, hardware\_service is the single owner of the pyvisa session for each USB device and that session is *meant* to persist for the container's lifetime. Clearing it on every script exit defeated the design and re-introduced the very `[Errno 16] Resource busy` race that Phase 2 set out to eliminate. The clears are removed; if you really need to force a reload (e.g., a script that opens its own pyvisa session out-of-band), you can still `curl -X POST http://:8080/cache/clear` manually.
* **Resilient first open against libusb's release-interface timing race.** `hardware_service._get_or_open_visa_resource` now retries `open_resource()` on `[Errno 16] Resource busy` with an exponential backoff (`0.2, 0.5, 1.0, 2.0` s) before giving up. pyvisa-py + libusb on Linux releases the USB interface asynchronously, so opening the same device too quickly after a close (e.g. after a manual `/cache/clear` or a TUI exit) could fail the first time and succeed the second. The retries hide the kernel's catch-up window without masking genuine "device unplugged" failures.
* **`POST /cache/clear` preserves shared pyvisa sessions; new `POST /cache/clear_all` for the old behavior.** The endpoint still drops cached driver wrappers from `device_cache` (so a wedged driver gets a fresh load on the next `/invoke`), but the per-VISA-address shared session that this release relies on is no longer torn down. This was the missing piece that caused V.5/V.6 hardware verification to fail even after the script-exit clear was removed from `lager python` — older clients (`lager-cli` ≤ 0.16.7) still POST `/cache/clear` on every script exit, and that was nuking the shared session out from under hardware\_service. With the endpoint now safe under Phase 2, those older clients no longer break dual-role workflows. If you actually need to force-close a shared session (e.g. you unplugged the instrument), `POST /cache/clear_all` does what `/cache/clear` used to do.
* **Cross-role concurrent use on a single Keithley 2281S now fails fast with a clear error.** Running `lager supply tui` and a concurrent `lager battery ` command (or vice-versa) against the **same** physical Keithley used to surface as cryptic SCPI timeouts or `[Errno 16] Resource busy` errors, because the 2281S's Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`) entry functions are mutually exclusive in firmware and the two clients were fighting over the entry function on every poll. The box now tracks the active monitoring sessions per role (`box/lager/http_handlers/state.py:conflicting_other_role_session`), records the resolved VISA address when a TUI starts, and refuses an opposite-role command at `/supply/command`, `/battery/command`, `start_supply_monitor`, and `start_battery_monitor` with a message that names the conflicting net and explains the hardware limitation. Sequential CLI cross-role workflows are unaffected and continue to work cleanly via Phase 2's shared pyvisa session.
## Known Limitations
* **Concurrent supply and battery operation on the same Keithley 2281S is not supported by the instrument itself.** The 2281S has two mutually-exclusive entry functions — Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`). Each Lager driver flips the entry function to its preferred mode before every SCPI command, so running a supply TUI in one terminal while running a battery CLI command in another terminal causes the two clients to fight over the entry function on every poll, producing intermittent SCPI errors and Resource busy events. **Configure either the supply role or the battery role on the Keithley 2281S, not both — or use them strictly sequentially in a single workflow** (which v0.16.9's shared-pyvisa-session work makes fast and clean). This is a property of the instrument, not Lager.
## Internal
* Drivers that share a single pyvisa session per VISA address are listed in `box/lager/hardware_service.py:_SHARED_VISA_DEVICE_NAMES` (currently `keithley`, `keithley_battery`). Adding a future dual-role instrument means adding its supply and battery `device_name` strings here and giving each `create_device` factory the `raw_resource=` kwarg pattern.
* `Keithley2281S.__init__` and `KeithleyBattery.__init__` accept a new `_owns_resource` kwarg (default `True` for backward compatibility). When `False`, `close()` drops the wrapper reference without closing the underlying pyvisa session.
* Single-role drivers (Rigol DP800/DP821, Keysight E36xxx, EA PSB, etc.) are unchanged — they continue to use the legacy per-driver-opens-its-own-session path.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.16.9
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.16.9/)
# Version 0.17.0
Source: https://docs.lagerdata.com/source/release-notes/v0.17.0
May 5, 2026
## Features
* **Concurrent J-Link probes on a single Lager Box.** Two J-Link probes plugged into one Box can now run independent debug sessions side-by-side. The box-side service in `box/lager/debug/service.py` resolves each debug net's J-Link USB serial from its VISA address and allocates a deterministic per-probe slot (read from `saved_nets.json` via `NetsCache`). Slot N owns a three-port window — GDB `2331+3N`, SWO `2332+3N`, telnet `2333+3N` — plus RTT base `9090+2N`. The auxiliary `-swoport` and `-telnetport` are passed explicitly to `JLinkGDBServer` so its defaults of `2332`/`2333` can't collide with another slot's GDB port. The service passes `-select USB=` to `JLinkGDBServer` and `-SelectEmuBySN ` to `JLinkExe`, writes per-serial PID and log files, and narrows `pkill` so disconnecting one probe no longer tears down the other. The CLI's `--gdb-port` default is now `None` rather than `2331`, so the box's allocator is honored unless an explicit port is requested; the effective `gdb_port` returned by the box is printed on connect. `start_box.sh` publishes the widened `2331-2342` Docker port range and `secure_box_firewall.sh` admits the same range, so existing hardened boxes need a firewall refresh to use the new slots. Nets without a parseable serial (legacy single-probe setups) fall back to slot 0 / GDB 2331 / RTT 9090 / `/tmp/jlink_gdbserver.pid` and continue to work unchanged.
* **RIGOL DP811 power supply detection.** `lager instruments` now lists the DP811 alongside the DP821 and DP832. The DP811 shares VID:PID `1ab1:0e11` with its siblings, so it is identified by USB serial prefix (`DP8H` or `DP81` → `Rigol_DP811`) in both `box/lager/http_handlers/usb_scanner.py` and `cli/impl/query_instruments.py`, and is added to the serial-disambiguated bucket so a generic VID:PID match cannot misclassify it as a DP821.
* **Multiple concurrent viewers per webcam stream.** Each `/stream` connection used to open its own `cv2.VideoCapture` against `/dev/videoN`, which V4L2 serves exclusively — the second viewer either failed or got blank frames. The streamer subprocess in `box/lager/automation/webcam/service.py` now spins up a single daemon capture thread on the first viewer that owns the device and broadcasts encoded JPEG frames to a shared buffer guarded by a `threading.Condition`. Any number of viewers can now subscribe to the same stream concurrently. Stop and re-start each webcam to pick up the regenerated streamer script.
## Bug Fixes
* **`LabJackADC.input()` no longer inherits sticky AIN register state from a previous tool.** ADC reads previously called `ljm.eReadName` with zero AIN register configuration, inheriting whatever the previous tool left in `AIN_RANGE`, `AIN_NEGATIVE_CH`, `AIN_RESOLUTION_INDEX`, and `AIN_SETTLING_US`. T7 register state persists in device RAM until USB power-cycle, so if a previous tool left an AIN in differential mode with a floating negative channel, every read saturated at \~10.10 V regardless of the actual signal — indistinguishable from a real wiring fault. Safe defaults (`RANGE=10.0`, `NEGATIVE_CH=199`, `RESOLUTION_INDEX=0`, `SETTLING_US=0`) are now written once per `(handle, channel)` tuple before the first `eReadName`, cached in a class-level set. Config-write failures are logged but do not raise.
## Improvements
* **Webcam capture forces MJPEG so two cameras can share a USB 2.0 bus.** Default OpenCV negotiation picked YUYV (uncompressed, \~150 Mbps at 640×480 30fps), which doesn't leave room for a second camera on the same bus — the kernel rejected `VIDIOC_STREAMON` with "Not enough bandwidth for altsetting". MJPEG is roughly 5× smaller and fits two cameras comfortably; the FourCC is now set before width/height/fps so negotiation honors it.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.17.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.17.0/)
# Version 0.18.0
Source: https://docs.lagerdata.com/source/release-notes/v0.18.0
May 12, 2026
## Features
* **`lager box config` — declarative per-box provisioning.** A new top-level command tree that replaces ad-hoc SSH-and-edit workflows with a single JSON manifest at `/etc/lager/box_config.json` per Lager Box. The file declares mounts, named Docker volumes, container environment variables, host apt packages, kernel sysctl settings, in-container pip packages, cargo crates, and npm packages — and `lager box config apply` reconciles the box to match. Re-applying the same config is a no-op via SHA-256 comparison against the last applied snapshot, so it's safe to wire into CI. The full operator surface: `init`, `show`, `validate`, `diff`, `apply` (with `--dry-run` and `--yes`), `audit`, `status`, `edit` (round-trips through `$EDITOR`/`nano`/`vi` with shim-side validation on save), `copy --from --to`, `import FILE`, `export FILE`, and `repair`. Multi-box fanout via `--box A,B,C` on `show` and `apply` for fleet operations. Every section has CRUD verbs: `mount add/remove/list`, `pip add/remove/list`, `apt add/remove/list`, `cargo add/remove/list`, `npm add/remove/list`, `sysctl set/unset/list`, `env set/unset/list`, `volume add/remove/list`.
* **npm support inside the container.** A new `npm_packages` first-class field on `box_config.json` lets you declare Node.js global packages alongside the existing pip and cargo lists. Scoped packages (`@types/node`) and versioned packages (`lodash@4.17.21`) are both supported. The container Dockerfile now ships `nodejs npm` and sets `NPM_CONFIG_PREFIX=/home/www-data/.npm-global` (pre-created and chowned to the `www-data` runtime user) so `npm install -g` works without root.
* **Rust toolchain baked into the container image.** rustup is now installed into `/opt/rust` (owned by `www-data`) with `RUSTUP_HOME`, `CARGO_HOME`, and `PATH` set in the Dockerfile, so `cargo install` runs cleanly from the post-bounce loop. No more manual rust installation per Lager Box. `cargo_packages` entries accept both `name` and `name@version`.
* **Audit log of every config mutation.** Every `add`/`set`/`remove`/`unset`/`apply` operation is recorded to `/etc/lager/box_config.audit.log` (JSONL, append-only) with an ISO-8601 timestamp. `lager box config audit` reads it back. Filters compose: `--tail 20`, `--since 1h`, `--verb apt-add`, `--json`. Useful for "what changed today" or "every apt operation ever."
* **Automatic rollback on failed bounces.** When `lager box config apply`'s container restart fails (for example, because docker rejected a malformed mount), the previously applied snapshot is restored to `/etc/lager/box_config.json` via SSH `sudo cp` and a re-bounce brings the box back up on the prior good config. Sysctl values are reverse-diffed to their previous state in the same pass. The restore goes through direct SSH file ops rather than the in-container shim because the container is necessarily dead by the time the rollback fires. `lager box config repair --box X` exposes the same recovery as a standalone command for situations that automatic rollback can't reach — for example, when an operator hand-edits the JSON to invalid syntax outside the CLI.
* **Sudoers auto-bootstrap.** `lager install` (on new boxes) and `lager update` (on existing boxes) now install `/etc/sudoers.d/lager-box-config` with the narrow NOPASSWD grants `lager box config apply` needs: `apt-get` with `SETENV:` for `DEBIAN_FRONTEND`, path-scoped `tee`/`rm`/`sysctl --system` for the sysctl conf, `mkdir`/`chown` for mount auto-prep, and a path-scoped `cp` for the rollback snapshot restore. A marker file at `/etc/lager/.boxcfg-sudoers-v2` lets `lager update` skip re-bootstrapping once the current rule shape is in place. Operators never type a sudoers snippet by hand.
## Bug Fixes
* **`lager update` container startup timeout raised from 5 to 10 minutes.** First-time docker builds with cargo and npm layers were timing out on slower Lager Boxes. `_bounce_container`'s SSH ceiling was also bumped from 300s to 900s for the same reason — covers cargo crate compilation + pip and npm install loops with headroom.
* **SSH user resolution.** The new shared SSH runner used by `lager box config` was calling `get_box_user(box_ip)` even though that helper keys by box *name*, so every Lager Box with a stored custom SSH user silently fell back to `lagerdata`. The runner now reverse-resolves the name via `get_box_name_by_ip` before the lookup, and uses `~/.ssh/lager_box` via `-i` to match the rest of the CLI's SSH conventions.
* **`DEBIAN_FRONTEND=noninteractive` actually propagates on apt installs.** Default Ubuntu sudoers' `env_reset` strips `DEBIAN_FRONTEND` set as a `sudo VAR=value cmd` argument unless `SETENV:` is granted. Packages with debconf prompts (`iptables-persistent` and similar) were hanging on a prompt that never showed. The new sudoers rule grants `SETENV:` only on `/usr/bin/apt-get` so the env var propagates.
* **`cargo` found inside the container during apply.** `start_box.sh`'s cargo install loop used `bash -lc` (login shell), which re-sourced `/etc/profile` and reset `PATH` — wiping the Dockerfile's `ENV PATH=/opt/rust/cargo/bin:...`. Switched to `bash -c` (non-login) so the docker `ENV` is honored. Same fix applied to the npm install loop.
* **Real exit codes captured from pip/cargo/npm install loops.** The previous `if ! cmd; then _rc=$?` pattern in `start_box.sh` captured `$?` *after* bash's `!` inversion — so `_rc` was always `0` even on real failures, and error messages reported `(rc=0)` for non-zero exits. Refactored to `if cmd; then : else _rc=$?` so error codes propagate accurately.
* **Env values with whitespace, `$`, backticks, or single quotes survive the bounce.** The docker-args renderer used to emit `--env 'KEY=hello world'` to stdout, which `start_box.sh` interpolated unquoted into `docker run` — bash variable expansion does not re-parse quotes, so values got word-split and the literal quote characters leaked through. The renderer now writes a bash-sourceable file declaring `BOX_CONFIG_MOUNTS`, `BOX_CONFIG_ENV`, and `BOX_CONFIG_HOST_PATHS` arrays via `shlex.quote`; `start_box.sh` sources that file and uses `"${BOX_CONFIG_MOUNTS[@]}"` so each element preserves its content verbatim.
* **`lager box config edit` no longer rejects valid saves with non-zero editor exit.** Some vim plugins return `1` from `:wq` even when the save succeeded. The command now compares tempfile contents before and after the editor exits — content changed AND non-zero rc means "user saved, proceed"; content unchanged AND non-zero rc means "abort." Bonus: `nano` is preferred over `vi` as the fallback when `$EDITOR` is unset.
## Improvements
* **`lager box config show` reads as a tree.** Bold uppercase `HOST` / `CONTAINER` group headers with horizontal-rule underlines, bold section labels indented two spaces, and `├── /└── ` branches under each section. Mount paths align around `->`; env/sysctl keys align around `=`; empty sections render as `(none)` leaves so operators discover what's configurable. The header carries a color-coded `[Up To Date]` / `[Unapplied Changes!]` marker driven by a `hash` vs `applied-hash` comparison.
* **`apply` shows the pending diff inline before confirming.** When `--yes` is not passed, the confirm prompt is preceded by a per-field diff of what's about to change — closes the most common pre-apply workflow ("run diff first, then apply") into a single command.
* **Tightened sudoers rule.** `tee`, `rm`, and `sysctl --system` in the recommended sudoers grant are now path-locked to the exact files and flags `apply` invokes, so a compromised `lagerdata` account cannot escalate to root via those binaries. `apt-get` and `mkdir`/`chown` stay unscoped because the package list and host paths are user-defined.
* **flock against the in-container shim.** Two concurrent `lager box config X` invocations against the same Lager Box used to do read-modify-write on `box_config.json` and silently drop one mutation. The shim now `flock`s `/etc/lager/box_config.lock` around the whole dispatch.
* **Post-apply consistency check.** After the bounce + API-ready probe but before recording the new applied-hash, the apply path re-runs `validate` + `show` against the box. If either drifts from what was bounced (the JSON was hand-edited mid-apply, say), `applied-hash` is left untouched and the operator is told to re-run apply.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
After upgrading, run `lager update --box ` on each existing Lager Box to deploy the matching box-side code and pick up the sudoers rule.
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.0/)
# Version 0.18.1
Source: https://docs.lagerdata.com/source/release-notes/v0.18.1
May 13, 2026
## Bug Fixes
* **J-Link GDB attach no longer halts the target.** Dropped the `-ir` flag from `JLinkGDBServer` and switched GDB into non-stop async mode before target attach, so attaching gdbserver/rtt no longer halts the target CPU on \~15% of attempts.
## Improvements
* **`lager usb` enable / disable / toggle \~2.6x faster.** Routes through a new Flask handler on the box's port 9000 instead of spawning a fresh Python subprocess per call (mirrors the supply/battery fast-path from 0.17.x). Backward compatible — falls back to the slow path against older box images.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.1/)
# Version 0.18.2
Source: https://docs.lagerdata.com/source/release-notes/v0.18.2
May 13, 2026
## Features
* **`lager box update` is now the canonical update command.** The top-level `lager update` keeps working as a hidden alias for existing scripts but prints a deprecation notice. Same flag set; runs through the same flow.
* **`--check` flag for dry-run updates.** `lager box update --box X --check` reports what would change (code, deps, container) without touching the box. Useful for CI gating and pre-deploy checks.
* **Docker cache auto-invalidates when `Dockerfile` or `requirements.txt` change.** No more remembering to pass `--force` after a deps bump — the next update detects the drift and rebuilds.
## Bug Fixes
* **Updates now take effect on the first run.** Fixes a cluster of bugs behind the recurring "had to run `lager update` 2–3 times before it stuck" reports: stale `/etc/lager/version` after the early-exit branch, the flatten heuristic re-fetching on every run, the 5-second post-restart sleep racing against an unready service, silent flatten failures producing broken images, and the cache-invalidation early-exit skipping rebuilds when only deps had changed.
* **Branch switches with conflicting root-level files no longer fail.** Adds `git checkout -f` so a previous flatten step that clobbered a tracked file (e.g. `README.md`) doesn't block the switch with "local changes would be overwritten."
* **Git errors are now shown.** `git checkout` and `git reset --hard` failures used to surface only "Failed to checkout version X" without git's underlying error message.
## Improvements
* **Consecutive no-op `lager box update` runs are \~10× faster.** SSH calls multiplex through a single OpenSSH ControlMaster connection — \~20s → \~1.6s.
* **Cleaner output.** Single green summary line, progress bar adapts to terminal width, elapsed time appears on the bar itself.
* **`--all`, `--force`, `--skip-restart` flags removed.** `--all` will return as its own command if needed; `--force` is obsoleted by auto cache-invalidation; `--skip-restart` had no real workflow.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.2/)
# Version 0.18.3
Source: https://docs.lagerdata.com/source/release-notes/v0.18.3
May 15, 2026
## Features
* **`lager box update --version ` rolls back.** Previously the update flow only counted commits the box was *behind* the target and treated any "ahead" state as in-sync, so you could not downgrade a box to an earlier branch or tag without manually `git reset --hard`-ing on the box. Now diverges in both directions; an explicit second confirmation prompt (skippable via `--yes`) gates the destructive direction so a typo'd `--version` argument can't silently downgrade. `--check` reports "will roll back N commit(s)" / "will switch (N ahead / M behind)".
## Bug Fixes
* **Update no longer aborts on git ≥2.36 with `fatal: 'cli/__init__.py' is not a directory`.** Cone-mode sparse-checkout (default since git 2.36) rejects single-file patterns; the pre-batching version of the sparse-checkout step ran in a separate SSH call whose exit was never checked, so the failure was silently swallowed. The new batched pull script chained it with `&&`, propagating the failure and aborting the whole pull. Now treats the `cli/__init__.py` add as best-effort. Affects boxes on newer git (e.g. git 2.43 on Debian bookworm-backports).
* **`/etc/lager/version` and the end-of-run summary report the box's actual code, not the CLI's version.** Previously a rollback or branch switch could write the running CLI's version (e.g. `0.18.3`) into the version file even when the code on the box was older (e.g. `v0.18.2`). The CLI now reads `__version__` from `cli/__init__.py` at the box's post-pull HEAD via `git show`, which works even on cone-mode boxes where the file isn't in the working tree.
## Improvements
* **No-op `lager box update` runs \~3× faster (\~5s → \~1.6s).** A single SSH probe collapses \~11 separate `test`/`cat`/`git`/`diff`/`stat` round-trips (git-repo check, remote URL, layout, current commit, build-cache hashes, udev rule state, sudoers ownership, box-config sudoers state, `/etc/lager/version`) into one structured shell script. Combined with merging fetch+rev-list, sparse-checkout+checkout+reset, flatten+verify, post-build directory setup, and verify+J-Link presence into single calls.
* **Typical `lager box update` \~6× faster on boxes with cargo/npm packages in `box_config.json` (\~1:40 → \~17s).** Adds two named Docker volumes — `lager-cargo` at `/opt/rust/cargo` and `lager-npm-global` at `/home/www-data/.npm-global` — to `start_box.sh`'s `docker run`, so user-installed cargo crates and global npm packages survive container recreation. Without them, every update reinstalled them from scratch (`cargo install` recompiled from source, \~50–60s per update). With them, the second-and-onward run sees "already installed" and finishes in seconds. The CLI wipes both volumes alongside `docker rmi lager` whenever the build-hash changes, so a Dockerfile rustup/node bump can't leave a stale toolchain in the volume. First update on each existing box is the same speed as today (the volumes seed themselves); the win shows up from the second update on.
* **`--verbose` output cleanup.** Probe results print as one tidy block instead of a dozen "Checking X... OK" lines; consistent step labels between the progress bar and verbose log; noise lines dropped (e.g. "Checking remote URL" only prints when it actually migrates SSH→HTTPS); a single label for the build step instead of two.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.3/)
# Version 0.18.4
Source: https://docs.lagerdata.com/source/release-notes/v0.18.4
May 20, 2026
## Bug Fixes
* **`lager python` scripts no longer miss tight response deadlines under streaming back-pressure.** A running script's stdout/stderr were drained from their kernel pipes *inline* on the same generator that forwards bytes back to the CLI over HTTP, so any stall on that socket (slow link, Nagle, retransmit) stopped pipe drainage. Once the 64 KiB kernel pipe filled, the script blocked on its next `print()`. For scripts with tight timing budgets — for example a DA14695 ROM-bootloader handshake that must reply within 50–120 ms of each received byte — this stretched response windows enough to fail roughly 90% of the time, even though the same script run directly on the host succeeded every time. Output is now drained on background threads into a bounded queue so HTTP-write latency can no longer back-pressure the script, stdout/stderr pipe buffers are enlarged to 1 MiB, and the interpreter runs unbuffered (`python -u`). The wire format and public API are unchanged.
* **Removed a potential deadlock when launching a `lager python` script.** The per-script scheduling-priority boost was applied inside the `fork()`/`exec()` window via a `preexec_fn`, which Python documents as unsafe in a multithreaded service. It is now applied from the parent process after the script starts, with identical effect and no window in which a concurrent request could deadlock the launch.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.4
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.4/)
# Version 0.18.5
Source: https://docs.lagerdata.com/source/release-notes/v0.18.5
May 22, 2026
## Bug Fixes
* **`lager debug ... flash` and `... erase` no longer fail with a 500 (`filedescriptor out of range in select()`) on a Lager Box that has been running for a long time.** The box debug service is a long-lived process. Its GDB controller helper rebuilds a fresh `gdb-multiarch` connection on every retry attempt — retries that happen routinely while a debugger connection is coming up during flash and RTT — but a *failed* attempt was never registered for cleanup, so each one leaked the `gdb-multiarch` subprocess and the pipe handles to it. After enough leaks the service crossed the operating system's 1024 file-descriptor limit for `select()`, at which point the tool that performs erase and flash crashed instead of running. Failed attempts are now closed immediately, and the erase/flash path uses `poll()` instead of `select()` so it is no longer bound by the 1024 limit even if descriptors run high. Recovering a box that has already hit this no longer needs anything beyond restarting the debug service.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.18.5
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.18.5/)
# Version 0.19.0
Source: https://docs.lagerdata.com/source/release-notes/v0.19.0
May 23, 2026
## Features
* **OpenOCD debug backend.** Non-Segger debug probes are now first-class peers of J-Link under `lager debug` — same `connect` / `gdbserver` / `flash` / `erase` / `reset` / `memrd` / RTT command surface, same multi-probe slot allocator, same Net Manager TUI. The Lager Box dispatches each debug net to the right backend automatically based on the probe's USB vendor ID. Auto-detected OpenOCD probes:
* **ST-Link V2 / V2-1 / V3** (STMicroelectronics, VID `0483`)
* **Raspberry Pi Debug Probe** (RP2040 Picoprobe / CMSIS-DAP, VID `2e8a`)
* **FTDI FT232H** (`0403:6014`, mapped to `c232hm.cfg`)
* **FTDI FT2232H** (`0403:6010`, mapped to `olimex-arm-usb-ocd-h.cfg`)
* **ARM DAPLink / NXP MK20 CMSIS-DAP** (VID `0d28`)
* **Atmel EDBG / mEDBG** (VID `03eb`)
* **Olimex ARM-USB-OCD-H** (VID `15ba`)
**FTDI FT4232H** is supported via a user-supplied OpenOCD config — the chip exposes four channels and Lager can't guess which one carries SWD without it. Other open-hardware probes whose VIDs aren't on the auto-list (Black Magic Probe, Glasgow, etc.) can also be used by setting `debug_backend: openocd` on the net and supplying an `openocd_config`. Probes already on a J-Link USB ID stay on the J-Link backend, so existing nets are unaffected. OpenOCD nets can run concurrently with J-Link nets on the same Lager Box; the existing J-Link multi-probe slot stride is reused, and OpenOCD adds its own per-slot telnet (`4444 + slot`) and TCL/RPC (`6666 + slot`) ports.
* **DA1469x flash programming over OpenOCD via the Apache Mynewt RAM-resident flash\_loader.** Mainline OpenOCD has no QSPI flash driver for the Dialog/Renesas DA1469x family, so before this release `lager debug SWD flash` against an FT4232H rig connected to a DA1469x silently did nothing despite a green `Flashed!` log line. The Lager Box now ports the upstream GDB-script protocol (`flash.gdb` / `erase.gdb` / `flash_loader.gdb`) to a pure OpenOCD TCL/RPC implementation: it brings the loader up in RAM, drives the `fl_cmd` command struct, programs in chunks, and software-resets on success. The CLI side is unchanged — `lager debug SWD flash --bin ,0x16000000` and `lager debug SWD erase` just work — and absolute XIP addresses are accepted with a clear error if the user passes a flash-relative offset by mistake. The two loader artefacts (`flash_loader.elf` + `flash_loader.elf.bin`) are dropped into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` once per box; `lager update` no longer wipes them out, and `start_box.sh` creates the directory tree on every container start so an operator can `scp` the pair in without first `mkdir -p`. Validated end-to-end on hardware after a few bring-up fixes for the loader's double-buffer pointer rotation, the `fl_cmd_rc` post-loop handshake, and the CLI's absolute-XIP-to-flash-offset translation. The J-Link DA1469x flash path is unchanged; this release adds a working OpenOCD path for the same target.
* **Concurrent multi-probe slots extended to OpenOCD.** A Lager Box can now run up to four debug probes simultaneously across any mix of J-Link and OpenOCD adapters. Each probe gets a deterministic per-slot port window — GDB on `2331+3·slot`, RTT base on `9090+2·slot`, OpenOCD telnet on `4444+slot`, OpenOCD TCL on `6666+slot`. The legacy single-probe configuration (slot 0: GDB 2331, RTT 9090, OpenOCD telnet 4444, OpenOCD TCL 6666) is preserved exactly as before. `lager python` scripts that resolve a debug net via `Net.get(name, NetType.Debug).connect()` now share the same slot pool as the HTTP debug service, so concurrent scripts no longer collide on slot-0 ports.
* **`lager nets add --openocd-config ` and an `openocd_config` field on `nets add-batch`.** Parallels the existing `--jlink-script` flag — the user's `.cfg` is stored on the saved net and materialised on the box before each `openocd` spawn. Required for FT4232H, supported on every other adapter as an escape hatch for vendor-supplied configs.
* **`lager nets set-script` / `show-script` / `remove-script` now work for both backends.** The script-routing trio is backend-agnostic: it detects the target backend from the probe VID + the file's extension and content, and writes to the right slot on the saved net (`jlink_script` for J-Link probes, `openocd_config` for OpenOCD). Pass `--backend jlink|openocd` to override; ambiguous cases are refused with a clear hint instead of silently guessing. `SCRIPT_PATH='-'` reads from stdin. A debug net carries either field but never both, and any switch clears the other slot with a yellow stderr notice.
* **`--jlink-version ` on `setup_and_deploy_box.sh`.** Pin the J-Link tools version installed on a new Lager Box at deploy time, instead of taking whatever Segger ships at the moment of the box build. The deployment options table in the README is also corrected to match the current flag set.
* **Documentation: ST-Link, RP2040, and FTDI listed under Debug & Flashing**, the OpenOCD RTT `chunk_size` knob is documented in the `lager nets` reference, and `lager nets` documents the new `--openocd-config` flag and the unified `set-script` / `show-script` / `remove-script` commands.
## Bug Fixes
* **The Net Manager TUI no longer lets you assign two roles to a single Keithley 2281S (or EA PSB).** A single physical Keithley 2281S can run as a `power-supply` *or* a `battery` but never both — its two firmware entry functions are mutually exclusive. The Add Net wizard's duplicate-detection was checking per-role, so once a `supply` net was saved on a Keithley the wizard kept offering a `battery` row for the *same* VISA address (and vice-versa). The two saved nets fought for the entry function on every command, surfacing as `[Errno 16] Resource busy` — the same hardware constraint that the v0.16.7 known-limitations entry and the v0.16.9 hardware-service shared-session work were spent papering over. The TUI now treats `_SINGLE_CHANNEL_INST` chips as one-net-per-(instrument, address) regardless of role, hiding the second-role row entirely once any role binds the chip; the user-visible message tightens from "Only one net per role may be added per ..." to "Only one net may be added per ...". The same hardening applies to EA PSB (`solar` / `supply`). Direct CLI paths (`lager nets add` / `add-batch`) are unaffected, so power users keep an escape hatch.
* **FTDI adapters whose EEPROM was never programmed now work end-to-end.** A FT4232H with no readable USB serial caused a chain of silent failures on the prior release: the UART scanner emitted bare interface indices (`"0"`/`"1"`/`"2"`/`"3"`) into the saved net's `pin` field, and the box-side UART dispatcher then failed at first use with `UART bridge with serial 2 not found`; the debug-probe regex rejected the empty serial slot in the VISA address and silently fell back to J-Link for what was actually an OpenOCD-backed FTDI, so `lager debug gdbserver` came back as the canned "Failed to connect to debugger" checklist with no real cause; and the `nets show` output labelled the overloaded `pin` field as "Channel:" regardless of role, hiding misconfigurations. Fixed across the whole stack: the scanner now matches `/dev/ttyUSB*` paths by sysfs node instead of by USB serial, the legacy `["0","1","2","3"]` static channel fallback is removed, the VISA regex tolerates the empty serial slot, the TUI refuses to persist a UART net with an unprogrammed-EEPROM placeholder pin (with an actionable message pointing at the EEPROM), and `nets show` is now role-aware (`Pin/serial:` for UART, `Device:` for debug). On `lager debug gdbserver` failures the CLI surfaces the box's structured error directly instead of falling back to the generic checklist.
* **OpenOCD flash failures are no longer reported as success.** OpenOCD's TCL/RPC channel returns `program ...`'s stdout as plain text even when the underlying flash write or verify failed, so a bad flash looked successful to callers — hence the long-standing "Flashed!" line that didn't actually flash. The box debug service now scans the response for `program_error` markers (`** Programming Failed **`, `** Verify Failed **`, etc.) and any `Error:` lines, and surfaces them up to the CLI. Side effect: `Erase complete!` / `Flashed!` no longer print on rigs whose `target.cfg` declares no flash bank — those calls now fail fast with the underlying error.
* **Custom OpenOCD configs uploaded via `lager nets set-script` were silently stored in the wrong slot.** `set-script` previously routed every upload to the `jlink_script` field regardless of which backend the probe used, so OpenOCD configs uploaded that way were ignored at run time. The new backend-detection in `set-script` writes to the correct slot, and the in-box `DebugNet` Python API also picks up `openocd_config` correctly — it was being looked up under the wrong key (`openocd_config_path`) and never decoded to disk, so custom OpenOCD configs had no effect when scripts ran `Net.get(name, NetType.Debug).connect()`. Same shape of bug for `jlink_script` on the in-box API path.
* **Custom OpenOCD configs failed to start with "adapter driver is not configured".** Lager was emitting `-c "adapter serial "` and `-c "transport select swd"` *before* `-f `, but those `-c` commands require an adapter driver that only gets set inside the user's cfg, so OpenOCD bailed out before the cfg ever loaded. The user cfg now occupies the same command-line slot the auto-detected interface cfg would, and the auto `transport select` is suppressed when a user cfg is supplied (vendor cfgs almost always call it themselves, and OpenOCD errors on duplicate sets).
* **Off-box GDB clients can now reach OpenOCD's gdb / telnet / TCL ports.** OpenOCD ≥ 0.11 defaults `bindto` to `127.0.0.1`, so `docker run -p 2331-2342:2331-2342` forwarded traffic to a listener that wasn't accepting it and clients timed out without an error. OpenOCD now binds all interfaces by default, matching `JLinkGDBServer`. The TCL/RPC channel remains 127.0.0.1-only on the wire because the box-side service drives it locally.
* **The Net Manager TUI's "script attached" indicator covers OpenOCD configs.** `has_script` was computed only from `jlink_script`, so debug nets carrying only an `openocd_config` (the new normal for FT4232H rigs) showed no indicator even though one was attached. Now checks both fields.
* **Hardened Lager Boxes admit OpenOCD telnet and TCL traffic.** `secure_box_firewall.sh`'s `LAGER_PORTS` allowlist was scoped to the J-Link-only port window, so on a box hardened with this script any external client reaching OpenOCD's telnet (`4444-4447`) or TCL (`6666-6669`) port was silently dropped while J-Link sessions kept working. The allowlist now mirrors the slot pool published by `start_box.sh`.
## Improvements
* **`DebugNet.connect()` and `.status()` are now symmetric across J-Link and OpenOCD.** `connect()` accepts `force=False` (restart the daemon if already running) and `ignore_if_connected=False` (return the existing status instead of raising) on both backends. `status()` always returns a dict containing at minimum `running`, `pid`, and `backend` keys regardless of which backend handled the probe — backend-specific extras pass through unchanged, so consumers writing portable code can rely on the three guaranteed fields.
* **OpenOCD speed-fallback ladder.** `connect_jlink` already walked `[requested, 4000, 1000, 500, 100]` kHz when the requested speed didn't take; OpenOCD's `adapter speed` is set once at daemon startup with no built-in retry, so a vendor cfg expecting 500 kHz against Lager's 4 MHz default would die silently at the first SWD transaction. The same ladder is now applied at the `DebugNet` layer for the OpenOCD branch.
* **VID/PID-based FTDI dispatch.** The original VID-only FTDI mapping fell over the moment a Lager Box had both an FT232H and an FT2232H plugged in. The dispatcher now keys on the full VID/PID pair and refuses ambiguous cases with a hint pointing at `lager nets set-script --backend openocd` for the FT4232H path.
* **Cleaner debug-script command surface.** The short-lived `set-openocd-config` / `show-openocd-config` / `remove-openocd-config` aliases (which existed only on this branch and never shipped in a tagged release) are removed in favour of the unified `set-script` / `show-script` / `remove-script` trio with `--backend openocd`. There's nothing to migrate; existing CLI usage is unchanged.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.19.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
After upgrading, run `lager update --box ` on each existing Lager Box to deploy the OpenOCD backend, the new firewall ports, and the matching box-side code. To use the new DA1469x OpenOCD flash path, drop the `flash_loader.elf` and `flash_loader.elf.bin` pair into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` on the box.
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.19.0/)
# Version 0.19.1
Source: https://docs.lagerdata.com/source/release-notes/v0.19.1
May 25, 2026
## Bug Fixes
* **`lager debug ... flash` and the DA1469x flash loader now quote the firmware path before handing it to OpenOCD.** OpenOCD parses its TCL commands word-by-word, so a `program` or `load_image` argument with a space in it was being chopped into two TCL words and the underlying flash op either failed loudly with `wrong # args` or hit the wrong file. In practice the path comes from `tempfile.NamedTemporaryFile()` or the fixed `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` tree (no spaces), so the bug never bit in normal operation; the fix is defensive and aligns `box/lager/debug/openocd.py`'s `OpenOcdRpc.program()` and `OpenOcdRpc.load_image()` with the existing quoting pattern in `OpenOcdRpc.rtt_setup()`. Notable for operators who relocate the flash-loader tree via `LAGER_FLASH_LOADERS_DIR=/path/with spaces/`.
* **DA1469x flash\_loader ELF parser now reports a clear error on a truncated symbol-table name instead of a Python `ValueError` traceback.** `box/lager/debug/da1469x_loader.py`'s ELF32 symbol walker used `bytes.index(b'\x00', ...)` to locate the null terminator for each name in the string table, which raised an unwrapped `ValueError` if the strtab itself was truncated. Switched to `bytes.find()` with an explicit error message that names the offending offset; `_resolve_loader_symbols()` still rewraps it as `Da1469xLoaderError` so the call site error type is unchanged.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.19.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.19.1/)
# Version 0.19.2
Source: https://docs.lagerdata.com/source/release-notes/v0.19.2
May 25, 2026
## Features
* **`--ip` now accepts DNS hostnames in addition to IP addresses** on `lager boxes add`, `lager boxes edit`, `lager install`, and `lager uninstall`. Lets a Lager Box sit behind a DNS name (e.g. `box.example.com`) or a Tailscale MagicDNS short name (e.g. `box-1.tailXYZ.ts.net`) instead of requiring the operator to look up and pin a numeric address. Validation is purely syntactic — IPv4/IPv6 (incl. Tailscale `100.x.x.x`) take the existing `ipaddress.ip_address` fast path; everything else is checked against RFC 1123 hostname rules (1–63 char alphanumeric/hyphen labels, ≤253 chars total, single-label allowed for MagicDNS), with actual resolution deferred to SSH/HTTP. The shared validator lives in the new `cli/address_utils.py` (covered by 34 unit tests in `test/unit/cli/test_address_utils.py`); the four call sites all share one error path that prints a "Valid formats:" cheatsheet on failure (`install` / `uninstall` previously printed only the bare error). Inputs that already carry a scheme, port, or path (e.g. `http://...`, `host:5000`, `host/api`) are rejected with a specific message instead of the previous generic "not a valid IP" — the rest of the CLI composes `http://{addr}:port/...` itself, so an embedded one of those would conflict.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.19.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.19.2/)
# Version 0.2.17
Source: https://docs.lagerdata.com/source/release-notes/v0.2.17
November 21, 2025
## Features
### Concurrent CLI Commands
* Enabled concurrent CLI commands while supply TUI is running
* Improved multi-tasking capabilities with TUI interfaces
## Bug Fixes
* Fixed `--line-ending` flag in UART WebSocket client
* UART communication patches and improvements
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.17
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.17/)
# Version 0.2.18
Source: https://docs.lagerdata.com/source/release-notes/v0.2.18
November 24, 2025
## Features
### Automatic Security Configuration
* Added automatic security configuration to `lager update`
* Enhanced Lager Box security during update process
* Automated firewall and security settings
## Improvements
* Updated Keysight E36300 device support
* Various testing and stability improvements
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.18
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.18/)
# Version 0.2.19
Source: https://docs.lagerdata.com/source/release-notes/v0.2.19
November 24, 2025
## Features
### Lager Binaries Command
* Added `lager binaries` command for managing binary files
* Enhanced binary file handling capabilities
## Bug Fixes
* Fixed `lager update` functionality
## Improvements
* Added progress bar to `lager update` for better visibility
* Added verbose flag to `lager update` for detailed output
* Proper firewall configuration during updates
* Cleaned up UART implementation
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.19
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.19/)
# Version 0.2.20
Source: https://docs.lagerdata.com/source/release-notes/v0.2.20
November 26, 2025
## Features
### Python Script Enhancements
* Nets now working inside `lager python` scripts
* Enhanced Python script execution environment
* Added support for UART in Python scripts
### Oscilloscope Web UI
* Added HTTP server for oscilloscope web UI on port 8080
* Added oscilloscope-streamer with WebSocket support
* Exposed oscilloscope visualization interface
## Bug Fixes
* Fixed `python --add-file` command functionality
* Fixed `lager update` udev rules path
* Fixed LabJack device open timeout causing indefinite hangs
## Improvements
* Improved Lager Box deployment scripts
* Enhanced `lager update` to include all necessary files in sparse checkout
* Moved oscilloscope-streamer to gateway/oscilloscope-daemon/
* Code cleanup and organization improvements
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.20
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.20/)
# Version 0.2.21
Source: https://docs.lagerdata.com/source/release-notes/v0.2.21
December 2, 2025
## Features
### Phidget Thermocouple Expansion
* Phidget thermocouple now supports 4 channels (previously limited to fewer channels)
* Enhanced multi-channel temperature measurement capabilities
### Keysight Python Support
* Added Keysight device support to `lager python` scripts
* Enabled Keysight instruments in Python execution environment
## Improvements
* Added dependencies for BLE test suite integration
* Enhanced `lager update` to include BLE test dependencies
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.21
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.21/)
# Version 0.2.22
Source: https://docs.lagerdata.com/source/release-notes/v0.2.22
December 2, 2025
## Features
### Hardware Invocation Service
* Added hardware invocation service for Device proxy pattern
* Enabled remote device method calls through proxy interface
* Improved hardware abstraction layer
### Keysight Python Support
* Added Keysight power supply support to `lager python` scripts
* Enhanced Python script execution with Keysight devices
## Bug Fixes
* Fixed hardware service to extract low-level device from SupplyNet wrappers
* Fixed hardware service: added import paths for supply/battery/eload modules
* Fixed hardware service: handle unhashable types in net\_info cache key
* Fixed PowerSupply Net initialization in get\_from\_saved\_json
* Fixed Net class: changed self.net.channel to self.channel
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.22
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.22/)
# Version 0.2.23
Source: https://docs.lagerdata.com/source/release-notes/v0.2.23
December 2, 2025
## Bug Fixes
* Fixed multi-channel USB resource sharing for Keysight devices
* Fixed Keysight device compatibility issues
## Improvements
* Enhanced Keysight device support
* Improved resource management for multi-channel instruments
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.23
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.23/)
# Version 0.2.24
Source: https://docs.lagerdata.com/source/release-notes/v0.2.24
December 4, 2025
## Features
### New Command
* Added `lager boxes add-all` command for bulk Lager Box management
## Bug Fixes
* Fixed UART data corruption issues
* Fixed NetType.Analog handling for Rigol oscilloscopes in get\_from\_saved\_json
## Improvements
* Updated CLI commands for better usability
* Enhanced oscilloscope integration
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.24
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.24/)
# Version 0.2.25
Source: https://docs.lagerdata.com/source/release-notes/v0.2.25
December 4, 2025
## Improvements
* Updated `lager devenv` command functionality
* Enhanced development environment setup
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.25
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.25/)
# Version 0.2.26
Source: https://docs.lagerdata.com/source/release-notes/v0.2.26
December 5, 2025
## Features
### Webcam Improvements
* Improved webcam interface with enhanced controls
* Reduced zoom latency for better responsiveness
* Cleaned up webcam sidebar interface
## Bug Fixes
* Fixed `lager exec` command
* Fixed net.py module functionality
* Added usb\_net\_wrapper.py for better USB network handling
## Improvements
* Enhanced webcam user experience
* Streamlined webcam code organization
* Optimized zoom operations
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.26
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.26/)
# Version 0.2.27
Source: https://docs.lagerdata.com/source/release-notes/v0.2.27
December 5, 2025
## Improvements
* Internal maintenance release
* Minor bug fixes and stability improvements
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.27
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.27/)
# Version 0.2.28
Source: https://docs.lagerdata.com/source/release-notes/v0.2.28
December 5, 2025
## Bug Fixes
* Fixed `lager exec` command execution
## Improvements
* Updated exec command functionality
* Enhanced remote execution capabilities
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.28
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.28/)
# Version 0.2.29
Source: https://docs.lagerdata.com/source/release-notes/v0.2.29
December 6, 2025
## Bug Fixes
* Fixed `lager python download` command functionality
* Fixed `lager exec` command execution
* Fixed `lager devenv` environment setup
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.29
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.29/)
# Version 0.2.30
Source: https://docs.lagerdata.com/source/release-notes/v0.2.30
December 8, 2025
## Features
### MCC USB-202 DAQ Support
* Added support for Measurement Computing USB-202 DAQ device
* Implemented ADC, DAC, and GPIO functionality for USB-202
* Added USB-202 to instrument detection system
* Enabled USB-202 configuration through lager nets
## Bug Fixes
* Fixed USB-202 VISA address parsing
* Fixed USB-202 GPIO toggle functionality
* Updated USB-202 channel naming for consistency
## Improvements
* Improved `lager update` interface and performance
* Streamlined update process for faster execution
* Cleaned up GPIO interface code
* Enhanced update interface user experience
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.30
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.30/)
# Version 0.2.31
Source: https://docs.lagerdata.com/source/release-notes/v0.2.31
December 10, 2025
## Features
### Oscilloscope Support
* Added support for PicoScope and Rigol oscilloscopes
* Implemented voltage measurements for oscilloscopes
* Added cursor measurement modes and autoscale functionality
## Bug Fixes
* Fixed critical bug in Device proxy: properly handle serialized enum dictionaries
* Fixed critical channel parameter bug when channel=None in Device proxy wrapper
* Fixed autoscale infinite recursion bug in RigolMSO5000 mapper
* Fixed asyncio deprecation warning in PicoScope commands
* Fixed oscilloscope measurement channel parameter bugs
* Fixed cursor timeout issues in oscilloscope operations
* Fixed trigger validation in oscilloscope commands
* Fixed mux.connect() error in oscilloscope interface
## Improvements
* Removed accidentally committed Rust build artifacts from repository
* Added missing clear\_measurement and disable\_cursor\_measure\_mode methods to RigolMso5000
* Improved get\_measure\_item error handling with better logging
* Enhanced oscilloscope command reliability
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.31
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.31/)
# Version 0.2.32
Source: https://docs.lagerdata.com/source/release-notes/v0.2.32
December 11, 2025
## Features
### J-Link Debugger Integration
* J-Link debugger installation now automated during Lager Box deployment
* J-Link automatically installed and configured during `lager update` operations
* Improved debug workflow reliability across all Lager Boxes
### Flexible Deployment
* Lager Boxes can now be deployed with custom usernames instead of requiring `lagerdata` username
* Enhanced deployment flexibility for different organizational setups
## Bug Fixes
* Fixed webcam documentation links in Mintlify docs
## Improvements
* Cleaned up webcam functionality and code organization
* Streamlined deployment process with better error handling
* Reorganized deployment documentation for improved clarity
* Enhanced J-Link integration stability
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.32
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.32/)
# Version 0.2.33
Source: https://docs.lagerdata.com/source/release-notes/v0.2.33
December 15, 2025
## Features
### Improved Hello Command
* `lager hello` now displays the actual Lager Box hostname instead of the Docker container ID
* Format changed from "Hello from DUT " to "Hello from ()"
* Mounted host's `/etc/hostname` file into container for hostname access
### Documentation
* Added Release Notes section to Mintlify documentation
* Created release notes pages for versions v0.2.32 through v0.2.17
* Updated RELEASE\_PROCESS.md with comprehensive release notes instructions
## Bug Fixes
* Fixed eload Net API and multi-channel USB caching issues
* Fixed cache clearing to use inline requests instead of test\_utils import
* Fixed Keithley mapper voltage handling when output is disabled
* Fixed OVP conflicts in voltage tests by raising OVP to 10V
* Fixed Keithley enable state issues with proper state reset
## Improvements
### Lager Update Enhancements
* Reduced password prompts to one input per `lager update`
* Improved third\_party directory mounting to container
* Streamlined update process with better error handling
* Cleaned up update code and output
### Testing & Caching
* Auto-clear hardware service cache after `lager python` scripts finish
* Added cache clearing utilities to all test files
* Increased voltage settling time from 0.3s to 1.0s for more accurate readings
* Removed unnecessary clear cache terminal output
### Power Supply & E-Load API
* Updated Python API for power supplies and e-loads
* Fixed multi-channel USB resource caching
* Improved Keithley device handling
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.33
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.33/)
# Version 0.2.35
Source: https://docs.lagerdata.com/source/release-notes/v0.2.35
December 17, 2025
## Features
### Python API Function Renaming
* Renamed 14 Python API functions for improved clarity and consistency
* Updated documentation to reflect new function names
## Bug Fixes
### ARM/Robot API
* Fixed serial port hangs by adding proper timeout handling
* Fixed position polling with buffer clearing and reduced frequency
* Fixed CLI to close serial port after commands complete
* Fixed movement commands to not wait for 'ok' response
* Removed problematic reset\_input\_buffer() calls that caused hangs
### Battery API
* Fixed Battery API mapper to properly delegate to Keithley methods
* Fixed device name conflict issues
* Fixed class alias placement
### Other Fixes
* Fixed Webcam API
* Fixed `lager update` command
* Fixed GDB `read_memory` response parsing
## Improvements
### Code Cleanup
* Removed unused modules and deprecated code
* Updated documentation and Python API
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.35
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.35/)
# Version 0.2.36
Source: https://docs.lagerdata.com/source/release-notes/v0.2.36
December 18, 2025
## Breaking Changes
### Terminology Restructure: Gateway/DUT to Box
This release standardizes terminology across the entire codebase, replacing "gateway" and "DUT" (Device Under Test) with the unified term "box".
**CLI Changes:**
* The `--dut` option is now `--box` (hidden alias kept for backward compatibility)
* The `--gateway` option is now `--box` where applicable
* All help text and error messages now use "box" terminology
**Configuration Changes:**
* The `DUTS` key in `.lager` configuration files is now `BOXES` (backward compatible - old format is still read)
* Box storage functions renamed (e.g., `load_duts()` → `load_boxes()`)
**Directory Structure:**
* `gateway/` directory renamed to `box/`
* `gateway/lager/lager/` flattened to `box/lager/`
* `gateway_http_server.py` renamed to `box_http_server.py`
* `start_lager.sh` renamed to `start_box.sh`
* Deployment scripts renamed:
* `setup_and_deploy_gateway.sh` → `setup_and_deploy_box.sh`
* `secure_gateway_firewall.sh` → `secure_box_firewall.sh`
* `verify_gateway_security.sh` → `verify_box_security.sh`
**Migration:**
* Existing `.lager` configuration files will continue to work
* The `--dut` CLI option works as a hidden alias for `--box`
* Update any scripts or automation to use the new `--box` option
## Features
### Unified Box Terminology
* Consistent "box" terminology throughout CLI, Python API, and documentation
* Simplified mental model for users - one term for all hardware targets
* Cleaner codebase with consistent naming conventions
### Flattened Directory Structure
* Removed redundant `gateway/lager/lager/` nesting
* More intuitive project navigation
* Cleaner import paths
## Improvements
### Documentation
* All documentation updated with "box" terminology
* Unified help text and error messages
* Updated training data for AI assistants
### Code Quality
* Removed deprecated modules and unused code
* Standardized naming conventions throughout codebase
* Improved code organization and readability
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.2.36
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Migration Guide
### For CLI Users
Replace `--dut` with `--box` in your commands:
```bash theme={null}
# Before
lager hello --dut my-device
lager supply voltage --dut my-device 3.3
# After
lager hello --box my-device
lager supply voltage --box my-device 3.3
```
### For Script Authors
Update any automation scripts to use the new flag names:
```bash theme={null}
# Before
BOX_NAME="my-device"
lager hello --dut $BOX_NAME
# After
BOX_NAME="my-device"
lager hello --box $BOX_NAME
```
### For Box Administrators
Update deployment commands:
```bash theme={null}
# Before
cd deployment
./setup_and_deploy_gateway.sh
# After
cd deployment
./setup_and_deploy_box.sh
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.2.36/)
# Version 0.20.0
Source: https://docs.lagerdata.com/source/release-notes/v0.20.0
May 27, 2026
This release is a direct response to the 2026-05-26 "battery net not responding" incident on a Keithley 2281S, where root-causing one EBUSY took \~2 hours across `lsof`, `dmesg`, bare `pyvisa` probes, and hardware-service introspection. The biggest items below — `lager diagnose`, the `usbtmc` blacklist, automatic ENODEV recovery, and cross-process device locks — collectively eliminate the most common failure modes that drove that session, and surface the rest (e.g. wedged instrument firmware that only mains-power-cycling can fix) with a single one-line diagnosis.
## Features
* **`lager diagnose --box [--type ]` — single-shot net diagnosis.** Polls three box-side endpoints in parallel (USB enumeration + USB-TMC interface-class detection + holder detection + `dmesg` + `lsmod` for usbtmc, bare `pyvisa` `*IDN?` probe, hardware-service in-process session cache) and classifies the net into one actionable bucket with the next step the user should take: `HOST-SIDE: usbtmc kernel module loaded` (→ `lager box update`), `HOST-SIDE: USB device claimed by multiple processes` (→ names the PIDs), `HOST-SIDE: USB device busy`, `TRANSIENT: device disappeared from USB`, `TRANSIENT: device enumerated as USB-TMC but pyvisa probe couldn't reach it` (→ stale libusb context recovery hint), `INSTRUMENT WEDGED` (→ mains-side power-cycle), `NOT ENUMERATED`, `NOT USB-TMC` (LabJack/Picoscope/Acroname use vendor SDKs), or `HEALTHY` (with the IDN string). `--type` is auto-detected from the box's saved nets if omitted. Backwards-compatible against pre-0.20 boxes (per-endpoint 404 fallbacks).
* **`usbtmc` kernel-module blacklist shipped with the box image** at `/etc/modprobe.d/blacklist-usbtmc.conf`. Without this, the kernel auto-binds the `usbtmc` driver to USB-TMC-class instruments (Keithley 2281S, Keysight, Rigol scopes) and claims interface 0; pyvisa-py's libusb backend then can't `set_configuration()` and returns `[Errno 16] Resource busy`. The blacklist is the only durable fix. Deployed by `setup_and_deploy_box.sh` (new boxes) and refreshed by `lager box update` (existing boxes).
* **Cross-process device locks for USB-TMC drivers** via the new `lager.util.device_lock` module. Generalizes the long-standing EA-solar/supply `DeviceLockManager` pattern (`fcntl.flock` on a lockfile keyed by VISA address) and adopts it in the Keithley battery + supply, Rigol DP800, Rigol DL3021 eload, Keysight E36000, and Rigol MSO5000 scope drivers. Guards against a second box-side `pyvisa` client racing the hardware service for the libusb interface-0 claim. Fails open if the locking infrastructure itself errors, so a transient filesystem hiccup can't take legitimate work offline.
* **Version-skew warning** prints once per CLI session to stderr when the CLI's minor version is ahead of the box's by one or more. The 2026-05-26 session started with a 0.19.2 CLI talking to a 0.18.3 box and the first error was opaque — this single line would have cut diagnosis time by hours. Cached per-process by box IP; fails open on any error so a flaky network can never break a working command.
* **Actionable error messages for `[Errno 16/19/110]`** in `lager battery` and `lager supply` commands. Errno 16 EBUSY → "USB device busy — another process holds the libusb interface" with a `Try: lager diagnose ` hint. Errno 19 ENODEV → "Instrument disappeared from USB (re-enumeration)" with a `Hw service should auto-recover; if not: sudo docker restart lager` hint. Errno 110 ETIMEDOUT → "Instrument did not respond to SCPI — firmware may be wedged" with a "mains-side power-cycle required" hint. Raw error remains available via `LAGER_DEBUG=1`.
* **`lager update` verbose status block now includes `modprobe.d:`** alongside the existing `udev rules:` line.
* **`lager diagnose` command-specific docs** at `docs/diagnose.md` covering the three endpoints, the classification decision tree, sample sessions for each bucket, and the `--type` semantics.
## Bug Fixes
* **`lager battery ` and `lager supply ` no longer return `[Errno 19] No such device` until `docker restart lager`** after a USB re-enumeration of the instrument (mains power-cycle, accidental unplug, USB hub port toggle). The hardware-service retry path was gated on a keyword tuple that did not match libusb's ENODEV signature — the existing retry never fired. The tuple is extended, a dedicated `_is_enodev_error()` helper is added, and on ENODEV the `/invoke` retry now evicts every sibling `device_cache` entry on the same VISA address and force-closes the shared `pyvisa` session pool entry. Live-verified on a Keithley 2281S via a USB driver unbind/bind sequence.
* **`lager diagnose` host-side holder detection now works on the actual box image.** The original `/diagnose/usb` endpoint shelled out to `sudo lsof /dev/bus/usb/` to find competing libusb claims, but neither `sudo` nor `lsof` ship in the lager container; the subprocess silently exited 127 and the endpoint always returned `lsof: []`. As a result the `HOST-SIDE: USB device claimed by multiple processes` and `HOST-SIDE: USB device busy` classifications could never fire in production. Replaced with a `/proc/*/fd/*` walk that reads `/proc//comm` for the process name. No external tools, no permission gymnastics.
* **`lager diagnose` classifier no longer misclassifies a healthy USB-TMC instrument as `NOT USB-TMC`** when pyvisa's fresh-probe path can't reach it (most common cause: a stale libusb context inside `box_http_server` after a USB re-enumeration; hw\_service runs in a separate process and recovers transparently). `/diagnose/usb` now reads the device's sysfs interface descriptors and surfaces `is_usbtmc` for USB-TMC class 0xFE / subclass 0x03 devices. The classifier disambiguates: enumerated USB-TMC + fresh-probe failure → new `TRANSIENT` bucket with a concrete recovery hint; enumerated non-USB-TMC → existing `NOT USB-TMC` hint preserved.
* **`lager diagnose` VISA-side error mapping catches all three libusb "device not reachable" message variants.** pyvisa-py emits `[Errno 19] No such device` (libusb's standard ENODEV after a re-enumeration), `[Errno 2] Entity not found` (authorized=0 or denied open), and `No device found.` (generic vendor-not-matched-or-stale path). All three now map to `error_class: nodev` so the classifier consistently returns `TRANSIENT` instead of falling through to `UNCLEAR`.
* **`lager diagnose` VISA section renders all five fields on endpoint-returned errors.** The pre-fix renderer short-circuited on any `error` key in the dict, collapsing the section to a single `error:` line and dropping the `error_class` and `elapsed_ms` context the user needs to interpret the failure.
* **`lager diagnose` prints an actionable message when the box is unreachable** instead of wrapping the raw urllib3 traceback. Now reads `Box '' unreachable at :5000 (connection refused). The lager container may be stopped. Check with: lager ssh --box -- "sudo docker ps"`. Connection-refused and timeout cases are tailored separately.
* **`/diagnose/visa` correctly consults hw\_service's session pool across processes.** `box_http_server` (port 9000) and `hardware_service` (port 8080) are separate processes; the original implementation imported `_visa_resources` from `lager.hardware_service` and saw its own empty copy of the dict rather than hw\_service's live state. The fresh probe then always ran and hit EBUSY on healthy boxes with a cached session. Now consulted via HTTP at `localhost:8080/diagnose/dispatcher`.
* **`device_lock` no longer truncates the lock file before acquiring.** The pre-fix `open(path, 'w')` erased the existing holder's PID at open time, leaving the file empty under contention even when our own acquire later timed out. Now opens via `os.open(O_RDWR|O_CREAT)` and only truncates + writes the PID after a successful flock acquisition.
* **`_dmesg_usb_tail` is robust against missing passwordless sudo.** The pre-fix shell pipeline used `sudo dmesg` (could hang on password prompt), `2>&1 | grep` (merged stderr into stdout where grep filtered it), and a final `tail` (whose rc masked upstream failures). Now uses `sudo -n dmesg` (fails fast on password prompt), does the filtering in Python, and the rc reflects what actually happened.
* **`lager update` Step 5b (new) re-detects the `modprobe_d/` source dir post-pull.** The update probe runs before the `git pull`; on the very first deploy that introduces the directory, the pre-pull probe correctly reports the source path empty and the install step would short-circuit. Re-detects via a fresh SSH round-trip if the pre-pull probe came up empty.
## Improvements
* **TUI WebSocket-failure messages call out the specific next step** instead of `WebSocket connection failed: Failed to connect to WebSocket server`. `lager battery tui` and `lager supply tui` now probe `http://:9000/health` on connect failure and emit one of four actionable messages depending on the response (box reachable but pre-0.20, services partially up, connect-timeout via Tailscale, container not running). Original WS error preserved in parentheses.
* **Documented "TUIs are laptop-only"** in `box/lager/README.md`. Running TUIs directly on the box was the suspected culprit of that incident (a second `pyvisa-py` client competing with hardware-service for interface 0). The OS-level `device_lock` makes this case detect-and-fail-clean instead of silent EBUSY, but the right answer is still to launch TUIs from the laptop CLI.
* **`lager diagnose` output labels clarified.** The header line reads `NetType: ` instead of `resolved role: ` to align with terminology elsewhere in the CLI. The USB section prints `usb-tmc class: yes/no` (newly surfaced from `/diagnose/usb`) so the user can see whether the classifier is treating the device as USB-TMC. The existing kernel-module-status line is renamed from the ambiguous `usbtmc:` to `usbtmc kmod:` so the two related fields are visually distinct.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.20.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.20.0/)
# Version 0.20.1
Source: https://docs.lagerdata.com/source/release-notes/v0.20.1
May 27, 2026
## Features
* **New `--force` option on `lager update`.** Re-runs an update even when Lager thinks your box is already up to date, and rebuilds the box cleanly from scratch. Reach for this if a previous update didn't finish and the box is acting strangely — `lager update --box --force`.
## Bug Fixes
* **Updates no longer hang or fail on some networks.** On certain setups a box couldn't reach GitHub while updating, which made `lager update` appear to freeze for around 15 minutes and then fail. Boxes now connect reliably during an update, and a brief network hiccup is retried automatically instead of stopping the whole update.
## Improvements
* **Updating a box is much faster — about 30 seconds instead of \~15 minutes.** Lager now reuses the work from your last update instead of rebuilding everything from scratch each time. (A longer update still happens when the box's software dependencies actually change.)
* **Use `lager update` to update a box.** This is now the one command to remember. The older `lager box update` has been removed.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.20.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.20.1/)
# Version 0.21.0
Source: https://docs.lagerdata.com/source/release-notes/v0.21.0
May 28, 2026
## Features
* **Pause a running script with `lager.pause()`.** Drop `lager.pause("why")` anywhere in a `lager python` script and it stops at that line mid-run, so you can check the bench from another terminal before it continues — useful for a long test that reaches a known trouble spot. A paused script doesn't lock the box, so your other `lager` commands (read a supply, toggle a GPIO, check a net) keep working while it waits.
* **Resume however suits you.** Press **Enter** in the script's terminal, run **`lager python --continue --box `** from anywhere, or just walk away — it auto-resumes after 5 minutes by default so an unattended run never hangs. The pause prints the `id` and the exact resume commands.
* **Inspect the paused script with a live Python console.** Add `pause(interactive=True)` and connect with **`lager python --console --box `** to get a Python prompt running inside the paused script — read any of its variables, evaluate expressions, or call its functions. This is also how you read a device the script is holding open (e.g. a LabJack), since the console runs in the same process.
## Improvements
* **The built-in `breakpoint()` now works in `lager python` scripts.** It previously errored out; calling `breakpoint()` now triggers the same interactive pause as `lager.pause()`.
* **Tune or disable the auto-resume.** Set a longer (or shorter) wait with `pause("...", timeout=1800)` or `lager python ... --env LAGER_BREAKPOINT_TIMEOUT=1800`; use `timeout=0` to wait indefinitely; set `LAGER_BREAKPOINTS=off` to turn every breakpoint into a no-op for a clean run.
See the [Breakpoints guide](/source/reference/python/breakpoints) for the full reference and a worked example.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.21.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.21.0/)
# Version 0.21.1
Source: https://docs.lagerdata.com/source/release-notes/v0.21.1
May 29, 2026
## Improvements
* **Help that actually shows you what to type.** Every net command now displays the real `lager [NET_NAME] [COMMAND] --box [BOX_NAME]` usage pattern instead of a generic placeholder, and each one comes with copy-pasteable examples. `lager --help` is now grouped into sections instead of one long alphabetical list, so it's far easier to find the command you want.
* **Clearer errors that tell you the fix.** When something goes wrong — a Box you can't reach, a bad config, an SSH or login failure, an instrument that's busy or unplugged, or a command missing its net name — Lager now prints a short message describing the problem and what to do about it, instead of a raw Python traceback. (Need the full technical detail? Re-run with `--debug`.)
## Bug Fixes
* **Fixed a couple of rough edges** in the command-line tooling: a broken internal entry point and an incorrect "defaults set" hint.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.21.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.21.1/)
# Version 0.21.2
Source: https://docs.lagerdata.com/source/release-notes/v0.21.2
May 29, 2026
## Bug Fixes
* **Fixed erratic input in `lager nets tui`.** Since 0.21.0, the interactive Net-Manager TUI could drop keystrokes and feel unresponsive — navigation and the rename/edit dialogs would lag or miss input. The TUI now correctly opts out of the `lager.pause()` stdin watcher that 0.21.0 added, so it no longer competes with the terminal for your keypresses. Breakpoint resume in `lager python` is unchanged.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.21.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.21.2/)
# Version 0.21.3
Source: https://docs.lagerdata.com/source/release-notes/v0.21.3
May 29, 2026
## Bug Fixes
* **Fixed erratic input in `lager supply tui` and `lager battery tui`.** This completes the 0.21.2 fix. Since 0.21.0, the `lager.pause()` breakpoint feature added a background thread that watches `stdin` so you can press Enter to resume a paused `lager python` script. 0.21.2 stopped `lager nets tui` from competing with that thread, but the power TUIs (and the `lager supply`/`lager battery`/`lager arm` confirmation prompts) hit the same problem through a different path — validating the net before launch left a stray `stdin` reader that then stole keypresses. Both TUIs now feel responsive again, and `y`/Enter confirmations are no longer intermittently swallowed.
* **Hardened the breakpoint watcher against future regressions.** The stdin watcher now only starts for a genuine interactive foreground run, so any command that captures script output internally can no longer leak a competing reader. Breakpoint resume in `lager python` is unchanged, including when piping output (e.g. `lager python script.py | tee log.txt`).
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.21.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.21.3/)
# Version 0.22.0
Source: https://docs.lagerdata.com/source/release-notes/v0.22.0
June 1, 2026
## Changes
* **Pin to a release with its tag.** `lager update --version X.Y.Z` and `lager install --version X.Y.Z` now resolve a release number to the matching `vX.Y.Z` tag instead of a same-named git branch. You can pass the bare number (`0.21.3`) or the tag form (`v0.21.3`) — both resolve to the tag, including pre-releases like `v0.22.0-rc1`. Branch targets such as `main`, `staging`, or a feature branch are unchanged and still pin to that branch. Existing `--version X.Y.Z` pins keep working unchanged.
## Bug Fixes
* **Tag pins fetch reliably on every box.** Updating to a tag now fetches it with an explicit refspec so the tag is created as a local ref on the box. Previously, on a box that didn't already have the tag, `lager update --check` could report "update state unknown" and the checkout could fail.
## Deprecations
* **Per-release version branches are deprecated.** Releases no longer publish a `X.Y.Z` branch alongside the `vX.Y.Z` tag — the tag is the single source of truth for pinning. Pin with the tag going forward.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.22.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.22.0/)
# Version 0.22.1
Source: https://docs.lagerdata.com/source/release-notes/v0.22.1
June 2, 2026
## Improvements
* **Standardized Lager Box references to placeholders across the repository.** All `--help` output, command docstrings, source comments, the CHANGELOG, release notes, and documentation now use the `` placeholder (and ``) for box names and addresses; test fixtures use a neutral `test-box` token. This is a documentation/metadata change only — no functional or API changes.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.22.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.22.1/)
# Version 0.22.2
Source: https://docs.lagerdata.com/source/release-notes/v0.22.2
June 3, 2026
## Bug Fixes
* **Multi-output power supplies now apply every command to the selected channel.** On Keysight E363xx and Rigol DP800 series supplies, the channels of a single instrument share one USB session, and the shared driver stayed bound to whichever channel was opened first. Commands that don't name a channel — setting voltage or current, enabling/disabling the output, and reading state — were applied to that first channel instead of the one you selected. On the Keysight E36312A this looked like a limits problem: a voltage setpoint above 6V on CH2 or CH3 (25V channels) was rejected, because the write was actually reaching CH1 (6V max). Each command now targets the correct channel.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.22.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.22.2/)
# Version 0.23.0
Source: https://docs.lagerdata.com/source/release-notes/v0.23.0
June 4, 2026
## Features
* **`lager box config udev add/list/remove` — add your own USB device rules.** Grant a USB device read/write access from inside the Lager Box container by vid:pid, e.g. `lager box config udev add 1209:0001 --box ` followed by `lager box config apply`. This fixes the common case where a freshly-plugged device is owned by root, so tools like `dfu-util` fail to open it ("No DFU capable USB device available"). Pass `--usbtmc` for SCPI/USBTMC instruments to also unbind the kernel `usbtmc` driver (needed for PyVISA/libusb access). Rules persist in the box config and are installed on the host on every `apply` — no more waiting for a new release to support a device.
* **`lager box config reset` — erase the box config to empty.** A single command that clears the config to a clean slate (unlike `init`, which seeds the default `box-tools` volume). Pass `--apply` to erase **and** restart the container in one step — handy before a test run.
* **`lager box config restart` — restart the container without changing config.** Brings up a fresh container with the same configuration, useful for per-test isolation.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.23.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.23.0/)
# Version 0.24.0
Source: https://docs.lagerdata.com/source/release-notes/v0.24.0
June 5, 2026
## Features
* **DUT context for AI agents — the MCP server now understands the board, not just the bench.** New `DUTContext`, `SubSystem`, and `DocRef` models capture a device-under-test's purpose, summary, MCU, key peripherals, and schematic/datasheet references (by URL or synced `repo_path`). New `discover_dut()` and `cite_schematic()` tools and `lager://dut/overview.md` resources surface it, and `discover_bench(net)` now returns the parent subsystem and relevant doc refs. Schematics are referenced, never stored — the agent fetches and analyzes them with its own tools.
* **`lager box dut show | edit | add-doc`** — author and inspect DUT context (subsystems, documentation references) stored in the Lager Box's `bench.json`.
* **`DebugNet.session()` and `DebugNet.rtt_defmt(elf=...)`.** `session()` is a context manager that scopes connect-on-entry and guaranteed teardown so the safe connect/disconnect ordering is encoded once. `rtt_defmt()` opens an RTT session and pipes it through `defmt-print`, yielding decoded log lines instead of raw bytes — so on-box `lager python` tests can assert directly on defmt-encoded firmware logs. `defmt-print` is now bundled in the Lager Box image.
* **MCP slash-command prompts** (`write_lager_test`, `explore_bench`, `assess_test_feasibility`) and a new "AI Agents (MCP)" documentation tab with a server overview and DUT-context guide.
## Improvements
* **The MCP server is now a read-only discovery & planning surface.** Its purpose is to let an agent learn the bench and DUT well enough to write and run a test; execution happens in the test script via `lager python … --box `. `discover_bench`/`discover_dut` echo the real address the client connected on and return a ready-to-run command, and `discover_bench` now reports instrument channels, capabilities, firmware, and authored specs/ranges.
* **Reconnect-aware RTT and self-healing reset/read\_memory on both J-Link and OpenOCD backends.** The RTT reader transparently re-attaches to the same port after a socket drop (it only re-attaches to an already-running server, never starts one), and `reset`/`erase`/`read_memory` reconnect automatically when no server is running — so scripted flash → attach → reset loops no longer thrash. A DA1469x guard avoids auto-starting an unhalted server.
* **Simplified agent-facing net metadata to `purpose` / `notes` / `tags`**, replacing the overlapping `description` / `dut_connection` / `test_hints` fields. `lager nets describe` now takes `--purpose` / `--notes` / `--tag`, and the Net Manager TUI edit dialog matches.
* **The MCP server auto-reloads bench config on change** (it watches the mtimes of `bench.json`, `saved_nets.json`, and `box_id`), so DUT/net edits are picked up on an agent's next request without a reload call or service restart. It also warns when a subsystem references a net that doesn't exist.
## Bug Fixes
* **Debug connect no longer burns its retries when the GDB remote rejects non-stop mode.** JLinkGDBServer rejects `set non-stop on`, which previously exhausted all connect retries and skipped target verification and RTT control-block auto-detection. The connect now detects that specific rejection and retries once with an all-stop controller; OpenOCD keeps non-stop, and any other target error still fails loudly.
* **`lager box dut add-doc` / `edit` now save `bench.json` without requiring passwordless sudo**, and no longer raise a `KeyError` on a Lager Box whose `bench.json` has no DUT block.
* **`lager box dut edit` and `lager box config edit` now honor `$EDITOR`/`$VISUAL` flags** (e.g. `subl -w`, `code -w`, `vim -p`) by parsing the editor string with `shlex.split()` instead of treating it as a single program name.
## Breaking Changes
* **The MCP server no longer exposes hardware I/O or mutation tools.** The `quick_io`, `install_dependency`, `run_python`, `pip`, `logs`, `defaults`, and `binaries` tools, the safety/preflight engine, the `run_lager` CLI passthrough, and the audit subsystem have been removed. Agents now execute tests via `lager python path/to/test.py --box ` rather than over MCP. Per-bench safety constraints are now advisory metadata surfaced in `discover_bench`, not enforced.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.24.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.24.0/)
# Version 0.25.0
Source: https://docs.lagerdata.com/source/release-notes/v0.25.0
June 11, 2026
## Features
* **Custom serial-device assignment — RS-232 instruments become first-class.** Instruments the Lager Box cannot identify by USB enumeration (first case: the Rigol DP711 power supply, reached through a generic Prolific USB-serial cable that enumerates as the cable, not the PSU) can now be assigned to their cable once with `lager nets assign`. From then on the instrument scans, nets, and drives exactly like an auto-detected device.
* **`lager nets assign`** — `--list` shows assignable devices, current assignments, and unassigned USB-serial cables; `lager nets assign Rigol_DP711 --serial ` (or `--port ` for serial-less clone cables) stores the assignment on the Lager Box, durable across reboots and replugs. `--baud` overrides the catalog default when the instrument's front panel differs; `--as-net [NAME]` creates the net in the same step; `--remove` unassigns.
* **Assign Device flow in the Net Manager TUI** — the interactive twin of `nets assign`: pick a cable, pick the instrument (with optional baud override), and name its net in a follow-up dialog. Assignments can be removed from the same screen.
* **Rigol DP711 (DP700-series) support** — single-channel RS-232 power supply driver with the DP800-compatible method surface, addressed by a durable `serial://:/serial/` (or `/port/`) identity that re-resolves to the live tty at open time — surviving tty renumbering, port moves, and replugs — with stale-session self-healing.
* **Generic `POST /net/command` endpoint** on the Lager Box HTTP server for Tier-1 instruments (GPIO, ADC, DAC, thermocouple, watt-meter, e-load), giving them the same warm in-process path the supply/battery endpoints use instead of a subprocess per call. The `netCommand` capability is advertised in `/status` only when the route actually registers.
## Improvements
* **Nets live and die with their assignment.** Removing (or replacing) a cable assignment deletes the saved nets bound to its address and reports them; pre-existing generic-UART nets on the cable are retired at assign time so a terminal session can never fight the instrument driver for one tty. A baud-only re-assign keeps existing nets.
* **The scanner reports assigned instruments, not their cables.** `lager instruments`, the TUI, and the Lager Box's `/instruments/list` show the catalog instrument at its durable `serial://` address; the cable's generic UART record is suppressed while assigned, and assigned ttys are excluded from the Dexarm G-code handshake probe.
* **Backend JSON parsing is robust to doubled output for objects, not just arrays** — both the CLI and TUI parsers now take the first complete JSON value, fixing a latent recovery bug for the known "double execution" Lager Box output.
## Bug Fixes
* **Manually-added supply and battery nets are driveable again.** `lager nets add`/`delete`/`add-batch` now normalize the legacy role tokens `supply` → `power-supply` and `batt` → `battery`; the short tokens were previously saved verbatim, producing nets that listed fine but that no supply/battery command could drive. The tokens remain accepted as input aliases, `delete` reaches legacy nets saved under either spelling, and channel validation for supplies now actually runs on `nets add`.
* **Documentation and error hints no longer reference the nonexistent `lager nets create` family** — renamed to the real `add`/`add-all`/`add-batch` commands across the docs, READMEs, and four runtime error messages, and the documented role vocabulary now matches what saved nets actually carry.
* **The DP700 driver reports a missing cable as a device-not-found error** when unplugged mid-session, instead of a raw Python traceback.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.25.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.25.0/)
# Version 0.26.0
Source: https://docs.lagerdata.com/source/release-notes/v0.26.0
June 11, 2026
## Security
* **Key authorization is rate limited.** Bad-token attempts against the Lager Box `/authorize-key` endpoint are limited per client IP (5 attempts per 60-second window, then HTTP 429); the window resets on a successful authorization.
* **The Lager Box web service `SECRET_KEY` persists across restarts.** Generated once and stored at `/etc/lager/secret_key` (mode 0600) instead of regenerated on every boot, so sessions survive container restarts.
* **`org_secrets.json` is held at owner-only permissions.** The on-box secrets file is tightened to mode 0600 at load time, and the boot-time permission fix is best-effort so an unexpected owner can no longer abort container startup.
* **Instrument device nodes are scoped to a dedicated `lager` group.** The shipped udev rules grant `MODE="0660", GROUP="lager"` instead of world-writable 0666; `lager update` creates the group on the Lager Box host when missing and the container joins it automatically. User-added udev rules (`lager box config udev add`) default to the same scoping — run `lager update` on a Lager Box before applying new user udev rules so the group exists.
## Features
* **Per-connect J-Link script override — `DebugNet.connect(script=...)`.** Pass a path on the Lager Box or a base64 blob to swap the J-Link script for one session phase (for example, a halt-in-place reset script for a memory read-back, then the stock script to reboot the target). The bytes are copied to the shared script path so `flash`/`reset`/`read_memory` pick the new script up immediately; an already-running gdbserver adopts it on relaunch (`force=True`). Invalid input is ignored and the net's saved script stays in effect.
* **Opt-in cache-coherent post-program verify for DA1469x QSPI images (experimental).** Set `LAGER_DA1469_UNCACHED_VERIFY=1` to read programmed `.bin` bytes back through the uncached QSPI mirror after a cache-controller flush: a matching image suppresses J-Link's stale-cache false "verification failed" report from no-reset attaches, a real mismatch is reported with its first differing address, and an inconclusive read-back leaves the original output untouched. `LAGER_DA1469_UNCACHED_VERIFY_BYTES` caps the compare (0 = whole file). Default off; flash output is unchanged when unset.
## Bug Fixes
* **`lager debug ... gdbserver --rtt` no longer leaves the target halted** on probes whose J-Link GDB server rejects non-stop mode: the RTT control-block scan implicitly halts the core in the all-stop fallback, and the core is now resumed after the scan. Non-stop and OpenOCD paths are unchanged.
* **`lager box config` host-side operations no longer dead-end on Lager Boxes with customer-managed SSH users.** The dedicated `~/.ssh/lager_box` key previously replaced ssh's default identity list, so a user whose own key was authorized (via `ssh-copy-id`) failed every host-side call with `Permission denied (publickey,password)` even though `lager ssh` worked. The SSH runner now retries once without the dedicated key on an auth failure, so default identities get their chance.
* **SSH transport failures are reported as SSH failures.** An unreachable or hung Lager Box host during mount pre-flight was misread as "path missing" (producing a wrong manual fix) or crashed with a raw traceback; it is now classified separately with the real user\@ip and actionable fixes, `mount add` persists the mount, and `apply` warns and continues.
* **Mount pre-flight runs after the confirm prompt and after apt/sysctl/udev provisioning**, so a mount of a file installed by an apt package in the same config (for example `/usr/bin/dfu-util`) works in a single `apply`, and the host is no longer mutated before the operator confirms. `apply --skip-restart` no longer runs the pre-flight at all.
* **Leaked file handles closed** in project packaging (`zip_dir`) and the gdb `--debugfile` read; **bare `except:` clauses replaced with specific exceptions** across the CLI so interrupts and unexpected errors surface instead of being silently swallowed.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.26.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.26.0/)
# Version 0.27.0
Source: https://docs.lagerdata.com/source/release-notes/v0.27.0
June 12, 2026
## Features
* **Custom LabJack pin selection for i2c/spi nets.** `lager nets add` now accepts `--sda`/`--scl` (i2c) and `--cs`/`--sck`/`--mosi`/`--miso` (spi). Any DIO pin (FIO0-FIO7, EIO0-EIO7, CIO0-CIO3, MIO0-MIO2) or raw DIO number can be assigned per signal; omit `--cs` for 3-pin SPI with manual chip select. Defaults are unchanged, and pins already used by another saved LabJack net warn without blocking.
* **Net TUI pin-picker dialog.** Adding a LabJack i2c/spi net in the TUI opens a pin dialog with the historical defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI: CS=FIO0/SCK=FIO1/MOSI=FIO2/MISO=FIO3). Duplicate pins block the save; pins claimed by saved nets warn live.
## Bug Fixes
* **Net TUI buttons no longer need multiple clicks** (a 0.25.0 regression): box round-trips ran on the UI thread and froze the interface for seconds per call, worst right after launch and on Assign Device. All box calls — assign flows, add/save, delete, rename, delete-all, edit details — now run in the background with busy indicators and disabled controls while in flight.
* **Fixed a `signal only works in main thread` crash** when TUI actions ran Lager Box scripts from worker threads; the Ctrl+C handler is now only installed for interactive command-line runs.
## Improvements
* `lager i2c` and `lager spi` display custom LabJack pin assignments with canonical pin names (for example `EIO0` instead of a raw DIO number).
* Net TUI startup is faster: the saved-nets list is no longer fetched a second time while the first screen paints.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.27.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.27.0/)
# Version 0.27.1
Source: https://docs.lagerdata.com/source/release-notes/v0.27.1
June 12, 2026
## Features
* **`lager authorize --box [BOX]`** sets up passwordless SSH to a Lager Box in one step: it creates `~/.ssh/lager_box` if needed, copies the key to the box (one password prompt), and verifies it works. Running it again against an already-authorized box just confirms it. When a command fails with `Permission denied (publickey,password)`, the error now points you straight at `lager authorize`.
## Bug Fixes
* **`lager nets` and `lager instruments` no longer cut off data.** UART serial-port paths (for example `/dev/ttyUSB0`) and full VISA/USB addresses now display in full instead of being truncated.
* **Clearer SSH error reporting in `lager box dut` and `lager box config`.** A failed connection now shows the real cause and the fix to run, instead of a raw error line or a misleading "no snapshot" message.
## Improvements
* **Cleaner `--help` usage lines.** Command groups now read `COMMAND [OPTIONS]`, and `lager nets` / `lager authorize` show `--box [BOX_NAME]`, consistent with commands like `lager supply`.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.27.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.27.1/)
# Version 0.28.0
Source: https://docs.lagerdata.com/source/release-notes/v0.28.0
June 13, 2026
## Features
* **`lager python` automatically reserves the box while it runs.** Every run acquires the box lock at start and releases it on exit, `Ctrl+C`, crash, or kill, with a server-side TTL + heartbeat reap as the backstop. Lock identity is CI-aware, so parallel CI matrix jobs queue against a shared box instead of colliding: collisions fail fast on a dev machine and wait (up to `LAGER_LOCK_WAIT`) in CI. `--detach` holds an eternal lock you release with `lager boxes unlock`. The box-mutating admin commands — `lager install`, `uninstall`, `update`, `install-wheel` — also hold the lock across their destructive steps so a concurrent test is never killed mid-run. Tune or disable via `LAGER_AUTO_LOCK_DISABLE`, `LAGER_LOCK_WAIT`, `LAGER_LOCK_TTL`, `LAGER_LOCK_HEARTBEAT`, and `LAGER_LOCK_HOLDER`.
* **Explicit `lager boxes lock` reservations are never disturbed** by any auto-locking command — they keep their no-expiry semantics on new and old box servers alike.
## Bug Fixes
* **The supply TUI now works on slow instruments (e.g. Keithley 2281S).** Previously its live readout could stay stuck at `00.000` with repeated "Hardware service unreachable" errors and timed-out commands. The box-side monitor now reads the full display state in a single call per update and paces itself to the instrument, so readings populate and commands respond promptly — even when the instrument is shared with the battery simulator.
* **TUI error messages now say what actually failed** (a timeout, a refused connection, or a device error) instead of an empty message, and a reachable box with a failed supply/battery session points you at the instrument (check it is on and shows in `lager instruments`) instead of suggesting an outdated box image.
* **`lager battery tui` with a non-battery net now exits with an error code** instead of reporting failure but exiting successfully.
## Improvements
* **Pinned `textual` and `python-socketio` to compatible ranges** so a fresh `pip install lager-cli` can't pull in a newer, breaking version of either library. Existing installs are unaffected.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.0
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.0/)
# Version 0.28.1
Source: https://docs.lagerdata.com/source/release-notes/v0.28.1
June 15, 2026
## Features
* **`spi`, `i2c`, and `energy-analyzer` instruments now run on the Lager Box's warm path.** These roles previously went through the box's per-command Python executor; they now run in the long-lived box server alongside gpio/adc/dac/eload/thermocouple/watt-meter, removing an interpreter spawn and device re-open on every command. Behavior and dashboard logs are unchanged, and energy-analyzer read durations are clamped to 0.1–30s. The rollout is back-compatible — an older Lager Box automatically falls back to the previous path.
* **WebSocket transport support added to the Lager Box.** The box image now bundles `simple-websocket`, so clients can use a native WebSocket connection instead of long-polling, negotiated automatically.
## Bug Fixes
* **`lager ssh` now uses the key set up by `lager authorize`.** Authorized Lager Boxes were still dropping to a password prompt because `lager ssh` didn't offer `~/.ssh/lager_box`; it now does when that key exists, while leaving the password fallback intact for boxes that haven't been authorized.
* **UART devices are now opened exclusively.** A second user of a serial port — another dashboard session, or `lager uart` while a Workbench session is live — now fails fast with a clear "device in use" message instead of silently interleaving reads on the same port.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.1
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.1/)
# Version 0.28.2
Source: https://docs.lagerdata.com/source/release-notes/v0.28.2
June 17, 2026
## Features
* **Save your devenv container setup in the project.** You can now store container settings in your project's `.lager` file instead of retyping them or keeping shell aliases. Use `devenv set`/`unset`/`show` for basic settings (image, shell, user, group, ports, and more), `devenv mount add`/`remove`/`list` for folders to share into the container, and `devenv env set`/`unset`/`list` for environment variables. Both `devenv terminal` and `lager exec` use these settings automatically, so they travel with the repo for everyone on the team.
* **Add settings for a single run.** `devenv terminal` and `lager exec` take `-v HOST:CONTAINER` to share a folder. `devenv terminal` also takes `-e FOO=BAR` to set a variable and `--passenv NAME` to forward one from your shell. Paths can use `~` and `${PROJECT_ROOT}`, so saved settings work on any machine.
* **Preview a session without launching it.** `devenv terminal --info` prints the exact `docker` command it would run, then exits without starting anything.
* **Skip the reset before a memory read.** `lager debug memrd --no-reset` skips the reset-and-halt the Lager Box normally does before reading a DA1469x — useful on a blank chip where you don't want to reboot it.
## Bug Fixes
* **Reading memory from a running DA1469x now works.** Live firmware turns off the debug port, so reads used to fail. The Lager Box now resets and halts the chip first. This reboots the device under test — pass `--no-reset` to skip it.
* **Some memory reads returned wrong values.** Reads of certain chip registers now return the correct values.
* **`devenv terminal --group` now works.** The group setting was being ignored before; it is now applied.
## Improvements
* **More predictable devenv settings.** When the same setting is given both on the command line and in `.lager`, the command line now wins. The container entrypoint can also be saved in `.lager`.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.2
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.2/)
# Version 0.28.3
Source: https://docs.lagerdata.com/source/release-notes/v0.28.3
June 18, 2026
## Features
* **`lager diagnose` now works on J-Link debug nets.** Point it at a `debug` net — `lager diagnose --box ` — and it walks the whole debug-probe stack and tells you exactly what's wrong and what to do about it: the probe isn't on USB (cable/power/hub), the J-Link software isn't installed on the Lager Box, the probe is held by another process or its firmware is wedged (power-cycle it), a debug server is wedged, the target board is unpowered, the target is locked by readout/IDCODE protection, the net's device/MCU name is wrong, or the probe is fine but can't reach the target over SWD/JTAG (wiring, reset, or speed). When a debug session is already running for the probe, diagnose reports from that session instead of interrupting it. OpenOCD/ST-Link probes get basic coverage (probe detection and debug-server state). Previously `lager diagnose` only covered USB-TMC instruments such as power supplies, electronic loads, and scopes.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.3
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.3/)
# Version 0.28.4
Source: https://docs.lagerdata.com/source/release-notes/v0.28.4
June 22, 2026
## Improvements
* **`lager update` is much faster.** Box image rebuilds now reuse a build cache for the Rust (`defmt-print`) and Python package layers, so a from-scratch rebuild reuses already-downloaded packages and compiled artifacts instead of redoing them — a cold rebuild that used to take around 20 minutes now finishes in a few minutes, and a warm rebuild takes seconds. `lager update` also checks up front that the box's Docker supports BuildKit and tells you how to upgrade if it doesn't.
* **`lager update` asks for the sudo password at most once.** The udev, modprobe, sudoers, and box-config setup steps used to each prompt separately, so a box that needed several of them could ask for the password multiple times in one run. They now run in a single step — at most one prompt, and none at all on a fully set-up Lager Box.
## Bug Fixes
* **A repeat `lager update` no longer rebuilds when nothing changed.** The box keeps a record of its build inputs so it can skip an unnecessary rebuild, but that record couldn't be updated and went stale, so every update rebuilt the image and restarted the box (about 30 seconds). It is now written reliably, so an unchanged box finishes in about a second.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.4
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.4/)
# Version 0.28.5
Source: https://docs.lagerdata.com/source/release-notes/v0.28.5
June 24, 2026
## Bug Fixes
* **`lager update` fails fast with a clear fix when a box is missing buildx.** The BuildKit work in 0.28.4 made the box image require Docker's `buildx` plugin, which a stock `docker.io` install (for example on Ubuntu) doesn't bundle. The up-front check used to pass on the Docker version alone and then the build died minutes later with a confusing "buildx component is missing or broken". `lager update` now verifies buildx is actually present, checks it before stopping the box's container (so a box that can't build is never taken offline), and tells you the exact command to install it.
* **A stale SSH connection no longer breaks `lager update`.** A leftover SSH control socket from an earlier interrupted run could be silently reused and surface as "Permission denied (publickey,password)" on the box state check, even when the key worked fine. `lager update` now clears any leftover connection before it starts.
* **`lager update` accepts a first-seen box host key.** Its SSH calls now auto-trust a brand-new box's host key the same way the key-setup step does, so updating a box that isn't yet in `known_hosts` no longer fails with "Host key verification failed".
## Improvements
* **Provisioning a box installs buildx automatically.** New box setup now installs the Docker `buildx` plugin (falling back to the official buildx binary when distro packages don't provide a working one), so a freshly provisioned box is ready to build the box image and never hits the update preflight error.
## Installation
To install this version:
```bash theme={null}
pip install lager-cli==0.28.5
```
To upgrade from a previous version:
```bash theme={null}
pip install --upgrade lager-cli
```
## Resources
[View Release on PyPI](https://pypi.org/project/lager-cli/0.28.5/)
# Version 0.29.0
Source: https://docs.lagerdata.com/source/release-notes/v0.29.0
June 29, 2026
## Features
* **`lager boxes add` now requires `--user` (breaking change).** The implicit `lagerdata` default has been removed — you must specify the box's login user explicitly when adding a box.
* **Read a USB net's state without changing it.** The new `lager usb state` reports whether a port is on or off, read-only.
* **Run a one-off command on a Lager Box over SSH.** `lager ssh --box