/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 if you move the cable to
a different port, the assignment breaks.
**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 you re-assign a cable to a different instrument, and when you
switch 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` - Lager Box 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` - Lager Box 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` - Lager Box 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` - Lager Box 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 you add an `i2c` or `spi` net. A pin dialog
opens with the defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI:
CS=FIO0/SCK=FIO1/MOSI=FIO2/MISO=FIO3). You can choose any DIO pin per signal,
and you can set CS to none for 3-pin SPI. A pin already used by a saved net
shows 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` - Lager Box 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` - Lager Box 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. That tells you which backend's script you see, and it does not pollute a redirect.
```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` - Lager Box 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` - Lager Box 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` - Lager Box 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, LabJack U3 |
| `dac` | Analog output | LabJack T7, LabJack U3 |
| `gpio` | Digital I/O | LabJack T7, LabJack U3 (`FIO4` and up), 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. It applies that script 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 (Lager Box), 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/thermocouple)**: 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 | | Lager Box 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. The box caps this for an attached run; with `--detach` no cap applies |
| `--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 as soon as the box accepts the job. It returns before the
script starts, and before any setup that the script needs (unpacking a module
directory, installing its `requirements.txt`)
* No stdout/stderr is streamed back
* The script continues running on the box
* Use `--reattach` to see its output, `--kill` to stop it later
The launch returns before the setup runs. A job can therefore fail to start
after the prompt comes back -- a `requirements.txt` that will not install, say.
You see that failure through the job, not at the prompt. `--reattach` shows the
failure and exits non-zero:
```bash theme={null}
lager python ./my_module --box my-lager-box --detach
# Process detached (Process ID: 7f3c...)
lager python --reattach 7f3c... --box my-lager-box
# ERROR: Could not find a version that satisfies the requirement ...
# Process exited with code 1
```
The box holds the box lock for as long as the detached job runs and releases it
when the job ends. See [locking](/source/reference/cli/locking).
## 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 the
packages that your scripts depend on, use the declarative `lager box-config pip`
commands. Those commands record the packages in the box config and rebuild the
container. The packages then 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 the REST
API that the router publishes, such as the API on a MikroTik hAP. Through that
net you can:
* inspect the interfaces, and toggle them
* list the wireless clients and the DHCP leases
* block internet access
* reset the router to a clean baseline
A router net is useful for connectivity tests and for 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` (Lager Box 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` | | Lager Box 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 — the devices that received an IP address.
```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
# 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` | Lager Box 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 must use the streaming capture commands to export waveform data for analysis.
**Common Options for all measure commands:**
| Option | Description |
| ----------- | ------------------------------------------ |
| `--box BOX` | Lager Box 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` | Lager Box 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 Lager Box. 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` | Lager Box 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` | Lager Box 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` | Lager Box 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` | Lager Box 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` | Lager Box 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 Lager Box, or run a single command on it
and return.
## Syntax
```bash theme={null}
lager ssh [OPTIONS] [COMMAND]...
```
## Options
| Option | Description |
| ----------- | ---------------------------- |
| `--box BOX` | Lager Box 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. There is 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 Lager Box 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. You can
fix a `Permission denied (publickey,password)` error without the key path and
without the `ssh-copy-id` incantation.
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` | Lager Box 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 the copy if already authorized** — if key authentication already works
for the box, it goes straight to step 6 (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.
6. **Registers** the public key on the box as
`/etc/lager/authorized_keys.d/lager-box--.pub`, over the key it
just installed, so no extra prompt.
Step 6 is what makes the key durable rather than merely present. `ssh-copy-id`
appends to `~/.ssh/authorized_keys` outside every marker block. `start_box.sh`
preserves such a line against its own rebuild. It cannot preserve that line
against another key manager that rebuilds the file from its own source. Such a
manager drops every line outside its own markers, and `start_box.sh` then
re-creates its block from the key directory alone. A key registered in the key
directory comes back; one that was only appended does not.
The write is attempted unprivileged first, then with `sudo -n`. On a box Lager
alone manages, the key directory is writable by the box user and no sudo is
involved. On a box hardened by another key manager, the directory is root-owned
deliberately. A writable key directory will let any user on the box authorize
any key. The `sudo -n` fallback covers that case wherever the box user has a
NOPASSWD grant broad enough.
If both attempts fail, the command warns and still reports success (the key
works; only its durability is lost) and prints the grant to add. **Lager does
not install this grant.** A fleet that scopes sudo tightly is the system that
made the directory root-owned. That fleet is therefore the right place to decide
who can write there. Add the grant through that fleet's own provisioning:
```
ALL=(ALL) NOPASSWD: /usr/bin/tee /etc/lager/authorized_keys.d/lager-box-*.pub
```
A sudoers wildcard does not match `/`, so the grant cannot reach outside the
directory. The grant does not restrict which key is written, because the
content is the box user's to choose. On a box where that user is already
root-equivalent, that changes nothing. On a box where the user is not
root-equivalent, read the key before you add the grant.
Do not widen the key directory's permissions instead. A writable
`/etc/lager/authorized_keys.d` lets any user on the box authorize any key, which
is exactly what a hardened box closed.
The key lives at `~/.ssh/lager_box`. That filename is not one of SSH's default
identities. So `lager ssh` passes `-i ~/.ssh/lager_box` explicitly when the key
exists (since lager 0.28.1), and an authorized box connects without a password.
`-i` replaces SSH's default identity list rather than adding to it. So
`lager ssh` also names each of your own default keys that exists, after
`lager_box`. Those keys are `id_rsa`, `id_ecdsa`, `id_ed25519`, and their `-sk`
forms. A box you authorized with your own key therefore still connects, even
when `lager_box` is not authorized there.
***
## 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
* [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box) — turn an Ubuntu machine into a Lager Box
# 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` | Lager Box 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 | Lager Box 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 | Lager Box 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` | Lager Box 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` | Lager Box 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 tripped (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 runs, other `lager supply` CLI commands (e.g., `lager supply PSU voltage 3.3 --yes`) will route through the TUI's WebSocket connection on port 9000. They share the one USB connection to the instrument. If the TUI is not open, 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):** If a new voltage will exceed the current OVP limit, the driver raises OVP by 10% headroom first. It then sets the new voltage and restores the desired OVP limit. 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
# 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 VCC
> boxes
> debug flash --hex 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
# Thermocouple
Source: https://docs.lagerdata.com/source/reference/cli/thermocouple
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` | Lager Box name or IP address |
| `--json` | Emit a machine-readable JSON object instead of formatted text |
| `--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`
# UART
Source: https://docs.lagerdata.com/source/reference/cli/uart
Connect to UART serial ports
Connect to UART serial ports on Lager Boxes 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` | Lager Box 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) |
| `--sessions` | List the UART sessions that hold a net, then exit |
| `--force` | Take over the net if another session holds it |
***
## 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
```
### A Net Held By Another Session
A UART device takes one reader at a time, so a second session on the same net
is refused:
```
Error: UART net 'SERIAL1' is already in use by another session
To take over the net: lager uart SERIAL1 --force --box my-lager-box
```
Ask the box who holds what:
```bash theme={null}
lager uart --sessions --box my-lager-box
```
```
Net Device Path Client Reader
==============================================
SERIAL1 /dev/ttyUSB0 connected running
```
`Client` reports whether the holder's end of the connection is still there.
A holder whose client reads `gone` releases the net on its own within a
moment. One that reads `connected` belongs to somebody, so check before you
take it:
```bash theme={null}
lager uart SERIAL1 --force --box my-lager-box
```
***
## 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 | | List what the command removes. Make no 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/99-instrument.rules`, `/etc/udev/rules.d/99-lager-user.rules`, and legacy `/etc/udev/rules.d/lager-*.rules`, then a rules reload |
| Modprobe config | `/etc/modprobe.d/blacklist-usbtmc.conf` |
| Sudoers config | `/etc/sudoers.d/lagerdata-udev`, `/etc/sudoers.d/lager-box-config`, `/etc/sudoers.d/lager-bench-json` |
| Firewall script | `/usr/local/lib/lager/secure_box_firewall.sh`, and a UFW reset to deny-incoming/allow-ssh |
| Sysctl config | `/etc/sysctl.d/99-lager-box-config.conf` |
| `lager` group | Removed if present |
| `~/third_party` | J-Link, custom binaries |
| Legacy deploy key | `~/.ssh/lager_deploy_key*` and SSH config entry (from pre-open-source deployments) |
Uninstall removes those three sudoers files **by name**. Lager never touches any
other file under `/etc/sudoers.d/`. A grant that you or your management platform
added in a separate file there is left alone. That holds both for uninstall and
for the installs and updates that regenerate Lager's own files.
## 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** - Offers `~/.ssh/lager_box`, then your own default identities, and falls back to password if neither authenticates
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 Lager Boxes with comprehensive progress tracking and automatic configuration.
## Syntax
```bash theme={null}
lager update [OPTIONS]
```
## Options
| Option | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `--box BOX` | Lager Box name or IP address |
| `--version VERSION` | Release tag, semver pin, branch, or full 40-character commit SHA to update to (default: main). See [Version pinning](#version-pinning). |
| `--yes` | Skip confirmation prompt |
| `--check` | Dry run: report what will change without modifying the box |
| `--force` | Update even if the box reports it is already up to date, and force a clean rebuild (wipes the cached image and the cargo/npm volumes) |
| `--pull` | Pull the pre-built box image from GHCR for a release tag instead of building on the box (falls back to a local build on any miss) |
| `--no-pull` | Never pull a pre-built image; always build on the box |
| `--verbose` / `-v` | Show detailed output (default shows progress bar) |
`lager update` targets one box per invocation. To update several, loop over them
in your shell — the `--all` flag and its multi-box loop were removed in v0.18.2.
## Pre-built box images
Release tags publish a box container image to the GitHub Container Registry. If a
pull is enabled, the client does three things:
1. It resolves the tag to an immutable digest.
2. It pulls that digest, pinned to the box's own architecture.
3. It verifies that the image reports the version you asked for.
Any miss at any step falls back to the local build, which is the original
behavior. A miss can be a branch target rather than a release tag, an unpublished
tag, an unreachable registry, or a mismatched image.
For `lager update`, pulling is **off by default**. It changes how the most
load-bearing command in the fleet obtains the code it runs, so it soaks on
Lager's own boxes first. Pass `--pull` to opt a single run in, or set
`LAGER_BOX_IMAGE_PULL=1` to opt in a whole shell.
This differs from [`lager install`](/source/reference/cli/install), which already
uses a pre-built image for a release tag by default. `--no-pull` works the same
way on both. It is the switch that routes a fleet around a bad published image,
and it needs no CLI release.
## Version pinning
Since **lager 0.22.0**, a semver value passed to `--version` resolves to the
release **tag** `vX.Y.Z`. The leading `v` is optional (e.g. `0.26.0` or
`v0.26.0`), and the common pre-release suffixes (`-rc1`, `-beta2`, `-alpha`,
`-preview`) are accepted too. 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
```
Since **lager 0.41.0**, a full 40-character commit SHA resolves to that exact
commit:
```bash theme={null}
lager update --box my-lager-box --version 5d84c68612384eed2854638c1e0941a4ff8b7893 --yes
```
Only the full 40 characters are accepted — a short hex prefix cannot be told
apart from a branch name. Use this when you need a target that does not move.
`--version main` is re-resolved against `origin/main` each time it runs, so it
can mean a different commit minutes later. A commit has no pre-built image,
because only release tags publish one. So a SHA builds on the box just as a
branch does. The commit must also be reachable from a branch or tag on the
remote.
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
```
### Updating Several Boxes
There is no built-in multi-box update. Loop in your shell, and use `--check`
first if you want to see which boxes are actually behind:
```bash theme={null}
# Report what would change on each box, without modifying anything
for box in my-lager-box staging-box pi-box; do
echo "== $box"
lager update --box "$box" --check
done
# Then update them
for box in my-lager-box staging-box pi-box; do
lager update --box "$box" --yes
done
```
A `--all` flag existed before v0.18.2 and was removed along with the multi-box
loop it drove. A plain shell loop continues to the next box when one fails, so an
unreachable box does not stop the run.
***
## 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 that the containers are up
***
## 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 several boxes
for box in my-lager-box staging-box; do lager update --box "$box" --yes; done
# Update several boxes to the staging branch
for box in my-lager-box staging-box; do lager update --box "$box" --version staging --yes; done
# 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
* `--check` reports what will change without modifying the box, so it is the safe
way to find out whether a box is behind
* `--force` updates even when the box reports it is already current, wiping the
cached image and the cargo/npm volumes (useful after major code changes)
# 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` | Lager Box 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`, `toggle`, `state`, `cycle`, or `recover` |
## Commands
| Command | Description |
| --------- | ---------------------------------------------------------- |
| `enable` | Enable (power on) the USB port |
| `disable` | Disable (power off) the USB port |
| `toggle` | Toggle the current power state |
| `state` | Show whether the port is enabled or disabled (read-only) |
| `cycle` | Power-cycle the port: off, wait, on |
| `recover` | Restore power after an interrupted command left a port off |
### `cycle` options
| Option | Description |
| -------------------- | --------------------------------------------------------------------- |
| `--off-time SECONDS` | How long to hold the port unpowered. Default `1.0`, range `0.5`-`10`. |
***
## 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
# Read the current state without changing it
lager usb USB1 state --box my-lager-box
# Power-cycle the port to cold-boot the device
lager usb USB1 cycle --box my-lager-box
# Hold the port off longer for a device with large capacitors
lager usb USB1 cycle --off-time 3 --box my-lager-box
```
### Power cycle a device
`cycle` cuts power, waits, restores it, and waits for the device to come back:
```bash theme={null}
lager usb USB1 cycle --box my-lager-box
# [OK] USB port 'USB1' power-cycled; device re-enumerated
```
If the hub reports nothing attached, it says so rather than claiming a device
returned:
```bash theme={null}
lager usb USB2 cycle --box my-lager-box
# [OK] USB port 'USB2' power-cycled; no device on this port to watch for, so
# re-enumeration was not confirmed
```
`cycle` returns as soon as the **hub** reports the device reconnecting, which is
a few hundred milliseconds. Linux does not finish re-enumerating at that instant.
A device node or `/dev/ttyUSB*` can still be absent, and a `/sys` read taken
immediately still shows the pre-cycle values. If the next step opens the device,
wait for its node. A return from `cycle` does not mean that the device is ready.
That is **not** the same as "the port is unused". A hub only sees a device that
pulls up its data lines. A **charge-only cable** powers a DUT but makes no data
connection, so it looks exactly like an empty socket. The port was still cut and
restored. Confirm the DUT by its own behavior: its UART, or a current
measurement.
Prefer it over a scripted `disable`/`sleep`/`enable`, for three reasons:
* It holds the hub for the whole sequence, so nothing else can switch the port
while it is dark.
* It restores power on every failure path, so a command that dies partway
through cannot strand a port.
* It reports whether the device actually came back, so you do not have to guess.
`--off-time` defaults to 1 second, comfortably above the slowest cold boot
measured on real hardware. **Too short an off time is the failure that
matters**: the device's rails do not fully discharge. The device then
warm-starts, but it looks like a device that was reset. Raise the off time for a
device with large bulk capacitance. A value below 0.5s or above 10s is refused.
**A powered-off port still appears in `lsusb` and still has its `/dev/ttyUSB*`.**
Hubs raise no change notification while a port is unpowered, so the kernel never
processes the disconnect until power returns. Do not script "is the device gone?"
as a check that a port is off — it is wrong in both directions. Use `state`,
which reads the hub's own power bit.
### Recover a port left unpowered
If a command was interrupted between powering a port down and back up, `recover`
restores power:
```bash theme={null}
lager usb USB1 recover --box my-lager-box
# [OK] USB port 'USB1': power restored on port(s) 1, 2, 3, 4
```
On a hub where lager can identify the whole device (such as a Plugable dock),
`recover` re-powers every port on it. You usually reach for `recover` because
something is off and it is not obvious what.
### Read port state (read-only)
`state` reports whether a port is currently enabled or disabled **without
changing it**. The value is read live from the hub hardware, so it always
reflects the real port state — nothing is cached or stored:
```bash theme={null}
lager usb USB1 state --box my-lager-box
# [OK] USB port 'USB1' is enabled
```
This is the read-only counterpart to `toggle`: use `state` to check a port,
`toggle` to flip it (which also reports the resulting state).
`toggle` reports the resulting state so you can tell which way it flipped:
```
[OK] USB port 'USB1' toggled → disabled
```
`enable` and `disable` confirm the action explicitly (`USB port 'USB1' enabled`).
***
## 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
lager usb USB1 cycle --box my-lager-box
# Re-power a port after an interrupted command
lager usb USB1 recover --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 |
| Plugable | RTS5411 docks (e.g. UD-CAM) | 4 external Type-A sockets; standard USB hub-class switching |
See [Supported Instruments](/source/supported-instruments/supported-instruments) for
which ports on a Plugable dock switch power and which do not.
***
## 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` | Lager Box 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
Lager formats readings with an SI prefix, so small magnitudes stay readable. A 52.34 µW load appears as `52.340 µW` rather than as `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 arrives within that time, the command exits with an error. The error names the two likely causes: a disconnected device, or a USB fault.
### 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 a value too small for even the nano prefix, the reading appears in scientific notation (e.g. `3.000e-13 W`). A nonzero reading is never lost to rounding.
* **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, a window longer than \~10 s uses the instrument's **on-device charge accumulator** (average current = Δcharge ÷ Δt). The JS220 does not buffer raw samples for such a window. That approach is **gapless**: it captures every transient, not just the sampled fraction. It also uses constant memory, so it scales to a window of any length — a minute, ten minutes, longer. A short window continues 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](/source/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. A value too small for the nano prefix falls back to scientific notation instead of `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` | Lager Box 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 must be compatible with V4L2
# 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 that
the planning tools score against, and 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 analyzes 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
```
For a document that needs authentication, you have three options:
* Share it with "anyone with the link."
* Give your agent a Google Drive connector or MCP server that holds the
credentials.
* 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.
You can also force a reload immediately, to confirm that a change took effect. A
connected agent can 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**. It 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"
}
}
}
```
Out of the box the MCP server is for **discovery and planning** only — it does
not run code or drive hardware. Two opt-in environment variables extend it past
that; see [Optional tool gates](#optional-tool-gates). To **execute** a test, the
agent writes a Python file locally. It then runs the file 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.
The URL above assumes a box that publishes its ports, which is the default. A box
started with `start_box.sh --no-publish` (or `LAGER_NO_PUBLISH=1`) does not publish
port 8100 on the host. The MCP server still runs and binds `0.0.0.0:8100` inside
the container. It is reachable only on the internal `lagernet` Docker network,
where a reverse proxy owns the host ports. On such a box, point the client at the
container's lagernet address or at the route the proxy exposes -- `:8100`
will not connect.
## 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. |
**By default the tool surface is read-only.** The seven tools above do not 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).
This default holds only while both gates below are off.
### Optional tool gates
Two environment variables, both **off** unless explicitly set, register additional
tools when the box's MCP server starts. Each widens what a connected agent can do
to the box, so treat them as deployment decisions rather than conveniences.
#### `LAGER_MCP_ALLOW_CONTROL`
Adds three scoped tools. These read bench state and power-cycle a hub port — they
do not execute arbitrary code.
| Tool | Purpose |
| ------------------------- | ----------------------------------------------------------------------------------------------- |
| `net_status(net)` | Compact read-only status for one net on the bench. |
| `debug_probe_status(net)` | Whether a debug net's probe is present on the USB bus. |
| `power_cycle_hub(hub)` | Power-cycle an Acroname-controlled USB hub port (disable, settle, enable). **Drives hardware.** |
#### `LAGER_MCP_ALLOW_EXEC`
This gate exposes **arbitrary command execution and file writes** on the box to any
agent that can reach the MCP port. An agent can run any command the box's service
user can run, and overwrite any file it can write. The server itself logs a warning
at startup when this is set. Enable it only on a bench you control, on a trusted
network, and never on a shared or production box.
| Tool | Purpose |
| ----------------------------------- | ----------------------------------------------------------------- |
| `box_exec(command, timeout_s, cwd)` | Run an arbitrary shell command on the box and capture its result. |
| `read_file(path, max_bytes)` | Read a file on the box (config, source, logs). |
| `write_file(path, content)` | Atomically write a file, backing up any prior version. |
| `list_dir(path)` | List the entries of a directory on the box. |
### Prompts
The server also registers a few **prompts**. These are slash-command-style entry
points that steer a client (e.g. Cursor) through the discover → plan → write →
run workflow. They do no work themselves. Each returns an instruction that the
agent follows with 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
* A movement timeout error indicates an obstruction or an out-of-bounds coordinate
* 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 |
| `model_catalog()` | List the battery models available on the instrument (read-only) |
| `read_model(slot)` | Read a saved model's curve points out of a memory slot (read-only) |
| `define_model(slot, voc, resistance)` | Write a custom battery model into a memory slot |
| `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 can clamp
the value to its supported range. It prints a warning when the applied value
differs from the requested value.
```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, the call raises an error. The error tells you to use
`'discharge'`, or to save a model first from the instrument front panel or with
`define_model()` below. Use `'discharge'` for basic battery simulation that works
on all instruments.
### `model_catalog()`
List the battery models available on the instrument. Returns a list covering the
numbered memory slots plus any firmware built-in models. Read-only — assembling the
catalog does not change instrument state.
```python theme={null}
for entry in batt.model_catalog():
print(entry)
```
On the Keithley 2281S, slots 1-9 are reported when a model occupies the slot. The
five firmware built-in models are listed without a slot. Discharge mode is not
listed, because it is front-panel-only and has no SCPI recall form on current
firmware.
### `read_model(slot)`
Read a saved model's curve points out of a memory slot. Returns a dict. Read-only —
exporting a model does not change which model is active.
```python theme={null}
model = batt.read_model(1)
```
On the Keithley 2281S, slots 1-9 hold saved models, and there is no exportable
slot 0. Each model is 101 points per element, for both VOC and resistance. A read
of an empty slot is rejected, with a pointer to `model_catalog()`.
### `define_model(slot, voc, resistance)`
Write a custom battery model into a memory slot, creating or overwriting it. A model is
two curves indexed by state of charge: `voc` (non-decreasing) and `resistance`.
```python theme={null}
batt.define_model(3, voc=voc_points, resistance=resistance_points)
```
A save to an occupied slot overwrites it silently. You cannot afterwards delete or
empty a slot, because the Keithley 2281S has no SCPI command for it. A slot can
only be overwritten. Gate overwrites behind an explicit confirmation in your own
code. Valid target slots are 1-9.
### `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 you uploaded to the Lager Box with the CLI. Use this
for 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 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, inspect the bench, then resume where the
script left off. You can inspect it with ad-hoc `lager` commands or with a live
Python prompt. This helps on a long test that reaches a known trouble spot. It
also helps when you must check 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. It is the script's total runtime
limit, and the box caps it at 300 s. It terminates the whole run when it elapses,
whether the script is paused or not. Leave it at its default (`0`, unlimited) when
you use 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. Changes that you make in the console do **not** carry back into the
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
cannot both be open at once in a single script. Examples are the two channels of
one Rigol DP821, `supply2`/`supply3`, and 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. Every `pause()` and `breakpoint()` call
then becomes a no-op, which gives a clean, non-interactive run of a script that
still has breakpoints 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, *, force, ignore_if_connected, script, openocd_config, jlink_script, halt)` | Connect to target device (optional per-connect debug-script override) |
| `disconnect()` | Disconnect from target |
| `reset(halt)` | Reset the device |
| `halt()` | Halt the target where it is, without a reset (OpenOCD only) |
| `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, *, force=False, ignore_if_connected=False, script=None, openocd_config=None, jlink_script=None, halt=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` | A per-connect debug-script override, on **either** backend: a path on the box **or** a base64-encoded blob. Copied to this net's path for whichever format it turns out to be, so subsequent `flash()` / `reset()` / `read_memory()` calls pick it up immediately. The format is classified by extension then content; a script for the *other* backend, or a blob that cannot be classified, raises `ValueError` rather than running the target under an attach sequence nobody asked for. |
| `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. |
| `openocd_config` | `str` | `None` | The unambiguous OpenOCD form of `script`, skipping format detection. Wins over `script` when both are given. Must be a **complete** cfg, not a fragment — see the warning below. |
| `jlink_script` | `str` | `None` | The unambiguous J-Link form of `script`, skipping format detection. Wins over `script` when both are given. Use this for a base64 blob: a blob has no filename to classify, and the two commonest J-Link forms (`void InitTarget(void)` and `JLINK_ExecCommand`) are not among the content markers, so such a blob raises rather than routing on a guess. |
| `halt` | `bool` | `False` | **OpenOCD only.** Run `reset halt` once the daemon is up. Note this is a *reset* followed by a halt — see [`halt()`](#halt) for stopping the core where it is. |
**Returns:** `dict` - Status dictionary with connection information
A script override takes effect only when the gdbserver relaunches. If a server is
already up, pass `force=True` to restart it with the new script.
`ignore_if_connected=True` returns early and does not relaunch, but it still
repoints the file for later operations. Invalid input means a missing path that
is not valid base64, or an empty string. The net ignores such input silently, and
the script that it materialized earlier stays in effect.
The override is scoped to **this net and this session**. Lager writes it to a
per-net path rather than to the box-wide config that the net record and the HTTP
debug service share. `disconnect()` clears the override. Two debug nets that
connect with different scripts therefore no longer clobber one another.
An `openocd_config` override must be a **complete** cfg, not a fragment. Lager
applies it after the interface and target configs. The launch line still carries
lager's own `-c 'ftdi channel N'` whenever the net has a probe channel. OpenOCD
does not recognize that command unless a cfg first selects the ftdi adapter
driver. A cfg holding only, say, `adapter speed 1000` dies at startup with
`invalid command name "ftdi"`.
### `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 is not reachable yet. On J-Link the cause is that the
restarted GDB server's PID is not observable yet. On OpenOCD the cause is a
transient daemon or RPC fault. A bare call raises inside that window. `reset()`
retries with bounded backoff on both the J-Link and OpenOCD backends, and it
restarts a server only when one is genuinely down. It never tears down a server
that is already up, so an attached RTT session stays intact. Callers no longer
need their own retry wrappers.
**DA1469x exception:** On DA1469x, `flash()` deliberately leaves the server down.
The flash ends in a software reset rather than a server restart, and the
documented flow is an explicit, halt-aware reconnect. So the self-heal still
retries on DA1469x, but it never auto-starts a server. A server that is genuinely
down still surfaces the original error, exactly as before. The self-heal never
brings a server up unhalted, because an unhalted server can return garbage
QSPI-XIP reads.
### `halt()`
Halt the target where it is, without a reset. **OpenOCD only.**
```python theme={null}
# Program, then stop the core on the image just written
dbg.flash('build/firmware.elf')
dbg.connect(force=True)
dbg.halt()
```
**Returns:** `str` - Combined output from the halt operation
This is **not** the same as `reset(halt=True)`. That runs OpenOCD's `reset halt`,
which pulses nRESET and re-enters through the reset vector. `halt()` issues a bare
`halt` instead, so the core stops where it is, and nRESET is never touched.
The distinction matters on parts that execute in place out of QSPI. After you
program such a part, `reset(halt=True)` re-runs the bootloader rather than
stopping on the image you just wrote. An unhalted re-attach risks garbage XIP
reads. Halting in place is how you attach after a program without disturbing
the image.
J-Link has no standalone halt-in-place primitive, because `reset_device` and
`gdb_reset` both reset first. On that backend `halt()` raises, and the error
names the halt-first `.JLinkScript` as the supported route.
### `flash(firmware_path, flash_address=None)`
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 -- a .bin carries no address, so pass the target's flash base
output = dbg.flash('/path/to/firmware.bin', 0x08000000)
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) |
| `flash_address` | `int` | Load address for a `.bin`. Required for `.bin`, ignored for `.hex` / `.elf`, which carry their own |
**Returns:** `str` - Combined output from flash operation
**Note:** A `.bin` has no embedded address, so `flash()` raises without
`flash_address` rather than defaulting to `0x0`. Pass the target's flash base:
STM32 `0x08000000`, nRF52 `0x00000000`, DA1469x QSPI `0x16000000`.
**DA1469x on OpenOCD:** mainline OpenOCD has no flash driver for the DA1469x's
external QSPI. On that family, `flash()` and `erase()` drive the RAM-resident
flash\_loader instead, the same path `lager debug flash` takes. Pass the
absolute XIP address (`0x16000000`), exactly as on J-Link. The loader artefacts
live on the box under
`/home/www-data/customer-binaries/openocd/flash-loaders/da1469x/`. A missing
loader raises rather than falling back to OpenOCD's `program`, which cannot
reach QSPI.
### `erase()`
Perform full chip erase. This erases ALL flash memory including protection settings.
On a DA1469x behind an OpenOCD probe this is the flash\_loader's range erase of
the first 1 MiB of QSPI, matching the J-Link path.
```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. It reconnects only when
no server is up, and it never disturbs 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"GDB server running: {status.get('running', False)}")
```
**Returns:** `dict` - `running` (bool), `pid` and `backend`.
This reports whether a gdbserver process is up for the probe. It is not a
statement about the target: a server can outlive the part it was attached to.
The CLI's `lager debug status` reports both states separately.
### `session(speed=None, transport=None, connect=True, ignore_if_connected=True, disconnect_on_exit=True)`
Scoped debug session. It connects on entry and guarantees teardown on exit. The
safe flash → attach-RTT → reset ordering is therefore encoded once, rather than
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). A `flash()` or `reset()` inside the session can bounce the GDB server.
Such a bounce does not kill a log stream that you 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):** the RTT reader re-attaches by itself when the
GDB server or daemon restarts under it.
* **J-Link.** A `flash()` briefly frees the probe's USB and restarts the GDB
server on the *same* ports, which drops the RTT socket. A `reset()` does the
same through its Commander grab. The reader re-attaches to the same RTT telnet
port instead of going silent. A long-lived `read_some()` or `rtt_defmt()` loop
therefore keeps producing across a flash.
* **OpenOCD.** The daemon stays up across an ordinary flash, so the socket rarely
drops. If it does drop, from a daemon force-restart or an rtt-server bounce, the
reader re-runs `rtt setup` and `rtt server start` and re-attaches.
Reconnection is bounded on both backends, and the default is 30 s. The reader
re-attaches only once the server or daemon is back up, and it never *starts* one.
A flash that deliberately leaves the server down, such as on DA1469x, therefore
cannot make the reader spin forever. The reader also cannot disturb a DA1469x that
you left down on purpose. 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 a 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. That looks like a host-side failure,
and it 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 the instrument can clamp them. It prints
a warning when 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
* The instrument can clamp resistance values below 0.02 ohms
# 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 |
| `close()` | Release instrument resources; a no-op on backends that need no teardown |
## 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 |
| FTDI FT232H / FT2232H | ADBUS0-7, ACBUS0-7 (16 bits per channel) | 3.3V |
| FTDI FT4232H | 8 pins per channel | 3.3V |
### 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 |
### Multi-channel FTDI adapters
An FTDI net takes its channel from `params.interface` on the net record, accepting
`A`-`D` or `0`-`3`. A net with no `interface` uses channel A — the only choice on a
single-channel FT232H.
| Part | Channels | Usable for GPIO |
| ------- | -------- | --------------- |
| FT232H | 1 (A) | A |
| FT2232H | 2 (A, B) | A, B |
| FT4232H | 4 (A-D) | A-D |
GPIO runs as asynchronous bitbang and needs no MPSSE engine, so every channel the part has is usable. Note the pin width differs: the FT232H and FT2232H expose ADBUS0-7 plus ACBUS0-7 (16 bits), while each FT4232H channel is 8 pins wide.
See [Nets](/source/reference/cli/nets) for how channels are assigned across net
types on one chip.
## 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 |
| FTDI FT232H / FT2232H / FT4232H | MPSSE-based I2C; channel selected per net |
### Multi-channel FTDI adapters
An FTDI net takes its channel from `params.interface` on the net record, accepting
`A`-`D` or `0`-`3`. A net with no `interface` uses channel A — the only choice on a
single-channel FT232H.
| Part | Channels | Usable for I2C |
| ------- | -------- | -------------- |
| FT232H | 1 (A) | A |
| FT2232H | 2 (A, B) | A, B |
| FT4232H | 4 (A-D) | A, B |
I2C runs over the FTDI part's MPSSE engine, and on an FT4232H only channels A and B have one. Asking for C or D fails at net construction naming the channel, rather than somewhere inside pyftdi.
See [Nets](/source/reference/cli/nets) for how channels are assigned across net
types on one chip.
## 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
Prefer to write your tests in Rust? The same nets are reachable from the
[Rust SDK](/source/reference/rust/overview), which runs against the box from
`cargo test` rather than on the box. Every net type documented below has a Rust
counterpart except scope and logic, which are not on the box's HTTP API yet.
### 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
The [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py)
is a more comprehensive example. It combines robot arm control, USB hub power
cycling, debug probe flashing, power supply management, and ADC measurement in a
single automated workflow.
# Router
Source: https://docs.lagerdata.com/source/reference/python/router
Drive a MikroTik router as a test net
A **router net** puts a real access point under test control. A test can then
change the SSID, break the network on purpose, and put it back. It is how you
exercise a DUT's reconnect, roaming, and offline behavior without touching
hardware by hand.
[WiFi](/source/reference/python/wifi) manages the Lager Box's own wireless
interface. A router net is different: it is a separate device on the bench that the
box drives over the MikroTik REST API.
## Import
```python theme={null}
from lager import Net, NetType
router = Net.get("router1", type=NetType.Router)
```
Credentials (`username`, `password`, `use_ssl`) come from the net's saved
configuration, so scripts never hard-code them.
## Connection and system
| Method | Description |
| ----------------------------- | --------------------------------------------------- |
| `connect()` | Verify connectivity by fetching the system identity |
| `get_system_info()` | Get system resource information |
| `reboot()` | Reboot the router |
| `wait_for_ready(timeout=120)` | Poll until the router is responsive after a reboot |
| `run(path, params=None)` | Run an arbitrary REST API GET call |
## Interfaces
| Method | Description |
| -------------------------------------------------------------- | ------------------------------------------------ |
| `get_interfaces()` | List all network interfaces |
| `set_interface_disabled(interface, disabled)` | Enable or disable a network interface |
| `get_wireless_interfaces()` | List wireless interfaces and their configuration |
| `configure_wireless(interface, **kwargs)` | Configure a wireless interface |
| `set_wireless_ssid(interface, ssid)` | Change an interface's SSID |
| `enable_interface(interface)` / `disable_interface(interface)` | Enable or disable a wireless interface |
| `wait_for_wireless_ready(interface, timeout=30)` | Poll until an interface is enabled and running |
## Security profiles
| Method | Description |
| ------------------------------------------------------ | ---------------------------------------------------- |
| `get_security_profiles()` | List all wireless security profiles |
| `create_security_profile(name, ...)` | Create a profile (defaults to WPA2-PSK with AES-CCM) |
| `create_open_security_profile(name='open')` | Create an open, unencrypted profile |
| `update_security_profile_password(name, new_password)` | Update a profile's WPA2 pre-shared key |
| `delete_security_profile(name)` | Delete a profile by name |
## Clients
| Method | Description |
| -------------------------------------------------------------- | ---------------------------------------------- |
| `get_wireless_clients()` | List currently connected wireless clients |
| `is_client_connected(mac_address=None)` | Check whether a client is currently associated |
| `set_client_isolation(interface, enabled=True)` | Enable or disable AP client isolation |
| `get_dhcp_leases()` | List clients that have received IP addresses |
| `get_access_list()` | List all wireless access list entries |
| `add_access_list_entry(mac_address, authentication=True, ...)` | Allow or deny a specific client |
| `remove_access_list_entry(mac_address)` | Remove all entries for one MAC address |
| `clear_access_list()` | Remove all test-tagged access list entries |
## Fault injection
These are the methods that make a router net worth having. They break the network
in specific, repeatable ways, so you can assert how firmware responds.
| Method | Description |
| ------------------------------------------------------------- | ---------------------------------------- |
| `block_internet()` | Drop all forwarded traffic |
| `block_dns()` | Drop port 53 traffic, UDP and TCP |
| `block_port(port, protocol='tcp')` | Block one port for all forwarded traffic |
| `add_firewall_rule(chain='forward', action='drop', **kwargs)` | Add a firewall filter rule |
| `remove_firewall_rules()` | Remove all test-tagged firewall rules |
| `add_bandwidth_limit(target, max_limit, name=None)` | Limit bandwidth for an IP or subnet |
| `remove_bandwidth_limits()` | Remove all test-tagged bandwidth queues |
| `enable_dhcp()` / `disable_dhcp()` | Enable or disable all DHCP servers |
| `set_dhcp_lease_time(lease_time='10m')` | Set the lease time on all DHCP servers |
## Test isolation
```python theme={null}
router.reset_to_defaults()
```
`reset_to_defaults(baseline_ssid=None, baseline_pass=None, wireless_interfaces=None)`
restores the router to a known baseline. As a rule of thumb, anything this API adds
is tagged as test state. The `remove_*` and `clear_*` methods above remove only
those tagged entries. A cleanup therefore cannot delete a rule that was already on
the router before the test ran.
## Examples
### Verify the DUT reconnects after the AP drops
```python theme={null}
from lager import Net, NetType
router = Net.get("router1", type=NetType.Router)
router.connect()
# Take the network away
router.disable_interface("wlan1")
assert not router.is_client_connected(mac_address=DUT_MAC), "DUT still associated"
# Give it back, and let the DUT find its way home
router.enable_interface("wlan1")
router.wait_for_wireless_ready("wlan1")
assert router.is_client_connected(mac_address=DUT_MAC), "DUT did not reconnect"
```
### Assert the DUT survives losing DNS but keeping the link
```python theme={null}
router.block_dns()
try:
# The DUT stays associated, but name resolution fails. Firmware should
# report an outage rather than a WiFi failure.
assert router.is_client_connected(mac_address=DUT_MAC)
finally:
router.remove_firewall_rules()
```
### Rotate the SSID password mid-session
```python theme={null}
router.update_security_profile_password("test-profile", "new-password-here")
router.wait_for_wireless_ready("wlan1")
```
## Supported hardware
| Manufacturer | Models | Notes |
| ------------ | ------------------------------------------ | ------------------------------------------------------- |
| MikroTik | RouterOS devices with the REST API enabled | Reached over HTTP, or HTTPS when the net sets `use_ssl` |
## Notes
* Every method is a REST call to the router, so failures surface as connection or
HTTP errors rather than as instrument errors.
* `reboot()` drops the connection by design. Always follow it with
`wait_for_ready()` before issuing further calls.
* Cleanup methods only remove test-tagged state, so they are safe to call in a
`finally` block on a shared bench router.
# 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 wait for 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 |
| FTDI FT232H / FT2232H / FT4232H | MPSSE-based SPI; channel selected per net |
### Multi-channel FTDI adapters
An FTDI net takes its channel from `params.interface` on the net record, accepting
`A`-`D` or `0`-`3`. A net with no `interface` uses channel A — the only choice on a
single-channel FT232H.
| Part | Channels | Usable for SPI |
| ------- | -------- | -------------- |
| FT232H | 1 (A) | A |
| FT2232H | 2 (A, B) | A, B |
| FT4232H | 4 (A-D) | A, B |
SPI runs over the FTDI part's MPSSE engine, and on an FT4232H only channels A and B have one. Asking for C or D fails at net construction naming the channel, rather than somewhere inside pyftdi.
See [Nets](/source/reference/cli/nets) for how channels are assigned across net
types on one chip.
## 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 |
| `set_mode()` | Put the instrument into DC power-supply mode, on instruments that have modes |
| `get_monitor_state(channel=None)` | Gather the full monitor state in a single call; returns a dict |
## 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 can 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) |
| `cycle(off_time=None)` | Power-cycle the port; returns whether the device came back |
| `recover()` | Restore power after an interrupted operation left a port off |
| `get_config()` | Return a copy of the net's raw configuration dict |
## 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
```
### `cycle(off_time=None)`
Power-cycle the port: off, wait, on. The return value tells you what the hub saw:
* `True` — the device re-enumerated.
* `False` — the device did not come back in time.
* `None` — the hub reports nothing attached, or the driver cannot observe
re-enumeration.
**`None` does not mean the port is unused.** A hub only sees a device that pulls
up its data lines. A **charge-only cable** carries power on the other end but no
data. Such a cable is therefore indistinguishable from an empty socket. Power is
cut and restored either way; there is simply nothing on the bus to watch come
back. Confirm a DUT on such a port by its own behavior instead: its UART output,
or a current measurement.
`off_time` is how long the port stays unpowered, defaulting to 1 second and
limited to 0.5-10 seconds. **Too short an off time is the failure that matters.**
The device's rails do not fully discharge, so the device warm-starts and only
appears to reset. Raise it for a device with large bulk capacitance.
```python theme={null}
from lager import Net, NetType
usb = Net.get('DUT_USB', type=NetType.Usb)
if usb.cycle(off_time=2):
print("DUT cold-booted and came back")
```
Prefer this over a hand-rolled `disable`/`sleep`/`enable`. It gives two guarantees
that the hand-rolled form does not:
* It holds the hub for the whole sequence, so nothing else can switch the port
while it is dark.
* It restores power on every failure path, so an exception partway through cannot
leave a port stranded.
`cycle` returns on the **hub's** reconnect signal, a few hundred milliseconds
after power returns — not on Linux finishing enumeration. So `/dev/ttyUSB*` can
be absent when it returns, and a `/sys` read taken immediately still shows the
pre-cycle device number. Poll for what you need rather than reading once.
**A powered-off port still appears in `lsusb` and keeps its `/dev/ttyUSB*`.**
Hubs raise no change notification while a port is unpowered, so the kernel does
not process the disconnect until power returns. Never check for a device's
absence to decide whether a port is off — use `state()`, which reads the hub's
own power bit.
### `recover()`
Restore power after an interrupted operation left a port unpowered. On hubs where
lager can identify the whole physical device, this re-powers every port on it.
```python theme={null}
from lager import Net, NetType
Net.get('DUT_USB', type=NetType.Usb).recover()
```
## 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}...")
came_back = usb.cycle(off_time=delay)
if came_back is False:
print(f"{net_name} did not re-enumerate")
else:
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):
"""
Reset a USB device by power cycling.
Args:
net_name: USB net name
reset_time: Time to keep power off (seconds)
"""
usb = Net.get(net_name, type=NetType.Usb)
print(f"Resetting {net_name} (power off for {reset_time}s)...")
# cycle() waits for the port to re-enumerate itself, so there is no
# settle_time to guess at -- and no risk of the guess being too short.
came_back = usb.cycle(off_time=reset_time)
if came_back is False:
raise RuntimeError(f"{net_name} did not come back after a power cycle")
print(f" {net_name} reset complete")
# Usage
reset_usb_device('DUT_USB', reset_time=2)
```
### 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 |
| Plugable RTS5411 dock | Per-port power switching on the four external Type-A sockets |
### Backend classes
`Net.get(name, type=NetType.Usb)` returns a wrapper that dispatches to the backend
for whichever hub the net names. Use that wrapper in most scripts. The per-backend
classes are also importable directly, for code that needs to name one:
```python theme={null}
from lager.automation import AcronameUSBNet, YKUSHUSBNet, PlugableUSBNet
```
They are exported from `lager.automation` and from `lager.automation.usb_hub`.
Prefer `Net.get`. If you construct a backend class directly, you tie the script to
one model of hub. To move that test to a bench with a different hub, you must then
edit code rather than the net record.
## 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). `cycle()`
does this waiting for you and tells you whether the device returned
* Power cycling can be useful for device reset/recovery; prefer `cycle()` over a
hand-rolled `disable`/`sleep`/`enable` so a failure cannot leave a port off
* A port that is powered off still appears in `lsusb` and keeps its device
nodes. Never use device presence to test whether a port is off
* 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 and operates in source mode,
where it supplies a configurable voltage of 0.8–5V. Voltage readings therefore
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 the stream is active, 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
# ADC
Source: https://docs.lagerdata.com/source/reference/rust/adc
Read analog voltages from an ADC net
Read a single-ended analog voltage from an ADC net on a LabJack, MCC or similar acquisition device.
## Handle
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let adc = lager.adc("adc1");
```
The handle is cheap and does no I/O until you call a method. The box resolves and
validates the net name on every request, so a typo fails loudly rather than reading
the wrong pin.
## Methods
| Method | Description |
| -------- | ---------------------------------- |
| `name()` | The net name this handle addresses |
| `read()` | Read the input voltage in volts |
## Method Reference
### `name() -> &str`
The net name this handle was created with. Does not touch the network.
```rust theme={null}
assert_eq!(lager.adc("adc1").name(), "adc1");
```
### `read() -> Result`
Read the voltage present on the ADC input, in volts.
```rust theme={null}
let volts = adc.read()?;
println!("{volts:.4} V");
```
**Returns:** `f64` — the measured voltage in volts. Signed: a negative reading is a
real negative voltage, not an error.
## Examples
### Assert a rail came up
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let supply = lager.supply("supply1");
let vbat = lager.adc("adc1");
supply.set_voltage(3.3)?;
supply.enable()?;
std::thread::sleep(std::time::Duration::from_millis(200));
let measured = vbat.read()?;
assert!((measured - 3.3).abs() < 0.2, "rail is at {measured:.3} V, expected 3.3");
supply.disable()?;
```
### Poll a decaying rail
`read()` is a single cheap request, so polling in a loop is reasonable.
```rust theme={null}
use std::time::{Duration, Instant};
let start = Instant::now();
while start.elapsed() < Duration::from_secs(5) {
let v = adc.read()?;
if v < 1.0 {
println!("rail collapsed after {:?}", start.elapsed());
break;
}
std::thread::sleep(Duration::from_millis(50));
}
```
## Supported Hardware
| Instrument | Channels | Notes |
| ----------- | ---------- | ---------------------- |
| LabJack T7 | AIN0-AIN13 | 14 single-ended inputs |
| MCC USB-202 | CH0-CH7 | 8 single-ended inputs |
## Notes
* Readings are **volts**, always. There is no unit selection on this net type.
* An unconnected input floats; a reading near zero on a disconnected pin is the
instrument reporting noise, not a failure.
* Each ADC net is one channel. A device with fourteen inputs is fourteen nets.
* The box serializes access per physical instrument, so concurrent reads across
several ADC nets on one device queue rather than interleave.
# Robot Arm
Source: https://docs.lagerdata.com/source/reference/rust/arm
Drive a robot arm to press buttons and move fixtures
Move a Rotrics Dexarm to press a DUT's buttons, place a fixture, or actuate anything
a test needs a physical hand for. All coordinates are millimetres.
## Handle
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let arm = lager.arm("arm1");
```
## Methods
| Method | Description |
| -------------------------- | ----------------------------------------------- |
| `name()` | The net name this handle addresses |
| `position()` | Current end-effector position |
| `move_to()` | Absolute move |
| `move_to_with_timeout()` | Absolute move with an explicit box-side timeout |
| `move_by()` | Relative move |
| `move_by_with_timeout()` | Relative move with an explicit box-side timeout |
| `go_home()` | Move to the home position |
| `enable_motor()` | Energize the motors |
| `disable_motor()` | Release the motors |
| `read_and_save_position()` | Read the position and persist it on the arm |
| `set_acceleration()` | Set acceleration in mm/s2 |
## Types
### `ArmPosition`
```rust theme={null}
pub struct ArmPosition {
pub x: f64, // mm
pub y: f64, // mm
pub z: f64, // mm
}
```
## Method Reference
### `position() -> Result`
The current end-effector position, in millimetres.
### `move_to(x: f64, y: f64, z: f64) -> Result`
Absolute move.
```rust theme={null}
let at = arm.move_to(50.0, 250.0, -20.0)?;
println!("arrived at ({:.1}, {:.1}, {:.1})", at.x, at.y, at.z);
```
**Returns:** the position after the move.
### `move_by(dx: f64, dy: f64, dz: f64) -> Result`
Relative move; returns the new position.
### `move_to_with_timeout(...)` and `move_by_with_timeout(...)`
The same moves with an explicit box-side timeout in seconds, for a move that needs
longer than the default 15. The client widens its own HTTP budget to match.
### `go_home() -> Result<()>`
Move to the home position, X0 Y300 Z0.
### `enable_motor() -> Result<()>` and `disable_motor() -> Result<()>`
Energize or release the steppers.
A released arm is back-driveable and will **sag under its own weight**. Do not leave
the motors disabled with the arm over a workpiece or a populated board.
### `read_and_save_position() -> Result`
Read the current position and persist it on the arm.
### `set_acceleration(acceleration: u32, travel_acceleration: u32, retract_acceleration: u32) -> Result<()>`
Set acceleration values in mm/s2.
## Examples
### Press a button and check the DUT saw it
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let arm = lager.arm("arm1");
let led = lager.gpio("status_led");
arm.enable_motor()?;
arm.go_home()?;
// Approach above the button, press, retract.
arm.move_to(50.0, 250.0, 0.0)?;
arm.move_to(50.0, 250.0, -18.0)?;
std::thread::sleep(std::time::Duration::from_millis(200));
arm.move_to(50.0, 250.0, 0.0)?;
assert!(led.input()?.is_high(), "DUT did not register the press");
arm.go_home()?;
```
## Supported Hardware
| Arm | Notes |
| -------------- | --------------------------------------- |
| Rotrics Dexarm | Workspace bounds enforced on the device |
## Notes
* **Coordinates are millimetres**, and the arm enforces its own workspace bounds. An
out-of-range target is refused, not clamped.
* **Moves block on the box** until the arm arrives, so do not poll around a
move. The box-side default move timeout is 15 seconds.
* Non-move actions get a flat 45-second client budget, because they still touch the
serial port and can queue behind a move on the device lock.
* The arm is physical and has no collision detection. Clear its workspace before
letting a test run unattended.
# Async Client
Source: https://docs.lagerdata.com/source/reference/rust/async
AsyncLagerBox, and exactly where it differs from the blocking client
`AsyncLagerBox` is the tokio/reqwest client. It mirrors `LagerBox` method for method,
and both run the same wire layer, so the request bodies and the parsing cannot drift
between them.
## Enabling
```toml theme={null}
[dev-dependencies]
lager = { package = "lager-net", version = "0.4", default-features = false, features = ["async"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
`blocking` is the default feature, so turning defaults off keeps `ureq` out of the
dependency tree if you only want the async client. Enabling both is fine — they
coexist.
## Using it
```rust theme={null}
use lager::AsyncLagerBox;
#[tokio::test]
async fn rail_comes_up() -> lager::Result<()> {
let lager = AsyncLagerBox::from_env()?; // not async
let supply = lager.supply("supply1"); // not async
supply.set_voltage(3.3).await?;
supply.enable().await?;
let state = supply.state().await?;
assert_eq!(state.enabled, Some(true));
supply.disable().await
}
```
Constructors and handle accessors are ordinary functions; only the calls that do I/O
are `async`. Handle types are prefixed: `AsyncSupply`, `AsyncGpio`, `AsyncDebugNet`
and so on.
## What the async client does not have
| Feature | Blocking | Async |
| ---------------------------------------------- | -------- | ------ |
| Every net type, every box query, safety limits | Yes | Yes |
| Lock, heartbeat, unlock | Yes | Yes |
| `lock_guard()` and `BoxLockGuard` | Yes | **No** |
| `uart()` sessions | Yes | **No** |
| `debug.rtt()` and `RttStream` | Yes | **No** |
| `debug.rtt_interactive()` | Yes | **No** |
The `uart` and `rtt` features both imply `blocking`, so enabling them pulls in the
blocking client whether or not you asked for it. `Scope` is shared: it is a stub on
both, with sync methods.
There is no RAII lock guard on the async client. Take and release the lock
explicitly, and make sure the release runs on the error path too.
## Running both
Nothing stops you using the async client for the parallel parts of a suite and the
blocking client for a UART or RTT session:
```rust theme={null}
let lager = AsyncLagerBox::from_env()?; // fan out reads
let blocking = lager::LagerBox::from_env()?; // for the UART session
let mut uart = blocking.uart("uart1")?;
```
## Concurrency
The box serializes access per physical instrument. Many concurrent requests to one
instrument do not interleave its I/O, because the requests queue on the box.
Concurrency buys you real parallelism only across *different* instruments.
```rust theme={null}
// Three different instruments: genuinely parallel.
// Bind the handles first -- a handle created inline is a temporary, and the
// future borrows it, so `try_join!` on inline constructors will not borrow-check.
let adc = lager.adc("adc1");
let tc = lager.thermocouple("tc1");
let meter = lager.watt_meter("watt1");
let (v, temp, power) = tokio::try_join!(
adc.read(),
tc.read(),
meter.power(0.5),
)?;
```
## Notes
* `reqwest` is pulled in with `default-features = false`, so there is no TLS stack.
Box traffic is plain HTTP on the local network or over Tailscale.
* `tokio` is required only with the `time` feature for the async client itself. The
`macros` and `rt-multi-thread` features in the snippet above are for writing the
tests.
* Gateway authentication works identically on both clients, including the automatic
token refresh.
# 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. The gateway
rejects unauthenticated traffic with 401 and an `X-Gateway-Auth-Url` header.
This is the same contract that 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).
## Credential precedence
Three sources, checked in this order. The first that yields a token wins.
1. `LagerBoxBuilder::bearer_token()`
2. The `LAGER_GATEWAY_TOKEN` environment variable (empty or whitespace-only is ignored)
3. The CLI's token store
A token from (1) or (2) is **pinned**: it is sent as-is and never refreshed or
replaced. If the gateway rejects it, the call fails immediately rather than retrying
with a different credential. Only a token resolved from the store participates in
refresh.
## The token store
The crate reads the same file that `lager login` writes, so a developer who signed in
needs no additional setup.
| Path | Source |
| -------------------------- | -------- |
| `$LAGER_GATEWAY_AUTH_FILE` | When set |
| `~/.lager_gateway_auth` | Default |
Its shape:
```json theme={null}
{
"boxes": { "": "" },
"authServers": { "": { "accessToken": "...", "cookies": { } } }
}
```
A missing or corrupt file is treated as empty rather than as an error. The file is
written back best-effort with mode 0600 on Unix.
## How discovery works
A gated box answers unauthenticated traffic with a denial status and an
`X-Gateway-Auth-Url` header naming its auth server.
1. On first contact, the crate learns the box-to-auth-server mapping from that header
and records it under `boxes`.
2. It resolves a credential, retries the request **once**, and thereafter attaches the
token proactively. A box already known to be gated gets its header on the very
first request of the next run.
3. An expired access token is refreshed against `POST /api/auth/refresh`,
replaying the stored cookies. Rotated cookies from the response are merged back in.
Expiry is read from the JWT's `exp` claim, decoded but **not** verified, with a
60-second refresh margin. An opaque non-JWT token reads as already expired, so the
crate attempts a refresh. When the crate has no refresh token, it sends the original
token as-is anyway.
A plain 401 with **no** `X-Gateway-Auth-Url` header is not treated as a gateway
denial. It surfaces as an ordinary `Error::Box { status: 401, .. }`, because it came
from the application rather than from the gateway.
## Where the token is attached
Everywhere, not just the port-9000 API:
* The box HTTP API on port 9000
* The debug service on port 8765
* The UART Socket.IO handshake
* The RTT Socket.IO handshake
A gated box therefore works for flashing and for streaming sessions with no extra
configuration.
## Denial mapping
| Status | Result |
| ------ | ------------------------------------------------------------------------------------------------------ |
| 401 | `Error::AuthRequired`, naming the auth server to sign into |
| 403 | `Error::Box`, `you are not authorized to use box ` — a missing access grant, not a missing token |
| 503 | `Error::Box`, the gateway could not reach its auth server; retry shortly |
The 401 message distinguishes the two cases: `box requires sign-in` when no
token was sent, and `your session was rejected by box ` when one was.
## Ungated boxes
No header is sent and no code path runs. Nothing here costs you anything on a box
that is not behind a gateway.
# Battery Simulation
Source: https://docs.lagerdata.com/source/reference/rust/battery
Drive a battery simulator by state of charge, capacity and open-circuit voltage
Present a programmable battery to your DUT: set capacity, open-circuit voltage and
state of charge, then watch how firmware behaves as the pack drains.
## Handle
```rust theme={null}
use lager::{BatteryMode, LagerBox};
let lager = LagerBox::from_env()?;
let battery = lager.battery("battery1");
```
## Methods
| Method | Description |
| ----------------------------------------- | ---------------------------------------------- |
| `name()` | The net name this handle addresses |
| `init_battery_mode()` | Put the instrument into battery-simulator mode |
| `set_soc()` | Set state of charge, 0-100 percent |
| `set_voc()` | Set open-circuit voltage |
| `set_volt_full()` | Set the voltage treated as full |
| `set_volt_empty()` | Set the voltage treated as empty |
| `set_capacity()` | Set pack capacity in amp-hours |
| `set_current_limit()` | Set the output current limit |
| `set_model()` | Load a predefined battery model by part number |
| `set_mode()` | Choose static or dynamic simulation |
| `set_ovp()` / `set_ocp()` | Set protection trips |
| `clear_ovp()` / `clear_ocp()` / `clear()` | Clear trips |
| `enable()` / `disable()` | Turn the simulated pack on or off |
| `state()` | Full structured state in a single transaction |
## Types
### `BatteryMode`
```rust theme={null}
pub enum BatteryMode { Static, Dynamic }
```
`Static` holds the state of charge where you set it. `Dynamic` lets it evolve with
the load current, which is what you want when testing a real discharge curve.
### `BatteryState`
```rust theme={null}
pub struct BatteryState {
pub netname: Option,
pub channel: Option,
pub error: Option,
pub terminal_voltage: Option, // V
pub current: Option, // A
pub esr: Option, // ohm
pub soc: Option, // percent
pub voc: Option, // V
pub enabled: Option,
pub mode: Option, // "Static" or "Dynamic"
pub model: Option, // e.g. "LI_ION4_2"
pub capacity: Option, // Ah
pub current_limit: Option, // A
pub ocp_limit: Option,
pub ovp_limit: Option,
pub volt_full: Option,
pub volt_empty: Option,
pub ocp_tripped: Option,
pub ovp_tripped: Option,
}
```
## Method Reference
### `init_battery_mode() -> Result<()>`
Put the instrument into battery-simulator mode.
Call this first, before any other battery method, on an instrument that also
serves a power-supply net. A Keithley 2281S is a supply until told otherwise, and
the battery setters have nothing to act on until it is switched over.
### `set_soc(percent: f64) -> Result<()>`
Set state of charge, 0 to 100.
```rust theme={null}
battery.set_soc(20.0)?; // nearly flat
```
### `set_voc(volts: f64)`, `set_volt_full(volts: f64)`, `set_volt_empty(volts: f64)`
Open-circuit voltage, and the voltages the model treats as full and empty.
### `set_capacity(amp_hours: f64) -> Result<()>`
Pack capacity in amp-hours.
### `set_current_limit(amps: f64) -> Result<()>`
Output current limit.
### `set_model(partnumber: &str) -> Result<()>`
Load one of the instrument's predefined battery models.
### `set_mode(mode: BatteryMode) -> Result<()>`
Switch between `Static` and `Dynamic`.
### `enable() -> Result<()>` and `disable() -> Result<()>`
Turn the simulated pack's output on or off. Disable in teardown.
### `state() -> Result`
Everything in one transaction. As with a supply, there are no individual getters.
## Examples
### Check the low-battery warning fires at the right threshold
```rust theme={null}
use lager::{BatteryMode, LagerBox};
let lager = LagerBox::from_env()?;
let battery = lager.battery("battery1");
let warn = lager.gpio("low_batt_led");
battery.init_battery_mode()?;
battery.set_capacity(2.0)?;
battery.set_volt_full(4.2)?;
battery.set_volt_empty(3.0)?;
battery.set_mode(BatteryMode::Static)?;
battery.enable()?;
for soc in [80.0_f64, 40.0, 20.0, 10.0, 5.0] {
battery.set_soc(soc)?;
std::thread::sleep(std::time::Duration::from_secs(2));
let s = battery.state()?;
println!("SOC {soc:>5.1}% terminal {:?} V warn={:?}",
s.terminal_voltage, warn.input()?);
}
battery.disable()?;
```
## Supported Hardware
| Instrument | Channels | Notes |
| -------------- | -------- | -------------------------------------------------- |
| Keithley 2281S | 1 | Also serves a power-supply net on the same address |
## Notes
* **`set_voc()` is a request, not an assignment.** The instrument derives terminal
voltage from the loaded model and the state of charge. On a Keithley 2281S with the
`LI_ION4_2` model, setting SOC to 50 moved `voc` to about 4.007 V regardless of an
earlier `set_voc(3.7)`. Read `state()` to learn what the pack actually presents.
* A battery net and a power-supply net can share one physical instrument. Switching
to battery mode changes what that instrument is for both, and the box serializes
them under a single per-instrument lock.
* `state()` returning `Ok` does not mean the instrument answered — check `error`,
which is populated when the gather itself failed.
* `esr` is the model's equivalent series resistance, which is what makes terminal
voltage sag under load rather than tracking open-circuit voltage exactly.
# BLE
Source: https://docs.lagerdata.com/source/reference/rust/ble
Scan for and connect to Bluetooth Low Energy devices from the box
Use the box's own Bluetooth adapter to find your DUT advertising, connect to it, and
enumerate its GATT services.
## Handle
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let ble = lager.ble();
```
BLE is a **box-level** capability, not a net: it drives the box's own adapter. There
is no net name and no `name()`.
## Methods
| Method | Description |
| -------------- | -------------------------------------------- |
| `scan()` | Scan for advertising devices |
| `scan_named()` | Scan, filtered by a name substring |
| `info()` | Connect briefly and enumerate GATT services |
| `connect()` | Connect, enumerating services to verify |
| `disconnect()` | Ensure a device is disconnected from the box |
## Types
```rust theme={null}
pub struct BleDevice {
pub name: String, // falls back to the address when unnamed
pub address: String, // XX:XX:XX:XX:XX:XX
pub rssi: Option, // dBm
pub uuids: Vec,
}
pub struct BleDeviceInfo {
pub address: String,
pub connected: bool,
pub services: Vec,
}
pub struct BleService {
pub uuid: String,
pub description: Option,
pub characteristics: Vec,
}
pub struct BleCharacteristic {
pub uuid: String,
pub description: Option,
pub properties: Vec, // e.g. ["read", "notify"]
}
```
## Method Reference
### `scan(timeout: f64) -> Result>`
Scan for advertising devices for `timeout` seconds, which must be between 0.1 and 300.
Named devices sort first.
```rust theme={null}
for d in ble.scan(5.0)? {
println!("{} ({}) {:?} dBm", d.name, d.address, d.rssi);
}
```
### `scan_named(timeout: f64, name_contains: &str) -> Result>`
The same scan, filtered by a case-insensitive name substring.
### `info(address: &str) -> Result`
Connect briefly and enumerate the device's GATT services.
### `connect(address: &str) -> Result` and `disconnect(address: &str) -> Result<()>`
Connect to, or ensure disconnection from, a device by address.
## Examples
### Assert the DUT advertises with the right service
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let ble = lager.ble();
let supply = lager.supply("supply1");
supply.set_voltage(3.3)?;
supply.enable()?;
std::thread::sleep(std::time::Duration::from_secs(3));
let found = ble.scan_named(10.0, "my-dut")?;
let dut = found.first().expect("DUT is not advertising");
println!("found {} at {:?} dBm", dut.name, dut.rssi);
let info = ble.info(&dut.address)?;
assert!(info.services.iter().any(|s| s.uuid.starts_with("0000180f")),
"battery service missing");
ble.disconnect(&dut.address)?;
supply.disable()?;
```
## Notes
* **One adapter per box.** The box serializes all BLE *and* BluFi work on it, so
concurrent calls queue rather than fail. A BluFi provisioning run and a BLE scan
cannot overlap.
* `capabilities.ble_command` being `true` means the box **serves the route**, not that
it can do the work. A box whose container has no BlueZ running answers with
`Error::Box` and HTTP 502 carrying
`The name org.bluez was not provided by any .service files`. This happens even with a
Bluetooth controller present on the USB bus. Check the error, not just the
capability flag.
* The box-side connect timeout for `info`, `connect` and `disconnect` is 10 seconds;
the client allows that plus 30.
* `rssi` is a snapshot from the advertisement that happened to be received. Treat it
as an ordering hint, not a measurement.
# BluFi
Source: https://docs.lagerdata.com/source/reference/rust/blufi
Provision an ESP32's WiFi credentials over BLE
Provision an ESP32 onto a WiFi network over Bluetooth with Espressif's BluFi protocol.
This is the flow that a phone app performs during onboarding, and a test drives it.
## Handle
```rust theme={null}
use lager::LagerBox;
let lager = LagerBox::from_env()?;
let blufi = lager.blufi();
```
BluFi is a **box-level** capability, not a net, and it shares the box's single
Bluetooth adapter with BLE.
## Methods
| Method | Description |
| ------------- | ---------------------------------------------- |
| `scan()` | Scan for BluFi-capable BLE devices |
| `connect()` | Connect by advertised BLE name |
| `provision()` | Provision the target onto a WiFi network |
| `wifi_scan()` | Ask the target to scan for networks it can see |
| `status()` | The target's current WiFi state |
| `version()` | The target's BluFi firmware version |
## Types
```rust theme={null}
pub struct BlufiStatus {
pub device_name: Option,
pub op_mode: Option, // 0 NULL, 1 STA, 2 SoftAP, 3 STA+SoftAP
pub op_mode_name: Option,
pub sta_conn: Option