# Adding your First Lager Box Source: https://docs.lagerdata.com/source/getting-started/adding-first-lager-box Install the Lager CLI and connect to your first Lager Box The **Lager CLI** (`lager`) enables direct control of your hardware from the command line and text editor. With this tool, you can: * **Control power supplies**: Programmatically set voltage and current for your DUT. * **Flash firmware images**: Update your device with new firmware builds. * **Monitor hardware in real-time**: Stream voltage, current, and other sensor data. * **Manipulate I/O pins**: Directly control GPIO for testing and automation. * And more! *** ## Prerequisites * A Lager Box -- see [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box) if you have not set one up yet * Python 3.10 or higher * `pip3` package manager *** ## Step 1: Install the CLI Package To install the **Lager CLI**, make sure you've fulfilled the above prerequisites and run the following command. ```bash theme={null} pip3 install -U lager-cli ``` You can check your version to make sure it's installed: ```bash theme={null} lager --version ``` **Expected output:** ``` lager-cli, version 0.16.1 ``` *** ## Step 2: Add Your Lager Box To interact with a Lager Box, you'll need its local IP or IP from your VPN. Once you have that, you can add it to your list of Lager Boxes and give it a name! **Finding your Lager Box IP address:** * **Tailscale VPN**: Run `tailscale status` to see all devices on your network. Look for your Lager Box name and its `100.x.y.z` address. * **Local network**: Check your router's DHCP client table, or ask your network administrator. * **Lager CLI**: Run `lager boxes` to see your boxes and their IP addresses. ```bash theme={null} lager boxes add --name my-lager-box --ip 100.64.1.42 --user ``` The command below lists every Lager Box you added. ```bash theme={null} lager boxes ``` This list is local to your computer. You can give each Lager Box any name you want. Agree on a naming convention with your team first. *** ## Step 3: Verify Connectivity Now that you've added a Lager Box, test that you can communicate with your Lager Box: ```bash theme={null} lager hello --box my-lager-box ``` **Expected output:** ``` my-lager-box says hello! ``` If your Lager Box says hello back, then you are ready to start using it! | Error | Cause | Fix | | -------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `No route to host` | The Lager Box is unreachable on the network | Make sure your VPN (Tailscale) is connected. Run `tailscale status` to verify. | | `Connection refused` | The Lager Box service is not running, or the box was never set up | The box may need a restart or a software update. Contact your administrator, or see [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box). | | `Connection timed out` | Network path exists but the box is not responding | Verify the IP address is correct with `lager boxes`. Check that the box is powered on. | | `command not found: lager` | The CLI is not installed or not in your PATH | Re-run `pip3 install -U lager-cli`. If using a virtual environment, make sure it is activated. | *** ## Next Steps Now that you can communicate with your Lager Box, set up the instruments connected to it: * **[Setting Up Your Instruments](/source/getting-started/setting-up-instruments)** -- Discover and configure the hardware connected to your box # Architecture Source: https://docs.lagerdata.com/source/getting-started/architecture Lager platform architecture overview An overview of how the Lager platform components fit together. It runs from the CLI commands on your laptop to the instruments wired to your DUT. ## High-Level Overview ```mermaid theme={null} flowchart LR subgraph entry["Entry points"] direction TB A["Developer CLI
lager supply psu1 voltage 3.3"] B["Developer script
lager python my_test.py"] C["CI runner"] end VPN{{"Tailscale VPN
WireGuard, encrypted"}} BOX["Lager Box
x86-64, Ubuntu 22.04+
Docker container: lager"] INST["Instruments
power supply, oscilloscope, LabJack T7, debug probe,
battery sim, e-load, USB hub, thermocouple"] DUT["DUT
Device Under Test"] A --> VPN B --> VPN C --> VPN VPN -->|"HTTP over the tunnel"| BOX BOX -->|"USB / Serial / VISA / LAN"| INST INST -->|"wires, probes, pins"| DUT ``` All three entry points reach the box through the same Tailscale tunnel. They end at the same drivers. Inside the box they take different routes. A command that drives a net through the box API posts directly to port 9000. `lager supply` is one. `lager python` instead uploads a script to the execution service on port 5000. That service runs the script as a subprocess. The subprocess gets full access to the `lager.*` hardware libraries. A CI runner is a developer machine that is ephemeral. It uses the same path as the command it runs. ## Terminology | Term | Definition | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CLI** | The `lager-cli` Python package (installed via `pip install lager-cli`). A Click-based command-line tool that runs on the developer's laptop. | | **Tailscale VPN** | A WireGuard-based mesh VPN that creates an encrypted tunnel between the developer's machine and the Lager Box. | | **Lager Box** | Any x86-64 machine running Ubuntu 22.04 or newer, physically co-located with the test instruments. Runs a Docker container hosting the box services and hardware drivers. | | **Net** | A logical name (e.g., `psu1`, `uart0`) that maps to a specific instrument + channel + address. Stored in `/etc/lager/saved_nets.json` on the box. | | **DUT** | Device Under Test -- the embedded board or product being tested. | | **Instrument** | A piece of test equipment (power supply, oscilloscope, LabJack, debug probe, etc.) connected to the box via USB, serial, or LAN. | | **`lager python`** | CLI command that uploads a user-written Python script to the box for execution with full access to `lager.*` hardware libraries. | ## Lager Box Internals ``` /etc/lager/ ├── saved_nets.json ├── available_instruments.json ├── box_id └── authorized_keys.d/ ~/third_party/ ├── JLink_Linux_*/ (optional) └── customer-binaries/ (optional) ``` A single Docker container named `lager` (started with `--restart always`) runs every service. The services are **peer processes, not a pipeline**. A start script launches each one and restarts it if it dies. Two of them call the hardware service on port 8080. The first is the box API on port 9000. The second is each user script that the execution service spawns. Both resolve the net name in their own process, then POST to `/invoke`. The debug and MCP services are independent. The hardware service is the sole owner of an instrument's VISA session. A caller that opens its own session races it for the USB device. A script drives an instrument directly only when it is **not** a VISA instrument. The direct drivers are LabJack, USB-202, FT232H, Aardvark, Joulescope and PPK2. Supplies, scopes, battery simulators, e-loads and solar simulators go over `/invoke` like any other caller. ```mermaid theme={null} flowchart TB subgraph container["Docker container: lager"] direction TB P9000[":9000 box API
Flask + SocketIO
UART, supply, battery, nets, lock"] P5000[":5000 python execution service
ThreadingHTTPServer"] P8765[":8765 debug service
GDB, OpenOCD"] P8100[":8100 MCP service"] SUB["user script subprocess"] subgraph hs[":8080 hardware service"] direction TB DRV["Drivers
VISA/SCPI, pySerial, LJM, pyOCD, aardvark_py"] OWN["Driver cache, per-device lock,
shared VISA session pool"] DRV --- OWN end P9000 -->|"resolve net, then POST /invoke"| DRV P5000 -->|"spawns"| SUB SUB -->|"VISA nets: POST /invoke"| DRV end INST["Instruments"] DUT["DUT"] DRV --> INST SUB -->|"non-VISA drivers only,
constructed in-process"| INST P8765 -->|"SWD / JTAG via probe"| INST INST --> DUT ``` **Each process holds its own `NetsCache`.** It is a per-interpreter singleton, not a box-wide one. The box API, the hardware service, and the debug and MCP services each hold one. So does every `lager python` subprocess. Each copy reads `saved_nets.json` and invalidates on that file's mtime. The copies therefore converge on their own. Three consequences follow. Every process pays its own first read. Two processes can disagree briefly, between a write and the next read. A service that dies and restarts comes back with a cold cache. ### Port Summary | Port | Service | Exposed | Purpose | | --------- | ---------------- | -------------------------- | --------------------------------------------------------------------------------------------------------- | | 9000 | Flask + SocketIO | Yes (VPN only) | Main box API: UART streaming, live supply/battery WebSockets, instrument discovery, net listing, box lock | | 5000 | HTTP | Yes (VPN only) | Python execution service: receives uploaded scripts and runs them as subprocesses | | 8765 | WebSocket | Yes (VPN only) | Debug sessions (GDB, flash, reset) | | 8100 | HTTP | Yes (VPN only) | MCP service for AI tool integration | | 8080 | Flask | Yes (VPN only) | Hardware service: instrument control via the Device proxy, called by the port 9000 API | | 8081 | HTTP | Yes (if PicoScope present) | Oscilloscope streaming UI | | 8082-8085 | TCP / WebSocket | Yes (if PicoScope present) | Oscilloscope daemon (commands, browser streaming, database streaming, CLI WebSocket) | | 8086+ | HTTP | Yes (if webcams present) | Webcam MJPEG streaming (one port per camera) | | 22 | SSH | Yes | Direct SSH access for deployment and debugging | The **Exposed** column describes a box that publishes its ports, which is the default. A box started with `start_box.sh --no-publish` (or `LAGER_NO_PUBLISH=1`) publishes none of them. Every service still listens inside the container. The `lagernet` Docker network still reaches it, and a reverse proxy owns the host ports. No `Yes` row answers at `:`. Port 22 is the exception. SSH is the host's own daemon rather than a published container port, so `--no-publish` does not affect it. ### Why there are two HTTP ports A box answers on `:9000` and on `:5000`, and the split is historical rather than functional. `:9000` is the box API and the primary one. Net metadata, instrument discovery, box locking, file download and version reporting all go there. `:5000` is the older script-upload path. `lager python` still uses it to send a script to the box and to stop a running one. Nothing new is added to it. Some state answers on both. Lock state is one: the box exposes it on each server, and the CLI reads it from `:9000`. Open both to your VPN. A box that publishes only `:9000` answers `lager nets` and `lager hello` but fails `lager python`. ## Optional Control Plane Integration A Lager Box publishes SSH keys from a key directory, `/etc/lager/authorized_keys.d/`. An external control plane can therefore provision access with no human typing SSH commands. Put a `.pub` file there, and the key reaches the box account's `~/.ssh/authorized_keys` in about five seconds. The box bind-mounts `/etc/lager` into the runtime container, so a control plane can write that file from inside the container. That is how it bootstraps before it has any SSH access to the box. `start_box.sh` owns only the region of `authorized_keys` between its `# BEGIN LAGER MANAGED KEYS` and `# END LAGER MANAGED KEYS` markers. It rebuilds that region from the key directory on every pass. Two consequences follow: * **Deleting a `.pub` revokes the key.** Nothing else does; editing `authorized_keys` by hand inside the marked region is undone on the next pass. * **Keys installed by other means stay untouched.** `ssh-copy-id` and cloud-init append outside the marked region, and `start_box.sh` preserves those lines verbatim. Any other system that manages this file must claim its own distinct marker pair. Two managers that share one pair each rebuild the other's region on every pass. * **Preserved is not the same as durable.** `start_box.sh` preserves a loose line against *its own* rebuild. It cannot preserve that line against someone else's. A second key manager rebuilds `authorized_keys` from its own source. It keeps only its own marked region, so it drops every loose line. `start_box.sh` then re-creates its region from the key directory alone. A key that never reached that directory does not come back. For this reason `lager ssh-setup`, `lager update`, and `lager install` do both. They append the public key, and they write it into the key directory as `lager-box--.pub`. Any tool that installs a key it expects to survive must do the same. Lager itself does not require or run a control plane -- this is a hook, not a dependency. Leaving the key directory absent or empty simply means no keys are published from it. The [Professional Services directory](https://lagerdata.com/professional-services) lists the commercial control planes that build on this hook. They add org, RBAC and SSO, audit logging, and scheduling on top of Lager. ## Net Abstraction A **Net** is the central abstraction that decouples CLI commands from physical hardware details. ```mermaid theme={null} flowchart LR A["CLI command
lager supply psu1 voltage 3.3"] B["saved_nets.json entry
name: psu1"] C["Physical hardware
Rigol DP832, channel 1
VISA: USB0::..."] A -->|"resolve net name"| B -->|"instrument + channel + address"| C ``` The record backing `psu1` looks like this: ```json theme={null} { "name": "psu1", "type": "power-supply", "channel": 1, "instrument": { "name": "rigol-dp832", "address": "USB0::0x1AB1::0x0E11::..." }, "params": { "voltage_limit": 5.0 } } ``` Swapping the physical supply means editing this record. Every command that names `psu1` keeps working. ### Supported Net Types | Net Type | Instruments | | -------------- | ------------------------------------------------ | | `power-supply` | Rigol DP800, Keithley 2200/2280, Keysight E36x00 | | `battery` | Keithley 2281S | | `eload` | Rigol DL3021 | | `solar` | EA PSI / EL series | | `analog` | Rigol MSO5000 (oscilloscope analog channel) | | `logic` | Rigol MSO5000 (logic analyzer channel) | | `adc` | LabJack T7, USB-202 | | `dac` | LabJack T7, USB-202 | | `gpio` | LabJack T7, USB-202 | | `thermocouple` | Phidget thermocouple | | `watt` | Yocto-Watt, Joulescope JS220 | | `debug` | J-Link, CMSIS-DAP, ST-Link (via pyOCD) | | `uart` | USB-to-serial adapters | | `i2c` | Aardvark, LabJack T7, FT232H | | `spi` | LabJack T7, FT232H | | `arm` | Rotrics Dexarm | | `usb-hub` | Acroname, YKUSH | ## Execution Flows ### CLI Command Execution Step-by-step data path for `lager supply psu1 voltage 3.3 --yes`: ```mermaid theme={null} sequenceDiagram autonumber participant C as CLI on the laptop participant N as Tailscale VPN participant B as Box API :9000 participant H as Hardware service :8080 participant I as Instrument C->>C: Resolve the box IP from config C->>C: Build the payload: netname, action, params C->>N: POST /supply/command N->>B: Encrypted tunnel B->>B: Resolve psu1 through its own NetsCache B->>H: POST /invoke, via the Device proxy H->>H: Instantiate and cache the driver, take the per-device lock H->>I: SCPI command I-->>H: Response H-->>B: Result JSON B-->>N: Result JSON N-->>C: Voltage set to 3.300V ``` The hardware service owns and caches the driver for each physical device. It serializes access under a per-device lock. Concurrent requests to the box API cannot interleave I/O on the same instrument. ### Custom Script Execution (`lager python`) The `lager python` command uploads a user-written Python script to the execution service on port 5000. This is a different path from the box API commands above. The service runs the script as its own subprocess, with its own interpreter and its own caches. What happens next depends on the instrument. A VISA instrument goes through the same `/invoke` proxy the box API uses. That covers supplies, scopes, battery simulators, e-loads and solar simulators. Everything else is constructed and driven inside the subprocess: LabJack, USB-202, FT232H, Aardvark, Joulescope and PPK2. Those direct-USB drivers claim their device exclusively. The execution service therefore asks the hardware service to release its own claims first. It leaves the shared VISA sessions open on purpose. Tearing those down is what produced `[Errno 16] Resource busy` on the next supply command. ```bash theme={null} $ lager python my_test.py --box mybox --env VOLTAGE=3.3 --timeout 300 ``` The script runs inside the Docker container with full access to the `lager.*` hardware libraries. The box streams its output back in real time. ## Physical Wiring How instruments physically connect between the Lager Box and the DUT: ```mermaid theme={null} flowchart TB subgraph box["Lager Box"] direction LR USB["USB-A ports"] LAN["LAN port"] end LJ["LabJack T7"] PROBE["Debug probe"] HUB["USB hub"] AA["Aardvark I2C/SPI"] PH["Phidget thermocouple"] VISA["VISA-over-LAN instruments
Rigol DP832, Rigol MSO5074, Keithley 2281S"] DUT["DUT (Device Under Test)
VCC, GND, SDA, SCL, SWD, TX, RX, GPIO, TEMP, USB"] USB --> LJ USB --> PROBE USB --> HUB USB --> AA USB --> PH LAN --> VISA LJ -->|"ADC / DAC / GPIO"| DUT PROBE -->|"SWD / JTAG"| DUT HUB -->|"USB"| DUT AA -->|"I2C / SPI"| DUT PH -->|"thermocouple"| DUT VISA -->|"banana jacks / BNC / probes"| DUT ``` ### Connection Types | Connection | Used For | Protocol | | ------------- | ----------------------------------- | --------------------------------- | | USB | LabJack, debug probes, serial, hubs | Vendor-specific, CDC-ACM | | USB-VISA | Rigol/Keithley/Keysight instruments | USBTMC (SCPI) | | LAN-VISA | Bench instruments on local network | VXI-11 / raw TCP (SCPI) | | Serial (UART) | DUT communication | RS-232 / TTL via USB adapter | | SWD / JTAG | Firmware flash, debug, reset | ARM debug (via probe) | | I2C / SPI | Peripheral communication with DUT | I2C / SPI via Aardvark or LabJack | ## Running from CI A CI runner drives a Lager Box with the same commands a developer uses. Lager needs no CI-specific infrastructure. There are two arrangements. A runner on a separate host reaches the box across the network, usually over a Tailscale VPN. A runner installed on the box needs no network hop and no secrets. It also serializes the jobs for that bench. [Using Lager in CI](/source/getting-started/using-lager-in-ci) covers both arrangements and the workflow for each. It also covers bench locking, firmware delivery, and cleanup after a cancelled job. # Your First Test Source: https://docs.lagerdata.com/source/getting-started/first-test A complete walkthrough of testing a device with Lager This guide walks you through a complete test workflow, in Python or in Rust. It starts at connectivity and ends at an automated test. At the end, you can use the CLI interactively and write your own Lager test scripts. First complete the [Getting Started](/source/getting-started/overview) guide. This tutorial assumes that you installed the CLI, added your box, and configured your instruments with nets. *** ## Part 1: CLI Walkthrough Let's step through a typical test flow using individual CLI commands. This is useful for ad-hoc testing, debugging, and getting familiar with your setup. ### Step 1: Verify your box is online ```bash theme={null} lager hello --box my-lager-box ``` **Expected output:** ``` my-lager-box says hello! ``` ### Step 2: Check connected instruments ```bash theme={null} lager instruments --box my-lager-box ``` This shows all instruments the box can see. Confirm your power supply, debug probe, and any other instruments appear. ### Step 3: View your configured nets ```bash theme={null} lager nets --box my-lager-box ``` This shows the named nets you'll use in subsequent commands. Note the net names -- you'll need them below. ### Step 4: Set defaults to reduce typing ```bash theme={null} lager defaults add --box my-lager-box lager defaults add --supply-net POWER lager defaults add --debug-net DEBUG_NET ``` With defaults set, you can omit `--box` and net names from subsequent commands. ### Step 5: Flash firmware ```bash theme={null} lager debug flash --hex firmware.hex ``` **Expected output:** ``` Flashing firmware.hex to target... Flash complete. 32768 bytes written. ``` ### Step 6: Power on your device ```bash theme={null} # Set voltage with protection thresholds lager supply voltage 3.3 --ovp 3.6 --ocp 0.5 --yes # Enable the output lager supply enable --yes ``` ### Step 7: Take a measurement ```bash theme={null} lager adc SENSOR_1 ``` **Expected output:** ``` ADC 'SENSOR_1': 2.450000 V ``` ### Step 8: Power down ```bash theme={null} lager supply disable --yes ``` Always disable power supplies when you're done testing. *** ## Part 2: Your First Test Script Now convert the manual CLI steps into a repeatable test. A script always cleans up after itself and disables power, even when an error occurs. You can write it in Python, which runs on the box through `lager python`. You can also write it in Rust, as an ordinary `cargo test` in your firmware repository that uses the [`lager-net` crate](/source/reference/rust/overview). Both versions below flash, power on, measure, assert, and clean up. ```python my_first_test.py theme={null} from lager import Net, NetType def main(): # Get our nets psu = Net.get('POWER', type=NetType.PowerSupply) debug = Net.get('DEBUG_NET', type=NetType.Debug) sensor = Net.get('SENSOR_1', type=NetType.ADC) try: # Flash firmware print("Flashing firmware...") debug.connect() debug.flash(['firmware.hex']) debug.reset() print("Flash complete.") # Power on the DUT print("Enabling power supply at 3.3V...") psu.set_voltage(3.3) psu.set_current(0.5) psu.enable() print("Power enabled.") # Take a measurement voltage = sensor.input() print(f"Sensor reading: {voltage:.4f} V") # Check the result if 2.0 <= voltage <= 3.0: print("PASS: Sensor voltage within expected range.") else: print(f"FAIL: Sensor voltage {voltage:.4f}V outside range [2.0, 3.0]") finally: # Always clean up, even if an error occurs print("Disabling power supply...") psu.disable() print("Done.") if __name__ == '__main__': main() ``` ```rust tests/my_first_test.rs theme={null} use lager::LagerBox; #[test] fn sensor_reads_in_range_after_boot() -> lager::Result<()> { // Reads LAGER_BOX_HOST from the environment. let lager = LagerBox::from_env()?; let psu = lager.supply("POWER"); let debug = lager.debug("DEBUG_NET"); let sensor = lager.adc("SENSOR_1"); // Flash firmware println!("Flashing firmware..."); debug.connect()?; debug.flash("firmware.hex")?; debug.reset(false)?; println!("Flash complete."); // Power on the DUT println!("Enabling power supply at 3.3V..."); psu.set_voltage(3.3)?; psu.set_current(0.5)?; psu.enable()?; println!("Power enabled."); // Take a measurement and check the result; disable power before // asserting so the DUT is never left energized by a failing test. let voltage = sensor.read()?; println!("Sensor reading: {voltage:.4} V"); psu.disable()?; assert!( (2.0..=3.0).contains(&voltage), "sensor voltage {voltage:.4} V outside range [2.0, 3.0]" ); Ok(()) } ``` Cleanup protects your hardware, so both versions do it. The Python version uses `try/finally`, which disables the power supply even when an error occurs. The Rust version disables power before its assertion. An earlier `?` error also ends the test, and the failure shows the state of the supply. *** ## Part 3: Running It Execute the Python script on your Lager Box: ```bash theme={null} lager python my_first_test.py --box my-lager-box ``` Or run the Rust test from your project (with `lager = { package = "lager-net", version = "0.4" }` in `[dev-dependencies]`): ```bash theme={null} LAGER_BOX_HOST= cargo test ``` **Expected output (Python):** ``` Flashing firmware... Flash complete. Enabling power supply at 3.3V... Power enabled. Sensor reading: 2.4500 V PASS: Sensor voltage within expected range. Disabling power supply... Done. ``` If you need to send additional files along with your script (firmware binaries, config files), use `--add-file`: ```bash theme={null} lager python my_first_test.py --box my-lager-box --add-file firmware.hex ``` *** ## What's Next You've completed your first test with Lager. Here are some directions to explore: * **[CLI Reference](/source/reference/cli/overview)** -- Full documentation for every CLI command * **[Python API](/source/reference/python/overview)** -- Complete Python SDK reference * **[Rust API](/source/reference/rust/overview)** -- Write your HIL suite as `cargo test` integration tests * **[Troubleshooting](/source/getting-started/troubleshooting)** -- Solutions when things go wrong * **[Glossary](/source/getting-started/glossary)** -- Definitions for technical terms used in the docs The [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py) is a larger example. It combines robot arm control, USB hub power cycling, debug probe flashing, and ADC measurement. # Glossary Source: https://docs.lagerdata.com/source/getting-started/glossary Definitions of technical terms used in Lager documentation A reference of terms, abbreviations, and acronyms used throughout the Lager documentation. ## ADC Analog-to-Digital Converter. Hardware that converts an analog voltage to a digital value. Used for reading sensor outputs, measuring voltages, etc. ## BLE Bluetooth Low Energy. A wireless communication protocol for short-range, low-power devices. ## CLI Command-Line Interface. The `lager` tool you install via `pip install lager-cli` and run in your terminal. ## DAC Digital-to-Analog Converter. Hardware that outputs a precise analog voltage from a digital value. Used for generating reference voltages or test signals. ## DUT Device Under Test. The embedded board or product that you test with Lager. ## E-Load Electronic Load. An instrument that draws a programmable amount of current from a power source, used to simulate real-world loads during testing. ## GDB GNU Debugger. A widely-used debugger for embedded development. Lager starts a GDB server on the box that you can connect to remotely. ## GPIO General-Purpose Input/Output. Digital pins that can be configured as inputs (reading HIGH/LOW) or outputs (driving HIGH/LOW). ## GPI General-Purpose Input. A GPIO pin configured for reading digital state (HIGH or LOW). ## GPO General-Purpose Output. A GPIO pin configured for driving digital state (HIGH or LOW). ## I2C Inter-Integrated Circuit (pronounced "eye-squared-see"). A two-wire serial protocol (SDA + SCL) commonly used to communicate with sensors, EEPROMs, and other peripherals. ## Lager Box Any x86-64 machine that runs Ubuntu 22.04 or newer, such as a compact mini PC. It sits on your bench, wired to your instruments and DUT. It runs the Lager container (Flask and SocketIO), which exposes HTTP and WebSocket APIs for hardware control. See [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box). ## Net A named logical connection to a physical instrument on a Lager Box. For example, `supply1` maps to channel 1 of a Rigol DP832 power supply. `uart0` maps to `/dev/ttyUSB0` at 115200 baud. A net separates test code from a specific hardware address. The same script keeps working when an instrument moves or changes. ## OCP Over-Current Protection. A safety threshold on a power supply. If the output current exceeds this limit, the supply automatically shuts off. Clear with `lager supply clear-ocp`. ## OVP Over-Voltage Protection. A safety threshold on a power supply. If the output voltage exceeds this limit, the supply automatically shuts off. Clear with `lager supply clear-ovp`. ## REPL Read-Eval-Print Loop. An interactive prompt where you type commands and see results immediately. The Lager Terminal (`lager terminal`) is a REPL. ## SCPI Standard Commands for Programmable Instruments (pronounced "skippy"). A text-based protocol used to control bench instruments like oscilloscopes and power supplies. ## SOC State of Charge. A percentage (0-100%) representing how charged a battery is. Used with battery simulator instruments. ## SPI Serial Peripheral Interface. A four-wire serial protocol (SCLK, MOSI, MISO, CS) used for high-speed communication with peripherals like flash memory and ADCs. ## SWD Serial Wire Debug. A two-pin debug interface (SWDIO + SWCLK) used by ARM Cortex-M microcontrollers. Used by debug probes like J-Link to flash firmware and debug code. ## Tailscale A WireGuard-based mesh VPN that creates encrypted tunnels between your computer and your Lager Boxes. This is how you access boxes remotely. ## TUI Text User Interface. An interactive terminal-based interface (as opposed to a graphical UI). Lager uses TUIs for net configuration (`lager nets tui`) and real-time power supply monitoring (`lager supply tui`). ## UART Universal Asynchronous Receiver/Transmitter. A serial communication protocol commonly used for debug console output and device communication. ## VISA Virtual Instrument Software Architecture. A standard for communicating with test instruments over USB, LAN, or GPIB. Many bench instruments (Rigol, Keysight, Keithley) use VISA. # Interacting With Nets Source: https://docs.lagerdata.com/source/getting-started/interacting-with-nets Control your instruments through the Lager Client using CLI or Lager Python Nets act as named interfaces to your instruments. After you configure your Nets, the Lager Client controls those instruments. Use either the CLI or the Python SDK. The examples below show how to command the common instrument types. These include power supplies, battery simulators, debuggers, and communication buses. *** ## Setting Defaults First, set default values for `--box` and for common nets. Defaults keep you from repeating them on every command: ```bash theme={null} # Set a default box so you don't need --box on every command lager defaults add --box my-lager-box # Set default nets for common instrument types lager defaults add --supply-net POWER lager defaults add --debug-net DEBUG_NET lager defaults add --i2c-net I2C_0 ``` With defaults set, you can run commands more concisely (e.g., `lager supply voltage 3.3` instead of `lager supply POWER voltage 3.3 --box my-lager-box`). See the [defaults reference](/source/reference/cli/defaults) for all options. *** ## CLI Examples ### Supply Nets Power supply Nets can be controlled using the `lager supply` command. Set voltage and protection thresholds (note: this does **not** enable output): ```bash theme={null} lager supply POWER voltage 5 --ovp 5.1 --ocp 0.5 --box my-lager-box ``` **OVP** (Over-Voltage Protection) and **OCP** (Over-Current Protection) are safety thresholds. If the output voltage or current exceeds these limits, the supply automatically shuts off to protect your device. You can clear a tripped fault with `lager supply POWER clear-ovp` or `clear-ocp`. Enable the output: ```bash theme={null} lager supply POWER enable --box my-lager-box ``` Disable the output: ```bash theme={null} lager supply POWER disable --box my-lager-box ``` *** ### Battery Nets Some programmable power supplies support battery simulation. These can be controlled using the `lager battery` command. Set the simulated battery's state of charge (SOC): ```bash theme={null} lager battery BATT soc 50 --box my-lager-box ``` **SOC** (State of Charge) is the battery's charge level as a percentage, from 0 to 100. An SOC of 50 simulates a half-charged battery. Use it to test how your device behaves at each battery level. Set max charge and discharge current: ```bash theme={null} lager battery BATT current-limit 1.0 --box my-lager-box ``` *** ### Debug Nets Debugger nets (e.g. J-Link) can be used to flash firmware, erase memory, and inspect devices. Flash a hex file to your device: ```bash theme={null} lager debug DEBUG_NET flash --hex firmware.hex --box my-lager-box ``` Where `DEBUG_NET` is the name of your debug net. You can also use a default debug net if configured. *** ### I2C and SPI Nets For communicating with peripheral devices over I2C or SPI buses: ```bash theme={null} # Scan the I2C bus for connected devices lager i2c I2C_0 scan --box my-lager-box # Read 2 bytes from register 0x00 on device at address 0x76 lager i2c I2C_0 transfer 2 --address 0x76 --data 0x00 --box my-lager-box # Read a SPI device ID (send 0x9F command, read 3 response bytes) lager spi SPI_0 transfer --data 0x9f 4 --box my-lager-box ``` *** ## Programmatic Control with Python The Lager Python SDK performs the same operations in code. Use it for more complex automation, or to integrate with a test framework. The following example shows how to write a Python script using the Net API: 1. **Create a Python script.** Save the following code to a file named `flash.py`: ```python theme={null} from lager import Net, NetType # Get a debug net by name dbg = Net.get('DEBUG_NET', type=NetType.Debug) # Reset and flash the firmware dbg.reset(halt=True) dbg.flash('path/to/firmware.hex') print("Firmware flashing complete.") ``` 2. **Execute the script with Lager.** Use the `lager python` command. It runs the script in the Lager Box environment, where the script can reach the connected hardware. ```bash theme={null} lager python flash.py --box my-lager-box ``` > For detailed Python API documentation, see the [Python Reference](/source/reference/python/overview) section. For a more comprehensive example combining power supply control, firmware flashing, ADC measurement, and safe cleanup, see the [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py). *** ## Next Steps You've completed the Getting Started guide. Here's where to go from here: * **[Your First Test](/source/getting-started/first-test)** -- Walk through a complete end-to-end test workflow * **[CLI Reference](/source/reference/cli/overview)** -- Full documentation for all CLI commands * **[Python API](/source/reference/python/overview)** -- Automate tests with the Python SDK * **[Defaults Reference](/source/reference/cli/defaults)** -- Reduce typing by setting default box and net values * **[Troubleshooting](/source/getting-started/troubleshooting)** -- Solutions for common issues # An Introduction to Lager Source: https://docs.lagerdata.com/source/getting-started/overview Learn how Lager adds efficiency to embedded development and hardware testing. **Lager** is an open-source platform for embedded software development. It gives you one programmable interface to your embedded hardware. Firmware engineers use it to build repeatable development and validation workflows. The same workflow runs on your desk, in CI, and under the control of an AI agent. Lager replaces one-off scripts and manual bench procedures with reusable automation. Your team builds more reliable embedded software with less effort. *** ## How Lager Works A Lager setup has three components that together expose your hardware through a single, programmable interface: ``` Your Laptop / CI / AI Agent → Lager Box → Bench Equipment + Device Under Test ``` ### 1. Lager Box The **Lager Box** is any x86-64 machine that runs Ubuntu 22.04 or newer, such as a compact mini PC. It sits next to your hardware. You connect it to your test equipment and to your Device Under Test (DUT). To turn a machine into one, see [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box). After you connect the instruments and the DUT, the Lager Box exposes them through one consistent programmable interface. You can drive that interface from your desk, from CI, or from an AI agent. ### 2. Lager CLI & Client Libraries To interact with a Lager Box, you'll need the **Lager CLI** - a command-line tool you install on your computer. It gives you a unified interface for working with your instruments and DUT. Beyond interactive CLI use, there are three equal ways to automate against a box — pick whichever fits your team: * **[Python library](/source/reference/python/overview)** - automate test suites as Python scripts, run with `lager python` * **[Rust crate](/source/reference/rust/overview)** - write your HIL suite as ordinary Rust integration tests. Run it with `cargo test`, next to your firmware * **[MCP server](/source/reference/mcp/overview)** - let AI agents discover your bench and run test scenarios directly Common workflows: * Flash and debug embedded devices * Control power supplies, battery simulators, and electronic loads * Monitor serial/UART output with interactive test runners * Capture oscilloscope waveforms and logic analyzer traces * Communicate with devices over I2C and SPI buses * Automate full regression test suites The CLI also includes **Lager Terminal**, an interactive REPL with tab completion and command history. Run `lager terminal` or just `lager` with no arguments to launch it. ### 3. Bench Equipment & Devices The Lager Box supports a wide range of professional test equipment that you connect with: * Power supplies, battery simulators, and electronic loads * Debug probes (J-Link, CMSIS-DAP, ST-Link) * Oscilloscopes and logic analyzers * ADC/DAC/GPIO modules (LabJack T7, MCC USB-202) * I2C/SPI adapters (Total Phase Aardvark, LabJack T7) * Power meters (Yocto-Watt, Joulescope JS220) * And more (see full list below) *** ## Lager Nets You must configure a Lager Box with a set of **Nets** before you can use it. Each Net names one instrument, one instrument channel, one serial port, or one other interface that you control. For example, your DUT draws power from channel 1 of a power supply. You create a Power Supply Net called `DUT_POWER` that maps to that channel. The net can then switch the DUT on and off, and use any other function that the supply supports. *** ## Next Steps Ready to get started? * **[Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box)** -- Turn an Ubuntu machine into a Lager Box * **[Adding your First Lager Box](/source/getting-started/adding-first-lager-box)** -- Install the CLI and verify connectivity Already set up and ready to automate? Jump to the [Python API](/source/reference/python/overview) or the [Rust API](/source/reference/rust/overview). # Setting Up a Lager Box Source: https://docs.lagerdata.com/source/getting-started/setting-up-a-lager-box Turn an Ubuntu machine into a Lager Box A **Lager Box** is an ordinary Ubuntu machine that runs the Lager Box software. This page turns a machine you already have into one. That machine can be a mini PC on your bench, a rack server, or a virtual machine. If someone has already set up a box for you, you do not need this page. Go straight to [Adding your First Lager Box](/source/getting-started/adding-first-lager-box). *** ## What You Need **On the machine that will become the box:** * An **x86-64** processor. This is the only supported box architecture. * **Ubuntu 22.04 or newer**, including 24.04 LTS. See the note on Ubuntu 25.10 and later below. * A network connection, and an IP address or hostname you can reach. * An SSH server, and a **login account with `sudo`** whose password you know. * **`git`** installed. Lager installs everything else the box needs, including Docker. Step 1 explains why you must spend the extra minute and install Docker yourself. **On your computer:** * **Python 3.10 or higher** and `pip3`. * An SSH client (`ssh` and `ssh-keygen`). Run this on the machine to check all three at once before you start: ```bash theme={null} uname -m # must be x86_64 lsb_release -ds # Ubuntu 22.04 or newer sudo --version # see the sudo-rs note below ``` A first install builds the Lager container on the box itself. That takes roughly 14 minutes on ordinary box hardware. Plan for the box to be busy. `lager install` bounds the step at 30 minutes by default. Slower hardware runs longer. An emulated guest, a low-power mini PC, or a throttled VM can exceed 30 minutes on a healthy build. Raise the budget with `--timeout ` or `LAGER_INSTALL_TIMEOUT`, so the build does not stop midway. **Keep your own machine awake for the whole install.** The build runs on the box, but it is driven over SSH from the machine you launched `lager install` from. If that machine sleeps and the connection drops, the build goes with it. On macOS, run the install under `caffeinate -i`. **Ubuntu 25.10 and later, including 26.04 LTS, install without any change.** Those releases default to `sudo-rs`, which rejects wildcards in command arguments. Lager used to write wildcard rules to `/etc/sudoers.d/`, so installation stopped early with `wildcards are not allowed in command arguments`. Since **lager 0.41.0** every rule Lager writes names its arguments exactly, and nothing needs switching back to classic `sudo`. On an older CLI, switch the box to classic `sudo` instead (`sudo update-alternatives --set sudo /usr/bin/sudo.ws`). Keep a second session open with a working root shell, in case the switch does not take. **The box login account is root-equivalent by design.** Provisioning a box requires root, so installation grants that account passwordless `sudo` for the commands Lager needs. Anyone who can log in as that account can obtain root on the box. Prefer a dedicated user on a dedicated machine, and treat its SSH key accordingly. The [Install reference](/source/reference/cli/install) describes exactly which grants are written. *** ## Step 1: Prepare the Machine SSH into the machine and install `git`: ```bash theme={null} sudo apt update && sudo apt install -y git ``` Confirm the account can use `sudo`. You will be asked for its password once during installation: ```bash theme={null} sudo true ``` **Optional, but it makes a failure easier to read: install Docker yourself now.** The installer installs Docker when the machine does not have it. It runs that step over SSH, and it reports package problems with less detail than apt gives you directly. Install Docker by hand first, and any problem surfaces with a real error message, in front of you: ```bash theme={null} sudo apt install -y docker.io docker-compose-v2 docker-buildx sudo systemctl enable --now docker sudo usermod -aG docker $USER ``` Log out and back in afterward so the group membership takes effect, then check `docker ps` works without `sudo`. The login account does not have to be named `lagerdata`. Pass its real name as `--user` in Step 3, and record that name with the box. Later commands then use the right account. Note the machine's IP address or hostname -- you need it in Step 3: ```bash theme={null} ip -br addr ``` *** ## Step 2: Install the Lager CLI On your own computer, not on the box: ```bash theme={null} pip3 install -U lager-cli ``` Check that it is available: ```bash theme={null} lager --version ``` *** ## Step 3: Run the Installer `lager install` does the rest. It configures SSH keys and sudo, and it deploys the box code. It installs Docker when the machine does not have it. It then builds and starts the Lager container, and configures the firewall. ```bash theme={null} lager install --ip --user ``` The command checks SSH connectivity and prints a summary of its planned work. It asks you to confirm before it changes anything. **The first time, SSH will not be authorized yet.** Lager notices, and offers to fix it: ``` No SSH key on this machine is authorized on the box. Set up the lager_box key now? (one box-password prompt, then the rest of the install runs unattended) [Y/n]: ``` Accept it and enter the box account's password once. Lager installs a dedicated key at `~/.ssh/lager_box`. Every later step, and every later command, then runs without a password. If you decline, the install stops. It tells you to run `lager ssh-setup --box `, which does the same thing on its own. Deployment then runs, streaming its output. Leave it alone until it finishes. **If it fails partway, fix what it reports and run the same command again.** `lager install` is safe to re-run. It rewrites its own configuration each time and skips finished work, so a second run continues rather than starts over. To install a specific release instead of the latest code, add `--version` -- for example `--version v0.39.0`. See the [Install reference](/source/reference/cli/install) for every option. *** ## Step 4: Networking and the Firewall Installation configures a UFW firewall on the box. The firewall restricts the Lager service ports to your VPN, the Docker bridge, and localhost. Port 22 always stays open, so the firewall cannot lock you out of the machine. How you point it at your network depends on what you use: **Tailscale.** Nothing to do. If Tailscale runs on the box, the installer detects the `tailscale0` interface and allows the Lager ports on it. **A corporate VPN.** Name the interface explicitly. Find it on the box: ```bash theme={null} ip -br link ``` Then pass it to the installer -- `cscotun0` for Cisco Secure Client, `tun0` for OpenConnect, and so on: ```bash theme={null} lager install --ip --user --corporate-vpn cscotun0 ``` The interface must exist on the box **at the moment you run the installer**. If the VPN is not connected, the firewall step fails with `Corporate VPN interface not found` and lists the interfaces it did find. Connect the VPN and run the install again. **Neither.** Add `--skip-firewall` to leave UFW alone entirely, and secure the machine by whatever means you already use. *** ## Step 5: Name the Box and Verify When deployment finishes, the installer offers to add the box to your local configuration: ``` Add this box to your configuration? [Y/n]: y Box name: bench-1 ``` The name is yours to choose and is local to your computer. If you skipped that prompt, add it by hand -- `--user` is required: ```bash theme={null} lager boxes add --name bench-1 --ip --user ``` Check that the box answers: ```bash theme={null} lager hello --box bench-1 ``` **Expected output:** ``` Box: bench-1 IP: 100.64.1.42 Version: 0.39.0 bench-1 is online and responding! ``` Then see what hardware it can find: ```bash theme={null} lager instruments --box bench-1 ``` An empty list is the correct answer before you connect any instruments. *** ## Running a Lager Box in a Virtual Machine A VM works as a Lager Box, provided the guest is x86-64 and runs Ubuntu 22.04 or newer. Everything above applies unchanged. The one thing that differs is hardware access. Instruments and debug probes are USB devices, and the Lager container reaches them through the host's `/dev`. **A USB device the guest cannot see is a device Lager cannot use.** So if the box will drive real hardware, configure USB passthrough in your hypervisor before you connect anything. Pass through an **entire USB controller** rather than individual devices. A debug probe re-enumerates when it resets, and Lager power-cycles USB hub ports as a normal part of testing. Both events change or drop the device identity that per-device passthrough pins to. A whole controller keeps working across them. A VM with no hardware attached is still useful when you try Lager out. It needs no passthrough at all. ### Emulated guests An **emulated** x86-64 guest works, such as an x86 VM on an ARM host. Its first install runs far slower than the figures above, and can take several hours. The container build is the reason. The box image compiles a USB DAQ library from source as a single-threaded C++ build. That takes a few minutes natively, and it is very slow under emulation. Expect that step alone to run for hours. Pull the pre-built image instead of building it. Deploy a release tag rather than `main`, so that a published image exists to pull: ```bash theme={null} lager install --ip --user --version lager update --box --version --pull ``` Any published release tag works. `lager update --version` accepts a tag, a semver pin, a branch, or a commit SHA. Only a release tag has a published image to pull. When no image matches, `--pull` falls back to building on the box. Watch the output. If it starts to compile, the pull missed and you are back on the slow path. See [pre-built box images](/source/reference/cli/update#pre-built-box-images). *** ## Troubleshooting Installation stops before deploying anything. Install `git` on the box and run the command again: ```bash theme={null} sudo apt update && sudo apt install -y git ``` Expected on a first install -- answer **yes** to the prompt that follows and enter the box account's password once. To do it as a separate step instead: ```bash theme={null} lager ssh-setup --box ``` If the box rejects the password, confirm that you use the box account's own password. Confirm also that the box allows password authentication for that account. The interface named by `--corporate-vpn` did not exist on the box when the firewall step ran. The error lists the interfaces that do exist. Connect the VPN, confirm the name with `ip -br link` on the box, and run the install again. A first install builds the container image on the box. That is the longest step by far, and it can run for many minutes with little output. Let it run. The command gives up on its own after 30 minutes. The box runs `sudo-rs`, the default `sudo` on Ubuntu 25.10 and later, and the CLI is older than 0.41.0. Installation stops at the passwordless-sudo step with `visudo: invalid sudoers file` before anything is deployed. Upgrade the CLI (`pip install --upgrade lager-cli`) and run the install again. Since 0.41.0 Lager writes no wildcard rules, and it installs cleanly under `sudo-rs`. If you cannot upgrade, switch the box to classic `sudo` instead. Keep a second session open with a working root shell, in case the switch does not take: ```bash theme={null} sudo update-alternatives --set sudo /usr/bin/sudo.ws sudo --version ``` Installation prints the manual commands to run on the box. Run them **one at a time** rather than as a block, so that you see which one fails. The installer reports this whole sequence as a single error: ```bash theme={null} sudo apt-get update && sudo apt-get install -y docker.io docker-compose-v2 sudo systemctl daemon-reload sudo systemctl enable docker sudo systemctl restart docker sudo usermod -aG docker ``` If `restart` is the step that fails, Docker itself does not start. A second install does not help. Ask it why: ```bash theme={null} systemctl status docker journalctl -xeu docker.service ``` Once Docker runs, log out and back in so the group membership takes effect, then run `lager install` again. It detects the working Docker and skips this step. x86-64 is the only supported box architecture. Check with `uname -m` on the machine. It must report `x86_64`. On any other architecture, an install can appear to succeed while the box has no hardware support. For problems that appear after the box is up and running, see [Troubleshooting](/source/getting-started/troubleshooting). *** ## Next Steps Your box is ready. Now connect to it and give it something to do: * **[Adding your First Lager Box](/source/getting-started/adding-first-lager-box)** -- Add the box on any other machine that needs to reach it * **[Setting Up Your Instruments](/source/getting-started/setting-up-instruments)** -- Discover the hardware connected to your box and define nets # Setting Up Your Instruments Source: https://docs.lagerdata.com/source/getting-started/setting-up-instruments Configure and manage instruments connected to a Lager Box Before you use instruments with the Lager Client, you must identify and organize the connected devices. This applies to both the CLI and the Python library. *** ## View Connected Instruments To view all instruments currently connected to a specific Lager Box, run: ```bash theme={null} lager instruments --box my-lager-box ``` This command detects and lists all physical instruments connected to the Lager Box via USB or network. **Example output:** ``` ┌─────────────────────────┬──────────┬────────────────────────────────┐ │ Instrument │ Channels │ Address │ ├─────────────────────────┼──────────┼────────────────────────────────┤ │ Rigol_DP832 │ CH1,CH2 │ USB0::0x1AB1::0x0E11::DP8... │ │ LabJack_T7 │ AIN0-13 │ T7-12345678 │ │ Aardvark │ I2C,SPI │ USB0::0x0403::0xE0D0::... │ │ Segger_JLink │ SWD │ USB::001::002 │ └─────────────────────────┴──────────┴────────────────────────────────┘ ``` To view configured nets (logical mappings to instruments), use: ```bash theme={null} lager nets --box my-lager-box ``` This shows the nets you've created, their types, and which instruments they're mapped to. ### Example Output: ``` Name Net Type Instrument Channel Address =============================================================================================== ADC_0 adc LabJack_T7 AIN0 USB0::0x0CD5::0x0007::::INSTR ARM arm Rotrix_Dexarm /dev/ttyACM0 USB0::0x0483::0x5740::206E399E4753::INSTR GPIO_0 gpio LabJack_T7 FIO3 USB0::0x0CD5::0x0007::::INSTR GPIO_1 gpio LabJack_T7 FIO2 USB0::0x0CD5::0x0007::::INSTR GPIO_2 gpio LabJack_T7 FIO1 USB0::0x0CD5::0x0007::::INSTR GPIO_3 gpio LabJack_T7 FIO0 USB0::0x0CD5::0x0007::::INSTR I2C_0 i2c Aardvark -- USB0::0x0403::0xE0D0::2420032::INSTR SPI_0 spi Aardvark -- USB0::0x0403::0xE0D0::2420032::INSTR TEMP_0 thermocouple Phidget 0 USB0::0x06C2::0x0046::751053::INSTR TEMP_1 thermocouple Phidget 1 USB0::0x06C2::0x0046::751053::INSTR UART uart SiLabs_CP210x 0001 USB0::0x10C4::0xEA60::0001::INSTR USB_0 usb Acroname_8Port 0 USB0::0x24FF::0x0013::807D0C12::INSTR USB_1 usb Acroname_8Port 1 USB0::0x24FF::0x0013::807D0C12::INSTR USB_2 usb Acroname_8Port 2 USB0::0x24FF::0x0013::807D0C12::INSTR USB_3 usb Acroname_8Port 3 USB0::0x24FF::0x0013::807D0C12::INSTR USB_4 usb Acroname_8Port 4 USB0::0x24FF::0x0013::807D0C12::INSTR USB_5 usb Acroname_8Port 5 USB0::0x24FF::0x0013::807D0C12::INSTR USB_6 usb Acroname_8Port 6 USB0::0x24FF::0x0013::807D0C12::INSTR USB_7 usb Acroname_8Port 7 USB0::0x24FF::0x0013::807D0C12::INSTR WEBCAM webcam Logitech_BRIO_HD /dev/video0 USB0::0x046D::0x085E::20786B34::INSTR ``` ## Automatic Net Creation When you plug an instrument into a Lager Box, Lager creates a default Net for each supported function. * A simple, single-function device like a power supply will create one `Supply` Net. * A multi-function device such as a LabJack T7 DAQ creates several Nets. It creates one each for `GPIO`, `ADC` and `DAC`, plus `I2C` and `SPI` when you configure them. * An I2C/SPI adapter supports both protocols. A Total Phase Aardvark, for example, creates an `I2C` Net and an `SPI` Net. Some instruments carry several channels of the same type, such as a 4-channel oscilloscope. You can assign and configure a Net for each channel. *** ## Modify or Assign Nets Using the TUI The interactive TUI (text user interface) assigns new Nets on a multi-channel instrument such as a LabJack or a PicoScope. It also renames an existing Net. To launch the Net TUI, run: ```bash theme={null} lager nets tui --box my-lager-box ``` ### Within the TUI, you can: * **Add** new Nets * **Rename** existing Nets * **Delete** unused Nets *** ## RS-232 Instruments (Manual Assignment) Some instruments have no USB control port. They connect through a USB-serial adapter instead, such as a Rigol DP711 power supply on its RS-232 port. The Lager Box sees only the adapter cable. It cannot tell which instrument is behind that cable, so nothing appears automatically. > **Rigol DP711 — crossover cable required.** The DP711's RS-232 port is > wired as DTE, the same as the RS232-to-USB adapter Rigol ships with it. > Connecting the two directly will **not** work — TX talks to TX and nothing > gets through. You must insert a **null-modem (crossover) cable or adapter** > between the DP711 and the USB-serial adapter so the TX/RX lines are > swapped. Without it the cable shows up in `lager nets assign --list` but the > supply never responds to commands. Tell the box what the cable is connected to, once per cable: ```bash theme={null} # See the unassigned USB-serial cables on the box lager nets assign --list --box my-lager-box # Assign the cable to the instrument — and create a supply net in one step lager nets assign Rigol_DP711 --serial 00000006 --as-net main_supply --box my-lager-box ``` The Net TUI (`lager nets tui`) offers the same flow under the **Assign Device** button. Pick the cable, pick the instrument, and name the net. After you assign it, the instrument appears in `lager instruments` and in the TUI like any auto-detected device. You add nets for it in the usual way. The box stores the assignment, which survives a reboot and a replug. See the [`nets assign` reference](/source/reference/cli/nets#assign) for port-pinned assignments, baud-rate overrides, and removal. *** ## Debug Nets For debug instruments (e.g., Segger J-Link, ST-Link), you must specify the target MCU when creating the net. This is easily done through the TUI, as it will prompt you to input the MCU type. > **Important:** The MCU type must match a valid target device supported by your debug probe. If the MCU type is not recognized, the debugger will not function correctly. *** ## Next Steps With your instruments configured, start controlling them: * **[Interacting With Nets](/source/getting-started/interacting-with-nets)** -- Send commands to your instruments via CLI or Python # Troubleshooting Source: https://docs.lagerdata.com/source/getting-started/troubleshooting Solutions for common Lager issues This page covers the most common problems in Lager, grouped by category. Each section gives the error or symptom, the likely cause, and the fix. *** ## Connection Issues **Cause:** Your computer cannot reach the Lager Box on the network. **Fix:** 1. Verify your VPN is connected: run `tailscale status` and confirm your Lager Box appears in the list. 2. Check the IP address is correct: run `lager boxes` to see your saved box IPs. 3. If using Tailscale, try `ping ` to verify network connectivity. 4. Ensure the Lager Box is powered on and connected to the network. **Cause:** The network path to the box works, but the Lager service on the box stopped. **Fix:** 1. If the Docker container on the box stopped, ask your administrator to restart it. 2. Run `lager hello --box ` to check the box's service status. 3. If you have SSH access, connect to the box and check Docker: `docker ps | grep lager`. 4. If nobody set the machine up as a Lager Box, see [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box). **Cause:** The network request leaves your computer but does not reach the box. A firewall or an incorrect IP usually causes this. **Fix:** 1. Double-check the IP address with `lager boxes`. 2. A box that moved or changed provisioning can have a new IP address. Check your Tailscale admin panel or your router DHCP table. 3. Try pinging the box: `ping `. **Cause:** The box lost power, lost network connectivity, or changed its IP address. **Fix:** 1. Verify the box is powered on. 2. Check your VPN connection: `tailscale status`. 3. If the box IP changed, update it: `lager boxes edit --name my-lager-box --ip `. 4. Run `lager hello --box ` to check box status. *** ## Instrument Detection **Cause:** No instruments are detected on the box's USB ports. **Fix:** 1. Verify instruments are physically connected via USB to the Lager Box (not to your laptop). 2. Check that USB cables are properly seated -- try a different cable or port. 3. Confirm that the Docker container runs with USB passthrough. Run `lager hello --box `. 4. Run `lager update --box ` to install the latest udev rules and drivers. **Cause:** The box does not recognize the instrument, or the instrument uses a different USB port. The drivers can also need an update. **Fix:** 1. Unplug and replug the instrument's USB cable. 2. Run `lager update --box ` to ensure udev rules are current. 3. Check that the instrument is powered on (some instruments need external power in addition to USB). 4. Verify the instrument model is [supported](/source/supported-instruments/supported-instruments). **Cause:** Another process (such as a TUI session or another CLI command) actively uses the instrument's USB connection. **Fix:** 1. Close any running TUI sessions (press `q` to exit). 2. Wait a moment, then retry. The previous command can still be running. 3. If the problem continues, the instrument handle is stuck. Run `lager update --box ` to restart the service. *** ## Power Supply Issues **Cause:** The output voltage or current went past the protection threshold you set. The supply shut off to protect your device. **Fix:** 1. Check the current state: `lager supply state --box `. 2. Clear the fault: `lager supply clear-ovp` or `lager supply clear-ocp`. 3. Adjust your protection thresholds if they are too tight, or investigate why the output exceeded the limit. 4. Re-enable the output: `lager supply enable --box `. **Cause:** The supply output is not enabled, or the load pulls the voltage down. **Fix:** 1. Verify the output is enabled: `lager supply state --box ` -- check that "Enabled" shows ON. 2. Confirm you set both the voltage and enabled the output (setting voltage alone does not enable it): ```bash theme={null} lager supply voltage 3.3 --yes --box lager supply enable --yes --box ``` 3. Check if OVP/OCP tripped (see above). *** ## Debug / Flash Issues **Cause:** The debug probe cannot communicate with the target MCU. **Fix:** 1. Verify the SWD/JTAG wiring between the debug probe and your DUT. 2. Ensure the DUT is powered (the debug probe does not always supply power). 3. Check that the MCU type in the debug net matches your actual device: `lager debug status --box `. 4. Try a lower SWD speed: `lager debug gdbserver --speed 100 --box `. **Cause:** A J-Link or pyOCD process from an earlier session is stuck. **Fix:** 1. Disconnect any existing session: `lager debug disconnect --box `. 2. Retry: `lager debug gdbserver --box `. 3. Check the debug probe health: `lager debug health --verbose --box `. *** ## Python Script Issues **Cause:** You run the script directly with `python` instead of `lager python`. **Fix:** The `lager` Python library is only available inside the Lager Box environment. Always run scripts with: ```bash theme={null} lager python my_script.py --box my-lager-box ``` Do **not** run `python my_script.py` directly on your laptop. **Cause:** The net name in your script does not match any net configured on the box. **Fix:** 1. List available nets: `lager nets --box `. 2. Check for typos in the net name. Net names are case-sensitive. 3. If the net doesn't exist, create it using `lager nets tui --box `. **Cause:** Two things opened a session against the same instrument. The hardware service on port 8080 owns an instrument's VISA session. A second `pyvisa` open against the same USB address races it and loses. Ordinary scripts do not reach this. A supply, scope, battery simulator, e-load or solar simulator is proxied to the hardware service, not opened locally. You reach the failure by importing a driver module directly. You also reach it by opening `pyvisa` yourself, in a script or a `docker exec`. **Fix:** 1. Drive the instrument through its net rather than its driver module: ```python theme={null} from lager import Net, NetType psu = Net.get("supply1", type=NetType.PowerSupply) psu.set_voltage(3.3) ``` 2. Check what already holds the address with `lager diagnose --box `. A VISA section that reports `REACHABLE (shared session)` means the hardware service holds it and skipped its own probe. See [`lager diagnose`](/source/reference/cli/diagnose). 3. To open the instrument yourself, take the same cross-process lock the drivers take. Direct-USB instruments are a different case: LabJack, USB-202, FT232H, Aardvark, Joulescope and PPK2. The execution service releases the hardware service's claims on those before it spawns your script. **Cause:** The script fails silently, or the script does not flush its output. **Fix:** 1. Add `print()` statements to confirm that the script executes. 2. Wrap your code in try/except to catch errors: ```python theme={null} try: # your code here except Exception as e: print(f"Error: {e}") ``` 3. Check stderr output -- errors from the box are shown in red in the terminal. *** ## Getting More Help If the solutions above don't resolve your issue: 1. **Check box logs:** `lager logs --box ` shows recent log output from the box's Docker container. 2. **Check box status:** `lager hello --box ` verifies the box is online and responsive. 3. **Open an issue:** Report problems on [GitHub](https://github.com/lagerdata/lager/issues) with your box name, the command you ran, and the error output. # Using Lager in CI Source: https://docs.lagerdata.com/source/getting-started/using-lager-in-ci Run hardware tests from GitHub Actions with the runner installed on the Lager Box ## 1. The location of the runner A Lager command must have a network connection to a box. There are two arrangements that give this connection. Your selection controls all the steps that follow. ### Arrangement A: the runner is on a different machine A different machine runs the job. The job connects to the box across the network. The job must install the CLI, log in, and get the address of the box. ```yaml theme={null} runs-on: [self-hosted, lager-bench] steps: - run: pip install lager-cli==0.43.0 - run: lager login "$AUTH_URL" --email "$EMAIL" --password "$PASSWORD" - run: lager boxes add --name BENCH-1 --ip "$BOX_IP" --user lagerdata --yes - run: lager python tests/hil --box BENCH-1 ``` This arrangement is correct if one runner controls more than one box. It is also correct if you keep your boxes off the CI network on purpose. This arrangement has three disadvantages: * Each job does the same preparation again. * The address of the box and the login data become repository secrets. * Two jobs on that runner can try to use the same bench at the same time. ### Arrangement B: the runner is on the box Install the GitHub Actions runner on the Lager Box. Give the runner one label. The label is the name of the box. ```yaml theme={null} runs-on: BENCH-1 env: LAGER_BOX: BENCH-1 steps: - uses: actions/checkout@v4 - run: lager python tests/hil ``` This is all the configuration that the job needs. This arrangement gives you four advantages: * **The label of the runner is the bench.** `runs-on: BENCH-1` and `--box BENCH-1` are the same text. There is no list to keep correct. A job cannot go to a runner that has no connection to the bench that the job needs. * **The runner makes the jobs sequential.** A self-hosted runner accepts one job at a time. Two different branches that use `BENCH-1` go into a queue. You do not add a `concurrency:` block to get this. * **You do not need secrets.** The CLI is on the box. The file `~/.lager` has the box in it. The gateway session is in the home directory of the runner account. A test system with this arrangement can operate with no Lager secrets. The only repository variables that it needs are the labels of the runners. * **There is no network connection between the runner and the box.** The commands go through the local interface. This arrangement has three disadvantages: * The box does the check-out and the artifact download. Thus the box is busy for all of the job. Build the firmware on a different machine. Refer to [Section 6](#6-how-to-put-the-firmware-on-the-dut). * All software that a workflow puts in the `PATH` of the box can control your bench. Give the box the same protection as a production machine. * One box does one job at a time. For more test jobs at the same time, you need more benches. The remainder of this document uses Arrangement B. *** ## 2. How to install the Actions runner on the box The steps that follow are the standard GitHub runner installation. The rules for the Lager Box control them. Do a check of the service account and the `PATH` on your first box. Then use these steps on all the boxes. The Lager Box has these requirements: * An x86-64 processor. * Ubuntu 22.04 or a subsequent version. * An IP address that other machines can find. * A login account with sudo permission. Do all the steps that follow **as the login account of the box**. This is the account that you gave to `lager install --user`. Do not install the runner as the root account. ### 2.1 Registration of the runner Get a registration token. In your repository, go to `Settings > Actions > Runners > New self-hosted runner`. Then do these steps on the box: ```bash theme={null} mkdir -p ~/actions-runner && cd ~/actions-runner curl -o actions-runner-linux-x64.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-x64-2.322.0.tar.gz tar xzf actions-runner-linux-x64.tar.gz ./config.sh \ --url https://github.com/my-org/my-firmware \ --token \ --name BENCH-1 \ --labels BENCH-1 \ --work _work \ --unattended \ --replace ``` Give the runner only one label. The label must be the name of the box. Then `runs-on: BENCH-1` selects that runner. `runs-on: [self-hosted, BENCH-1]` also selects it. Do not give two boxes the same label. If two boxes have the same label, a job can go to the wrong bench. That bench can lack the nets that the job needs. Then you must add a list in the YAML that connects each runner to its box. ### 2.2 Installation as a service ```bash theme={null} sudo ./svc.sh install "$USER" sudo ./svc.sh start sudo ./svc.sh status ``` `svc.sh install "$USER"` makes a systemd unit. The service operates as the account that you give to it. It does not operate as root. Give `$USER` in the command. The runner account and the Lager Box account must be the same account. If they are not the same, the job cannot read the `~/.lager` file of the box account. ### 2.3 A check of the job environment The runner gets its environment from the systemd unit. It does not get the environment of your login shell. The Lager installer puts a symbolic link to `lager` in `~/.local/bin`. A login shell finds that directory, but the service can fail to find it. Do this check in a workflow. Do not do it in an SSH session. ```yaml theme={null} - name: Runner sanity run: | echo "runner: $RUNNER_NAME" echo "user: $(id -un)" echo "arch: $(uname -m)" command -v lager || { echo "::error::lager not on PATH for the runner account"; exit 1; } lager --version ``` If the runner cannot find `lager`, do these steps: 1. Make the file `~/actions-runner/.env`. 2. Put this line in the file: `PATH=/home//.local/bin:/usr/local/bin:/usr/bin:/bin` 3. Start the service again. The runner reads this file when it starts. ### 2.4 Do this for each bench Each box has its own runner, its own name, and its own label. The boxes do not share these items. *** ## 3. How to prepare the runner account The runner account is the box account. Thus you do this preparation one time on the machine. You do not do it in each job. ### 3.1 The CLI ```bash theme={null} pip install --user 'lager-cli==0.43.0' lager --version ``` Obey these rules: * **Use Python 3.10 or a subsequent version.** The CLI needs it. * **Set the version of the CLI.** If you do not set the version, the version changes when a person makes an unrelated change to the machine. Set the version in one location. Change it only when you decide to change it. * **Keep the version of the CLI the same as the version of the box, or more recent.** The CLI compares the two versions with each command. It gives a warning if they are different. The command `lager boxes` shows each box as `current`, `needs update`, or `newer`. * **Always give a subcommand to `lager`.** If you give no subcommand, the CLI starts an interactive session. In CI, the job then continues until its time limit. ### 3.2 The list of boxes The CLI finds the value of `--box` in the `BOXES` section of `~/.lager`. This file is a JSON file. ```json theme={null} { "BOXES": { "BENCH-1": { "ip": "10.0.1.42", "user": "lagerdata", "version": "0.43.0" } }, "DEFAULTS": { "gateway_id": "BENCH-1" } } ``` To put this file on a new box, export it from a machine that has it: ```bash theme={null} # on your computer lager boxes export -o boxes.json # on the box, as the runner account lager boxes import boxes.json --merge --yes ``` You can also add one box directly: ```bash theme={null} lager boxes add --name BENCH-1 --ip 10.0.1.42 --user lagerdata --yes ``` `--user` is the SSH account of the box. The commands `lager update`, `lager logs`, `lager box-config` and `lager ssh` use it. It has no default value. ### 3.3 How the CLI selects the box The CLI uses this sequence. The first item has the highest priority. 1. The `--box` flag. 2. The `LAGER_BOX` environment variable. 3. `DEFAULTS.gateway_id` in `~/.lager`. 4. If the CLI finds no value, it gives an error. Set `LAGER_BOX` one time in the `env:` block of the job. Then do not use `--box` in the steps. Each workflow is then the same on each bench. To change the name of a box, you change one line. ```yaml theme={null} jobs: hil: runs-on: BENCH-1 env: LAGER_BOX: BENCH-1 ``` You can also put the name in a repository variable and use it two times: ```yaml theme={null} runs-on: ${{ vars.HIL_BENCH }} env: LAGER_BOX: ${{ vars.HIL_BENCH }} ``` Do this if you move the test suite to a different bench. Then you change one variable. You do not change each workflow. ### 3.4 Two problems with the configuration file * **`~/.lager` must be a file. It must not be a directory.** If other software makes `~/.lager` a directory, each CLI command on that machine fails. The commands on your computer continue to operate. Thus you can find this fault only on the box. * **Do not let a virtual environment hide the CLI.** The CLI gives a warning if the `lager` in the `PATH` is not the `lager` of the active environment. Do not ignore this warning. The two files can have different versions. `LAGER_CONFIG_FILE_DIR` sets a different directory for `~/.lager`. `LAGER_CONFIG_FILE_NAME` sets a different name for the file. *** ## 4. Your first workflow ```yaml theme={null} name: HIL on: push: branches: [main] workflow_dispatch: permissions: contents: read jobs: hil: name: Hardware tests runs-on: BENCH-1 timeout-minutes: 30 env: LAGER_BOX: BENCH-1 steps: - uses: actions/checkout@v4 - name: Bench reachable run: | for attempt in 1 2 3; do if lager hello; then exit 0; fi echo "::warning::lager hello failed (attempt ${attempt}/3); retrying" sleep 5 done echo "::error::bench unreachable after 3 attempts" exit 1 - name: Run the suite run: lager python tests/hil ``` Three parts of this workflow need more data. **`lager hello` shows only that the box has a connection.** It does not show that the box is serviceable. The command gives exit code 0 also when the box sends an HTTP error. Only a connection failure or a timeout gives a different exit code. But do not use exit code 0 as proof that the box is fully serviceable. Do the command again after a failure. On a box behind a gateway, the first command after a new login can fail one time. Refer to [Section 13](#13-how-to-log-in-to-a-gateway-from-ci). **`lager python` sends your script to the box. The box runs the script.** The argument is a file or a directory. The CLI uploads a directory as a module. The script operates in the Python container of the box. It does not operate on the runner. Refer to [Section 7](#7-how-to-make-sure-that-the-box-has-the-code-under-test) and [Section 15](#15-troubleshooting). **`timeout-minutes` is the maximum time that the job can hold the bench.** A job that stops in an unusual condition holds the bench and its lock until GitHub stops the job. Set a time that is acceptable to you. Do not use the default value of 360 minutes. ### How to give arguments to your test The CLI reads all the text before `--`. Your script reads all the text after `--`. ```yaml theme={null} - run: lager python tests/hil/charge -- --target-soc 80 --timeout-min 45 ``` ### How to send other files `lager python` uploads the script and its directory. It does not upload other files. To send a firmware image, a debug script, or a table of limits, use `--add-file`. The file goes adjacent to the script. Use its base name. ```yaml theme={null} - run: | lager python tests/hil/flash \ --add-file ./artifacts/firmware.hex \ --add-file tools/debug/target.script \ -- --image firmware.hex ``` ### How to get files back ```yaml theme={null} - run: lager python tests/hil --download results.json --allow-overwrite ``` The CLI downloads the files after the script stops. It does not download them during the test. *** ## 5. How to share one bench between jobs A bench is one item of physical equipment. Three independent mechanisms keep the jobs separate. Each mechanism has a different purpose. Read about all three before you use one of them. ### Mechanism 1: the runner A self-hosted runner accepts one job at a time. Use one runner for each box. Then the runner makes all the jobs for that bench sequential. This applies across branches, across workflows, and across repositories. You get this mechanism with no configuration. In most conditions it is enough. This mechanism puts the jobs in a queue. It does not remove old jobs. Five pushes to a branch make five jobs. The bench does all five. ### Mechanism 2: workflow concurrency Use `concurrency:` when a new job must replace an older job. ```yaml theme={null} concurrency: group: hil-BENCH-1-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true ``` Two details are important. **Put `concurrency:` on the job that holds the bench.** Do not put it at workflow level. A workflow-level group does not go into a workflow that this workflow calls. Thus the job that uses the hardware is not in the group. **Use the pull request number as the key. Use `run_id` as the alternative.** A key such as `github.head_ref || github.ref_name` gives the same text for two different conditions. A `workflow_dispatch` can run on a branch that also has a pull request. It then gives the same text as that pull request. Then the manual job and the pull request job cancel each other. `run_id` is different for each run. Thus each non-pull-request run gets its own group. Use `cancel-in-progress: true` only if you also clean the bench. If GitHub cancels a job during a test, the bench can stay in an unsafe condition. A power supply can stay on. A battery simulator can stay at a dangerous voltage. A debug buffer can stay isolated. Use the cleanup step in [Section 8](#8-how-to-make-the-bench-safe-after-a-job). For nightly jobs and post-merge jobs, use `cancel-in-progress: false`. Let a job that does a measurement continue to the end. ### Mechanism 3: the Lager lock Lager locks the box automatically. There is no `--lock` flag, no `--lock-wait` flag, and no `--no-lock` flag. Environment variables control this function. **The identity of the lock holder is different in CI.** In GitHub Actions, the identity has this format: ``` ci:github:#-/@: ``` The text always ends with `:`. Thus two matrix jobs cannot get the same identity. `lager boxes` shows the identity in a format that is easy to read. **The behavior after a collision is different in CI.** Lager finds CI from the `CI=true` variable and a variable such as `GITHUB_RUN_ID`. | Environment | Behavior after a collision | | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | | CI | The command waits. It examines the lock each 2 seconds. The maximum time is `LAGER_LOCK_WAIT`. The default value is 1800 seconds. | | A user computer | The command gives an error and stops immediately. | GitHub Actions sets `CI` and `GITHUB_RUN_ID` for each `run:` step. Thus jobs wait automatically. A `lager` command on the same box that does not operate in an Actions step is not in CI. This applies to cron, to systemd, and to an SSH session. Such a command stops immediately after a collision. A maintenance script on the box thus fails each time that CI operates. **A lock collision gives exit code 1.** All other errors also give exit code 1. To find a lock collision, look for the text `is locked by` in the error output. The lock has a maximum life. The default value is 1800 seconds. The CLI sends a heartbeat each 60 seconds. The heartbeat makes the maximum life start again. Thus the maximum life does not limit the length of your test. It limits how long a lock stays after the CLI stops in an unusual condition. Add a step that releases the lock: ```yaml theme={null} - name: Release the bench lock if: always() run: lager boxes unlock --box "$LAGER_BOX" 2>/dev/null || true ``` Do not use `lager boxes unlock --force` in a job. This command releases a lock that a different person holds. If a lock stays after your job ends, a different person or a different job holds it. If you release that lock, you take the bench from that person. Let the lock end at its maximum life. ### Commands that use the lock These commands get the lock automatically: * `lager python` * the instrument commands `adc`, `dac`, `gpi`, `gpo`, `thermocouple`, `watt`, `energy`, `scope`, `logic` * the communication commands `spi`, `i2c`, `uart`, `usb`, `wifi`, `ble`, `blufi`, `router` * the power commands `supply`, `battery`, `eload`, `solar` * the equipment commands `debug`, `arm`, `webcam` * the administration commands `install`, `uninstall`, `update`, `install-wheel` These commands do not use the lock: * `lager hello`, `lager boxes`, `lager instruments` * `lager nets` and all its subcommands * `lager defaults`, `lager logs`, `lager binaries`, `lager dut` * `lager ssh`, `lager exec`, `lager devenv` * `lager login`, `lager logout`, `lager whoami` Because of this difference, a `lager hello` job and a `lager nets state` step are safe when a different job holds the bench. ### Environment variables for the lock | Variable | Function | | ------------------------- | ------------------------------------------------------------------- | | `LAGER_LOCK_WAIT` | Seconds to wait after a collision. CI default 1800. User default 0. | | `LAGER_LOCK_TTL` | Maximum life of the lock. Use `none` for a lock with no limit. | | `LAGER_LOCK_HEARTBEAT` | Seconds between two heartbeats. Default 60. | | `LAGER_LOCK_HOLDER` | A different identity for the lock holder. | | `LAGER_AUTO_LOCK_DISABLE` | Set to `1` to stop the automatic lock. | Use `LAGER_AUTO_LOCK_DISABLE` only on a bench that one person uses. Do not use it to prevent a collision. `lager python --detach` gives the lock to the box. The box holds the lock until the detached job ends. This is after your workflow step ends. *** ## 6. How to put the firmware on the DUT ### Build the firmware on a different machine Do not build your firmware on the bench runner. A build uses the bench for all of its length, but it does not use the hardware. The bench is your most limited resource. Divide the work. Build on a GitHub-hosted runner or a general-purpose self-hosted runner. Upload the image as an artifact. Then the bench job downloads it. ```yaml theme={null} jobs: build: runs-on: ubuntu-latest outputs: artifact: ${{ steps.meta.outputs.name }} steps: - uses: actions/checkout@v4 - run: ./build.sh - id: meta run: echo "name=firmware-${{ github.sha }}" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@v4 with: name: firmware-${{ github.sha }} path: build/firmware.hex if-no-files-found: error hil: needs: build runs-on: BENCH-1 env: LAGER_BOX: BENCH-1 steps: - uses: actions/checkout@v4 with: ref: ${{ github.sha }} - uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.artifact }} path: ./firmware - run: lager debug SWD flash --hex ./firmware/firmware.hex - run: lager debug SWD reset - run: lager python tests/hil ``` ### Build with `lager exec` If the build commands are in your `.lager` file, `lager exec` runs them. `lager exec` has two behaviors: * On a developer machine, it starts a container from the DEVENV `image`. * In a CI job that is already in a container, it runs the command directly. The job is in the image, so there is nothing to start. A job container also has no Docker. To get the second behavior, give the image to the job with `container:`: ```yaml theme={null} jobs: build: runs-on: ubuntu-latest container: image: ghcr.io/example/devenv:latest steps: - uses: actions/checkout@v4 - run: lager exec build-ci ``` GitHub Actions, GitLab CI, Drone, and Bitbucket Pipelines get the second behavior. A Jenkins agent, and a runner with no `container:`, get the first. The command runs in the directory of the check-out. It does not run in `mount_dir`, because there is no bind-mount. These options do nothing when the command runs directly: `--mount`, `--volume`, `--user`, and `--group`. The CLI gives a warning for each one. These `.lager` keys also do nothing: `network`, `ports`, `platform`, `macaddr`, `hostname`, `volumes`, `user`, and `group`. The `--env` option and the `environment` key operate as usual. To start a container from a CI job, set `LAGER_CI_OVERRIDE=1`. The job must have Docker. ### Set the check-out to `github.sha` On a `pull_request` event, the default check-out uses the variable reference `refs/pull//merge`. If a push happens when the bench job starts, the job uses new test code with old firmware. The job then reports this result for the first commit. To prevent this, give `ref: ${{ github.sha }}`. Then the test code and the firmware come from the same commit. ### Do a check that the flash operation was successful A programmer tool can report a connection failure and still give exit code 0. The device is then erased but not programmed. The tests fail subsequently, and the cause is not clear. Do not use the exit code only. Record the output and look for the failure text of your tool. ```yaml theme={null} - name: Flash run: | set -o pipefail log=$(mktemp) lager debug SWD flash --hex ./firmware/firmware.hex 2>&1 | tee "$log" if grep -qE 'Cannot power up debug port|Could not connect to the target device' "$log"; then echo "::error title=flash::the programmer reported a fatal error but the command returned success. The DUT is likely erased but not reprogrammed." exit 1 fi lager debug SWD reset ``` Change the text pattern to the failure text of your programmer tool. The rule is more important than the example: a flash step must do a check of its own result. ### How to flash from a test Some test suites program the device in their first test. They use the Python API on the box. They do not use a separate CLI step. This is also correct. Sometimes it is better. The test that programs the device is also the test that shows the program operation is correct. The Python `DebugNet` methods give the output of the programmer as text. They do not give an error when the programmer reports a failure. Thus the test must do a check of its own result. ### The debug subcommands ``` lager debug [NET] gdbserver # attach; --rtt to stream RTT lager debug [NET] flash # --hex / --elf / --bin ADDR lager debug [NET] erase lager debug [NET] reset lager debug [NET] memrd lager debug [NET] status lager debug [NET] health lager debug [NET] disconnect ``` There is no separate `connect` command. The commands `flash`, `reset` and `erase` make the connection. There is no `lager debug rtt` command. For RTT, use `gdbserver --rtt`. *** ## 7. How to make sure that the box has the code under test This is the most frequent cause of incorrect CI results. It is a result of the operation of `lager python`. **`lager python` sends your script to the box. The box runs the script.** The runner gives the script. The box gives the Python environment, the instrument drivers, the net definitions, and the Lager Box software. Thus a check-out of your branch does not test the software on the box. The box continues to use the version of its last installation. If your repository has only test scripts, this is not a problem. The scripts come from the check-out. If your CI also tests software that operates on the box, do a check of the box version: ```yaml theme={null} - name: Verify the box is running the ref under test run: | rc=0 out=$(lager update --check --box "$LAGER_BOX" --version "$GITHUB_SHA" 2>&1) || rc=$? echo "$out" if [ "$rc" -gt 1 ]; then echo "::error title=box state::could not determine box state (exit ${rc})" exit "$rc" fi if [ "$rc" -eq 1 ]; then echo "::error title=box version::the box is not running ${GITHUB_SHA}" exit 1 fi ``` `lager update --check` is a dry run. It reports the changes that it will make. It does not change the box. Its exit code has three values: | Exit code | Meaning | | --------- | --------------------------------------------------------------------- | | 0 | The box is correct. No change is necessary. | | 1 | An update is necessary. The code, the dependencies, or the container. | | 2 | The command cannot find the condition of the box. | The difference between 1 and 2 is important. Exit code 1 tells you that the box is old. Exit code 2 tells you that the check did not operate. Exit code 2 gives you no data about the box. To change a box to a specified version: ```bash theme={null} lager update --box BENCH-1 --version main --yes lager update --box BENCH-1 --version v0.43.0 --yes ``` `--version` accepts a release tag or a version number, with or without an initial `v`. It also accepts a branch name or a full 40-character commit SHA. The default value is `main`. The other flags are `--force`, `--pull`, `--no-pull`, `--verbose` and `--yes`. `lager update` changes one box for each command. Use a loop in your shell for more boxes. ### How to install the Python dependencies of the box Your test scripts operate in the container of the box. Thus you install their dependencies on the box. Do not install them on the runner. Use the box configuration. It stays after a container restart and after a box update. ```bash theme={null} lager box-config pip add pyserial rich --box BENCH-1 lager box-config pip list --box BENCH-1 ``` Export the configuration and import it to make each bench the same: ```bash theme={null} lager box-config export --box BENCH-1 -o bench-config.json lager box-config import bench-config.json --box BENCH-2 ``` Put that file in your repository. It is the only record of the necessary contents of your benches. *** ## 8. How to make the bench safe after a job A HIL job that stops during a test does not only leave files. It leaves the **hardware** in the condition of the test. A power supply can stay on at an incorrect voltage. A load can continue to take current. A heater can stay on. An enable signal can stay high. The next job gets this condition. The next person at the bench also gets it. Put two steps at the two ends of each hardware job. ### Preparation, before all other steps ```yaml theme={null} - name: Bench bring-up run: ./tools/bench.sh bring-up ``` Make this a separate step. Do not put it in the flash step. A cold bench has the supply off and the enable signals low. Some instruments keep their output path open until a session sets them. Then the DUT has no power. A DUT with no power gives this message at the **flash** step: "cannot connect to the target". This message looks like a debugger failure. With a separate step, the step that failed gives the correct cause. ### Cleanup, after a cancel or a failure ```yaml theme={null} - name: Bench cleanup if: cancelled() || failure() run: | exec < /dev/null # any interactive prompt sees EOF instead of hanging rc=0 ./tools/bench.sh bring-up --recover || rc=$? lager debug SWD reset || rc=$? if [ "$rc" -ne 0 ]; then echo "::error title=cleanup::exited ${rc}; ${LAGER_BOX} may be unsafe for the next job" exit 1 fi ``` Obey these rules for the cleanup step: * **Use `if: cancelled() || failure()`. Do not use `if: always()`.** A job that is successful must end with its own procedure. A cleanup step that also operates after a successful job hides the faults in that procedure. * **Close the standard input.** If a subcommand asks a `[y/N]` question, the job continues until its time limit. * **Continue after a failure.** An unserviceable instrument must not prevent the remainder of the cleanup. * **Set the equipment to a safe condition. Do not release the lock.** The job that got the lock releases it. If a lock stays after that, a different person holds it. * **Do not use privileges.** The runner account has little sudo permission, or none. All cleanup commands must operate with no password. ### Make sure that a cancel signal goes to your test If a shell script starts your test, a `SIGTERM` from the runner goes to the shell. The shell does not send it to the test. The test continues until GitHub stops the job. Thus the test operates the hardware after you asked it to stop. Use `exec` to replace the shell with the test: ```yaml theme={null} - run: exec ./tools/run-suite.sh --box "$LAGER_BOX" ``` Then the signal goes to the correct process. *** ## 9. How to find the difference between a bench failure and a firmware failure A HIL test suite must find faults. It must also be correct when it did not test anything. These failures are not firmware faults: * A debug probe that did not connect to the USB bus. * A debug session that did not start. * A box that was not available. It is correct to do these tests again. An incorrect device identifier, or a device that does not start, **is** a firmware fault. If you do that test again, you hide the fault that you made the bench to find. Put this difference in the exit code. ### The rule Use this rule in your tests: | Exit code | Meaning | Do the test again? | | --------- | -------------------------------------------------------------------------------------------------- | ------------------ | | **0** | The test is successful. | Not applicable | | **1** | Device failure. Incorrect identifier, incorrect image, no start, or a measurement out of limits. | **No** | | **2** | Equipment failure. The probe, the net, the connection or the bench preparation prevented the test. | **Yes** | Lager does not make this rule. Your test scripts make it. Your CI uses it. `lager python` gives the exit code of your script with no change. This is why the rule operates. `lager python` can also give these exit codes: | Exit code | Meaning | | --------- | ------------------------------------------------- | | 124 | The `--timeout` time ended. The CLI sent SIGTERM. | | 137 | The `--timeout` time ended. The CLI sent SIGKILL. | | 255 | The CLI could not get the exit code from the box. | | 130 | A person or a signal stopped the command. | Put 124, 137 and 255 in the equipment-failure group. ### A script that does the test again Put this script in `tools/retry-hil.sh`. It does the test again only after an equipment failure. It removes the power from the probe and the DUT between two attempts. It also changes a test that does not stop into a failure that it can do again. ```bash theme={null} #!/bin/bash # # Do a HIL test again. Set the bench to a known condition between attempts. # # retry-hil.sh -- lager python tests/hil/flash --add-file firmware.hex # # Exit codes: 0 success / 1 device failure / 2 equipment failure. # The script does the test again only after an equipment failure. # The exit code of the script is the exit code of the last attempt. # # Environment variables: # LAGER_BOX the box (necessary) # HIL_RETRY_ATTEMPTS number of attempts, default 3 # HIL_ATTEMPT_TIMEOUT seconds for each attempt, default 300 (0 = no limit) # HIL_PROBE_NET USB net of the debug probe, default USB_DEBUG # HIL_PROBE_SETTLE seconds to wait after you set the probe on, default 8 # HIL_POWER_CYCLE_DUT also remove the power from the DUT, 1/0, default 1 # HIL_DUT_VBUS_NET USB net of the DUT, default USB_CHARGE # HIL_DUT_POWER_NET supply or battery net of the DUT, default BATT # HIL_DUT_SETTLE seconds to wait after the DUT starts, default 3 set -u [ "${1:-}" = "--" ] && shift if [ "$#" -eq 0 ]; then echo "retry-hil.sh: no command given" >&2 exit 2 fi BOX="${LAGER_BOX:?retry-hil.sh: LAGER_BOX must be set}" ATTEMPTS="${HIL_RETRY_ATTEMPTS:-3}" ATTEMPT_TIMEOUT="${HIL_ATTEMPT_TIMEOUT:-300}" PROBE_NET="${HIL_PROBE_NET:-USB_DEBUG}" PROBE_SETTLE="${HIL_PROBE_SETTLE:-8}" POWER_CYCLE_DUT="${HIL_POWER_CYCLE_DUT:-1}" DUT_VBUS_NET="${HIL_DUT_VBUS_NET:-USB_CHARGE}" DUT_POWER_NET="${HIL_DUT_POWER_NET:-BATT}" DUT_SETTLE="${HIL_DUT_SETTLE:-3}" # Put each attempt in `timeout`. Then a test that does not stop gives exit code # 124. The script can do that test again. If you do not do this, the test # continues until the time limit of the job. if [ "$ATTEMPT_TIMEOUT" -gt 0 ] 2>/dev/null && command -v timeout >/dev/null 2>&1; then run_attempt() { timeout "$ATTEMPT_TIMEOUT" "$@"; } else run_attempt() { "$@"; } fi # Use `disable` and then `enable`. Do not use `toggle`. The probe must be on at # the end. This is correct for all conditions that the failed attempt made. power_cycle_probe() { echo " - power-cycling ${PROBE_NET}" >&2 lager usb "$PROBE_NET" disable --box "$BOX" || true sleep 2 lager usb "$PROBE_NET" enable --box "$BOX" || true sleep "$PROBE_SETTLE" } # A new connection of the probe cannot start a device that is asleep. Only a # removal of the board power can start it. Set VBUS on last. Then the board # starts with VBUS present. Each command can fail: a net that this bench does # not have does nothing. power_cycle_dut() { [ "$POWER_CYCLE_DUT" = "1" ] || return 0 echo " - power-cycling the DUT" >&2 lager usb "$DUT_VBUS_NET" disable --box "$BOX" >/dev/null 2>&1 || true lager supply "$DUT_POWER_NET" disable --yes --box "$BOX" >/dev/null 2>&1 \ || lager battery "$DUT_POWER_NET" disable --yes --box "$BOX" >/dev/null 2>&1 || true sleep 2 lager supply "$DUT_POWER_NET" enable --yes --box "$BOX" >/dev/null 2>&1 \ || lager battery "$DUT_POWER_NET" enable --yes --box "$BOX" >/dev/null 2>&1 || true lager usb "$DUT_VBUS_NET" enable --box "$BOX" >/dev/null 2>&1 || true sleep "$DUT_SETTLE" } # A box connection failure gives exit code 1. A device failure gives the same # exit code. But a connection failure is an equipment failure. Find it in the # message. Keep the pattern small. Then the script cannot hide a device failure. CONN_FAIL_RE='Timed out connecting to the box|did not respond in time|Failed to connect|Connection refused|Could not connect' # A lock collision also gives exit code 1. It is also not a device failure. LOCK_RE='is locked by' out="$(mktemp "${TMPDIR:-/tmp}/retry-hil.XXXXXX")" trap 'rm -f "$out"' EXIT rc=2 for attempt in $(seq 1 "$ATTEMPTS"); do echo "=== attempt ${attempt}/${ATTEMPTS}: $* ===" >&2 run_attempt "$@" 2>&1 | tee "$out" rc=${PIPESTATUS[0]} [ "$rc" -eq 0 ] && exit 0 if [ "$rc" -eq 1 ]; then if grep -qiE "$CONN_FAIL_RE|$LOCK_RE" "$out"; then echo "::warning::exit 1 but the output shows a box connection or lock failure; treating as infrastructure" >&2 else echo "::error::device failure (exit 1); not retrying" >&2 exit 1 fi fi if [ "$attempt" -lt "$ATTEMPTS" ]; then echo "::warning::exit ${rc} (infrastructure) on attempt ${attempt}/${ATTEMPTS}; recovering and retrying" >&2 power_cycle_probe power_cycle_dut fi done echo "::error::still failing (exit ${rc}) after ${ATTEMPTS} attempts" >&2 exit "$rc" ``` Use the script for each hardware step: ```yaml theme={null} - name: Flash and verify run: | bash tools/retry-hil.sh -- \ lager python tests/hil/flash --add-file ./firmware/firmware.hex ``` Two parts of the script are important. The `timeout` command changes a test that does not stop into a failure that the script can do again. The text patterns change an exit code 1 into an equipment failure. This is necessary because a box that was not available gives the exit code of a device failure. *** ## 10. Net names ### The box holds the nets. Your repository does not. The Lager Box holds the net definitions. They stay after a restart. Your repository cannot make them. Your repository can only tell which nets it needs. It can also give a clear failure when a bench does not have them. This division is correct. But it makes the bench configuration difficult to examine. Two methods help. **Make the nets from a file in the repository.** `lager nets add-batch` reads a JSON file of net definitions: ```bash theme={null} lager nets add-batch bench/nets.json --box BENCH-1 ``` Keep `bench/nets.json` in the repository. Then you can build the bench again. If you do not do this, one person configures the bench one time by hand. **Make a list of the nets in the job.** `lager nets state --json` gives data that a program can read. It does not use the lock. Thus a preparation step can make sure that the bench has the necessary nets. The step can give the name of the net that is not present. Without this step, a test fails subsequently with a Python error. ### Give each net a name that tells its function A net has these fields: `name`, `role`, `instrument`, `channel` and `address`. The **role** is the type of the net. Examples are `usb`, `gpio`, `uart`, `debug`, `power-supply`, `battery` and `adc`. The box keeps one default net for each role. There is no field for the function. Thus when a bench has two nets with the same role, **only the name tells the function of each net**. Two USB ports both have the role `usb`. Only the name tells which port charges the device and which port supplies the debug probe. Use these rules: 1. **For a role with more than one net, put the role first and the function second.** Examples are `USB_CHARGE`, `USB_DEBUG`, `UART_CONSOLE`, `ADC_VBUS` and `GPIO_NRST`. The name and the role must agree. Then you can find an incorrect connection. 2. **Give the power nets the name of the instrument, not the function.** Use `BATT` for a battery-simulator net and `SUPPLY` for a programmable-supply net. Then the role and the name give the same data on purpose. 3. **For a role with only one net, use the bare name.** Use `SWD` for the one debug probe and `UART` for the one console. These roles usually need the rules: * `usb`. The hub can only set a port on or off. Thus only the name gives the function of the port. * `gpio`. Each pin has the role `gpio`. * `uart`. * The measurement roles `adc`, `dac`, `scope`, `logic`, `thermocouple` and `watt-meter`. The advantage is large. A test can find the charge port on each bench that obeys the rules. It does not need a configuration for each bench. Thus a second bench is easy to add. ### Do not change a shared net from CI `lager nets set-script` changes the configuration of the net **for all users**. A CI job that sets a debug script leaves the bench in that condition. The next person can need a different script. Send the script with the job. Use it only for that job: ```yaml theme={null} - run: | lager python tests/hil/flash \ --add-file tools/debug/halt-first.script \ -- --script halt-first.script ``` Do the same for all other changes. **A CI job must leave the net configuration of the bench in its initial condition.** ### Net commands ```bash theme={null} lager nets --box BENCH-1 # list (no `list` subcommand) lager nets show USB_CHARGE --box BENCH-1 --json lager nets state --box BENCH-1 --json # machine-readable inventory lager nets add NAME ROLE CHANNEL ADDRESS --box BENCH-1 lager nets add-batch nets.json --box BENCH-1 lager nets add-all --box BENCH-1 --yes # auto-generate from connected instruments lager nets delete NAME ROLE --box BENCH-1 --yes lager nets describe NAME -p "what it is for" --box BENCH-1 ``` The command is `lager nets` with an `s`. The subcommand is `delete`, not `remove`. These commands do not use the lock. *** ## 11. How to use more than one bench One bench does not need this section. More than one bench needs a method to give three answers in code: * Which benches are present. * What each bench can do. * Which tests each bench can do. ### Declare the function of a bench, not its identity Put the benches into **roles**. A role gives the nets that a bench must have. It also gives each function that is not one net. `bench/roles.toml`: ```toml theme={null} # Each role declares the nets that a bench of that role must have. The # `capabilities` field gives the bench functions that are not one net. Two # benches can give the same function with different equipment. A test that # needs the function does not need to know the equipment. [standard] nets = ["UART", "SWD", "USB_CHARGE", "USB_DEBUG", "BATT"] [power] nets = ["UART", "SWD", "USB_CHARGE", "USB_DEBUG", "BATT"] capabilities = ["current_measurement"] [supply-fed] # A bench supply gives the battery rail. A charger does not. Thus there is no # charge port. A test that charges the DUT must get the same condition with a # different method. nets = ["UART", "SWD", "USB_DEBUG", "SUPPLY"] ``` A test declares its requirements. The net options that it accepts give the necessary nets. A module-level statement such as `REQUIRED_CAPABILITIES = ["current_measurement"]` gives the other requirements. Then the test runner sends the test only to a role that has them. The result for each test on each bench: | Condition | Result | | ---------------------------------------------------- | ----------------------------------------- | | This role does not have the necessary function. | **N/A** — a different role does the test. | | This role does not have the necessary net. | **N/A** | | The quarantine list of this bench includes the test. | **QUARANTINED** | | The role has the net but the bench does not. | **FAIL** — the role is not correct. | | All requirements are satisfied. | **RUN** | The difference between the last two conditions is important. With only PASS and FAIL, two different results look the same. One is "this test is not applicable here". The other is "this bench is unserviceable". ### One file for each bench `bench/boxes/BENCH-1.yml`: ```yaml theme={null} role: standard enabled: true quarantine: - reason: "BENCH-1's supply collapses under load; the DUT browns out mid-test" nets: [SUPPLY] - reason: "actuator strikes too softly to register on this bench" tests: [gesture_stress, double_tap] ``` `enabled: false` removes a bench from the test system. This is a change of one line that a person can examine. It is not a change to a workflow file. The quarantine list stops the incorrect results of an unserviceable bench. No person disables the test for all benches. ### Make the matrix ```yaml theme={null} jobs: matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.gen.outputs.matrix }} empty: ${{ steps.gen.outputs.empty }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.sha }} # pin: the fleet is the one this commit declares - id: gen run: | set -euo pipefail rows=() for f in bench/boxes/*.yml; do name=$(basename "$f" .yml) enabled=$(yq -r '.enabled' "$f") [ "$enabled" = "true" ] || continue role=$(yq -r '.role' "$f") rows+=("$(jq -nc --arg n "$name" --arg r "$role" '{name:$n, role:$r}')") done if [ "${#rows[@]}" -eq 0 ]; then echo "empty=true" >> "$GITHUB_OUTPUT" echo "matrix={\"include\":[]}" >> "$GITHUB_OUTPUT" exit 0 fi echo "empty=false" >> "$GITHUB_OUTPUT" printf '%s\n' "${rows[@]}" | jq -sc '{include: .}' \ | sed 's/^/matrix=/' >> "$GITHUB_OUTPUT" validate: runs-on: ubuntu-latest needs: matrix steps: - run: | if [ "${{ needs.matrix.outputs.empty }}" = "true" ]; then echo "::error title=no benches::no enabled entries in bench/boxes/." exit 1 fi hil: needs: [build, matrix] if: needs.matrix.outputs.empty != 'true' name: ${{ matrix.name }} (${{ matrix.role }}) runs-on: ${{ matrix.name }} timeout-minutes: 90 strategy: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} env: LAGER_BOX: ${{ matrix.name }} concurrency: group: hil-${{ matrix.name }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true steps: - uses: actions/checkout@v4 with: ref: ${{ github.sha }} # ... download firmware, flash, run suite, clean up ``` Four details are important: * **Use `fail-fast: false`.** One unserviceable bench must not cancel the other benches. You need the result of each bench. * **Add the `validate` job.** An empty matrix makes zero jobs, and the workflow is then successful. A separate job that fails makes the difference clear. Then "no bench did a test" is not the same as "each test was successful". * **Give the job a correct name.** GitHub uses `/` to divide the parts of a job name. Some displays show only the last part. Thus `BENCH-1 / standard` becomes `standard`, and you lose the name of the bench. Put both names in one part with parentheses. * **Use one concurrency group for each bench.** Then different benches operate at the same time. A new job replaces an older job on the same bench. ### Use a gate job for the result of all benches Each bench reports only the tests that it did. A test that is `N/A` on **each** bench gives a green result and no test coverage. This happens with a new test that no role accepts. It also happens when no role has the necessary net. Add a gate job on a GitHub-hosted runner. The job collects the results of all the benches. ```yaml theme={null} gate: name: HIL Gate runs-on: ubuntu-latest needs: [matrix, validate, hil] if: always() steps: - name: No bench failed run: | r='${{ needs.hil.result }}' if [ "$r" = "failure" ] || [ "$r" = "cancelled" ]; then echo "::error::one or more bench jobs failed or were cancelled" exit 1 fi - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 continue-on-error: true with: pattern: hil-results-* path: ./results - name: Every test passed somewhere run: python3 tools/hil_coverage.py --results ./results --tests tests/hil ``` Make this gate job the necessary status check for branch protection. Do not use the bench jobs. The set of bench jobs changes when you enable or disable a bench. A necessary check that is not always present is worse than no check. Workflow annotations have one line. They cannot show a table. Thus write the results of all the benches to `$GITHUB_STEP_SUMMARY` from the gate job. Use one row for each test and one column for each bench. Stop the summary of each bench. Then there is one location to look at. ### Report the tests that you did not do Your test system can limit its own coverage. Examples are a quarantine, a disabled bench, a cache of previous results, and a limit on the number of tests. Write a message for each one. If you do not, the report looks the same as a report of full coverage. A HIL system must not do this. *** ## 12. Test results after a retry The GitHub function "Re-run failed jobs" erases the work directory. If your test suite does not do the tests that were successful before, the data about those tests must stay. **`actions/cache` cannot do this.** The cache from attempt N has the current `run_id` as part of its key. Attempt N+1 of the **same** run cannot find it. Cache keys do not change, thus `restore-keys` also cannot find it. **The artifact from the last attempt can do this.** Upload the results after each attempt. Use `overwrite: true`. Download them when `github.run_attempt` is more than 1. ```yaml theme={null} - name: Restore results from the previous attempt if: github.run_attempt > 1 continue-on-error: true uses: actions/download-artifact@v4 with: name: hil-results-${{ matrix.name }} path: .test_state # ... run the suite ... - name: Stage results if: always() id: stage run: | stage="${RUNNER_TEMP}/hil-state" rm -rf "$stage" && mkdir -p "$stage" for f in results.json meta.json; do [ -f ".test_state/$f" ] && cp ".test_state/$f" "$stage/$f" done echo "dir=$stage" >> "$GITHUB_OUTPUT" - name: Upload results if: always() uses: actions/upload-artifact@v4 with: name: hil-results-${{ matrix.name }} path: ${{ steps.stage.outputs.dir }} overwrite: true retention-days: 7 ``` Two conditions can give you incorrect results. **Use `$RUNNER_TEMP`. Do not use `/tmp`.** On a self-hosted runner, the contents of `/tmp` stay between two jobs. Thus an old `results.json` from a previous run can go into the artifact. This also happens when this run made no results. The next attempt then does not do the tests, because the old results show that they were successful. GitHub erases `$RUNNER_TEMP` at the start and at the end of each job. **Put the identity of the firmware with the results.** Use a version text or a content hash. Erase all the results when this identity is not the same as the firmware of this attempt. If you do not do this, an attempt with a different image reports the results of the previous image. Upload the results also after a failure. Use `if: always()`. The next attempt and the gate job need the results of a job that stopped during a test. *** ## 13. How to log in to a gateway from CI Boxes behind an access gateway need a session. Boxes with no gateway need no session. If your boxes have no gateway, do not read this section. The runner is on the box. Thus you log in one time on the machine. You do not log in in each job. The CLI keeps the session in the home directory of the runner account. The CLI also makes the session current again automatically. ```bash theme={null} # one time, as the runner account lager login https://gateway.example.com lager whoami ``` Some conditions need a login in the job. Examples are a runner on a different machine, or a box that you install again at intervals. For these conditions, use the flags: ```yaml theme={null} - name: Sign in env: AUTH_URL: ${{ vars.LAGER_AUTH_URL }} CI_EMAIL: ${{ secrets.LAGER_CI_EMAIL }} CI_PASSWORD: ${{ secrets.LAGER_CI_PASSWORD }} run: lager login "$AUTH_URL" --email "$CI_EMAIL" --password "$CI_PASSWORD" ``` These three names are your names. The CLI does not read them. Read the password from a secret. Do not write the password in the workflow. A password on a command line is visible in the list of processes on the box. Obey these rules: * **Use a CI account with no multi-factor authentication.** The `--email` and `--password` flags cannot answer a multi-factor question. The command then waits for an input that never comes. * The CLI keeps the session in `~/.lager_gateway_auth` with permission 0600. `LAGER_GATEWAY_AUTH_FILE` sets a different location. * **The Python CLI does not read a token from an environment variable.** A variable such as `LAGER_GATEWAY_TOKEN` applies to the Rust SDK. It does not apply to `lager`. Use `lager login`. ### Do the first command two times On a box with a gateway, the **first** command after a new login can fail one time. The system records the connection between the box and the authentication server at that moment. The next command is successful. Thus the connection test does the command two times: ```yaml theme={null} - run: lager hello || lager hello ``` Keep this even if you have not seen the failure. It is the cost of one more command. It removes a failure that happens on the first job of the day. *** ## 14. Reference data ### Environment variables that the CLI reads | Variable | Function | | ------------------------- | ---------------------------------------------------------------------------------------------- | | `LAGER_BOX` | The default box when there is no `--box` flag. The CLI also sends it to the script on the box. | | `LAGER_CONFIG_FILE_DIR` | The directory of the global `.lager` file. Default `~`. | | `LAGER_CONFIG_FILE_NAME` | The name of the configuration file. Default `.lager`. | | `LAGER_GATEWAY_AUTH_FILE` | A different location for `~/.lager_gateway_auth`. | | `LAGER_USER` | The user identity. It is the lock holder on a user computer. | | `LAGER_LOCK_HOLDER` | A different identity for the lock holder. | | `LAGER_LOCK_WAIT` | Seconds to wait after a lock collision. | | `LAGER_LOCK_TTL` | Maximum life of the lock. Use `none` for no limit. | | `LAGER_LOCK_HEARTBEAT` | Seconds between two heartbeats. Default 60. | | `LAGER_AUTO_LOCK_DISABLE` | Set to `1` to stop the automatic lock. | | `LAGER_DEBUG` | Show the full error data. The same as `--debug`. | | `LAGER_NO_UPDATE_CHECK` | Stop the background version check. | | `CI` | The value `true` selects the CI lock behavior. Actions sets this. | `lager python` sends these variables **to** your script on the box: `LAGER_BOX`, `LAGER_RUNNABLE`, `LAGER_PROCESS_ID` and `LAGER_OUTPUT_CHANNEL`. ### Exit codes | Command | Exit code | Meaning | | ---------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | `lager python` | the code of the script | The CLI does not change it. | | | 124 | The `--timeout` time ended. SIGTERM. | | | 137 | The `--timeout` time ended. SIGKILL. | | | 255 | The CLI could not get the exit code from the box. | | | 130 | A person or a signal stopped the command. | | `lager update --check` | 0 / 1 / 2 | Correct / an update is necessary / the condition is not known. | | `lager exec` | the code of the command | The CLI does not change it. The command is in a container, or in the job, as [section 6](#build-with-lager-exec) shows. | | `lager ssh -- cmd` | the code of the remote command | 255 shows an SSH failure. | | all commands | 1 | A general error. **This includes a lock collision.** | | all commands | 2 | An error in the command line. An unknown flag or a missing argument. | ### The `.lager` file The global file `~/.lager` is a JSON file. It is not an INI file. | Section | Contents | | ---------- | ------------------------------------------------------------------------ | | `BOXES` | The name of each box, with `{ip, user, version}`. | | `NETS` | The net definitions for each box. | | `DEFAULTS` | `gateway_id` (the default box), `user`, and a default net for each role. | The CLI also reads a `.lager` file in the project. It looks in the work directory and then in each directory above it. | Section | Contents | | ---------- | ------------------------------------------------------------------------------- | | `DEVENV` | The container image, the mount point, the shell, the volumes, and the commands. | | `DEBUG` | The name of a debug net, with the path of a local debug script. | | `includes` | More directories to upload with `lager python`. | ### Commands that do not exist These commands do not exist. If you find them in an old example, that example is not current. | Not a command | Use this instead | | --------------------------------------------------- | -------------------------------------------------------- | | `lager test` | `lager python ` | | `lager net add` | `lager nets add` | | `lager nets remove` | `lager nets delete` | | `lager connect`, `lager debug NET connect` | `flash`, `reset` and `erase` make the connection. | | `lager debug NET rtt` | `lager debug NET gdbserver --rtt` | | `lager gdbserver` | `lager debug [NET] gdbserver` | | `lager box update` | `lager update` | | Any lock flag: `--lock`, `--lock-wait`, `--no-lock` | The lock is automatic. Use the `LAGER_LOCK_*` variables. | | `lager update --all` | Use a loop in your shell. | *** ## 15. Troubleshooting **Message: `Error: Box 'BENCH-1' is locked by ...`** A different job holds the bench. In CI, the command waits for `LAGER_LOCK_WAIT` seconds before it gives this message. The default is 30 minutes. On a user computer, the message comes immediately. To find the holder, use `lager boxes`. A holder that starts with `ci:github:` shows the repository, the run, the job and the runner. Wait, or speak to the holder. Do not use `unlock --force` in a job. **A `lager` command on the box fails immediately, but CI has no failure.** The lock behavior changes with the `CI=true` variable. A command from cron, from systemd, or from an SSH session is not in CI. It fails immediately. This is correct. Set `LAGER_LOCK_WAIT` for that command if it must wait. **The job does not stop and gives no output.** Usually the cause is a `lager` command with no subcommand. That command starts an interactive session. The other cause is an interactive `[y/N]` question. Add `--yes` if the command accepts it. Add `exec < /dev/null` in a cleanup step. **`lager exec` does not stop, or it gives an error about a TTY.** The defaults are `--interactive` and `--tty`. Give `--no-tty` in CI. This applies when `lager exec` starts a container. A job that is already in a container does not use these two options. **`lager exec` gives the error `Docker is not installed or not in PATH`.** The job is in a container, but the CLI tried to start one. Make sure that `LAGER_CI_OVERRIDE` is not set. Some earlier versions of the CLI always start a container; make the CLI current. See [section 6](#build-with-lager-exec). **Your test cannot read an environment variable from the workflow.** The `env:` block of a step applies to the runner. Your script operates on the box. Only `--env FOO=bar` and `--passenv FOO` send a variable to the box. ```yaml theme={null} - run: lager python tests/hil --env LOG_LEVEL=debug --passenv GITHUB_SHA ``` **A background test stops with `SIGTTIN`.** `lager python` starts an interactive function when the standard input is a TTY. The function waits for the Enter key. A background process group that reads the terminal gets a `SIGTTIN` signal. Send the standard input from `/dev/null`. **Message: `[warning] Box BENCH-1 is on lager X; CLI is on Y.`** The two versions are different. Update the box with `lager update --box BENCH-1`. You can also set the CLI of the runner to the version of the box. A box that reports no version has an image that is too old for this CLI. **The tests are successful, but you changed the box software and nothing tested it.** `lager python` runs your script in the environment of the box. The box software comes from the installation on the box. It does not come from your check-out. Refer to [Section 7](#7-how-to-make-sure-that-the-box-has-the-code-under-test). **An attempt reports that tests were successful, but it did not do those tests.** The cause is old results in `/tmp` on a self-hosted runner. The other cause is results with no check of the firmware identity. Refer to [Section 12](#12-test-results-after-a-retry). **The flash operation is successful, but each subsequent test fails.** The programmer reported a connection failure but gave exit code 0. The device is erased. Look for the failure text in the output of the flash command. Refer to [Section 6](#6-how-to-put-the-firmware-on-the-dut). **The bench is in an unsafe condition after a cancelled job.** The workflow has `cancel-in-progress: true` and no cleanup step. Refer to [Section 8](#8-how-to-make-the-bench-safe-after-a-job). **The workflow is successful, but no hardware did a test.** An empty `strategy.matrix` makes zero jobs, and the workflow is then successful. Add the `validate` job from [Section 11](#11-how-to-use-more-than-one-bench). *** ## Appendix: a full workflow for one bench ```yaml theme={null} name: HIL on: push: branches: [main] schedule: - cron: '0 15 * * *' workflow_dispatch: permissions: contents: read concurrency: group: hil-${{ vars.HIL_BENCH }} cancel-in-progress: false jobs: build: runs-on: ubuntu-latest outputs: artifact: firmware-${{ github.sha }} steps: - uses: actions/checkout@v4 - run: ./build.sh - uses: actions/upload-artifact@v4 with: name: firmware-${{ github.sha }} path: build/firmware.hex if-no-files-found: error reachable: runs-on: ${{ vars.HIL_BENCH }} timeout-minutes: 5 env: LAGER_BOX: ${{ vars.HIL_BENCH }} steps: - name: Bench reachable run: | for attempt in 1 2 3; do if lager hello; then exit 0; fi echo "::warning::lager hello failed (attempt ${attempt}/3); retrying" sleep 5 done echo "::error::bench unreachable after 3 attempts" exit 1 hil: needs: [build, reachable] runs-on: ${{ vars.HIL_BENCH }} timeout-minutes: 60 env: LAGER_BOX: ${{ vars.HIL_BENCH }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.sha }} - uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.artifact }} path: ./firmware - name: Bench bring-up run: ./tools/bench.sh bring-up - name: Flash run: | set -o pipefail log=$(mktemp) lager debug SWD flash --hex ./firmware/firmware.hex 2>&1 | tee "$log" if grep -qE 'Cannot power up debug port|Could not connect to the target device' "$log"; then echo "::error title=flash::programmer reported a fatal error but the command returned success" exit 1 fi lager debug SWD reset - name: Boot and identity run: bash tools/retry-hil.sh -- lager python tests/hil/boot - name: Console commands run: bash tools/retry-hil.sh -- lager python tests/hil/console - name: Charge behaviour if: github.event_name == 'schedule' run: | bash tools/retry-hil.sh -- \ lager python tests/hil/charge -- --target-soc 80 --timeout-min 45 - name: Bench cleanup if: cancelled() || failure() run: | exec < /dev/null rc=0 ./tools/bench.sh bring-up --recover || rc=$? lager debug SWD reset || rc=$? if [ "$rc" -ne 0 ]; then echo "::error title=cleanup::exited ${rc}; ${LAGER_BOX} may be unsafe for the next job" exit 1 fi - name: Release the bench lock if: always() run: lager boxes unlock --box "$LAGER_BOX" 2>/dev/null || true ``` This workflow shows the full method: 1. Build the firmware on a different machine. 2. Do a connection test before you use the bench. 3. Set the check-out to the commit. 4. Do a check of the flash operation. 5. Put each hardware step in the retry script. 6. Make the bench safe at the end. # ADC Source: https://docs.lagerdata.com/source/reference/cli/adc Read analog-to-digital converter values Read analog-to-digital converter (ADC) values from your box. Supports LabJack T7 and MCC USB-202 hardware with per-hardware channel naming and voltage ranges. ## Syntax ```bash theme={null} lager adc [NET] [OPTIONS] ``` ## Arguments | Argument | Description | | -------- | -------------------------------------------------------- | | `NET` | Name of the ADC net to read (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 ### Read ADC Value Read voltage from an ADC net: ```bash theme={null} lager adc SENSOR_1 --box my-lager-box ``` **Output:** ``` ADC 'SENSOR_1': 2.450000 V ``` The result is returned in volts with 6 decimal places. ### List ADC Nets When invoked without a net name (and no default is set), lists all available ADC nets: ```bash theme={null} lager adc --box my-lager-box ``` **Output:** ``` Name Net Type Instrument Channel Address ================================================================ VOLTAGE_SENSOR adc LabJack_T7 AIN0 USB::470026574 TEMP_SENSOR adc LabJack_T7 AIN1 USB::470026574 CURRENT_MON adc MCC_USB202 CH0 USB0::0x09DB::0x012B::... ``` ## Supported Hardware | Manufacturer | Model | Channels | Voltage Range | Input Mode | | --------------------- | ------- | --------------- | ------------- | ------------ | | LabJack | T7 | 14 (AIN0-AIN13) | +/-10 V | Single-ended | | Measurement Computing | USB-202 | 8 (CH0-CH7) | +/-10 V | Single-ended | ### Hardware Comparison | Feature | LabJack T7 | MCC USB-202 | | ---------------- | ----------------------------------- | --------------------------------- | | Channel count | 14 | 8 | | Channel names | AIN0-AIN13 | CH0-CH7 | | Pin input format | `0`-`13` or `AIN0`-`AIN13` | `0`-`7` or `CH0`-`CH7` | | Voltage range | +/-10 V (bipolar) | +/-10 V (bipolar) | | Resolution | 16-bit (\~0.3 mV/LSB) | 12-bit (\~4.9 mV/LSB) | | Connection | Shared handle (with DAC, GPIO, SPI) | Per-transaction open/close | | Device selection | Auto-discovered (no address needed) | Via serial number or VISA address | ### Channel Naming When creating ADC nets, the channel name depends on the hardware: **LabJack T7:** ```bash theme={null} # Both forms accepted: lager nets add SENSOR_1 adc AIN0 USB::470026574 lager nets add SENSOR_1 adc 0 USB::470026574 # Numeric shorthand ``` Numeric pins `0`-`13` map to `AIN0`-`AIN13` internally. **MCC USB-202:** ```bash theme={null} # Both forms accepted (case-insensitive): lager nets add SENSOR_1 adc CH0 USB0::0x09DB::... lager nets add SENSOR_1 adc 0 USB0::0x09DB::... # Numeric shorthand ``` Numeric pins `0`-`7` and named pins `CH0`-`CH7` are both accepted. Channel names are case-insensitive. ### Instrument Name Matching The backend driver is selected based on the instrument name in the net configuration: | Pattern | Driver | | ------------------------------------------------------------- | ----------- | | `labjack` + `t7` (case-insensitive, flexible separators) | LabJack T7 | | `mcc` + `usb` + `202` (case-insensitive, flexible separators) | MCC USB-202 | ## Default Net Set a default ADC net to avoid specifying the name each time: ```bash theme={null} lager defaults add --adc-net SENSOR_1 ``` Then: ```bash theme={null} lager adc ``` ## Examples ```bash theme={null} # Read ADC value from voltage sensor lager adc VOLTAGE_SENSOR --box my-lager-box # Read ADC value from temperature sensor lager adc TEMP_SENSOR --box my-lager-box # Read ADC value from current monitor lager adc CURRENT_MONITOR --box my-lager-box # List all ADC nets on the box lager adc --box my-lager-box # Use default net (if configured) lager adc ``` ## Scripting Example ```bash theme={null} #!/bin/bash # Read sensor and check threshold RESULT=$(lager adc VOLTAGE_SENSOR --box my-lager-box) echo "$RESULT" # Extract numeric value VOLTAGE=$(echo "$RESULT" | grep -oP '[\d.]+(?= V)') if (( $(echo "$VOLTAGE > 3.0" | bc -l) )); then echo "Voltage too high: $VOLTAGE V" exit 1 fi echo "Voltage OK: $VOLTAGE V" ``` ## Notes * Results are returned in volts with 6 decimal places * Both hardware backends support bipolar measurement (+/-10 V range) * ADC nets must be configured before use with `lager nets add adc
` * Default net can be set with `lager defaults add --adc-net` * LabJack T7 shares a connection handle with DAC, GPIO, and SPI operations on the same device * USB-202 opens and closes the connection on each read * Use `lager instruments --box ` to verify the ADC device is detected ## See Also * [DAC](/source/reference/cli/dac) -- Digital-to-analog converter output (the complement of ADC) * [Python ADC API](/source/reference/python/adc) -- Read ADC values in Python scripts # Robot Arm Source: https://docs.lagerdata.com/source/reference/cli/arm Control robot arm position and movement Control robot arm operations for your device - motion commands, motor control, and position utilities. All positions are in millimeters (mm). ## Syntax ```bash theme={null} lager arm [OPTIONS] [NETNAME] COMMAND [ARGS]... ``` ## Global Options | Option | Description | | ------------ | -------------------------- | | `--box TEXT` | Lager Box name or IP | | `--help` | Show help message and exit | ## Arguments | Argument | Description | | --------- | ----------------------------------------- | | `NETNAME` | Arm net name (optional if default is set) | ## Commands | Command | Description | | ------------------------ | ---------------------------------------------- | | `position` | Get current arm position | | `move` | Move to an absolute XYZ position | | `move-by` | Move by relative dX dY dZ offsets | | `go-home` | Move the arm to its home position (X0 Y300 Z0) | | `enable-motor` | Enable arm motors | | `disable-motor` | Disable arm motors | | `read-and-save-position` | Save current position as calibration reference | | `set-acceleration` | Set arm acceleration parameters | *** ## Command Reference ### `position` Get the current arm position in millimeters. ```bash theme={null} lager arm [NETNAME] position [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Example:** ```bash theme={null} lager arm ARM1 position --box my-lager-box ``` *** ### `move` Move the arm to an absolute XYZ position in millimeters. ```bash theme={null} lager arm [NETNAME] move [OPTIONS] ``` **Options:** * `--x FLOAT` - Target X position (mm) * `--y FLOAT` - Target Y position (mm) * `--z FLOAT` - Target Z position (mm) * `--box TEXT` - Lager Box name or IP * `--timeout FLOAT` - Move timeout in seconds (default: 5.0) * `--yes` - Confirm the action without prompting Each axis is a named option, not a positional argument, so you can move along one axis without restating the others. **Examples:** ```bash theme={null} # Move to specific coordinates lager arm ARM1 move --x 100 --y 200 --z 50 --box my-lager-box --yes # Move with custom timeout lager arm ARM1 move --x 150 --y 250 --z 75 --timeout 10.0 --yes ``` *** ### `move-by` Move the arm by relative offsets (delta movement) in millimeters. ```bash theme={null} lager arm [NETNAME] move-by [OPTIONS] ``` **Options:** * `--dx FLOAT` - Delta X offset (mm) * `--dy FLOAT` - Delta Y offset (mm) * `--dz FLOAT` - Delta Z offset (mm) * `--box TEXT` - Lager Box name or IP * `--timeout FLOAT` - Move timeout in seconds (default: 5.0) * `--yes` - Confirm the action without prompting Omitted axes are left unchanged, so a single-axis jog needs only that one option. **Examples:** ```bash theme={null} # Jog the arm by +5 mm in every axis lager arm ARM1 move-by --dx 5 --dy 5 --dz 5 --box my-lager-box --yes # Move only in Z axis lager arm ARM1 move-by --dz 10 --yes # Move with custom timeout lager arm ARM1 move-by --dx 10 --timeout 3.0 --yes ``` *** ### `go-home` Move the arm to its home position (X0 Y300 Z0). ```bash theme={null} lager arm [NETNAME] go-home [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--yes` - Confirm the action without prompting **Example:** ```bash theme={null} lager arm ARM1 go-home --box my-lager-box --yes ``` *** ### `enable-motor` Enable the arm's motor drivers. ```bash theme={null} lager arm [NETNAME] enable-motor [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Example:** ```bash theme={null} lager arm ARM1 enable-motor --box my-lager-box ``` *** ### `disable-motor` Disable the arm's motor drivers. Use this before manual manipulation of the arm. ```bash theme={null} lager arm [NETNAME] disable-motor [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Example:** ```bash theme={null} lager arm ARM1 disable-motor --box my-lager-box ``` *** ### `read-and-save-position` Read the current position and save it as a calibration reference. ```bash theme={null} lager arm [NETNAME] read-and-save-position [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Example:** ```bash theme={null} lager arm ARM1 read-and-save-position --box my-lager-box ``` *** ### `set-acceleration` Set arm acceleration parameters for movement control. ```bash theme={null} lager arm [NETNAME] set-acceleration [OPTIONS] ``` **Options:** * `--acceleration INTEGER` - Acceleration value (>= 0) * `--travel INTEGER` - Travel acceleration value (>= 0) * `--retract INTEGER` - Retract acceleration value (>= 0) * `--box TEXT` - Lager Box name or IP **Example:** ```bash theme={null} # Set acceleration parameters lager arm ARM1 set-acceleration --acceleration 100 --travel 80 --retract 60 --box my-lager-box ``` *** ## Listing Arm Nets When invoked with only `--box` and no subcommand, lists all arm nets on the box: ```bash theme={null} lager arm --box my-lager-box ``` **Output:** ``` Name Net Type Instrument Channel Address ARM1 arm Rotrics_Dexarm 0 /dev/ttyUSB0 ``` *** ## Examples ```bash theme={null} # List arm nets lager arm --box my-lager-box # Get current position lager arm ARM1 position --box my-lager-box # Move to home position lager arm ARM1 go-home --box my-lager-box --yes # Move to specific coordinates lager arm ARM1 move --x 100 --y 250 --z 30 --box my-lager-box --yes # Jog the arm by +5 mm in X direction lager arm ARM1 move-by --dx 5 --box my-lager-box --yes # Disable motors for manual adjustment lager arm ARM1 disable-motor --box my-lager-box # Re-enable motors after adjustment lager arm ARM1 enable-motor --box my-lager-box # Save current position as reference lager arm ARM1 read-and-save-position --box my-lager-box # Configure acceleration lager arm ARM1 set-acceleration --acceleration 100 --travel 80 --retract 60 --box my-lager-box ``` *** ## Supported Hardware | Manufacturer | Model | Description | | ------------ | ------ | ------------------------------------- | | Rotrics | Dexarm | Desktop robot arm with 3-axis control | *** ## Notes * All positions are in millimeters (mm) * Home position is X0 Y300 Z0 * Use `--yes` flag for non-interactive scripts and CI pipelines * Always re-enable motors after disabling them to resume normal operation * The `--timeout` option prevents commands from hanging if the arm fails to reach position * Default net can be set with `lager defaults add --arm-net` # Battery Simulation Source: https://docs.lagerdata.com/source/reference/cli/battery Control battery simulator settings and output Control and monitor battery simulator Nets (Keithley 2281S) through the Lager CLI for battery simulation and testing. ## Syntax ```bash theme={null} lager battery [OPTIONS] [NETNAME] COMMAND [ARGS]... ``` ## Global Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | --------------- | -------------------------------------------------------------- | | `mode` | Set or read battery simulation mode (static/dynamic) | | `set` | Initialize battery simulator mode | | `soc` | Set or read state of charge (%) | | `voc` | Set or read open circuit voltage (V) | | `batt-full` | Set or read fully charged voltage (V) | | `batt-empty` | Set or read fully discharged voltage (V) | | `capacity` | Set or read battery capacity (Ah) | | `current-limit` | Set or read max charge/discharge current (A) | | `ovp` | Set or read over-voltage protection (V) | | `ocp` | Set or read over-current protection (A) | | `model` | Set or read battery model | | `models` | List battery models saved on the instrument | | `model-create` | Create a custom battery model in a memory slot from a CSV file | | `model-export` | Export a saved battery model's curve to a CSV file | | `state` | Get comprehensive battery state | | `enable` | Enable battery simulator output | | `disable` | Disable battery simulator output | | `clear` | Clear all protection trip conditions | | `clear-ovp` | Clear OVP trip condition | | `clear-ocp` | Clear OCP trip condition | | `tui` | Launch interactive terminal UI | ## Listing Battery Nets When invoked with only `--box` and no subcommand, lists all battery nets on the Lager Box: ```bash theme={null} lager battery --box my-lager-box ``` ## Command Reference ### `mode` Set or read battery simulation mode type. ```bash theme={null} lager battery NETNAME mode [static|dynamic] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `[static|dynamic]` - Mode type (omit to read current mode) **Options:** * `--box TEXT` - Lager Box name or IP **Examples:** ```bash theme={null} # Read current mode lager battery batt1 mode --box my-lager-box # Set static mode lager battery batt1 mode static --box my-lager-box ``` ### `set` Initialize battery simulator mode. Prepares the instrument for battery simulation. ```bash theme={null} lager battery NETNAME set [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP ### `soc` Set or read battery state of charge in percent. ```bash theme={null} lager battery NETNAME soc [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - State of charge percentage (0-100), omit to read **Options:** * `--box TEXT` - Lager Box name or IP **Examples:** ```bash theme={null} # Read current SOC lager battery batt1 soc --box my-lager-box # Set SOC to 80% lager battery batt1 soc 80 --box my-lager-box ``` ### `voc` Set or read battery open circuit voltage in volts. ```bash theme={null} lager battery NETNAME voc [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - Open-circuit voltage (volts), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `batt-full` Set or read battery fully charged voltage in volts. ```bash theme={null} lager battery NETNAME batt-full [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - Voltage at 100% SOC (volts), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `batt-empty` Set or read battery fully discharged voltage in volts. ```bash theme={null} lager battery NETNAME batt-empty [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - Voltage at 0% SOC (volts), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `capacity` Set or read battery capacity limit in amp-hours. ```bash theme={null} lager battery NETNAME capacity [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - Battery capacity (Ah), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `current-limit` Set or read maximum charge/discharge current in amps. ```bash theme={null} lager battery NETNAME current-limit [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - Maximum current limit (amps), omit to read. Must be between 0 and 6.0 A (Keithley 2281S limit); values outside this range are rejected. **Options:** * `--box TEXT` - Lager Box name or IP ### `ovp` Set or read over-voltage protection limit in volts. ```bash theme={null} lager battery NETNAME ovp [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - OVP limit (volts), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `ocp` Set or read over-current protection limit in amps. ```bash theme={null} lager battery NETNAME ocp [VALUE] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `VALUE` - OCP limit (amps), omit to read **Options:** * `--box TEXT` - Lager Box name or IP ### `model` Set or read battery model preset. ```bash theme={null} lager battery NETNAME model [PARTNUMBER] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `PARTNUMBER` - Battery model (e.g., 18650, nimh, lead-acid), omit to read **Options:** * `--box TEXT` - Lager Box name or IP **Supported Models:** * `18650` - Standard lithium-ion cell * `nimh` - Nickel-metal hydride * `lead-acid` - Lead acid battery * Custom part numbers from your battery library ### `models` List the battery models available on the instrument: memory slots with a saved model plus the firmware built-in models. Read-only. ```bash theme={null} lager battery NETNAME models [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net **Options:** * `--box TEXT` - Lager Box name or IP The printed slots and names are valid inputs to the `model` command. ### `model-create` Create a custom battery model in a memory slot (1-9) from a CSV file. ```bash theme={null} lager battery NETNAME model-create SLOT --csv FILE [--force] [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `SLOT` - Target memory slot (1-9) **Options:** * `--csv FILE` - CSV curve file (required) * `--force` - Overwrite the slot if it already holds a model * `--box TEXT` - Lager Box name or IP **CSV format:** Two columns, `voc,resistance` (header row optional), ordered from empty battery to full: open-circuit voltage in volts (non-decreasing) and internal resistance in ohms (non-increasing). Exactly 11 or 101 data rows — 11-row files are interpolated to 101 points by the instrument. ``` voc,resistance 3.0,0.25 3.3,0.24 ... ``` The file is validated on your machine before anything is sent to the instrument. If the slot already holds a model, the command refuses unless `--force` is given. `model-create` overwrites the slot's previous model. The instrument has no way to delete a model from a slot. Once a slot holds a model, you can only overwrite it with a different one. After a successful create, `model SLOT` loads the model and `models` lists it. ### `model-export` Export a saved battery model's curve from a memory slot (1-9) to a CSV file. ```bash theme={null} lager battery NETNAME model-export SLOT --csv FILE [OPTIONS] ``` **Arguments:** * `NETNAME` - Name of the battery Net * `SLOT` - Memory slot to export (1-9) **Options:** * `--csv FILE` - Output CSV file to write (required) * `--box TEXT` - Lager Box name or IP Writes the slot's 101 `voc,resistance` points in the format `model-create` accepts. You can therefore export a saved model, edit it, and write it back to a slot (`model-export` → edit → `model-create`). Read-only: exporting does not recall or change the active model. Exporting an empty slot is an error — use `models` to see which slots hold a saved model. ### `state` Get comprehensive battery state including all current settings and measurements. ```bash theme={null} lager battery NETNAME state [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Example Output:** ``` Battery State: Mode: static SOC: 80% VOC: 3.7V Voltage Range: 3.0V - 4.2V Capacity: 2.5Ah Current Limit: 1.5A OVP: 4.5V OCP: 2.0A Output: Enabled ``` ### `enable` Enable battery simulator output. ```bash theme={null} lager battery NETNAME enable [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--yes` - Skip confirmation prompt ### `disable` Disable battery simulator output. ```bash theme={null} lager battery NETNAME disable [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--yes` - Skip confirmation prompt ### `clear` Clear all protection trip conditions (OVP and OCP). ```bash theme={null} lager battery NETNAME clear [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP ### `clear-ovp` Clear over-voltage protection trip condition. ```bash theme={null} lager battery NETNAME clear-ovp [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP ### `clear-ocp` Clear over-current protection trip condition. ```bash theme={null} lager battery NETNAME clear-ocp [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP ### `tui` Launch interactive terminal UI for real-time monitoring and control. ```bash theme={null} lager battery NETNAME tui [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **TUI Features:** * Real-time voltage/current display * SOC adjustment slider * Enable/disable controls * Protection status indicators * Keyboard navigation ## Examples ```bash theme={null} # List all battery nets lager battery --box my-lager-box # Configure a Li-ion cell simulation lager battery batt1 batt-full 4.2 --box my-lager-box lager battery batt1 batt-empty 3.0 --box my-lager-box lager battery batt1 capacity 2.5 --box my-lager-box lager battery batt1 current-limit 1.5 --box my-lager-box # Set protection limits lager battery batt1 ovp 4.5 --box my-lager-box lager battery batt1 ocp 2.0 --box my-lager-box # Set initial state and enable lager battery batt1 soc 80 --box my-lager-box lager battery batt1 enable --yes --box my-lager-box # Check battery status lager battery batt1 state --box my-lager-box # Read current SOC lager battery batt1 soc --box my-lager-box # Clear protection faults lager battery batt1 clear --box my-lager-box # Use a preset model lager battery batt1 model 18650 --box my-lager-box # Launch interactive UI lager battery batt1 tui --box my-lager-box # Disable when done lager battery batt1 disable --yes --box my-lager-box ``` ## Supported Hardware | Instrument | Description | | -------------- | --------------------------------------- | | Keithley 2281S | Battery Simulator with dynamic modeling | ## Notes * All value commands (soc, voc, ovp, etc.) read the current value when called without an argument * Use `--yes` flag to skip confirmation prompts for enable/disable * Protection limits help prevent damage during testing * SOC can be set from 0-100% for realistic battery simulation * The `state` command provides a comprehensive view of all settings * Mode can be `static` (fixed parameters) or `dynamic` (SOC-based modeling) * The TUI allows concurrent CLI access while monitoring ## See Also * [Power Supply](/source/reference/cli/supply) -- Standard power supply control * [Electronic Load](/source/reference/cli/eload) -- Programmable electronic loads * [Python Battery API](/source/reference/python/battery) -- Automate battery simulation in Python scripts # Binaries Source: https://docs.lagerdata.com/source/reference/cli/binaries Manage custom binaries on Lager Boxes Upload, list, and remove custom binaries on Lager Boxes for use in Python scripts. ## Syntax ```bash theme={null} lager binaries COMMAND [OPTIONS] ``` ## Commands | Command | Description | | -------- | ----------------------------------- | | `add` | Upload a binary to a Lager Box | | `list` | List custom binaries on a Lager Box | | `remove` | Remove a binary from a Lager Box | *** ## Command Reference ### `add` Upload a binary file to the Lager Box. ```bash theme={null} lager binaries add BINARY_PATH [OPTIONS] ``` **Arguments:** * `BINARY_PATH` - Local path to the binary file **Options:** * `--box BOX` - Lager Box name or IP address * `--name NAME` - Name for the binary on Lager Box (defaults to filename) * `--yes` - Skip confirmation prompt **Examples:** ```bash theme={null} # Upload with default name lager binaries add ./my_tool --box my-lager-box # Upload with custom name lager binaries add ./rt_newtmgr_v1.2 --name rt_newtmgr --box my-lager-box # Skip confirmation lager binaries add ./firmware_flasher --box my-lager-box --yes ``` ### `list` List all custom binaries on a Lager Box. ```bash theme={null} lager binaries list --box BOX ``` Output: ``` Custom binaries on my-lager-box: rt_newtmgr (1.2 MB) firmware_flasher (856 KB) custom_tool (234 KB) ``` ### `remove` Remove a binary from the Lager Box. ```bash theme={null} lager binaries remove BINARY_NAME [OPTIONS] ``` **Arguments:** * `BINARY_NAME` - Name of the binary to remove **Options:** * `--box BOX` - Lager Box name or IP address * `--yes` - Skip confirmation prompt **Examples:** ```bash theme={null} # Remove with confirmation lager binaries remove old_tool --box my-lager-box # Remove without confirmation lager binaries remove old_tool --box my-lager-box --yes ``` *** ## Storage Locations Binaries are stored in: | Location | Path | | ---------------- | ------------------------------------------------ | | Host (Lager Box) | `/home/lagerdata/third_party/customer-binaries/` | | Container | `/home/www-data/customer-binaries/` | *** ## Using Binaries in Python Scripts Once uploaded, binaries can be called from Python scripts running on the Lager Box: ```python theme={null} import subprocess # Call the binary with arguments result = subprocess.run( ['/home/www-data/customer-binaries/rt_newtmgr', 'arg1', 'arg2'], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: print(f"Success: {result.stdout}") else: print(f"Error: {result.stderr}") ``` *** ## Adding to PATH (Optional) To call binaries without the full path, modify the Lager Box Dockerfile: ```dockerfile theme={null} # In box/lager/docker/box.Dockerfile ENV PATH="/home/www-data/customer-binaries:${PATH}" ``` Then rebuild the container: ```bash theme={null} lager update --box my-lager-box --yes ``` Now you can call binaries directly: ```python theme={null} subprocess.run(['rt_newtmgr', 'arg1', 'arg2'], ...) ``` *** ## Examples ```bash theme={null} # Complete workflow # 1. Upload binary lager binaries add ./my_custom_tool --box my-lager-box --yes # 2. Verify it's there lager binaries list --box my-lager-box # 3. Use in Python script lager python ./test_script.py --box my-lager-box # 4. Clean up when done lager binaries remove my_custom_tool --box my-lager-box --yes ``` *** ## Use Cases ### Device Communication Tools Upload vendor-specific tools for device interaction: ```bash theme={null} lager binaries add ./vendor_cli --box my-lager-box ``` ### Firmware Tools Upload custom firmware manipulation tools: ```bash theme={null} lager binaries add ./sign_firmware --box my-lager-box lager binaries add ./encrypt_image --box my-lager-box ``` ### Test Utilities Upload test-specific utilities: ```bash theme={null} lager binaries add ./stress_test --box my-lager-box lager binaries add ./validate_output --box my-lager-box ``` *** ## Notes * Binaries must be Linux x86\_64 compatible * Files are automatically made executable * Container restart is not required (volume mount) * File sizes are displayed in human-readable format * Use `--yes` to skip confirmation in scripts # BLE Source: https://docs.lagerdata.com/source/reference/cli/ble Scan and connect to Bluetooth Low Energy devices Scan for and interact with Bluetooth Low Energy (BLE) devices through the Lager CLI. ## Syntax ```bash theme={null} lager ble COMMAND [OPTIONS] ``` ## Commands | Command | Description | | ------------ | ---------------------------- | | `scan` | Scan for BLE devices | | `info` | Get BLE device information | | `connect` | Connect to a BLE device | | `disconnect` | Disconnect from a BLE device | ## Command Reference ### `scan` Scan for nearby BLE devices. ```bash theme={null} lager ble scan [OPTIONS] ``` **Options:** * `--box BOX` - Lager Box name or IP address * `--timeout FLOAT` - Scan duration in seconds (default: 5.0) * `--name-contains STRING` - Filter devices by name (partial match) * `--name-exact STRING` - Filter devices by exact name match * `--verbose` - Include UUIDs in output **Examples:** ```bash theme={null} # Scan for 5 seconds (default) lager ble scan --box my-lager-box # Scan for 10 seconds lager ble scan --timeout 10 # Filter by name lager ble scan --name-contains "Sensor" # Verbose output with UUIDs lager ble scan --verbose ``` ### `info` Get detailed information about a BLE device. ```bash theme={null} lager ble info ADDRESS [--box BOX] ``` **Arguments:** * `ADDRESS` - BLE device address (e.g., `AA:BB:CC:DD:EE:FF`) Returns device information including: * Device name * Services and characteristics * Manufacturer data ### `connect` Connect to a BLE device. ```bash theme={null} lager ble connect ADDRESS [--box BOX] ``` **Arguments:** * `ADDRESS` - BLE device address to connect to ### `disconnect` Disconnect from a BLE device. ```bash theme={null} lager ble disconnect ADDRESS [--box BOX] ``` **Arguments:** * `ADDRESS` - BLE device address to disconnect from *** ## Examples ```bash theme={null} # Scan for BLE devices lager ble scan --box my-lager-box # Scan with filtering lager ble scan --name-contains "Nordic" --timeout 15 # Get device info lager ble info AA:BB:CC:DD:EE:FF # Connect to device lager ble connect AA:BB:CC:DD:EE:FF # Disconnect lager ble disconnect AA:BB:CC:DD:EE:FF ``` *** ## Output Format ### Scan Results The scan command returns a table with: * Device address (MAC address format) * Device name (if advertised) * RSSI (signal strength in dBm) * Manufacturer data (if available) ### Device Info The info command shows: * Complete device name * All advertised services (UUIDs) * Characteristics for each service * Read/write/notify properties *** ## Use Cases ### Device Discovery Use BLE scanning to discover devices for testing: ```bash theme={null} # Find all devices lager ble scan --timeout 10 # Find specific device type lager ble scan --name-contains "Heart Rate" ``` ### Automated Testing Integrate BLE operations into test scripts: ```bash theme={null} # Verify device is discoverable lager ble scan --name-exact "MyProduct" --timeout 5 # Connect and verify services lager ble info AA:BB:CC:DD:EE:FF ``` *** ## Notes * BLE scanning requires Bluetooth hardware on the box * Address format is `XX:XX:XX:XX:XX:XX` (colon-separated hex) * Scan timeout affects how long the box searches for devices * Some devices do not advertise their name until they connect # BluFi Source: https://docs.lagerdata.com/source/reference/cli/blufi Provision ESP32 WiFi credentials over BLE (BluFi protocol) Provision WiFi credentials to an ESP32 device over Bluetooth Low Energy using the BluFi protocol. Use it to scan for BluFi-capable devices, push SSID/password credentials, and read back connection status and firmware version. It brings an unprovisioned ESP32 DUT onto the network as part of a test. ## Syntax ```bash theme={null} lager blufi COMMAND [ARGS] [OPTIONS] ``` Every subcommand accepts `--box BOX` (Lager Box name or IP; uses the default box if omitted). All commands except `scan` take a `DEVICE_NAME` argument identifying the target BluFi device. ## Commands | Command | Description | | ----------- | --------------------------------------------------------- | | `scan` | Scan for BluFi-capable BLE devices | | `connect` | Connect to a BluFi device and retrieve version and status | | `provision` | Provision WiFi credentials to a BluFi device | | `wifi-scan` | Scan for WiFi networks via a BluFi device | | `status` | Get WiFi connection status from a BluFi device | | `version` | Get firmware version from a BluFi device | *** ## Command Reference ### `scan` Scan for BluFi-capable BLE devices. | Option | Default | Description | | ----------------- | ------- | ------------------------------------------------- | | `--timeout` | `10.0` | Total time (seconds) the box spends scanning | | `--name-contains` | | Filter to devices whose name contains this string | ```bash theme={null} lager blufi scan --box my-lager-box lager blufi scan --name-contains ESP --box my-lager-box ``` ### `connect` Connect to a BluFi device and retrieve its version and status. ```bash theme={null} lager blufi connect ESP32-DEVICE --box my-lager-box ``` | Option | Default | Description | | ----------- | ------- | -------------------------------- | | `--timeout` | `20.0` | BLE connection timeout (seconds) | ### `provision` Provision WiFi credentials to a BluFi device. ```bash theme={null} lager blufi provision ESP32-DEVICE --ssid HomeNet --password secret123 --box my-lager-box ``` | Option | Default | Description | | ------------ | ---------- | -------------------------------- | | `--ssid` | (required) | WiFi network SSID to provision | | `--password` | (required) | WiFi network password | | `--timeout` | `20.0` | BLE connection timeout (seconds) | ### `wifi-scan` Scan for WiFi networks via a BluFi device. ```bash theme={null} lager blufi wifi-scan ESP32-DEVICE --box my-lager-box ``` | Option | Default | Description | | ---------------- | ------- | ------------------------------------------ | | `--timeout` | `20.0` | BLE connection timeout (seconds) | | `--scan-timeout` | `15.0` | WiFi scan duration on the device (seconds) | ### `status` Get the WiFi connection status from a BluFi device. ```bash theme={null} lager blufi status ESP32-DEVICE --box my-lager-box ``` | Option | Default | Description | | ----------- | ------- | -------------------------------- | | `--timeout` | `20.0` | BLE connection timeout (seconds) | ### `version` Get the firmware version from a BluFi device. ```bash theme={null} lager blufi version ESP32-DEVICE --box my-lager-box ``` | Option | Default | Description | | ----------- | ------- | -------------------------------- | | `--timeout` | `20.0` | BLE connection timeout (seconds) | *** ## Typical Flow ```bash theme={null} # 1. Find the device lager blufi scan --name-contains ESP --box my-lager-box # 2. Provision credentials lager blufi provision ESP32-DEVICE --ssid HomeNet --password secret123 --box my-lager-box # 3. Confirm it joined the network lager blufi status ESP32-DEVICE --box my-lager-box ``` *** ## See Also * [BLE](/source/reference/cli/ble) — scan and connect to generic BLE devices # Box Config Source: https://docs.lagerdata.com/source/reference/cli/box-config Declaratively provision a Lager Box's container — USB device permissions, packages, mounts, environment, and more `lager box-config` manages a declarative configuration for a Lager Box's container. You describe what the box must have: USB device permissions (udev rules), apt packages, bind mounts, environment variables, pip/cargo/npm packages, and sysctl values. Then `apply` puts that description into effect. The configuration persists across container restarts and box updates. ## Syntax ```bash theme={null} lager box-config COMMAND [OPTIONS] ``` ## Global Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | ## How It Works Editing the config and putting it into effect are two separate steps: 1. **Change the config** — `udev add`, `apt add`, `mount add`, `env set`, etc. These only edit the stored config; nothing happens on the box yet. 2. **Apply it** — `lager box-config apply` validates the config and restarts ("bounces") the container so the changes take effect. Host-side pieces (apt packages, udev rules, sysctl) are installed on the box host during apply; everything else is mounted into the fresh container. ```bash theme={null} lager box-config udev add 1209:0001 --box my-lager-box # 1. edit lager box-config apply --box my-lager-box # 2. apply ``` ## Commands **Lifecycle** | Command | Description | | ------------------- | ------------------------------------------------------------------- | | `show` | Print the current config | | `status` | One-line summary of config state | | `diff` | Show pending changes vs. the last applied config | | `validate` | Validate the current config | | `apply` | Validate, then restart the container so the new config takes effect | | `init` | Create the config with defaults | | `reset` | Erase the config to empty | | `restart` | Restart the container without changing the config | | `repair` | Restore the config from the last applied snapshot and restart | | `edit` | Open the config in `$EDITOR` | | `import` / `export` | Replace the config from / write the config to a local JSON file | | `copy` | Copy one box's config to another box | | `audit` | Show recent config changes recorded on the box | **Provisioning** | Group | Description | | ----------------------- | ------------------------------------------------------- | | `udev` | Host udev rules granting USB device access (by vid:pid) | | `apt` | Host-side apt packages | | `mount` | Host-to-container bind mounts | | `volume` | Named docker volumes attached to the container | | `env` | Container environment variables | | `pip` / `cargo` / `npm` | In-container language packages | | `sysctl` | Host sysctl values persisted across reboots | *** ## Command Reference ### `udev` Grant a USB device read/write access from inside the container, by USB vendor/product id. Use this when a freshly-plugged device is owned by `root` and a tool inside the container cannot open it. For example, `dfu-util` fails with *"No DFU capable USB device available"*. ```bash theme={null} lager box-config udev add VID:PID [VID:PID ...] [--mode 0666] [--usbtmc] lager box-config udev list [--json] lager box-config udev remove VID:PID [VID:PID ...] ``` | Option | Description | | ------------- | ----------------------------------------------------------------------------------------------------------- | | `--mode TEXT` | Octal device-node permission mode (default `0666`) | | `--usbtmc` | Also emit the `usbtmc` driver-unbind rule, required for SCPI/USB‑TMC instruments accessed via PyVISA/libusb | VID and PID are 4 hex digits each. A `0x` prefix and uppercase are accepted and normalized (so `0x1AB1:0E11` becomes `1ab1:0e11`). Re-adding the same vid:pid updates it in place. ```bash theme={null} # Let dfu-util open a generic test device, then apply lager box-config udev add 1209:0001 --box my-lager-box lager box-config apply --box my-lager-box # A SCPI power supply that also needs the usbtmc driver unbound lager box-config udev add 1ab1:0e11 --usbtmc --box my-lager-box lager box-config apply --box my-lager-box ``` On `apply`, the rules are installed to `/etc/udev/rules.d/99-lager-user.rules` on the box host and udev is reloaded, so existing devices pick up the new permissions. ### `init` Create `/etc/lager/box_config.json` with defaults, seeding the default `box-tools` volume. Does nothing if the file already exists unless you pass `--force`. ```bash theme={null} lager box-config init --box # Replace an existing config with the defaults lager box-config init --box --force ``` | Option | Description | | ------------ | ----------------------------------------- | | `--box TEXT` | Lager Box name or IP | | `--force` | Overwrite the config if it already exists | `--force` discards the current config. Take a copy with `lager box-config export ./box.json` first if the box has any customisation you care about. ### `reset` Erase the config to a truly empty state. Unlike `init` (which re-seeds the default `box-tools` volume), `reset` clears everything — a clean slate. ```bash theme={null} lager box-config reset [--yes] [--apply] ``` | Option | Description | | --------- | -------------------------------------------------------------------------- | | `--yes` | Skip the confirmation prompt | | `--apply` | Also restart the container so you get a fresh, empty container in one step | ```bash theme={null} # Wipe the config and bring up a fresh container lager box-config reset --apply --yes --box my-lager-box ``` ### `restart` Restart the container without changing the config — a fresh container with the same setup. Useful for test isolation between runs. Unlike `apply`, it restarts unconditionally (it does not skip when the config is unchanged). ```bash theme={null} lager box-config restart [--yes] --box my-lager-box ``` ### `apply` Validate the config and restart the container so changes take effect. ```bash theme={null} lager box-config apply [OPTIONS] --box my-lager-box ``` | Option | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | | `--yes` | Skip the confirmation prompt | | `--force` | Restart even if the config is unchanged | | `--dry-run` | Preview what `apply` does, but make no changes | | `--skip-restart` | Validate and record the config without restarting | | `--no-auto-prep` | Skip host-path re-verification before restart | | `--recursive-chown` | For any configured mount whose host path is wrong-owned and populated, recursively chown it to uid 33 (www-data) | `--box` accepts a comma-separated list to apply across multiple boxes. ### `apt` Host-side apt packages (installed on the box host during `apply`). ```bash theme={null} lager box-config apt add usbutils dfu-util --box my-lager-box lager box-config apt list [--json] lager box-config apt remove dfu-util ``` ### `mount` Bind-mount a host path into the container. ```bash theme={null} lager box-config mount add HOST_PATH CONTAINER_PATH [--readonly] --box my-lager-box lager box-config mount list [--json] lager box-config mount remove HOST_PATH CONTAINER_PATH [--yes] ``` `mount add` options: | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------ | | `--readonly` | Mount as read-only | | `--no-auto-prep` | Skip the auto mkdir/chown of the host path (use when the directory is provisioned externally) | | `--recursive-chown` | If the host path already exists with the wrong owner and contains files, recursively chown it to uid 33 (www-data) | ### `env` Container environment variables. ```bash theme={null} lager box-config env set KEY=VALUE [KEY=VALUE ...] --box my-lager-box lager box-config env list [--json] lager box-config env unset KEY [KEY ...] ``` ### `pip` / `cargo` / `npm` In-container language packages, installed when the container starts. ```bash theme={null} lager box-config pip add requests rich --box my-lager-box lager box-config cargo add ripgrep --box my-lager-box lager box-config npm add left-pad --box my-lager-box # each group also supports: list [--json], remove ``` `pip add` validates against PyPI by default; pass `--no-validate-pypi` to skip. ### `sysctl` Host sysctl values, persisted across reboots. ```bash theme={null} lager box-config sysctl set net.ipv4.ip_forward=1 --box my-lager-box lager box-config sysctl list [--json] lager box-config sysctl unset net.ipv4.ip_forward ``` ### `volume` Named docker volumes attached to the container (persist data across restarts). ```bash theme={null} lager box-config volume add my-vol /opt/my-vol --box my-lager-box lager box-config volume list [--json] lager box-config volume remove my-vol [--yes] ``` ### Inspecting and editing ```bash theme={null} lager box-config show --box my-lager-box # full config lager box-config status --box my-lager-box # one-line summary (clean / drift) lager box-config diff --box my-lager-box # pending changes vs. last applied lager box-config validate --box my-lager-box # check for errors lager box-config audit --box my-lager-box # recent changes (supports --verb, --since, --tail) lager box-config edit --box my-lager-box # open in $EDITOR, then apply ``` ### Backup, restore, and recovery ```bash theme={null} lager box-config export ./box.json --box my-lager-box # save current config to a file lager box-config import ./box.json --box my-lager-box # replace config from a file lager box-config copy --from BOX_A --to BOX_B # clone config between boxes lager box-config repair --box my-lager-box # restore the last applied config and restart ``` *** ## Notes * Most editing commands only change the stored config — run `apply` to put changes into effect. * udev rules, apt packages, and sysctl values are applied to the box **host**; mounts, env, and pip/cargo/npm apply inside the **container**. * The config persists across container restarts and box updates. A user udev file (`99-lager-user.rules`) is preserved across `lager update`. * `--box` accepts a name (from `lager boxes`) or an IP address. # Boxes Source: https://docs.lagerdata.com/source/reference/cli/boxes Manage Lager Box configurations Manage Lager Box names, IP addresses, and configurations for local development. ## Syntax ```bash theme={null} lager boxes COMMAND [OPTIONS] ``` ## Commands | Command | Description | | ------------ | ------------------------------------------ | | `add` | Add a new box configuration | | `add-all` | Add all boxes from Tailscale network | | `delete` | Delete a box configuration | | `edit` | Edit an existing box configuration | | `list` | List all configured boxes | | `delete-all` | Delete all box configurations | | `export` | Export box configuration to JSON | | `import` | Import box configuration from JSON | | `lock` | Lock a box to prevent others from using it | | `unlock` | Unlock a box | *** ## Command Reference ### `add` Add a new Lager Box configuration. ```bash theme={null} lager boxes add --name NAME --ip IP --user USER [OPTIONS] ``` **Options:** * `--name` (required) - Name to assign to the box * `--ip` (required) - IP address of the box * `--user` (required) - SSH username for the box (the account you log in as) * `--version` - Lager Box version/branch (e.g., staging, main) * `--yes` - Confirm without prompting `--user` is **required**. It previously defaulted to `lagerdata`, but since most boxes use a different login account, the default was removed so the correct user is always recorded for SSH, updates, and `lager ssh`. **Examples:** ```bash theme={null} # Add a basic box lager boxes add --name my-lager-box --ip --user lager # Add with a Raspberry Pi default account lager boxes add --name pi-lager-box --ip --user pi # Add with version tracking lager boxes add --name staging-lager-box --ip --user lager --version staging ``` ### `add-all` Automatically add all Lager Boxes found on your Tailscale network. ```bash theme={null} lager boxes add-all [--yes] ``` **Options:** * `--yes` - Confirm without prompting This command scans your Tailscale network for devices with names 5-8 characters long, the typical Lager Box naming convention. It adds each one as a box with an uppercase name. **How it works:** 1. Runs `tailscale status` to discover devices 2. Filters for devices with names 5-8 characters long 3. Converts names to uppercase 4. Skips boxes that already exist with the same IP 5. Adds new boxes to your configuration **Example:** ```bash theme={null} # Scan and add all boxes lager boxes add-all # Output: Scanning Tailscale network for lager boxes... Found 3 lager box(es): LABGW1 → TESTGW → DEVBOX → Add all 3 box(es)? [Y/n]: y LABGW1: added TESTGW: added DEVBOX: already exists (skipped) Summary: Added: 2 Skipped: 1 [OK] Successfully added 2 box(es) ``` ```bash theme={null} # Add without confirmation prompt lager boxes add-all --yes ``` ### `delete` Delete a box configuration. ```bash theme={null} lager boxes delete --name NAME [--yes] ``` **Examples:** ```bash theme={null} # Delete with confirmation prompt lager boxes delete --name old-lager-box # Delete without confirmation lager boxes delete --name old-lager-box --yes ``` ### `edit` Edit an existing box configuration. ```bash theme={null} lager boxes edit --name NAME [OPTIONS] ``` **Options:** * `--name` (required) - Name of the box to edit * `--ip` - New IP address * `--user` - New SSH username * `--version` - New Lager Box version/branch * `--new-name` - Rename the box * `--yes` - Confirm without prompting **Examples:** ```bash theme={null} # Change IP address lager boxes edit --name my-lager-box --ip # Rename a box lager boxes edit --name old-name --new-name new-name # Update SSH user and version lager boxes edit --name pi-lager-box --user pi --version staging ``` ### `list` List all configured boxes with live version status. This is also the default behavior when running `lager boxes` with no subcommand. ```bash theme={null} lager boxes list lager boxes # same as list ``` The command queries each box's `/cli-version` endpoint to display real-time version and status information. **Output:** ``` CLI version: 0.3.22 ┌──────────────┬─────────────────┬───────────┬─────────┬───────────────┐ │ Name │ IP │ User │ Version │ Status │ ├──────────────┼─────────────────┼───────────┼─────────┼───────────────┤ │ my-lager-box │ │ lagerdata │ 0.3.22 │ current │ │ staging-box │ │ lagerdata │ 0.3.20 │ needs update │ │ pi-box │ │ pi │ 0.3.23 │ newer │ │ offline-box │ │ lagerdata │ --- │ unreachable │ └──────────────┴─────────────────┴───────────┴─────────┴───────────────┘ Summary: 1 current, 1 needs update, 1 newer, 1 unreachable ``` **Status Colors:** | Status | Color | Meaning | | -------------- | ------ | -------------------------------------- | | `current` | Green | Box version matches CLI version | | `needs update` | Yellow | Box version is older than CLI | | `newer` | Cyan | Box version is newer than CLI | | `unreachable` | Red | Box could not be contacted | | `timeout` | Red | Connection timed out | | `old box` | Red | Box does not support version reporting | ### `delete-all` Delete all box configurations. ```bash theme={null} lager boxes delete-all [--yes] ``` ### `export` Export box configuration to JSON file. ```bash theme={null} lager boxes export [--output FILE] ``` **Options:** * `--output` / `-o` - Output file path (prints to stdout if not specified) **Examples:** ```bash theme={null} # Export to file lager boxes export --output boxes.json # Export to stdout lager boxes export ``` ### `import` Import box configuration from JSON file. ```bash theme={null} lager boxes import FILE [--merge] [--yes] ``` **Options:** * `FILE` - Path to JSON file to import * `--merge` - Merge with existing boxes (default: replace) * `--yes` - Confirm without prompting **Examples:** ```bash theme={null} # Replace all boxes with imported config lager boxes import boxes.json --yes # Merge imported boxes with existing lager boxes import new-boxes.json --merge --yes ``` ### `lock` Lock a box to prevent other users from using it. See [Box Locking](/source/reference/cli/locking) for full details. ```bash theme={null} lager boxes lock --box NAME ``` **Options:** * `--box` (required) - Name of the box to lock **Example:** ```bash theme={null} lager boxes lock --box my-lager-box ``` ### `unlock` Unlock a box to allow other users to use it. See [Box Locking](/source/reference/cli/locking) for full details. ```bash theme={null} lager boxes unlock --box NAME [--force] ``` **Options:** * `--box` (required) - Name of the box to unlock * `--force` - Force unlock even if locked by another user **Examples:** ```bash theme={null} # Unlock your own lock lager boxes unlock --box my-lager-box # Force unlock another user's lock lager boxes unlock --box my-lager-box --force ``` *** ## Configuration Storage Box configurations are stored in `.lager` file in your project directory: ```json theme={null} { "boxes": { "my-lager-box": { "ip": "", "user": "lagerdata", "version": "main" }, "pi-box": "" } } ``` Entries can be: * **Simple**: Just an IP address string * **Full**: Object with ip, user, and version fields *** ## Validation The boxes commands perform validation: * **Duplicate detection**: Prevents adding boxes with same name or IP * **IP validation**: Validates IP address format * **Confirmation**: Shows before/after state for edit operations *** ## Examples ```bash theme={null} # Set up a new bench lager boxes add --name my-lager-box --ip --user lager lager boxes add --name staging-box --ip --user lager lager boxes add --name pi-box --ip --user lager # Export configuration for team sharing lager boxes export -o bench-config.json # Import on another machine lager boxes import bench-config.json --yes # Clean up lager boxes delete-all --yes ``` *** ## Notes * Box names must be unique * IP addresses must be unique (no duplicate IPs) * `--user` is required when adding a box (there is no default SSH user) * Use `--merge` when importing to preserve existing boxes * Sync command requires Lager Boxes to be online and accessible # DAC Source: https://docs.lagerdata.com/source/reference/cli/dac Control DAC output on box Control DAC (Digital-to-Analog Converter) output on your device. The DAC command sets a precise analog voltage on a DAC net, useful for generating reference voltages, bias signals, or test stimuli. ## Syntax ```bash theme={null} lager dac [OPTIONS] NET [VOLTAGE] ``` ## Global 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 | ## Arguments * `NET` - Name of the DAC Net to control * `VOLTAGE` - Voltage value in volts to output ## Supported Hardware | Manufacturer | Model | Channels | Output Range | Resolution | | --------------------- | ------- | ------------- | ------------ | ---------- | | LabJack | T7 | 2 (DAC0-DAC1) | 0 to 5 V | 16-bit | | Measurement Computing | USB-202 | 2 (DAC0-DAC1) | 0 to 5 V | 12-bit | ### Channel Naming When creating DAC nets, the channel name depends on the hardware: **LabJack T7:** `DAC0`, `DAC1`, or numeric `0`, `1` **MCC USB-202:** `DAC0`, `DAC1`, `AOUT0`, `AOUT1`, or numeric `0`, `1` ## Default Net Set a default DAC net to avoid specifying the name each time: ```bash theme={null} lager defaults add --dac-net VOLTAGE_OUTPUT ``` Then: ```bash theme={null} lager dac 3.3 ``` ## Examples ```bash theme={null} # Set DAC output to 3.3V lager dac VOLTAGE_OUTPUT 3.3 --box my-lager-box # Set DAC output to 5.0V lager dac POWER_RAIL 5.0 --box my-lager-box # Set DAC output to 1.8V lager dac REFERENCE_VOLTAGE 1.8 --box my-lager-box # Set DAC output to 0V lager dac SIGNAL_GENERATOR 0.0 --box my-lager-box ``` ## Troubleshooting | Issue | Cause | Fix | | ------------------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Output stuck at 0V | Net not configured or wrong channel | Verify net configuration with `lager nets --box ` | | Voltage inaccurate | Resolution limit or load effects | Both LabJack T7 and USB-202 output 0-5V; verify your circuit doesn't draw too much current from the DAC output | | "Net not found" | Typo or net not created | Check available nets with `lager nets --box `. Net names are case-sensitive. | ## Notes * Net names (e.g., `VOLTAGE_OUTPUT`, `POWER_RAIL`) refer to names assigned when setting up your testbed * Voltage values are specified in volts * Only works with nets of type `dac` * Ensure the target box has DAC nets properly configured * DAC outputs provide precise voltage control for analog circuits * Both supported hardware models output 0-5V * USB-202 channels can be specified as `0`-`1`, `DAC0`-`DAC1`, or `AOUT0`-`AOUT1` ## See Also * [ADC](/source/reference/cli/adc) -- Analog-to-digital converter input (the complement of DAC) * [Python DAC API](/source/reference/python/dac) -- Set DAC output in Python scripts # Debug Source: https://docs.lagerdata.com/source/reference/cli/debug Debug firmware and manage debug sessions Control debugger operations for embedded development including flashing, GDB server management, memory access, and RTT logging. ## Syntax ```bash theme={null} lager debug [OPTIONS] [NET_NAME] COMMAND [ARGS]... ``` ## Global Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | ------------ | ---------------------------------- | | `gdbserver` | Start JLinkGDBServer for debugging | | `disconnect` | Stop JLinkGDBServer | | `flash` | Flash firmware to target | | `reset` | Reset target device | | `erase` | Erase all flash memory | | `memrd` | Read memory from target | | `status` | Show debug net status | | `health` | Check debug service health | ## Command Reference ### `gdbserver` Start JLinkGDBServer for remote debugging. This is the primary command to establish a debug connection. ```bash theme={null} lager debug [NET_NAME] gdbserver [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--force / --no-force` - Force new connection (default: reuse existing) * `--halt / --no-halt` - Halt device when connecting (default: no-halt) * `--speed KHZ` - SWD/JTAG speed in kHz (e.g., 100, 4000) or "adaptive" * `--quiet` - Suppress informational messages * `--json` - Output results in JSON format * `--rtt` - Automatically stream RTT logs after starting GDB server * `--rtt-reset` - Reset device then stream RTT (captures boot sequence) * `-i, --interactive` - Bi-directional RTT: forward stdin to the target's RTT down-channel while streaming the up-channel to stdout (requires `--rtt` or `--rtt-reset`) * `--rtt-channel N` - RTT channel to stream, in both directions (default: `0`) * `--reset` - Reset device after starting GDB server * `--gdb-port PORT` - Override the auto-allocated GDB server port. By default the box picks a port from the probe's slot (2331 for the first probe, 2334 for the second). Pass this only when you need a specific port. Avoid it on multi-probe boxes. * `--rtt-search-addr HEX` - RAM start address for the RTT control block search (hex, e.g., `0x20020000`) * `--rtt-search-size HEX` - Size of the RAM region to search for the RTT control block (hex, e.g., `0x4000`) * `--rtt-chunk-size HEX` - Read chunk size for the RTT search (hex, e.g., `0x1000`) **Examples:** ```bash theme={null} # Start GDB server on default debug net lager debug gdbserver --box my-lager-box # Start GDB server on specific net with halt lager debug debug1 gdbserver --box my-lager-box --halt # Start GDB server and stream RTT logs lager debug gdbserver --box my-lager-box --rtt # Capture boot sequence via RTT lager debug gdbserver --box my-lager-box --rtt-reset # Send commands to the target while reading its RTT output lager debug gdbserver --box my-lager-box --rtt --interactive # Use custom speed and port lager debug gdbserver --box my-lager-box --speed 4000 --gdb-port 3333 ``` **Connecting with GDB:** ```bash theme={null} # After starting gdbserver, connect with: arm-none-eabi-gdb firmware.elf -ex 'target remote :2331' ``` ### `disconnect` Stop JLinkGDBServer and free debug resources. ```bash theme={null} lager debug [NET_NAME] disconnect [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--keep-server` - Keep JLinkGDBServer running for external connections **Examples:** ```bash theme={null} # Stop GDB server completely lager debug disconnect --box my-lager-box # Disconnect but keep server running lager debug disconnect --box my-lager-box --keep-server ``` ### `flash` Flash firmware to target. Supports Intel HEX, ELF, and binary file formats. ```bash theme={null} lager debug [NET_NAME] flash [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--hex FILE` - Path to Intel HEX file * `--elf FILE` - Path to ELF executable * `--bin ADDRESS FILE` - Path to binary file with load address * `--verbose` - Show detailed J-Link output * `--force-reconnect` - Force clean reconnect before flash * `--no-erase` - Skip the erase step (by default `flash` erases before programming for a clean state) * `--halt / --no-halt` - Halt device after flashing (default: no-halt) `flash` erases before programming **by default**, so no flag is needed for a clean state (this is what RTT initialization wants). Pass `--no-erase` only when you intentionally want to preserve existing flash contents. The older `--erase` flag is now a no-op kept for backward compatibility. **Examples:** ```bash theme={null} # Flash Intel HEX file lager debug flash --hex build/firmware.hex --box my-lager-box # Flash ELF file lager debug flash --elf build/firmware.elf --box my-lager-box # Flash binary with base address lager debug flash --bin 0x08000000 build/firmware.bin --box my-lager-box # Flash while preserving existing flash contents (skip the default erase) lager debug flash --hex build/firmware.hex --no-erase --box my-lager-box # Flash and halt for debugging lager debug flash --elf build/firmware.elf --halt --box my-lager-box # Verbose output for troubleshooting lager debug flash --hex build/firmware.hex --verbose --box my-lager-box ``` ### `reset` Reset the target device. ```bash theme={null} lager debug [NET_NAME] reset [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--halt / --no-halt` - Halt after reset (default: no-halt) * `--force-reconnect` - Force clean reconnect before reset **Examples:** ```bash theme={null} # Reset and run lager debug reset --box my-lager-box # Reset and halt (for debugging) lager debug reset --halt --box my-lager-box # Force clean state before reset lager debug reset --force-reconnect --box my-lager-box ``` ### `erase` Erase all flash memory on target. **This is a destructive operation.** ```bash theme={null} lager debug [NET_NAME] erase [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--speed KHZ` - SWD/JTAG speed in kHz (default: 4000) * `--yes` - Skip confirmation prompt * `--quiet` - Suppress warning messages * `--json` - Output results in JSON format * `--halt / --no-halt` - Halt after erase (default: no-halt) **Examples:** ```bash theme={null} # Erase with confirmation prompt lager debug erase --box my-lager-box # Erase without confirmation lager debug erase --box my-lager-box --yes # Erase and halt afterward lager debug erase --box my-lager-box --yes --halt ``` ### `memrd` Read memory from the target device. ```bash theme={null} lager debug [NET_NAME] memrd START_ADDR LENGTH [OPTIONS] ``` **Arguments:** * `START_ADDR` - Starting memory address (e.g., 0x20000000) * `LENGTH` - Number of bytes to read **Options:** * `--box TEXT` - Lager Box name or IP * `--json` - Output results in JSON format * `--halt / --no-halt` - Halt device during read (default: no-halt). `--no-halt` overrides the auto-halt for DA1469x QSPI XIP * `--no-reset` - **DA1469x only.** Skip the reset+halt the box performs before the read. A running DA1469x has SWD disabled, so without the reset the read fails — use this only on a blank/awake part to avoid rebooting it **Examples:** ```bash theme={null} # Read 16 bytes of SRAM lager debug memrd 0x20000000 16 --box my-lager-box # Read with device halted (more reliable) lager debug memrd 0x20000000 64 --halt --box my-lager-box # Output as JSON lager debug memrd 0x08000000 32 --json --box my-lager-box # DA1469x: read a blank/awake part without rebooting it lager debug memrd 0x20000000 16 --no-reset --box my-lager-box ``` **Output:** ``` 0x20000000: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x20000008: 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f ``` ### `status` Show debug net status and configuration information. ```bash theme={null} lager debug [NET_NAME] status [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP **Examples:** ```bash theme={null} lager debug status --box my-lager-box lager debug debug1 status --box my-lager-box ``` **Output:** ``` Debug Net Information: Name: debug1 Device Type: STM32F407VG Architecture: ARM Cortex-M4 Probe: J-Link GDB server running: True Target attached: Yes ``` `GDB server running` is about the process on the box. `Target attached` is about the part: the box reads the target to answer it. The two fields differ, so the CLI reports them separately. A gdbserver can outlive the device it drove, and a command that flashes or erases cares about the second field, not the first. `Target attached` reads `Unknown` when the box cannot establish an answer. Two cases give that result: a box older than this field, or a probe that cannot run because a debugger already holds the session. `Unknown` does not mean the target is absent. ### `health` Check debug service health and resource usage. ```bash theme={null} lager debug [NET_NAME] health [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--verbose` - Show detailed health information **Examples:** ```bash theme={null} # Basic health check lager debug health --box my-lager-box # Detailed health information lager debug health --box my-lager-box --verbose ``` **Output (verbose):** ``` Debug Service Health: Status: healthy Version: 1.2.0 Uptime: 2.5 days (216000s) J-Link Running: True J-Link PID: 12345 GDB Controllers Cached: 1 Active Connections: 1 ``` ## Listing Debug Nets When invoked with only `--box` and no subcommand, lists all debug nets on the Lager Box: ```bash theme={null} lager debug --box my-lager-box ``` **Output:** ``` Name Net Type Instrument Channel Address debug1 debug J-Link STM32F407VG USB::001::002 debug2 debug CMSIS-DAP nRF52840 USB::001::003 ``` ## RTT (Real-Time Transfer) Logging RTT provides low-latency logging over the debug probe. Use the `--rtt` or `--rtt-reset` flags with `gdbserver`: ```bash theme={null} # Stream RTT after connecting lager debug gdbserver --box my-lager-box --rtt # Reset device and capture boot messages lager debug gdbserver --box my-lager-box --rtt-reset # Pipe to defmt-print for formatted output lager debug gdbserver --box my-lager-box --rtt 2>/dev/null | defmt-print -e firmware.elf ``` ### Decoding defmt logs Most Rust (and much C) firmware logs via [defmt](https://defmt.ferrous-systems.com/), a compressed binary format. **Raw RTT bytes from defmt firmware are not human-readable.** `defmt-print` must decode them, and it needs the *exact* ELF that is flashed on the target. ```bash theme={null} # Flash the build under test, then stream + decode a bounded window lager debug SWD flash --elf build/app.elf --box my-lager-box timeout 15 lager debug SWD gdbserver --box my-lager-box --rtt-reset 2>/dev/null \ | defmt-print -e build/app.elf ``` Two things to watch: * **Redirect stderr.** The RTT payload is written to **stdout**; status messages (`JLinkGDBServer started!`, etc.) go to **stderr**. Pipe stdout only — append `2>/dev/null` (or `2>debug.log`) so status lines never corrupt `defmt-print`'s input. * **The stream never ends.** `--rtt` runs until the process is killed. In scripts or non-interactive sessions, wrap it in `timeout ` to capture a fixed window. When you kill the `lager` process, the pipe closes and `defmt-print` exits on EOF. Install `defmt-print` with `cargo install defmt-print` on the machine where you run the pipe (the same machine that holds the `.elf`). ### Interactive (bi-directional) RTT Add `--interactive` to send data *to* the target as well as read from it. Whatever you type on stdin is forwarded to the target's RTT down-channel, which is how firmware that exposes a command console over RTT is driven: ```bash theme={null} lager debug SWD gdbserver --box my-lager-box --rtt --interactive ``` stdout remains the raw up-channel byte stream, so this composes with `defmt-print` exactly as plain `--rtt` does: ```bash theme={null} lager debug SWD gdbserver --box my-lager-box --rtt --interactive 2>/dev/null \ | defmt-print -e build/app.elf ``` Your keystrokes are echoed by your terminal, not written to stdout, so they never reach the decoder. Use `--rtt-channel N` to work on a channel other than 0; the same channel is used in both directions. The firmware must declare an RTT **down** buffer on the channel you use. The `defmt-rtt` crate sets up only the up buffer. With no down buffer, the target discards what it receives and gives no indication that it did so. That reads as a host-side failure, but it is not one. Firmware using `rtt-target`'s down channel, or a `SEGGER_RTT` down buffer, works as expected. `--interactive` expects a terminal. In scripts and other non-interactive contexts, keep using plain `--rtt` with `timeout`. Only one interactive session can be attached to a given probe and channel at a time, because the underlying RTT connection accepts a single client. A second attempt is refused rather than silently taking the stream from the first. ## Typical Workflows ### Development Cycle ```bash theme={null} # Flash and debug lager debug flash --elf build/app.elf --box my-lager-box lager debug gdbserver --box my-lager-box --halt # In another terminal, connect GDB arm-none-eabi-gdb build/app.elf -ex 'target remote :2331' ``` ### RTT Debugging ```bash theme={null} # Flash for a clean RTT state (erase happens by default) lager debug flash --elf build/app.elf --box my-lager-box # Start GDB server and stream RTT with boot capture lager debug gdbserver --box my-lager-box --rtt-reset ``` ### Memory Inspection ```bash theme={null} # Halt device and read memory lager debug gdbserver --box my-lager-box --halt lager debug memrd 0x20000000 256 --box my-lager-box ``` ### Clean Up ```bash theme={null} # Stop debug session lager debug disconnect --box my-lager-box # Full chip erase before new project lager debug erase --box my-lager-box --yes ``` ## JLinkScript Support JLinkScript files allow you to customize J-Link debug probe behavior for specific hardware configurations. They can handle custom reset sequences, clock initialization, pin configurations, and other device-specific operations that the standard J-Link connection flow does not cover. ### Configuring JLinkScript There are three ways to attach a J-Link script to a debug net: **1. During net creation:** ```bash theme={null} lager nets add debug1 debug STM32F407VG USB::001::002 \ --jlink-script ./my_device.JLinkScript --box my-lager-box ``` **2. On an existing net:** ```bash theme={null} lager nets set-script debug1 ./my_device.JLinkScript --box my-lager-box ``` **3. Per-project in `.lager` config:** ```json theme={null} { "DEBUG": { "debug1": "./scripts/my_device.JLinkScript" } } ``` ### Script Priority When both a net-level script (stored on the box via `set-script`) and a project-level script (in `.lager` config) exist, the project-level script takes priority. This allows you to override the box-stored script for specific projects. ### Managing Scripts ```bash theme={null} # View attached script lager nets show-script debug1 --box my-lager-box # Save script to local file lager nets show-script debug1 --box my-lager-box > script.JLinkScript # Remove script from net lager nets remove-script debug1 --box my-lager-box ``` Once attached, the script is used automatically for all debug operations (connect, flash, erase, reset) without any additional flags. ## Supported Debug Probes | Probe | Backend | Notes | | ----------- | -------------- | -------------------------------------- | | J-Link | JLinkGDBServer | Full feature support, RTT, JLinkScript | | J-Link Plus | JLinkGDBServer | Full feature support, RTT, JLinkScript | | CMSIS-DAP | pyOCD | Open source, wide device support | | ST-Link | pyOCD | STM32 devices | | Flasher ARM | JLinkGDBServer | Production programming | ## Supported Device Families Lager supports 70+ ARM Cortex-M device families with automatic architecture detection. The device type is specified as the channel when creating a debug net (e.g., `STM32F407VG`, `nRF52840`). ### Cortex-M0/M0+ (ARMv6-M) | Family | Manufacturer | | ---------------------- | -------------------- | | RP2040 | Raspberry Pi | | nRF51 | Nordic Semiconductor | | STM32C0 | STMicroelectronics | | STM32F0 | STMicroelectronics | | STM32G0 | STMicroelectronics | | STM32L0 | STMicroelectronics | | LPC8xx, LPC11xx | NXP | | ATSAMD, ATSAML, ATSAMC | Microchip/Atmel | | EFM32 Zero Gecko | Silicon Labs | ### Cortex-M3 (ARMv7-M) | Family | Manufacturer | | -------------------------------- | ------------------ | | STM32F1 | STMicroelectronics | | STM32F2 | STMicroelectronics | | STM32L1 | STMicroelectronics | | LPC13xx, LPC17xx, LPC18xx | NXP | | LM3, LM4F (Stellaris/Tiva-C) | Texas Instruments | | EFM32 Giant/Leopard/Wonder Gecko | Silicon Labs | ### Cortex-M4/M7 (ARMv7E-M) | Family | Manufacturer | | ------------------------------ | -------------------- | | nRF52 | Nordic Semiconductor | | STM32F3 | STMicroelectronics | | STM32F4 | STMicroelectronics | | STM32F7 | STMicroelectronics | | STM32G4 | STMicroelectronics | | STM32H7 | STMicroelectronics | | STM32L4 | STMicroelectronics | | STM32WB | STMicroelectronics | | STM32WL | STMicroelectronics | | MKxxxx (Kinetis K) | NXP | | LPC4xxx, LPC54xxx | NXP | | MIMXRT (i.MX RT) | NXP | | TM4C | Texas Instruments | | MSP432 | Texas Instruments | | CC26xx, CC13xx | Texas Instruments | | ATSAM4, ATSAME, ATSAMS, ATSAMV | Microchip/Atmel | | EFM32, EFR32 | Silicon Labs | | CY, PSoC | Infineon | | DA145x, DA146x, DA148x | Dialog Semiconductor | ### Cortex-M23 (ARMv8-M Base) | Family | Manufacturer | | -------- | ------------ | | LPC55S0x | NXP | ### Cortex-M33/M55 (ARMv8-M Main) | Family | Manufacturer | | ----------------- | -------------------- | | nRF53 | Nordic Semiconductor | | nRF91 | Nordic Semiconductor | | STM32L5 | STMicroelectronics | | STM32U5 | STMicroelectronics | | STM32H5 | STMicroelectronics | | STM32WBA | STMicroelectronics | | LPC55S | NXP | | R7FA (Renesas RA) | Renesas | Devices not in the table above default to Cortex-M4 (ARMv7E-M) architecture. If your device is not detected correctly, specify the full device part number (e.g., `STM32F407VG` rather than just `STM32F4`) when creating the debug net. ## Notes * Debug nets are created with `lager nets add debug
` * The system auto-connects when needed for commands like `flash` and `reset` * `flash` erases before programming by default, giving a clean state for RTT initialization (use `--no-erase` to opt out) * RTT streaming requires the device to have RTT support in firmware * Memory reads are more reliable with `--halt` to pause the CPU * Use `lager debug health --verbose` to diagnose connection issues * JLinkScript files are base64-encoded for storage and decoded automatically on the box ## See Also * [Python Debug API](/source/reference/python/debug) -- Automate flashing and debugging in Python scripts * [Python Command](/source/reference/cli/python) -- Run test scripts on the box * [Glossary](/source/getting-started/glossary) -- Definitions of GDB, SWD, and other terms # Defaults Source: https://docs.lagerdata.com/source/reference/cli/defaults Manage default settings for CLI commands Set default values for Lager Box, nets, and other CLI options to simplify commands. ## Syntax ```bash theme={null} lager defaults COMMAND [OPTIONS] ``` ## Commands | Command | Description | | ------------ | -------------------------------- | | `add` | Set default values | | `list` | List current default settings | | `delete` | Delete specific default settings | | `delete-all` | Delete all default settings | *** ## Command Reference ### `add` Set default values for various options. ```bash theme={null} lager defaults add [OPTIONS] ``` **Lager Box Options:** * `--box BOX` - Set default Lager Box **Net Options:** * `--supply-net NAME` - Default power supply net * `--battery-net NAME` - Default battery net * `--solar-net NAME` - Default solar net * `--scope-net NAME` - Default oscilloscope net * `--logic-net NAME` - Default logic analyzer net * `--adc-net NAME` - Default ADC net * `--dac-net NAME` - Default DAC net * `--gpio-net NAME` - Default GPIO net * `--debug-net NAME` - Default debug net * `--eload-net NAME` - Default electronic load net * `--usb-net NAME` - Default USB hub net * `--webcam-net NAME` - Default webcam net * `--watt-meter-net NAME` - Default watt meter net * `--thermocouple-net NAME` - Default thermocouple net * `--uart-net NAME` - Default UART net * `--arm-net NAME` - Default robotic arm net **Other Options:** * `--serial-port PATH` - Default serial port path * `--user TEXT` - Default username for box locking **Examples:** ```bash theme={null} # Set default Lager Box lager defaults add --box my-lager-box # Set default power supply net lager defaults add --supply-net VDD_MAIN # Set multiple defaults at once lager defaults add --box my-lager-box --supply-net POWER --debug-net DEBUG_SWD # Set the default username used for box locking lager defaults add --user alice ``` ### `list` Display all current default settings. ```bash theme={null} lager defaults list ``` Output: ``` Current defaults: box: my-lager-box supply-net: VDD_MAIN battery-net: VBAT debug-net: DEBUG_SWD serial-port: /dev/ttyUSB0 ``` ### `delete` Delete specific default settings. The `delete` command has subcommands for each type of default. ```bash theme={null} lager defaults delete SUBCOMMAND [OPTIONS] ``` **Available subcommands:** * `box` - Delete default Lager Box * `serial-port` - Delete default serial port * `supply-net` - Delete default supply net * `battery-net` - Delete default battery net * `solar-net` - Delete default solar net * `scope-net` - Delete default scope net * `logic-net` - Delete default logic analyzer net * `adc-net` - Delete default ADC net * `dac-net` - Delete default DAC net * `gpio-net` - Delete default GPIO net * `debug-net` - Delete default debug net * `eload-net` - Delete default electronic load net * `usb-net` - Delete default USB hub net * `webcam-net` - Delete default webcam net * `watt-meter-net` - Delete default watt meter net * `thermocouple-net` - Delete default thermocouple net * `uart-net` - Delete default UART net * `arm-net` - Delete default robotic arm net * `user` - Delete default user **Options (for all subcommands):** * `--yes` - Skip confirmation prompt **Examples:** ```bash theme={null} # Delete default Lager Box lager defaults delete box # Delete default supply net without confirmation lager defaults delete supply-net --yes # Delete default serial port lager defaults delete serial-port ``` ### `delete-all` Delete all default settings. ```bash theme={null} lager defaults delete-all ``` *** ## How Defaults Work When you run a command without specifying an option, the CLI checks for a default: ```bash theme={null} # Without defaults - must specify everything lager supply VDD_MAIN voltage 3.3 --box my-lager-box # With defaults set lager defaults add --box my-lager-box --supply-net VDD_MAIN # Now you can simply run lager supply voltage 3.3 ``` *** ## Default Resolution Order 1. Command-line option (highest priority) 2. Default setting from `lager defaults` 3. Error if required and no default *** ## Configuration Storage Defaults are stored in the `.lager` configuration file: ```ini theme={null} [LAGER] default_lager_box = my-lager-box default_supply_net = VDD_MAIN default_battery_net = VBAT default_serial_port = /dev/ttyUSB0 ``` *** ## Workflow Example ```bash theme={null} # Initial setup - set your common defaults lager defaults add --box my-bench lager defaults add --supply-net POWER lager defaults add --debug-net SWD lager defaults add --uart-net SERIAL # Now commands are simpler lager supply voltage 3.3 # Uses POWER net on my-bench lager debug flash fw.hex # Uses SWD net on my-bench lager uart --interactive # Uses SERIAL net on my-bench # Override defaults when needed lager supply OTHER_SUPPLY voltage 5.0 lager debug flash fw.hex --box other-lager-box ``` *** ## Notes * Defaults are per-project (stored in `./.lager`) * Net names must match configured nets on the Lager Box * Box names are validated against saved boxes * Use `lager defaults list` to verify current settings # Devenv Source: https://docs.lagerdata.com/source/reference/cli/devenv Manage a local Docker-based development environment for your project `lager devenv` manages a reproducible, Docker-based development environment for a project. It records the image, mount directory, shell, saved commands, bind mounts, and environment variables in the `DEVENV` section of your project's `.lager` config file. Every engineer, and your CI, then builds and tests in the same container. The environment is consumed by [`lager exec`](/source/reference/cli/exec) (run a command in the container) and `lager devenv terminal` (open an interactive shell in the container). ## Syntax ```bash theme={null} lager devenv COMMAND [ARGS]... ``` ## Subcommands | Command | Description | | ----------------------- | ---------------------------------------------------- | | `create` | Create the `DEVENV` config (image, mount dir, shell) | | `terminal` | Open an interactive shell in the container | | `show` | Print the resolved `DEVENV` configuration | | `set` | Set a scalar config key (or append to `ports`) | | `unset` | Remove a config key entirely | | `add` | Save a named command | | `delete` | Remove a saved command | | `commands` | List saved commands | | `mount add/remove/list` | Manage persistent bind-mounts / volumes | | `env set/unset/list` | Manage persistent environment variables | ## Prerequisites Docker must be installed and on your `PATH`. Install it from [docker.com](https://docs.docker.com/get-docker/). On Linux, add your user to the `docker` group so you don't need `sudo`: ```bash theme={null} sudo usermod -aG docker $USER # then log out/in, or run: newgrp docker ``` *** ## create Create the `DEVENV` section in your project's `.lager` config. Run this once per project before using `lager exec` or `lager devenv terminal`. ```bash theme={null} lager devenv create ``` | Option | Default | Description | | ------------------ | -------------------------- | ------------------------------------------------------ | | `--image TEXT` | `lagerdata/devenv-cortexm` | Docker image to use | | `--mount-dir TEXT` | `/app` | Where your source code is mounted inside the container | | `--shell TEXT` | `/bin/bash` | Shell executable inside the image | The image name is validated against standard Docker naming (`name`, `name:tag`, `registry/name`, `registry/name:tag`). For `lagerdata/*` images the shell defaults to `/bin/bash`; for other images you'll be prompted. If a `DEVENV` section already exists you'll be asked before overwriting it. *** ## terminal Open an interactive shell inside the development container. Your project directory is bind-mounted at the configured `mount_dir`, and the container is removed on exit (unless `--detach` is used). ```bash theme={null} lager devenv terminal ``` | Option | Short | Description | | ------------------------ | ----- | --------------------------------------------------------------------------- | | `--mount TEXT` | `-m` | Mount a named Docker volume at `mount_dir` instead of the source dir | | `--user TEXT` | `-u` | User to run as (overrides the `user` config key) | | `--group TEXT` | `-g` | Group to run as (overrides the `group` config key) | | `--name TEXT` | `-n` | Set the container name | | `--detach / --no-detach` | `-d` | Run the container detached | | `--port TEXT` | `-p` | Publish a port (`HOST:CONTAINER`). Repeatable | | `--entrypoint TEXT` | | Override the container entrypoint | | `--network TEXT` | | Docker network mode | | `--platform TEXT` | | Target platform (e.g. `linux/amd64`) | | `--attach TEXT` | `-a` | Attach a shell to an already-running container by name | | `--shell TEXT` | `-s` | Shell to use when attaching (default: config shell or `/bin/bash`) | | `--volume TEXT` | `-v` | Bind-mount a host path (`HOST:CONTAINER[:ro]`). Repeatable | | `--env FOO=BAR` | `-e` | Set an environment variable. Repeatable | | `--passenv NAME` | | Pass a variable through from your current shell. Repeatable | | `--info` | | Print the resolved `docker` command and config, then exit without launching | CLI flags take precedence over the matching keys stored in the `DEVENV` config. The CLI applies the config-defined `ports`, `volumes`, and `environment` first, then appends anything you pass on the command line. `terminal` also wires up SSH for you automatically. It forwards your `SSH_AUTH_SOCK` agent socket, and it mounts `~/.ssh/id_ed25519` and `~/.ssh/known_hosts` (read-only) when they exist. It also mounts your global `.lager` config into the container, so nested `lager` calls are authenticated. Use `--info` to see what runs, without launching anything: ```bash theme={null} lager devenv terminal --info ``` ### Attach to a running container ```bash theme={null} # Open a second shell in a container started with --detach --name build lager devenv terminal --attach build --shell /bin/bash ``` *** ## show Print the resolved `DEVENV` configuration — scalar keys, list keys (ports, volumes, environment), and saved commands. ```bash theme={null} lager devenv show ``` *** ## set / unset Set or remove individual configuration keys without re-running `create`. ```bash theme={null} lager devenv set image lagerdata/devenv-cortexm:latest lager devenv set mount_dir /workspace lager devenv set port 8080:8080 # appends to the ports list lager devenv unset platform ``` Scalar keys (replaced on `set`): `image`, `mount_dir`, `shell`, `user`, `group`, `entrypoint`, `hostname`, `macaddr`, `network`, `platform`, `repo_root_relative_path`. The `ports` key is a list and is **appended** to (the singular alias `port` is accepted). Edit `volumes` and `environment` with `lager devenv mount` and `lager devenv env` respectively — `set` will refuse them and point you at the right command. *** ## Saved commands: add / delete / commands Save shell commands under a name so they can be run with [`lager exec `](/source/reference/cli/exec). Commands are stored as `cmd.` keys in the `DEVENV` section. ```bash theme={null} lager devenv add build "make -j4" lager devenv add test "pytest tests/ --tb=short" lager devenv commands # list saved commands lager devenv delete build # remove one ``` Command names can contain only letters, numbers, dashes, and underscores. If you omit the command string, `add` prompts for it. | Subcommand | Option | Description | | ---------- | ---------------------- | ------------------------------------------------------------------ | | `add` | `--warn` / `--no-warn` | Warn when overwriting an existing command (default: `--warn`) | | `delete` | `--devenv NAME` | Delete the command from a named devenv rather than the current one | *** ## Persistent bind-mounts: mount Persist host bind-mounts / named volumes in the config so they're applied on every `lager devenv terminal` and `lager exec` run. ```bash theme={null} lager devenv mount add /host/cache:/root/.cache # host bind-mount lager devenv mount add toolchain:/opt/toolchain # named volume lager devenv mount add /etc/ssl/certs:/etc/ssl/certs:ro lager devenv mount list lager devenv mount remove /host/cache:/root/.cache ``` Specs use Docker `-v` form: `HOST:CONTAINER[:ro]` for a bind-mount or `NAME:CONTAINER` for a named volume. For portability across machines, specs can use `~`, environment variables, and `${PROJECT_ROOT}` (which expands to your project's `.lager` directory). *** ## Persistent environment variables: env Persist environment variables in the config so they're set on every run. ```bash theme={null} lager devenv env set CFLAGS=-O2 lager devenv env set DEBUG=0 lager devenv env list lager devenv env unset DEBUG ``` `env set` replaces any existing value for the same variable. *** ## How it relates to `lager exec` | Command | Purpose | | ------------------------------------------ | ------------------------------------------------------------------ | | `lager devenv ...` | Configure the local container (image, mounts, env, saved commands) | | `lager devenv terminal` | Open an interactive shell in that container | | [`lager exec`](/source/reference/cli/exec) | Run a one-off or saved command in that container | All three read the same `DEVENV` section. A command saved with `lager devenv add` runs under `lager exec`, and a mount added with `lager devenv mount add` applies to both `terminal` and `exec`. ## Notes * Configuration is stored in the `DEVENV` section of the nearest `.lager` config file, discovered by walking up from the current directory. * `terminal` launches the container with `--rm` by default, so it's removed on exit unless you pass `--detach`. * Exit codes from the container are propagated to the CLI. # Diagnose Source: https://docs.lagerdata.com/source/reference/cli/diagnose Single-shot diagnosis for a misbehaving instrument net `lager diagnose --box [--type ]` is a single-shot diagnosis for a misbehaving instrument net. It collapses the manual debug workflow (`lsof`, `dmesg`, bare `pyvisa` probes, hardware-service introspection) into one CLI call. That call returns an actionable classification: host-side, instrument-wedged, or healthy. Introduced in **lager 0.20.0** for USB-TMC (pyvisa) instruments. Extended in **0.28.3** to diagnose `debug` nets (SEGGER J-Link, and a basic OpenOCD/ST-Link path) — see [Debug nets (J-Link)](#debug-nets-j-link) below. ## Syntax ```bash theme={null} lager diagnose NET [OPTIONS] ``` ## Options | Option | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address (uses the default box if omitted) | | `--type ROLE` | Net role. Defaults to `auto`, which looks up the net's role from the box's saved nets. Pass an explicit role (`battery`, `power-supply`, `scope`, `debug`, `usb`, `adc`, …) to override, or to diagnose a net that isn't saved. A `debug` net routes to the [J-Link path](#debug-nets-j-link). | `NET` is the name of the net to diagnose (e.g. `battery1`, `supply1`). *** ## Usage ```bash theme={null} # Diagnose a net, auto-detecting its role from saved nets lager diagnose battery1 --box my-lager-box # Override the role explicitly lager diagnose battery1 --box my-lager-box --type battery ``` The command queries three box-side endpoints in parallel and prints a section for each, followed by a one-line classification with the next step. *** ## Output sections ### USB (host-side) From `GET /diagnose/usb` on box port 9000. Reports: * `enumerated` — does the device show up on the host's USB bus? * `sysfs` — kernel sysfs path (e.g. `/sys/bus/usb/devices/1-4`). * `device` — `/dev/bus/usb/BBB/DDD` path used by `lsof`/`fuser`. * `usbtmc` — whether the `usbtmc` kernel module is loaded (and therefore racing libusb for interface 0). * `lsof` — `command(pid)` list of processes holding the USB device file. * `dmesg tail` — last few USB / usbtmc kernel messages. ### VISA (instrument-side) From `GET /diagnose/visa` on box port 9000. Opens a *fresh* `pyvisa` session and queries `*IDN?` with a short timeout. If the hardware service already holds a shared session for this address, it skips the open and says so. A collision will either hang or return garbage. Reports: * `idn` — the IDN string if the instrument answered. * `elapsed` — wall-clock ms. * `error` / `error_class` — classified as `busy`, `nodev`, `timeout`, or `other`. * `skipped` — set when the hardware service holds the address. ### Dispatcher (hw\_service in-process) From `GET /diagnose/dispatcher` on hardware-service port 8080. Reports the in-process state for this address: * `cached_session` — whether the shared `pyvisa` session pool has it. * `cached_drivers` — driver instances cached against this address. * `shared_pool` — total pool size. *** ## Classifications The decision tree, in order (first match wins): | Classification | Trigger | | -------------------------------------------------------- | --------------------------------------------------------------------------------- | | `HOST-SIDE: usbtmc kernel module loaded` | the `usbtmc` kernel module is bound (run `lager update` to install the blacklist) | | `HOST-SIDE: USB device claimed by multiple processes` | VISA `busy` and two or more holders in `lsof` | | `HOST-SIDE: USB device busy` | VISA `busy` with a single holder | | `TRANSIENT: device disappeared from USB` | VISA `nodev` (re-enumeration) | | `INSTRUMENT WEDGED` | VISA `timeout` — enumerates and opens, but won't answer `*IDN?` | | `NOT ENUMERATED` | the device does not show up on USB (check power/cable) | | `REACHABLE` | `*IDN?` returned (IDN string shown) | | `REACHABLE (shared session)` | the open was skipped because hw\_service holds an active session | | `TRANSIENT: enumerated as USB-TMC but fresh open failed` | USB-TMC class but the fresh pyvisa open failed | | `NOT USB-TMC` | a vendor-SDK instrument (LabJack/LJM, Picoscope, Acroname, …), not pyvisa | | `UNCLEAR` | fallback — review the per-section output | *** ## Sample session ``` $ lager diagnose battery1 --box my-lager-box lager diagnose — my-lager-box → battery1 NetType: battery address: USB0::0x05E6::0x2281::4518305::INSTR == USB (host-side) == enumerated: True usbtmc kmod: not loaded (good) lsof: no holders == VISA (instrument-side) == idn: KEITHLEY INSTRUMENTS,MODEL 2281S-20-6,4518305,01.08b elapsed: 429 ms == Dispatcher (hw_service in-process) == cached_session: False shared_pool: 0 entry/entries Classification: REACHABLE — IDN: KEITHLEY INSTRUMENTS,MODEL 2281S-20-6,4518305,01.08b. USB, the VISA session and *IDN? are all good. This does not exercise the instrument's function (e.g. whether a supply will actually enable its output). ``` A wedged instrument surfaces clearly so you stop trying software-only recoveries: ``` Classification: INSTRUMENT WEDGED: device enumerates and accepts session open, but won't respond to *IDN?. The instrument firmware is stuck — a mains-side power-cycle of the instrument itself is required. ``` Vendor-SDK instruments (LabJack, Picoscope, Acroname) don't go through pyvisa, so `lager diagnose` points you at the role-specific command instead of returning a misleading `UNCLEAR`. *** ## Debug nets (J-Link) A `debug` net isn't USB-TMC, so the pyvisa `*IDN?` probe above can't reach it. When the net's role is `debug` (auto-detected, or forced with `--type debug`), `lager diagnose` takes a J-Link-aware path instead. It fetches the same host-side **USB** section plus a dedicated `/diagnose/jlink` endpoint. Then it walks the debug stack outside-in: software → USB → probe-visible → gdbserver → target connect. That order means the most specific actionable fault wins. ```bash theme={null} lager diagnose swd1 --box lab-lager-box # auto-detects the debug role lager diagnose swd1 --box lab-lager-box --type debug ``` ### J-Link / debug probe section In addition to the **USB (host-side)** section, a debug net prints a **J-Link / debug probe** section reporting: * `backend` — the probe backend (`jlink`, or an OpenOCD/ST-Link backend). * `jlink software` — whether the SEGGER J-Link tools are installed on the box. * `probe enum` — does the probe show up on the host's USB bus? * `probe visible` — does `JLinkExe` actually enumerate the probe (with the emulator product/serial list)? * `holders` — `command(pid)` of any process holding the probe (usually a stale gdbserver). * `gdbserver` — whether a J-Link gdbserver is alive, its PID, and whether its logfile looks healthy. * `connect` — the result of a target-connect probe: `connect_ok`, an error class, `VTref` (target reference voltage), and the detected `core`. The raw `JLinkExe` output is shown when the failure can't be classified. A SEGGER probe gets the full stack above. A non-J-Link OpenOCD/ST-Link probe reports a lighter `openocd-basic` section (backend, probe enumeration, and gdbserver state) — deep target diagnosis is J-Link-only for now. ### Debug classifications | Classification | Trigger | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `HEALTHY: J-Link connected to ` | target connect succeeded (core, and VTref when J-Link reports it) | | `HEALTHY: J-Link gdbserver running` | a gdbserver is up, listening, and its log is clean (active debug session) | | `J-LINK SOFTWARE MISSING on box` | the SEGGER J-Link tools aren't installed (`lager update` installs them) | | `PROBE NOT ON USB` | the probe isn't enumerated — check cable, probe power, upstream hub port | | `PROBE CLAIMED` | probe is on USB but `JLinkExe` can't see it because another process holds it (usually a stale gdbserver — `lager debug disconnect`) | | `PROBE WEDGED` | probe is on USB but `JLinkExe` enumeration is empty — power-cycle the probe | | `GDBSERVER WEDGED` | server process is up but its log shows a target-connection failure | | `TARGET UNPOWERED` | probe is fine but VTref is too low — the target board has no power on the debug header (or VTref isn't wired) | | `TARGET LOCKED` | debug access is blocked by readout/IDCODE/AP protection — a mass-erase/unlock is required (e.g. `nrfjprog --recover`) | | `DEVICE NAME` | `JLinkExe` rejected the configured device — fix the net's device/MCU field | | `NO TARGET COMMS` | probe + target power OK but SWD/JTAG connect failed — check SWDIO/SWCLK wiring, nRST pull-up, SWD-vs-JTAG, try a lower speed | | `INCONCLUSIVE` / `UNCLEAR` | connect probe was skipped or returned an unrecognized class — review the section output and rerun | ### Sample debug session ``` $ lager diagnose swd1 --box lab-lager-box --type debug lager diagnose — lab-lager-box → swd1 NetType: debug address: 50105878 == USB (host-side) == enumerated: True usbtmc kmod: not loaded (good) lsof: no holders == J-Link / debug probe == backend: jlink jlink software: installed probe enum: True probe visible: True (emus: J-Link/50105878) holders: none gdbserver: running=False pid=None log_ok=None connect: ok=True class=ok VTref=3.300V core=Cortex-M4 Classification: HEALTHY: J-Link connected to NRF52840_XXAA (Cortex-M4, VTref=3.300V). ``` A locked target surfaces clearly so you reach for the right recovery: ``` Classification: TARGET LOCKED: debug access is blocked by readout/IDCODE/AP protection. A mass-erase/unlock is required (e.g. `nrfjprog --recover` for nRF, or the vendor unlock flow). ``` *** ## Backwards compatibility Against a pre-0.20 box, each endpoint returns 404. The CLI then notes that the section is unavailable, because the box can be on a lager \< 0.20 image. The remaining sections still run — `lager diagnose` is useful against an older box, just less informative. *** ## See Also * [Instruments](/source/reference/cli/instruments) — list attached instruments and their VISA addresses * [Nets](/source/reference/cli/nets) — list saved nets and their roles * [Debug](/source/reference/cli/debug) — connect, flash, and gdbserver control for debug nets * [Hello](/source/reference/cli/hello) — basic box-side connectivity and version check # DUT Context Source: https://docs.lagerdata.com/source/reference/cli/dut Author the device-under-test context that the MCP server hands to AI agents `lager dut` manages the **DUT context** stored in `/etc/lager/bench.json` on a Lager Box. This context tells AI agents (via the [MCP server](/source/reference/mcp/overview)) what the box tests: purpose, MCU, key peripherals, subsystem groupings, and references to schematics and datasheets. See [Authoring DUT Context](/source/reference/mcp/dut-context) for the concepts and workflow. ## Syntax ```bash theme={null} lager dut [COMMAND] [OPTIONS] ``` ## Global Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | --------- | -------------------------------------------------------------- | | `show` | Print the current DUT context as JSON | | `edit` | Open the DUT context in `$EDITOR` for live editing | | `add-doc` | Attach a schematic / datasheet / firmware reference to the DUT | ## Command Reference ### Show Print the current DUT context as JSON. ```bash theme={null} lager dut show --box my-lager-box ``` ### Edit Round-trip the DUT context through `$EDITOR` (falls back to `nano`, then `vi`). On save, the new JSON is validated and written back to `/etc/lager/bench.json`. ```bash theme={null} lager dut edit --box my-lager-box ``` The editable block accepts these fields: | Field | Meaning | | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `name` | DUT slot name (e.g. `main`). | | `active` | Whether this slot is the active DUT. | | `purpose` | One-line description of what the box tests. | | `summary` | Markdown paragraph: what the DUT is, known quirks. | | `mcu` | The DUT's microcontroller (e.g. `STM32H7`). | | `key_peripherals` | List of notable peripherals. | | `schematic_refs` / `datasheet_refs` / `firmware_refs` / `extra_docs` | Lists of document references. | | `subsystems` | Functional blocks, each with `name`, `summary`, `nets`, and `doc_refs`. | ### Add Doc Attach a single document reference to the active DUT without hand-editing JSON. The box records only a pointer — it does **not** store the file. The agent fetches and analyses it with its own tools. ```bash theme={null} lager dut add-doc --kind schematic \ --title "Main board" --repo-path docs/sch.pdf --pages 3-5 --box my-lager-box ``` **Options:** | Option | Description | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `--kind` | `schematic`, `layout`, `datasheet`, `firmware`, `manual`, `errata`, or `other` (default `schematic`). | | `--title TEXT` | Human label for the document (required). | | `--url TEXT` | External URL. | | `--repo-path TEXT` | Path relative to your test project (synced to the box on `lager python`). | | `--pages TEXT` | Optional page/sheet hint, e.g. `"3-5"` or `"POWER sheet"`. | | `--notes TEXT` | Optional free-form note. | You must supply at least one of `--url` or `--repo-path`. The reference is appended to the list matching `--kind` (`schematic` → `schematic_refs`, `datasheet` → `datasheet_refs`, `firmware` → `firmware_refs`; everything else → `extra_docs`). ## After editing The MCP server reads the bench config at startup. To make agents see your changes, take one of two actions. Have a connected agent call the `box_manage` tool with `action="reload"`, which re-reads `/etc/lager/bench.json` and rebuilds the capability graph. Or restart the box service. # Electronic Load Source: https://docs.lagerdata.com/source/reference/cli/eload Control electronic load settings and modes Control electronic load Nets through the Lager CLI. Electronic loads are used to simulate various load conditions for testing power supplies, batteries, and other power sources. ## Syntax ```bash theme={null} lager eload [OPTIONS] NET_NAME COMMAND [ARGS]... ``` ## Global Options | Option | Description | | ----------- | ---------------------------- | | `--box BOX` | Lager Box name or IP address | | `--help` | Show help message and exit | Every subcommand below (`cc`, `cv`, `cr`, `cp`, `state`) also accepts `--json`, which emits a machine-readable JSON object instead of formatted text. ## Commands | Command | Description | | ------- | ---------------------------------------- | | `cc` | Set or read constant current mode (A) | | `cv` | Set or read constant voltage mode (V) | | `cr` | Set or read constant resistance mode (Ω) | | `cp` | Set or read constant power mode (W) | | `state` | Display electronic load state | ## Command Reference ### `cc` Set or read constant current (CC) mode in amps. ```bash theme={null} lager eload NET_NAME cc [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Current value in amps (0–40 A). If omitted, reads current setting. Values outside the range are rejected. In CC mode, the electronic load maintains a constant current draw regardless of voltage changes. **Examples:** ```bash theme={null} # Set constant current to 2.5A lager eload LOAD1 cc 2.5 # Read current CC setting lager eload LOAD1 cc ``` ### `cv` Set or read constant voltage (CV) mode in volts. ```bash theme={null} lager eload NET_NAME cv [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Voltage value in volts (0–150 V). If omitted, reads current setting. Values outside the range are rejected. In CV mode, the electronic load adjusts current to maintain a constant voltage at its terminals. ### `cr` Set or read constant resistance (CR) mode in ohms. ```bash theme={null} lager eload NET_NAME cr [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Resistance value in ohms (0.03–10000 Ω). If omitted, reads current setting. Values outside the range are rejected. In CR mode, the electronic load behaves as a fixed resistance, with current varying according to Ohm's law (I = V/R). ### `cp` Set or read constant power (CP) mode in watts. ```bash theme={null} lager eload NET_NAME cp [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Power value in watts (0–200 W). If omitted, reads current setting. Values outside the range are rejected. In CP mode, the electronic load adjusts voltage and current to maintain constant power dissipation. ### `state` Display the current state of the electronic load. ```bash theme={null} lager eload NET_NAME state [--box BOX] ``` Returns information about the current operating mode, settings, and measurements. *** ## Examples ```bash theme={null} # Set constant current mode to 1.5A lager eload ELOAD1 cc 1.5 --box my-lager-box # Set constant voltage mode to 5V lager eload ELOAD1 cv 5.0 # Set constant resistance mode to 10 ohms lager eload ELOAD1 cr 10.0 # Set constant power mode to 50W lager eload ELOAD1 cp 50.0 # Check current state lager eload ELOAD1 state ``` *** ## Operating Modes ### Constant Current (CC) The load draws a fixed current regardless of voltage: * Use for testing power supply regulation * Ideal for battery discharge testing * Current remains stable as voltage varies ### Constant Voltage (CV) The load maintains a fixed voltage at its terminals: * Simulates a voltage-clamping load * Useful for testing current-limited supplies * Current varies to maintain voltage ### Constant Resistance (CR) The load behaves as a fixed resistor: * Current proportional to voltage (Ohm's law) * Simulates resistive loads * Natural response for many real-world loads ### Constant Power (CP) The load maintains constant power dissipation: * P = V × I remains constant * Current increases as voltage drops * Simulates switching power supplies and similar loads *** ## Supported Hardware | Manufacturer | Model Series | Features | | ------------ | ------------ | ------------------------------- | | Rigol | DL3000 | CC/CV/CR/CP modes, programmable | *** ## Notes * Net names refer to names assigned when setting up your testbed * Use `lager nets` to see available e-load nets * Electronic loads can dissipate significant power; ensure adequate cooling * Always verify load ratings before applying high power levels # Energy Analyzer Source: https://docs.lagerdata.com/source/reference/cli/energy Measure energy, charge, and power statistics using an energy-analyzer net Integrate energy and charge over time, or compute current/voltage/power statistics, using an energy-analyzer net connected to a Lager Box. ## Syntax ```bash theme={null} lager energy NET_NAME read [OPTIONS] lager energy NET_NAME stats [OPTIONS] ``` ## Commands | Command | Description | | ----------------------------- | -------------------------------------------------------- | | `lager energy NET_NAME read` | Integrate energy and charge over a duration | | `lager energy NET_NAME stats` | Compute mean/min/max/std for current, voltage, and power | *** ## `lager energy NET_NAME read` Integrate current and power over a configurable duration. Returns energy in joules and watt-hours, and charge in coulombs and amp-hours. ### Options | Option | Description | | ------------------ | ------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address | | `--duration FLOAT` | Integration duration in seconds (default: 10.0) | | `--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 energy-analyzer net (optional if default is set) | ### Output ``` Energy 'POWER_METER' (10.0s): Energy: 12.500 mWh (45.000 mJ) Charge: 3.472 mAh (12.500 mC) ``` ### Examples ```bash theme={null} # Integrate over the default 10 seconds lager energy POWER_METER read --box my-lager-box # Integrate over 60 seconds lager energy POWER_METER read --box my-lager-box --duration 60 # Use the default net lager energy read ``` *** ## `lager energy NET_NAME stats` Compute mean, minimum, maximum, and standard deviation for current, voltage, and power over a configurable duration. ### Options | Option | Description | | ------------------ | ------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address | | `--duration FLOAT` | Measurement duration in seconds (default: 1.0) | | `--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 energy-analyzer net (optional if default is set) | ### Output ``` Stats 'POWER_METER' (1.0s): Current (A): mean=0.015200 min=0.014900 max=0.015600 std=0.000120 Voltage (V): mean=3.301000 min=3.300500 max=3.301500 std=0.000200 Power (W): mean=0.050175 min=0.049185 max=0.051516 std=0.000400 ``` ### Examples ```bash theme={null} # Stats over the default 1 second lager energy POWER_METER stats --box my-lager-box # Stats over 5 seconds for better averaging lager energy POWER_METER stats --box my-lager-box --duration 5 # Use the default net lager energy stats ``` *** ## Default Net To avoid specifying the net name each time: ```bash theme={null} lager defaults add --energy-net POWER_METER ``` Then: ```bash theme={null} lager energy read lager energy stats ``` ## 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 only, see [Watt Meter](/source/reference/cli/watt). ## Scripting Examples ### Energy Budget Verification ```bash theme={null} #!/bin/bash # Verify a device's energy consumption during a 10-second test RESULT=$(lager energy POWER_METER read --box my-lager-box --duration 10) echo "$RESULT" # Extract mWh value for pass/fail MWH=$(echo "$RESULT" | grep -oP '[\d.]+(?= mWh)') if (( $(echo "$MWH > 50" | bc -l) )); then echo "FAIL: Energy consumption too high: ${MWH} mWh" exit 1 fi echo "PASS: Energy within budget" ``` ### Sleep Current Verification ```bash theme={null} #!/bin/bash # Check sleep current is below 100 uA RESULT=$(lager energy POWER_METER stats --box my-lager-box --duration 5) echo "$RESULT" # Extract mean current in amps, convert to uA CURRENT_A=$(echo "$RESULT" | grep "Current" | grep -oP 'mean=\K[\d.]+') CURRENT_UA=$(echo "$CURRENT_A * 1000000" | bc -l) if (( $(echo "$CURRENT_UA > 100" | bc -l) )); then echo "FAIL: Sleep current ${CURRENT_UA} uA exceeds 100 uA limit" exit 1 fi echo "PASS: Sleep current ${CURRENT_UA} uA" ``` ## Troubleshooting | Error | Cause | Fix | | ------------------ | ------------------------------------------- | ----------------------------------------------------------- | | Timeout (120s) | Device disconnected or measurement too long | Check USB connection; reduce `--duration` | | Connection refused | Box service not running | Check box: `lager hello --box ` | | Device not found | Energy analyzer not detected | Verify device is connected: `lager instruments --box ` | | Net not found | Net not configured as `energy-analyzer` | Check net type: `lager nets --box ` | ## Notes * The default command timeout is 120 seconds to accommodate long integrations * Joulescope JS220 samples continuously; accuracy improves with longer durations * Nordic PPK2 operates in source mode (supplies a configurable voltage 0.8–5V and measures current); voltage readings reflect the configured value * Use `lager instruments --box ` to verify the device is detected * Net names refer to names assigned when configuring your testbed * Use `lager nets` to see available energy-analyzer nets # Exec Source: https://docs.lagerdata.com/source/reference/cli/exec Execute commands in a local Docker development container Run shell commands inside a Docker development container on your local machine. Commands can be run inline or saved as named aliases for reuse. ## Syntax ```bash theme={null} lager exec [OPTIONS] [COMMAND] [EXTRA_ARGS]... ``` ## Options | Option | Short | Type | Default | Description | | ---------------------------------- | ----- | -------- | --------------- | --------------------------------------------------------------------------------- | | `--command TEXT` | | string | | Raw shell command to execute (e.g., `'make build'`) | | `--save-as TEXT` | | string | | Save the command under this alias for later use | | `--warn / --no-warn` | | flag | `--warn` | Warn when overwriting a saved command | | `--env FOO=BAR` | | multiple | | Set environment variable in the container | | `--passenv NAME` | | multiple | | Inherit environment variable from current shell | | `--mount NAME` | `-m` | string | | Docker volume to mount | | `--volume HOST:CONTAINER[:ro]` | | multiple | | Bind-mount a host path into the container. Repeatable; append `:ro` for read-only | | `--interactive / --no-interactive` | `-i` | flag | `--interactive` | Keep STDIN open | | `--tty / --no-tty` | `-t` | flag | `--tty` | Allocate a pseudo-TTY | | `--user TEXT` | `-u` | string | current UID | User to run as in the container | | `--group TEXT` | `-g` | string | current GID | Group to run as in the container | | `--verbose` | `-v` | flag | | Show the full Docker command being executed | | `--help` | | | | Show help message and exit | ## Arguments | Argument | Description | | ------------ | ------------------------------------------------------- | | `COMMAND` | Name of a previously saved command to run | | `EXTRA_ARGS` | Additional arguments appended to the command at runtime | ## Prerequisites A development environment must be created first: ```bash theme={null} lager devenv create ``` This configures the Docker image, mount directory, and shell used by `lager exec`. ## Command Reference ### Run an Inline Command ```bash theme={null} lager exec --command 'make build' lager exec --command 'pytest tests/ -v' ``` ### Save a Command for Reuse ```bash theme={null} lager exec --command 'make clean && make build' --save-as build ``` ### Run a Saved Command ```bash theme={null} lager exec build ``` ### Append Extra Arguments ```bash theme={null} # Runs: make clean && make build --verbose --debug lager exec build --verbose --debug ``` ### Pass Environment Variables ```bash theme={null} # Set explicitly lager exec --command 'make build' --env CFLAGS="-O2" --env DEBUG=0 # Inherit from current shell lager exec --command 'make build' --passenv PATH --passenv HOME ``` ## How It Differs from Other Commands | Command | Target | Purpose | | -------------- | ---------------------- | -------------------------------------------------------- | | `lager exec` | Local Docker container | Run build/test commands in a reproducible environment | | `lager python` | Remote Lager Box | Execute Python scripts that interact with test equipment | | `lager ssh` | Remote Lager Box | Open an interactive SSH shell for box administration | ## Saved Command Management Saved commands are stored in the devenv section of your `.lager` config. Use `lager devenv` to manage them: ```bash theme={null} lager devenv commands # List saved commands lager devenv add "" # Add a command lager devenv delete # Remove a command lager devenv terminal # Open an interactive shell in the container ``` ## Examples ```bash theme={null} # One-off build lager exec --command 'make -j4' # Save and reuse a test command lager exec --command 'pytest tests/ --tb=short' --save-as test lager exec test # Run with verbose Docker output lager exec --verbose --command 'gcc main.c -o main' # Non-interactive mode for CI lager exec --no-interactive --no-tty --command 'make check' ``` ## Notes * The container is created with `--rm` so it is removed after each command * Exit codes from the container are propagated to the CLI * Source code is mounted from your local filesystem into the container * Lager configuration (`~/.lager`) is mounted into the container when present * The `COMMAND` argument and `--command` option are mutually exclusive; use one or the other # GPI Source: https://docs.lagerdata.com/source/reference/cli/gpi Read GPIO input state Read the digital input state of GPIO pins, with optional blocking wait for a target level. ## Syntax ```bash theme={null} lager gpi [NETNAME] [OPTIONS] ``` ## Arguments | Argument | Description | | --------- | -------------------------------------------------------------------------------------- | | `NETNAME` | GPIO net name (optional if default is set). If omitted, lists all available GPIO nets. | ## Options | Option | Description | | ------------------------- | --------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address | | `--wait-for LEVEL` | Block until pin reaches this level (`high`, `low`, `1`, or `0`) | | `--timeout SECONDS` | Timeout in seconds for `--wait-for` (default: wait forever) | | `--scan-rate HZ` | LabJack streaming sample rate in Hz (advanced) | | `--scans-per-read N` | LabJack scans per read batch (advanced) | | `--json` | Emit a machine-readable JSON object instead of formatted text | | `--poll-interval SECONDS` | Poll interval in seconds for non-streaming drivers (advanced) | *** ## Usage ### Basic Read ```bash theme={null} # Read input state lager gpi BUTTON1 --box my-lager-box # Using default net lager gpi # List available GPIO nets (omit net name) lager gpi --box my-lager-box ``` ### Wait for Level Block until a pin reaches a target level. Useful for waiting on hardware events like button presses, interrupt lines, or device ready signals. ```bash theme={null} # Wait for pin to go high lager gpi BUTTON1 --wait-for high --box my-lager-box # Wait for pin to go low with 10-second timeout lager gpi INT_PIN --wait-for low --timeout 10 # Wait for rising edge (pin goes to 1) lager gpi READY --wait-for 1 --timeout 30 ``` ### Advanced Streaming Options For LabJack T7 hardware, the `--wait-for` command uses high-speed streaming to detect level changes. You can tune the streaming parameters: ```bash theme={null} # Custom scan rate (default varies by driver) lager gpi TRIGGER --wait-for high --scan-rate 10000 --timeout 5 # Custom scans per read batch lager gpi TRIGGER --wait-for high --scan-rate 5000 --scans-per-read 500 # For non-streaming drivers, adjust poll interval lager gpi BUTTON --wait-for low --poll-interval 0.05 --timeout 10 ``` *** ## Output ### Basic Read Returns the digital state: * `0` - Low (0V) * `1` - High (3.3V or 5V depending on hardware) ```bash theme={null} $ lager gpi BUTTON1 1 ``` ### Wait for Level Returns the elapsed time in seconds when the target level is reached: ```bash theme={null} $ lager gpi INT_PIN --wait-for low --timeout 10 Pin reached LOW after 2.34s ``` If the timeout expires before the target level is reached, the command exits with an error. *** ## Supported Hardware | Device | Pins | Voltage | Wait-for Method | | ----------- | ----------------------------------- | ----------- | ---------------------- | | LabJack T7 | FIO0-FIO7 | 3.3V logic | Streaming (high-speed) | | MCC USB-202 | DIO0-DIO7 (0-7) | 3.3V/5V TTL | Polling | | Aardvark | 0-5 (SCL, SDA, MISO, SCK, MOSI, SS) | 3.3V | Polling | | FT232H | 0-15 (AD0-AD7, AC0-AC7) | 3.3V | Polling | Aardvark and FT232H GPIO support is currently disabled. A future release can enable it again. LabJack T7 and MCC USB-202 are the active GPIO backends. ### Aardvark Pin Mapping The Aardvark I2C/SPI adapter exposes 6 GPIO pins on its 10-pin header. Pins can be specified by number or signal name: | Pin | Name | Header Pin | | --- | ---- | ---------- | | 0 | SCL | 1 | | 1 | SDA | 3 | | 2 | MISO | 5 | | 3 | SCK | 7 | | 4 | MOSI | 8 | | 5 | SS | 9 | ### FT232H Pin Mapping The FT232H provides 16 GPIO pins across two ports: | Pins | Names | Description | | ---- | ------- | ------------------- | | 0-7 | AD0-AD7 | Port A data pins | | 8-15 | AC0-AC7 | Port A control pins | *** ## Examples ```bash theme={null} # Check if button is pressed STATE=$(lager gpi BUTTON1 --box my-lager-box) if [ "$STATE" -eq "1" ]; then echo "Button pressed" fi # Read multiple inputs lager gpi BUTTON1 --box my-lager-box lager gpi SENSOR_INT --box my-lager-box lager gpi FAULT_PIN --box my-lager-box # Wait for device ready signal lager gpi READY_PIN --wait-for high --timeout 30 --box my-lager-box # Wait for interrupt (active-low) lager gpi INT_N --wait-for low --timeout 5 --box my-lager-box # Scripted: wait for button press, then proceed echo "Press the button..." lager gpi BUTTON --wait-for high --timeout 60 --box my-lager-box && echo "Button pressed!" ``` *** ## Related Commands * [`lager gpo`](/source/reference/cli/gpo) - Set GPIO output level *** ## Notes * GPI is for reading input pins only * Use `lager gpo` to set output pins * Default net can be set with `lager defaults add --gpio-net` * Pin must be configured as input in net configuration * USB-202 channels can be specified as `0`-`7` or `DIO0`-`DIO7` * `--wait-for` blocks the process until the target level is detected or the timeout expires * `--scan-rate` and `--scans-per-read` only apply to LabJack T7 streaming; they are ignored by other drivers * `--poll-interval` applies to non-streaming drivers (USB-202, Aardvark, FT232H); ignored by LabJack # GPO Source: https://docs.lagerdata.com/source/reference/cli/gpo Set GPIO output level Set the digital output level of GPIO pins, with an optional hold mode that keeps the output asserted until interrupted. ## Syntax ```bash theme={null} lager gpo [NETNAME] LEVEL [OPTIONS] ``` ## Arguments | Argument | Description | | --------- | -------------------------------------------------------------------------------------- | | `NETNAME` | GPIO net name (optional if default is set). If omitted, lists all available GPIO nets. | | `LEVEL` | Output level (see below) | ## Level Values The following values are accepted (case-insensitive): | Value | Result | | ----------------- | -------------------- | | `high`, `on`, `1` | Set pin high | | `low`, `off`, `0` | Set pin low | | `toggle` | Invert current state | ## Options | Option | Description | | ----------- | ------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address | | `--hold` | Hold output state (keeps process alive until Ctrl+C) | | `--json` | Emit a machine-readable JSON object instead of formatted text | *** ## Usage ```bash theme={null} # Set pin high lager gpo LED1 high --box my-lager-box # Set pin low lager gpo LED1 low # Toggle pin state lager gpo LED1 toggle # Using numeric values lager gpo LED1 1 lager gpo LED1 0 # List available GPIO nets (omit net name and level) lager gpo --box my-lager-box ``` ### Hold Mode The `--hold` flag keeps the process alive after it sets the output level. The pin holds its state until you press Ctrl+C. Use it to assert a signal for manual testing, or when the pin state otherwise resets between CLI invocations. ```bash theme={null} # Hold reset line low until manually released lager gpo RESET_N low --hold --box my-lager-box # Press Ctrl+C to release # Hold enable pin high during manual testing lager gpo EN high --hold # Press Ctrl+C when done ``` *** ## Supported Hardware | Device | Pins | Voltage | | ----------- | ----------------------------------- | ----------- | | LabJack T7 | FIO0-FIO7 | 3.3V logic | | MCC USB-202 | DIO0-DIO7 (0-7) | 3.3V/5V TTL | | Aardvark | 0-5 (SCL, SDA, MISO, SCK, MOSI, SS) | 3.3V | | FT232H | 0-15 (AD0-AD7, AC0-AC7) | 3.3V | Aardvark and FT232H GPIO support is currently disabled. A future release can enable it again. LabJack T7 and MCC USB-202 are the active GPIO backends. ### Aardvark Pin Mapping The Aardvark I2C/SPI adapter exposes 6 GPIO pins on its 10-pin header. Pins can be specified by number or signal name: | Pin | Name | Header Pin | | --- | ---- | ---------- | | 0 | SCL | 1 | | 1 | SDA | 3 | | 2 | MISO | 5 | | 3 | SCK | 7 | | 4 | MOSI | 8 | | 5 | SS | 9 | ### FT232H Pin Mapping The FT232H provides 16 GPIO pins across two ports: | Pins | Names | Description | | ---- | ------- | ------------------- | | 0-7 | AD0-AD7 | Port A data pins | | 8-15 | AC0-AC7 | Port A control pins | *** ## Examples ```bash theme={null} # Control an LED lager gpo LED1 on --box my-lager-box sleep 1 lager gpo LED1 off --box my-lager-box # Toggle reset line lager gpo RESET_N low sleep 0.1 lager gpo RESET_N high # Blink pattern for i in {1..5}; do lager gpo LED1 toggle sleep 0.5 done # Hold a signal during manual testing lager gpo BOOT0 high --hold --box my-lager-box # Ctrl+C to release, then flash firmware # Assert chip select for manual SPI debugging lager gpo CS_N low --hold --box my-lager-box ``` *** ## Related Commands * [`lager gpi`](/source/reference/cli/gpi) - Read GPIO input state *** ## Notes * GPO is for setting output pins only * Use `lager gpi` to read input pins * Default net can be set with `lager defaults add --gpio-net` * Pin must be configured as output in net configuration * Toggle reads current state and inverts it * USB-202 channels can be specified as `0`-`7` or `DIO0`-`DIO7` * `--hold` keeps the process running; press Ctrl+C to release the pin and exit * Without `--hold`, the output level is set and the command exits immediately; the pin retains its state until the next command # Hello Source: https://docs.lagerdata.com/source/reference/cli/hello Validate CLI installation and Lager Box connection Quick command to validate your CLI installation, Lager Box connection, and display the box version. ## Syntax ```bash theme={null} lager hello [OPTIONS] ``` ## Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | *** ## Usage ```bash theme={null} # Basic hello command lager hello # Hello with specific Lager Box lager hello --box my-lager-box # Hello with Lager Box IP lager hello --box ``` *** ## Output The hello command displays a success message along with the Lager Box version: ```bash theme={null} $ lager hello --box my-lager-box Hello from my-lager-box! Version: 0.3.22 ``` If the box version cannot be determined (older box software that does not support version reporting), the version is shown as unknown: ``` $ lager hello --box old-box Hello from old-box! Version: Unknown ``` *** ## Examples ```bash theme={null} # Verify connectivity after initial setup lager hello --box my-lager-box # Quick check that default box is reachable lager defaults add --box my-lager-box lager hello # Check version of a specific box lager hello --box my-lager-box ``` *** ## Notes * Simple validation command to test CLI installation * Verifies connectivity to the Lager Box * Displays the Lager Box software version by querying the `/cli-version` endpoint * Useful for troubleshooting connection issues * No arguments required when a default box is set * Returns a success message when the connection works # I2C Source: https://docs.lagerdata.com/source/reference/cli/i2c Perform I2C data transfers Perform I2C (Inter-Integrated Circuit) data transfers with devices connected to a Lager Box. I2C is a synchronous serial protocol using two lines: SDA (data) and SCL (clock). ## Syntax ```bash theme={null} lager i2c [NETNAME] [OPTIONS] [SUBCOMMAND] ``` ## Arguments | Argument | Description | | --------- | ---------------------------------------------------------------------------- | | `NETNAME` | I2C net name (optional if default is set via `lager defaults add --i2c-net`) | ## Options | Option | Description | | ----------- | ---------------------------- | | `--box BOX` | Lager Box name or IP address | When invoked without a subcommand, lists I2C nets on the box (or shows configuration for the specified net). *** ## Subcommands ### `config` Configure I2C bus parameters. Settings persist across subsequent commands. ```bash theme={null} lager i2c NETNAME config [OPTIONS] ``` | Option | Description | | -------------------- | ------------------------------------------------ | | `--box BOX` | Lager Box name or IP address | | `--frequency FREQ` | Clock frequency (e.g., `100k`, `400k`, `1M`) | | `--pull-ups on\|off` | Enable/disable internal pull-ups (Aardvark only) | **Examples:** ```bash theme={null} # Set I2C clock to 400kHz with internal pull-ups lager i2c MY_I2C config --frequency 400k --pull-ups on # Set clock to 100kHz (standard mode) lager i2c MY_I2C config --frequency 100k ``` *** ### `scan` Scan the I2C bus for connected devices. Probes each address and reports those that respond with an ACK. ```bash theme={null} lager i2c NETNAME scan [OPTIONS] ``` | Option | Description | Default | | -------------- | ---------------------------- | ------- | | `--box BOX` | Lager Box name or IP address | | | `--start ADDR` | Start address in hex | `0x08` | | `--end ADDR` | End address in hex | `0x77` | The default range `0x08`-`0x77` excludes reserved I2C addresses. **Examples:** ```bash theme={null} # Scan default address range lager i2c MY_I2C scan --box my-lager-box # Scan specific range lager i2c MY_I2C scan --start 0x20 --end 0x27 ``` *** ### `read` Read bytes from an I2C device. ```bash theme={null} lager i2c NETNAME read NUM_BYTES [OPTIONS] ``` | Argument | Description | | ----------- | ----------------------------------- | | `NUM_BYTES` | Number of bytes to read (0 or more) | | Option | Description | Default | | ------------------ | ----------------------------------------------- | ------------ | | `--box BOX` | Lager Box name or IP address | | | `--address ADDR` | Device address in hex (e.g., `0x48`) | **Required** | | `--frequency FREQ` | Clock frequency override (e.g., `100k`, `400k`) | | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | **Examples:** ```bash theme={null} # Read 4 bytes from device at address 0x48 lager i2c MY_I2C read 4 --address 0x48 # Read 2 bytes with JSON output lager i2c MY_I2C read 2 --address 0x48 --format json # Read with frequency override lager i2c MY_I2C read 4 --address 0x48 --frequency 100k ``` *** ### `write` Write bytes to an I2C device. ```bash theme={null} lager i2c NETNAME write DATA [OPTIONS] ``` | Argument | Description | | -------- | ---------------------------------------------------- | | `DATA` | Hex data to write (e.g., `0x0A03`, `0a 03`, `0a,03`) | | Option | Description | Default | | ------------------ | ------------------------------------- | ------------ | | `--box BOX` | Lager Box name or IP address | | | `--address ADDR` | Device address in hex (e.g., `0x48`) | **Required** | | `--data-file PATH` | File containing binary data to write | | | `--frequency FREQ` | Clock frequency override | | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | Provide data either as the `DATA` argument or via `--data-file`, but not both. **Examples:** ```bash theme={null} # Write register address 0x0A followed by value 0x03 lager i2c MY_I2C write 0x0A03 --address 0x48 # Write using space-separated hex bytes lager i2c MY_I2C write "0a 03" --address 0x48 # Write from a binary file lager i2c MY_I2C write --data-file config.bin --address 0x48 ``` *** ### `transfer` Write then read in a single I2C transaction using a repeated start condition. This is the standard pattern for reading registers: write the register address, then read the register value without releasing the bus. ```bash theme={null} lager i2c NETNAME transfer NUM_BYTES [OPTIONS] ``` | Argument | Description | | ----------- | ----------------------------------- | | `NUM_BYTES` | Number of bytes to read (0 or more) | | Option | Description | Default | | ------------------ | --------------------------------------------------------- | ------------ | | `--box BOX` | Lager Box name or IP address | | | `--address ADDR` | Device address in hex (e.g., `0x48`) | **Required** | | `--data DATA` | Hex data to write before reading (e.g., register address) | | | `--data-file PATH` | File containing data to write before reading | | | `--frequency FREQ` | Clock frequency override | | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | **Examples:** ```bash theme={null} # Read 2 bytes from register 0x0A on device 0x48 lager i2c MY_I2C transfer 2 --address 0x48 --data 0x0A # Read temperature from a sensor (register 0x00, 2 bytes) lager i2c MY_I2C transfer 2 --address 0x76 --data 0x00 # Read with JSON output lager i2c MY_I2C transfer 4 --address 0x48 --data 0x0A --format json ``` *** ## Hex Data Formats Data arguments accept multiple hex formats: | Format | Example | Parsed As | | --------------------- | -------- | -------------- | | Prefixed continuous | `0x0a03` | `[0x0a, 0x03]` | | Unprefixed continuous | `0a03` | `[0x0a, 0x03]` | | Space-separated | `0a 03` | `[0x0a, 0x03]` | | Comma-separated | `0a,03` | `[0x0a, 0x03]` | | Single byte | `0x0a` | `[0x0a]` | All values must be within byte range (`0x00`-`0xFF`). *** ## Address Format I2C addresses are 7-bit values (`0x00`-`0x7F`). You can specify addresses in hex or decimal: | Format | Example | Value | | -------------- | ------- | ----- | | Hex prefixed | `0x48` | 72 | | Hex unprefixed | `48` | 72 | | Decimal | `72` | 72 | *** ## Frequency Format Clock frequencies accept numeric values with optional suffixes: | Format | Example | Value | | ---------- | ---------- | ------- | | Plain Hz | `100000` | 100 kHz | | kHz suffix | `100k` | 100 kHz | | MHz suffix | `1M` | 1 MHz | | Hz suffix | `400000hz` | 400 kHz | *** ## Supported Hardware | Adapter | Pins | Pull-ups | Notes | | ------------ | --------------------------------------- | --------------------- | -------------------------- | | LabJack T7 | Configurable FIO pins (e.g., FIO4/FIO5) | External only | \~450 kHz max (throttle=0) | | Aardvark USB | Fixed SDA/SCL | Internal (switchable) | Up to 800 kHz | | FT232H | Configurable | External only | MPSSE-based I2C | *** ## Net Configuration I2C nets are configured in `saved_nets.json` on the box. Example net record: ```json theme={null} { "name": "my_i2c", "role": "i2c", "instrument": "labjack_t7", "pin": "FIO4-FIO5", "params": { "sda_pin": 4, "scl_pin": 5, "frequency_hz": 100000, "pull_ups": false } } ``` For Aardvark adapters: ```json theme={null} { "name": "my_i2c", "role": "i2c", "instrument": "aardvark", "pin": "I2C0", "params": { "frequency_hz": 400000, "pull_ups": true } } ``` *** ## Output Formats | Format | Description | | ------- | -------------------------------------------- | | `hex` | Space-separated hex bytes (e.g., `0a 03 ff`) | | `bytes` | Raw byte values | | `json` | JSON object with data array and metadata | *** ## Examples ```bash theme={null} # List all I2C nets on a box lager i2c --box my-lager-box # Show configuration for a specific net lager i2c MY_I2C --box my-lager-box # Configure bus speed and pull-ups lager i2c MY_I2C config --frequency 400k --pull-ups on # Scan for devices lager i2c MY_I2C scan # Read WHO_AM_I register from an accelerometer lager i2c MY_I2C transfer 1 --address 0x68 --data 0x75 # Write configuration to a sensor lager i2c MY_I2C write 0x2003 --address 0x76 # Read 6 bytes of sensor data lager i2c MY_I2C read 6 --address 0x76 ``` *** ## Troubleshooting ### No Devices Found on Scan * Verify SDA and SCL wiring * Check that pull-up resistors are present (4.7k typical for 100kHz) * For Aardvark: try `--pull-ups on` to enable internal pull-ups * Confirm device power supply is connected ### Bus Errors * LabJack T7: The first transaction after connection can return a bus error. This is normal, and the driver handles it automatically * Try reducing frequency with `--frequency 100k` * Check for bus contention (multiple masters) ### NACK Errors * Verify the device address (some datasheets show 8-bit shifted addresses) * Ensure the device is powered and not in reset * Check for address conflicts with other devices on the bus *** ## Notes * Default I2C net can be set with `lager defaults add --i2c-net NETNAME` * LabJack T7 operates at \~450 kHz regardless of requested frequency due to hardware limitations * Aardvark adapters support internal pull-ups that can be toggled via `config --pull-ups` * The `transfer` command uses I2C repeated start for atomic write-then-read operations ## See Also * [SPI](/source/reference/cli/spi) -- SPI bus communication (the other common serial protocol) * [Python I2C API](/source/reference/python/i2c) -- Automate I2C operations in Python scripts * [Glossary](/source/getting-started/glossary) -- Definitions of I2C, SPI, and other terms # Install Source: https://docs.lagerdata.com/source/reference/cli/install Install Lager box code onto a box Deploy the Lager Box software, Docker container, and supporting tools onto a new or existing box. ## Syntax ```bash theme={null} lager install [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 or DNS hostname | | `--user TEXT` | string | `lagerdata` | SSH username | | `--version TEXT` | string | `main` | Box version to deploy: a release tag (e.g. `v0.15.0`), a git branch, or a full 40-character commit SHA | | `--skip-jlink` | flag | | Skip J-Link installation (pyOCD is always installed) | | `--skip-firewall` | flag | | Skip UFW firewall configuration | | `--skip-verify` | flag | | Skip post-deployment verification | | `--corporate-vpn TEXT` | string | | Corporate VPN interface name for firewall rules (e.g., `tun0`) | | `--yes` | flag | | Skip confirmation prompts | | `--pull` | flag | on | Use the pre-built box image when the target is a release tag (the default) | | `--no-pull` | flag | | Always build the box image on the box | | `--timeout INTEGER` | int | `1800` | Max seconds for the deploy step, which includes the container build. `0` disables the budget. Overridden by `LAGER_INSTALL_TIMEOUT` | | `--help` | | | Show help message and exit | Either `--box` or `--ip` is required. If both are provided, the command exits with an error. ## The Box Image The slowest part of an install is the box's Docker image. A build on the box takes roughly 14 minutes, and an install always pays the full cost. The deployment prunes the builder cache before it starts, so there is never a warm layer cache to reuse. Every release tag publishes a pre-built image, and `lager install` uses it by default: about 2 minutes instead of 14. This only applies when `--version` names a **release tag**. Branches such as `main` and `staging` have no published image, so they always build on the box. Pinning a release tag is the difference between a two-minute install and a fourteen-minute one. The image is verified before it is used. Lager fetches it by immutable digest rather than by tag, and the image must carry a label naming the exact version you requested. Lager discards an image that is unlabeled or that claims a different version. Three things can go wrong: the tag has no published image, the box cannot reach the registry, or the image fails verification. In each case the install falls back to a build on the box, exactly as it always did. A slow install that works beats a fast one that does not. Pass `--no-pull` to skip the pre-built image and always build. Setting `LAGER_BOX_IMAGE_PULL=0` in the environment does the same for every command in that shell. ## What Gets Installed | Component | Description | | ---------------- | ----------------------------------------------------------------------------- | | Docker container | Lager service container (ports 5000 and 8765) with auto-restart | | pyOCD | Open-source debug probe tool (automatic) | | J-Link | SEGGER debug probe software (optional, skipped with `--skip-jlink`) | | UFW firewall | Restricts service ports to VPN and localhost (skipped with `--skip-firewall`) | | Box code | Python libraries and services in `~/box` | ## Installation Flow 1. **Resolve target** - Looks up box IP from `--box` name or uses `--ip` directly 2. **Verify SSH** - Tests key-based authentication and settles which identity the rest of the command offers 3. **Show summary** - Displays what will be installed and asks for confirmation 4. **Deploy** - Runs the deployment script. About 2 minutes when the pre-built image is used; a cold build on ordinary box hardware is roughly 14 minutes (see below). The step is bounded by `--timeout`, 30 minutes by default -- raise it on slower hardware, where a healthy build can legitimately exceed the default 5. **Store version** - Writes the CLI version to `/etc/lager/version` on the box 6. **Add to config** - Optionally adds the box to your local `.lager` config ## Examples ```bash theme={null} # Install to a new box by IP lager install --ip 192.168.1.100 # Install to a stored box lager install --box my-lager-box # Install a specific release tag lager install --ip 192.168.1.100 --version v0.15.0 # Install a specific branch with a custom user lager install --ip 192.168.1.100 --user pi --version staging # Install with corporate VPN firewall support lager install --ip 192.168.1.100 --corporate-vpn tun0 # Skip optional components lager install --ip 192.168.1.100 --skip-jlink --skip-firewall # Force a local build of the box image instead of using the published one lager install --box my-lager-box --version v0.39.1 --no-pull # Non-interactive installation lager install --ip 192.168.1.100 --yes ``` ## Passwordless sudo Installation configures passwordless `sudo` for the box login user. The CLI drives the box over non-interactive SSH, where `sudo` has no terminal to prompt against. The grants must be in place before provisioning runs. Lager writes these files, and only these files, under `/etc/sudoers.d/`: | File | Written by | Grants | | ------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `lagerdata-udev` | `lager install` | udev rule deployment, modprobe blacklist, Docker group and service control, firewall helper install/run, `/etc/lager` file writes | | `lager-box-config` | `lager install`, `lager update` | `apt-get`, `sysctl`, and the path-scoped writes `lager box-config apply` needs | | `lager-bench-json` | Operator, on boxes predating the grant in `lagerdata-udev` | Writing `/etc/lager/bench.json` | **The box login user is root-equivalent by design.** Provisioning a box requires root. Deploying udev rules runs commands as root by construction, and `apt-get` executes arbitrary commands as root through its own configuration. Lager writes the grants above as specific commands, to keep the blast radius small and the file readable. They are **not a privilege boundary**. Anyone who can log in as the box user can obtain root on that box. Treat the box login account as equivalent to root when deciding who holds its SSH key, and prefer a dedicated user on a dedicated machine. Each of the three files is **regenerated in full** on every run — that is what keeps a box on the current grant shape. A grant added *inside* one of them is lost the next time it is written, so each file opens with a header saying so. Lager never reads, edits, or removes any other file in `/etc/sudoers.d/`. If you or a box-management platform need additional grants, put them in a separate file there. For example, `/etc/sudoers.d/zz-local` sorts after Lager's files, so its rules win. Lager will leave that file alone, including during `lager uninstall`. ## SSH Authentication Install requires key-based SSH authentication. It offers `~/.ssh/lager_box` — the key `lager ssh-setup` and `lager install` generate — explicitly, because that is not a filename ssh tries on its own. If the box does not accept it, install falls back to your own default identities, so a box you authorized with `ssh-copy-id` works unchanged. If neither authenticates, install offers to set the key up for you: ``` No SSH key on this machine is authorized on the box. Set up the lager_box key now? (one box-password prompt, then the rest of the install runs unattended) [Y/n]: ``` Accepting prompts for the box password **once**, installs the key, and every later step of the install runs unattended. `--yes` accepts it without asking. Declining stops the install and points you at `lager ssh-setup --box `, which does the same thing as a separate step. Install does not offer a password fallback for the deployment itself. A box configured with `PasswordAuthentication no` never receives the password. The resulting "password failed" message therefore described something that never happened. For new hosts, the SSH host key is accepted automatically. If the host key changed since a previous connection, the command asks you to verify the change manually first. Install does not write to `~/.ssh/config`. Earlier versions added a per-IP `Host` block naming the key. Another tool that regenerated the file deleted that block, and the block also disabled host-key verification for the box. The identity is passed per command instead. ## Notes * For a step-by-step walkthrough, see [Setting Up a Lager Box](/source/getting-started/setting-up-a-lager-box) * Requires SSH client tools (`ssh`, `ssh-keygen`) to be installed locally * The deployment script is bundled with the `lager-cli` package * After installation, verify connectivity with `lager hello --box ` * Use `lager update` to deploy code updates to an already-installed box * Use `lager uninstall` to remove Lager software from a box # Install Wheel Source: https://docs.lagerdata.com/source/reference/cli/install-wheel Install a local Python wheel file on a Lager Box Upload and install a local Python wheel (`.whl`) into the lager container on a Lager Box. Use it to push a locally built package onto a box, without publishing it to an index first. It suits an iteration loop on a box-side library or driver. Before installing, any previously installed version of the same package is uninstalled, so the version number does not need to be bumped on every rebuild. The package name is parsed from the wheel filename per the wheel specification. ## Syntax ```bash theme={null} lager install-wheel [OPTIONS] WHEEL_PATH ``` ## Arguments | Argument | Description | | ------------ | ---------------------------------------- | | `WHEEL_PATH` | Path to the local `.whl` file to install | ## Options | Option | Description | | ----------- | -------------------------------------------------------------- | | `--box BOX` | Lager Box name or IP address (uses the default box if omitted) | *** ## Usage ```bash theme={null} # Install a locally built wheel on a specific box lager install-wheel dist/mypackage-0.1.0-py3-none-any.whl --box my-lager-box # Install on the default box lager install-wheel dist/mypackage-0.1.0-py3-none-any.whl ``` *** ## How It Works 1. **Validates** the file locally: it must exist, end in `.whl`, be readable, and be within the box upload size limit. 2. **Acquires the box lock** for the duration of the install. Because the install runs `pip install` inside the box container, the lock prevents a concurrent `lager python` test from racing on Python/import state. See [Box Locking](/source/reference/cli/locking). 3. **Uninstalls** any previously installed version of the package (best effort). 4. **Installs** the uploaded wheel with `pip install --force-reinstall`. ``` Installing mypackage-0.1.0-py3-none-any.whl on my-lager-box... Uninstalled previous version of mypackage Successfully installed mypackage ``` *** ## Notes * The wheel is uploaded to the box and installed inside the lager container; it is not installed on your local machine. * The file must have a `.whl` extension and conform to the wheel filename format (`name-version-...whl`); the package name is derived from the leading segment. * For installing or updating the box code itself (not a Python package), use [`lager install`](/source/reference/cli/install) and [`lager update`](/source/reference/cli/update). *** ## See Also * [Install](/source/reference/cli/install) — install the Lager Box code onto a box * [Update](/source/reference/cli/update) — update the Lager Box code * [Box Config](/source/reference/cli/box-config) — manage Python packages (`box config pip`) and other declarative box configuration # Instruments Source: https://docs.lagerdata.com/source/reference/cli/instruments List attached instruments on a Lager Box Discover and list all test instruments connected to a Lager Box. ## Syntax ```bash theme={null} lager instruments [OPTIONS] ``` ## Options | Option | Description | | ----------- | ---------------------------- | | `--box BOX` | Lager Box name or IP address | *** ## Usage ```bash theme={null} # List instruments on default Lager Box lager instruments # List instruments on specific Lager Box lager instruments --box my-lager-box ``` *** ## Output The command displays a table of connected instruments: ``` ┌─────────────────────────┬──────────┬────────────────────────────────┐ │ Instrument │ Channels │ Address │ ├─────────────────────────┼──────────┼────────────────────────────────┤ │ Rigol_DP832 │ CH1,CH2 │ USB0::0x1AB1::0x0E11::DP8... │ │ Rigol_MSO5074 │ 1,2,3,4 │ USB0::0x1AB1::0x0515::MS5... │ │ Keithley_2281S │ - │ USB0::0x05E6::0x2281::912... │ │ LabJack_T7 │ AIN0-13 │ T7-12345678 │ │ MCC_USB202 │ CH0-7,DAC0-1,DIO0-7 │ USB::9999::USB202 │ │ FTDI_USB_Serial │ uart │ /dev/ttyUSB0 (A12BC3...) │ │ Acroname_8Port │ 1-8 │ USB-HUB-SERIAL │ └─────────────────────────┴──────────┴────────────────────────────────┘ ``` *** ## Instrument Types The following instrument types are automatically detected: ### Power Supplies * Rigol DP800 series (DP811, DP821, DP832) * Keysight E36200/E36300 series * Keithley 2200/2280 series * EA PSB series ### Oscilloscopes * Rigol MSO5000 series * PicoScope ### Battery/Solar Simulators * Keithley 2281S * EA PSI/EL series ### Electronic Loads * Rigol DL3000 series ### Data Acquisition * LabJack T7 (ADC, DAC, GPIO) * MCC USB-202 (ADC, DAC, GPIO) * Phidget thermocouples * Yocto watt meters ### USB Hubs * Acroname (4-port, 8-port) * YKUSH ### Debug Probes * Segger J-Link * CMSIS-DAP * ST-Link ### Serial Adapters * FTDI USB-to-serial *** ## Multiple Device Warning The command warns if multiple instances of multi-hub devices are detected: ``` [WARNING] Multiple LabJack_T7 devices detected. Consider using net configuration to specify which device to use. ``` This applies to: * LabJack\_T7 * Acroname\_8Port * Acroname\_4Port *** ## Address Formats Different instruments use different address formats: | Type | Format | Example | | ------- | ------------- | ------------------------------ | | VISA | USB resource | `USB0::0x1AB1::0x0E11::DP8...` | | MCC USB | USB resource | `USB::9999::USB202` | | LabJack | Serial number | `T7-12345678` | | UART | Device path | `/dev/ttyUSB0` | | USB Hub | Serial | `USB-HUB-SERIAL` | Long UART serial numbers are truncated to 10 characters for readability. *** ## Examples ```bash theme={null} # Check what instruments are connected lager instruments --box my-lager-box # Use with nets command to configure lager instruments --box my-lager-box lager nets --box my-lager-box # Then configure nets for discovered instruments ``` *** ## Troubleshooting | Issue | Cause | Fix | | ----------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | No instruments listed | USB cables disconnected, or Docker not running | Check physical USB connections. Run `lager hello --box ` to verify the service is running. | | Specific instrument missing | Instrument needs power, USB re-seat, or updated udev rules | Unplug and replug the USB cable. Ensure the instrument is powered on. Run `lager update --box ` for latest drivers. | | "Multiple devices detected" warning | More than one of the same instrument type connected | This is informational. Use net configuration to specify which device to use for each net. | ## Notes * Instruments must be connected via USB to the Lager Box * Some instruments require specific drivers (udev rules) * Run `lager update` to install latest udev rules * Use `lager nets` to configure how instruments are used ## See Also * [Nets](/source/reference/cli/nets) -- Configure logical names for your instruments * [Setting Up Instruments](/source/getting-started/setting-up-instruments) -- Getting started guide for instrument setup # The .lager Configuration File Source: https://docs.lagerdata.com/source/reference/cli/lager-file Complete reference for the .lager JSON configuration file The `.lager` file is a JSON configuration file that stores settings for the Lager CLI. There are **two distinct versions** of this file, and they serve different purposes. A **global** file is shared across all projects, and a **project-local** file is specific to a single project directory. ## Two Files, Two Purposes | | Global `.lager` | Project-Local `.lager` | | -------------- | ------------------------------------------------------------ | --------------------------------------------------------------------- | | **Location** | `~/.lager` | Any directory in your project (found by searching upward from cwd) | | **Created by** | `lager boxes add`, `lager defaults add`, `lager nets add` | `lager devenv create`, or manually | | **Purpose** | Machine-wide box registry, net definitions, command defaults | Project-specific Docker dev environment, debug scripts, file includes | | **Sections** | `DEFAULTS`, `BOXES`, `NETS` | `DEVENV`, `DEBUG`, `includes` | | **Shared** | One file for all projects | One per project (committed to version control) | The CLI always knows which file to use. Commands like `lager boxes` and `lager defaults` read and write the global file. Commands like `lager devenv` and `lager exec` search upward from your current directory for a project-local file. The two files never conflict -- they contain entirely different sections. When `lager devenv terminal` or `lager exec` starts a Docker container, it mounts the global `~/.lager` file inside the container at `/lager/.lager`, with `LAGER_CONFIG_FILE_DIR=/lager`. Box and net definitions are therefore available inside the container. *** ## Global File (`~/.lager`) The global file lives in your home directory and is shared across all projects. It stores your box registry, hardware net configurations, and command defaults. ### DEFAULTS Stores default values so you can omit common options from CLI commands. When you run a command without specifying `--box` or a net name, the CLI checks this section. Managed with `lager defaults`. **Fields:** | Config Key | CLI Option | Description | | ------------------ | -------------------- | --------------------------- | | `gateway_id` | `--box` | Default box name | | `serial_device` | `--serial-port` | Default serial port path | | `net_power_supply` | `--supply-net` | Default power supply net | | `net_battery` | `--battery-net` | Default battery net | | `net_solar` | `--solar-net` | Default solar net | | `net_scope` | `--scope-net` | Default oscilloscope net | | `net_logic` | `--logic-net` | Default logic analyzer net | | `net_adc` | `--adc-net` | Default ADC net | | `net_dac` | `--dac-net` | Default DAC net | | `net_gpio` | `--gpio-net` | Default GPIO net | | `net_debug` | `--debug-net` | Default debug net | | `net_eload` | `--eload-net` | Default electronic load net | | `net_usb` | `--usb-net` | Default USB hub net | | `net_webcam` | `--webcam-net` | Default webcam net | | `net_watt_meter` | `--watt-meter-net` | Default watt meter net | | `net_thermocouple` | `--thermocouple-net` | Default thermocouple net | | `net_uart` | `--uart-net` | Default UART net | | `net_arm` | `--arm-net` | Default robotic arm net | **Example:** ```json theme={null} { "DEFAULTS": { "gateway_id": "my-lager-box", "serial_device": "/dev/ttyUSB0", "net_power_supply": "VDD_MAIN", "net_debug": "SWD", "net_uart": "SERIAL_DBG" } } ``` **CLI commands:** ```bash theme={null} lager defaults add --box my-lager-box --supply-net VDD_MAIN lager defaults list lager defaults delete box lager defaults delete-all ``` **Resolution order:** When a command needs a box or net name, it checks: 1. Command-line option (`--box`, net argument) -- highest priority 2. `LAGER_BOX` environment variable (for box only) 3. `DEFAULTS` section in global `~/.lager` 4. Error if required and not found *** ### BOXES Maps human-readable box names to their IP addresses. This is the box registry that all other commands use to resolve box names to IPs. Managed with `lager boxes`. Each entry can be either a simple IP string (legacy format) or an object with additional metadata. **Fields (object format):** | Field | Required | Description | | --------- | -------- | ------------------------------------------------------------------ | | `ip` | Yes | IP address of the box (typically a Tailscale IP) | | `user` | No | Username for SSH access | | `version` | No | Branch or version the box is running (e.g., `"main"`, `"staging"`) | **Example:** ```json theme={null} { "BOXES": { "my-lager-box": "100.64.0.10", "staging-box": { "ip": "100.64.0.11", "user": "admin", "version": "staging" }, "legacy-box": "192.168.1.50" } } ``` **CLI commands:** ```bash theme={null} lager boxes add --name my-lager-box --ip 100.64.0.10 lager boxes add --name staging-box --ip 100.64.0.11 --user admin --version staging lager boxes list lager boxes edit --name my-lager-box --ip 100.64.0.12 lager boxes delete --name my-lager-box lager boxes delete-all lager boxes export # Print boxes as JSON lager boxes import --file boxes.json # Import boxes from JSON ``` *** ### NETS Stores hardware net configurations organized by box name. Each net maps a human-readable name to a physical hardware connection (channel on an instrument). Nets are stored in the global file, but the actual net data lives on the box. This section is a local cache that `lager nets` manages. **Structure:** A dictionary keyed by box name, where each value is an array of net objects. **Net object fields:** | Field | Required | Description | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Yes | Unique name for the net (e.g., `"VDD_MAIN"`, `"SWD"`) | | `role` | Yes | Net type: `supply`, `battery`, `solar`, `eload`, `adc`, `dac`, `gpio`, `debug`, `scope`, `logic`, `uart`, `i2c`, `spi`, `usb`, `watt`, `thermocouple`, `webcam`, `arm` | | `instrument` | Yes | Instrument model name (e.g., `"Rigol_DP831"`, `"LabJack_T7"`) | | `address` | Yes | Instrument address (USB or network path) | | `pin` | Yes | Channel or pin number on the instrument | | `jlink_script` | No | Base64-encoded J-Link script (debug nets only) | | `device_path` | No | Direct device path (UART nets with USB serial, e.g., `"/dev/ttyUSB0"`) | | `channel` | No | Port number (UART nets) | **Example:** ```json theme={null} { "NETS": { "my-lager-box": [ { "name": "VDD_MAIN", "role": "supply", "instrument": "Rigol_DP831", "address": "USB0::0x1AB1::0x0E11::DP8XXXXXXX::INSTR", "pin": "1" }, { "name": "SWD", "role": "debug", "instrument": "JLink", "address": "USB0::JLink", "pin": "0", "jlink_script": "base64encodedcontent..." }, { "name": "I2C_BUS", "role": "i2c", "instrument": "Aardvark", "address": "USB0::Aardvark", "pin": "0" } ] } } ``` **CLI commands:** ```bash theme={null} lager nets # List all nets lager nets add VDD_MAIN supply 1
# Add a net lager nets add-all # Auto-create all possible nets lager nets add-batch nets.json # Batch add from JSON file lager nets delete VDD_MAIN supply # Delete a net lager nets delete-all # Delete all nets lager nets rename VDD_MAIN VDD_3V3 # Rename a net lager nets set-script SWD ./my_device.JLinkScript # Attach J-Link script lager nets remove-script SWD # Remove J-Link script lager nets show-script SWD # Display J-Link script lager nets tui # Interactive TUI manager ``` *** ## Project-Local File (`./.lager`) The project-local file lives in your project directory (or any parent directory). The CLI finds it by searching upward from your current working directory. It is typically committed to version control so that all developers on a project share the same development environment configuration. This file is completely separate from the global `~/.lager` -- it contains different sections and is read by different commands. ### How the local file is found When you run `lager devenv terminal`, `lager exec`, or `lager debug`, the CLI starts in your current directory. It then walks up the directory tree until it finds a `.lager` file that is not the global `~/.lager`. The CLI uses the first one it finds. ``` /home/user/projects/my-firmware/.lager <-- found first (used) /home/user/projects/.lager <-- also exists but not used /home/user/.lager <-- global file (separate) ``` ### DEVENV Configures a Docker-based development environment for your project. Managed with `lager devenv`. When you run `lager devenv terminal`, the CLI reads this section. The section names which Docker image to launch, where to mount your source code, and how to configure the container. When you run `lager exec `, it reads the saved commands from this section. **Fields:** | Field | Required | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image` | Yes | Docker image name (e.g., `"lagerdata/devenv-cortexm"`) | | `mount_dir` | Yes | Directory inside the container where your source code is mounted (e.g., `"/app"`) | | `shell` | Yes | Shell executable path inside the container (e.g., `"/bin/bash"`) | | `user` | No | User to run as inside the container | | `group` | No | Group to run as inside the container | | `macaddr` | No | MAC address to assign to the container | | `hostname` | No | Hostname to assign to the container | | `repo_root_relative_path` | No | Relative path from the `.lager` file to the repo root. Used when the `.lager` file is in a subdirectory -- the CLI mounts the repo root and sets the working directory to the correct subdirectory. | | `volumes` | No | List of additional host paths to bind-mount into the container, each in Docker `-v` form (`"HOST:CONTAINER"`, optionally with `:ro`). Applied to both `lager devenv terminal` and `lager exec`. Managed with `lager devenv mount add/remove/list`. | | `environment` | No | List of environment variables (`"FOO=bar"`) to set inside the container. Applied to both `lager devenv terminal` and `lager exec`. Managed with `lager devenv env set/unset/list`. | | `network` | No | Docker network mode (e.g. `"host"`). Applied to both commands; `--network` overrides it on `terminal`. | | `platform` | No | Docker platform (e.g. `"linux/amd64"`). Applied to both commands; `--platform` overrides it on `terminal`. | | `ports` | No | List of port mappings (`"HOST:CONTAINER"`). Applied to both commands; combined with any `-p` flags on `terminal`. | | `entrypoint` | No | Container entrypoint (e.g. `"/bin/bash"`). Use when the image's default entrypoint isn't an interactive shell; `--entrypoint` overrides it on `terminal`. | Paths in `volumes` can use `~`, environment variables, and `${PROJECT_ROOT}` (the directory containing `.lager`) so a committed `.lager` stays portable across machines — e.g. `"${PROJECT_ROOT}:/workspace"`. CLI flags (`--user`, `--group`, `--network`, `--platform`, `--entrypoint`) take precedence over the config value when both are present. Any scalar key above can be set with `lager devenv set `, removed with `lager devenv unset `, and the whole section printed with `lager devenv show`. \| `cmd.` | No | Custom named commands that can be executed with `lager exec ` | **Example:** ```json theme={null} { "DEVENV": { "image": "lagerdata/devenv-cortexm:latest", "mount_dir": "/app", "shell": "/bin/bash", "user": "1000", "group": "1000", "hostname": "devbox", "repo_root_relative_path": "..", "volumes": [ "/home/me/shared-libs:/opt/libs:ro", "/home/me/build-cache:/root/.cache" ], "environment": [ "TOOLCHAIN=arm-none-eabi", "VERBOSE=1" ], "cmd.build": "make -j$(nproc)", "cmd.flash": "openocd -f board.cfg -c 'program build/fw.elf verify reset exit'", "cmd.test": "ctest --output-on-failure" } } ``` **CLI commands:** ```bash theme={null} lager devenv create # Interactive setup (creates DEVENV section) lager devenv terminal # Start interactive Docker shell lager devenv terminal -v /data:/data # ...with an extra host bind-mount (repeatable) lager devenv terminal -e API_KEY=xyz # ...with an extra env var (repeatable) lager devenv terminal --info # print the resolved `docker run` command + config, don't launch lager devenv add build "make -j4" # Add a named command lager devenv delete build # Remove a named command lager devenv commands # List all named commands # Persist mounts/env in .lager so `lager devenv terminal` needs no flags: lager devenv mount add cursor-data:/root/.cursor # Add a volume to `volumes` lager devenv mount remove cursor-data:/root/.cursor lager devenv mount list lager devenv env set HISTFILE=/root/.local/state/bash/history # Add/replace in `environment` lager devenv env unset HISTFILE lager devenv env list lager devenv set network host # Set any scalar key (image, network, platform, ...) lager devenv set platform linux/amd64 lager devenv set port 8080:8080 # Append to the `ports` list lager devenv unset network # Remove a key lager devenv show # Print the resolved DEVENV config lager exec build # Run a named command in Docker lager exec --command 'make clean' # Run an ad-hoc command in Docker lager exec --command 'make' --save-as mk # Run and save for later ``` *** ### DEBUG Maps debug net names to local J-Link script file paths. Paths can be relative (resolved relative to the `.lager` file location) or absolute. This is separate from the `jlink_script` field on net objects in the `NETS` section of the global file. The `DEBUG` section provides project-local script overrides -- `lager debug` commands check this section first before using the script stored on the box. This lets you keep J-Link scripts in your project repo and have them used automatically. **Example:** ```json theme={null} { "DEBUG": { "SWD": "./scripts/my_device.JLinkScript", "JTAG": "/absolute/path/to/other.JLinkScript" } } ``` *** ### includes Maps destination names to source directories that the CLI uploads alongside Python scripts run with `lager python`. This lets your test scripts import from external directories outside the project. Paths are resolved relative to the `.lager` file location. **Example:** ```json theme={null} { "includes": { "dtest": "../dtest", "shared_lib": "/absolute/path/to/shared" } } ``` When you run `lager python test_script.py`, the CLI checks the local `.lager` for an `includes` section. It then uploads the referenced directories to the box, so they are available as imports. *** ## Environment Variables These environment variables override the default file location and behavior: | Variable | Description | | ------------------------ | ------------------------------------------------------------------------------------- | | `LAGER_CONFIG_FILE_DIR` | Override the directory where the global `.lager` file is located (default: `~`) | | `LAGER_CONFIG_FILE_NAME` | Override the filename (default: `.lager`) | | `LAGER_BOX` | Override the default box for all commands (takes priority over `DEFAULTS.gateway_id`) | ```bash theme={null} # Use a custom config directory export LAGER_CONFIG_FILE_DIR=/opt/lager # Override default box for this session export LAGER_BOX=staging-box ``` *** ## Legacy Format Migration Older `.lager` files can use lowercase section names. The CLI automatically upgrades these when it writes them: | Legacy Key | Current Key | | ---------- | ----------- | | `duts` | `BOXES` | | `DUTS` | `BOXES` | | `LAGER` | `DEFAULTS` | | `nets` | `NETS` | | `devenv` | `DEVENV` | | `debug` | `DEBUG` | No manual migration is required. The CLI reads both formats and writes back the current uppercase format. *** ## Complete Examples ### Global `~/.lager` ```json theme={null} { "DEFAULTS": { "gateway_id": "my-lager-box", "net_power_supply": "VDD_MAIN", "net_debug": "SWD", "net_uart": "SERIAL_DBG", "net_adc": "ADC_SENSE" }, "BOXES": { "my-lager-box": { "ip": "100.64.0.10", "version": "main" }, "staging-box": "100.64.0.11", "legacy-box": "192.168.1.50" }, "NETS": { "my-lager-box": [ { "name": "VDD_MAIN", "role": "supply", "instrument": "Rigol_DP831", "address": "USB0::0x1AB1::0x0E11::DP8XXXXXXX::INSTR", "pin": "1" }, { "name": "SWD", "role": "debug", "instrument": "JLink", "address": "USB0::JLink", "pin": "0" }, { "name": "SERIAL_DBG", "role": "uart", "instrument": "Unknown_UART_Device", "address": "USB0::uart", "pin": "/dev/ttyUSB0", "device_path": "/dev/ttyUSB0" }, { "name": "ADC_SENSE", "role": "adc", "instrument": "LabJack_T7", "address": "USB0::LabJack", "pin": "AIN0" } ] } } ``` ### Project-local `./my-firmware/.lager` ```json theme={null} { "DEVENV": { "image": "lagerdata/devenv-cortexm", "mount_dir": "/app", "shell": "/bin/bash", "cmd.build": "make -j$(nproc)", "cmd.flash": "make flash" }, "DEBUG": { "SWD": "./scripts/my_device.JLinkScript" }, "includes": { "test_framework": "../shared/test_framework" } } ``` # Box Locking Source: https://docs.lagerdata.com/source/reference/cli/locking Shared access control for Lager Boxes When multiple users — or multiple CI jobs — share a Lager Box, locks prevent two callers from clobbering each other. Lager provides two locking mechanisms: 1. **Automatic test / admin lock** — `lager python` and the box-mutating admin commands (`lager install`, `lager uninstall`, `lager update`, `lager install-wheel`) reserve the box for the lifetime of the command. 2. **User lock** — `lager boxes lock` explicitly reserves a box until you unlock it. ## Automatic test lock Every `lager python ` invocation automatically acquires the box lock at start and releases it at end. This includes failures, `Ctrl+C`, crashes, and signal-killed runs. The lock is released through a `finally` block, a signal handler, an `atexit` net, and (worst case) a server-side TTL reap. ### Which commands auto-lock | Category | Commands | Lock window | Why | | ------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------- | ----------------------------------------------------------------- | | Test runner | `lager python` | Full test run (acquire → heartbeat → release) | Canonical test runner. | | Measurement | `gpi`, `gpo`, `adc`, `dac`, `thermocouple`, `watt`, `energy`, `scope`, `logic` | Single command invocation | Hardware I/O — interleaved access corrupts readings or pin state. | | Communication | `spi`, `i2c`, `uart`, `wifi`, `ble`, `blufi`, `usb`, `router` | Single command invocation | Bus/protocol transactions must not interleave across users. | | Power | `supply`, `battery`, `eload`, `solar` | Single command invocation (subcommands only, not listing) | Concurrent voltage/current changes are dangerous. | | Development | `debug` (flash/connect/erase/reset/etc.), `arm`, `webcam` | Single command invocation | Flashing or debug sessions must not collide. | | Admin | `install`, `uninstall`, `update`, `install-wheel` | Destructive section only | Container restart/mutation mid-test would kill the test. | Read-only commands (`lager hello`, `lager boxes list`, `lager boxes lock` / `unlock` itself, net-listing paths like `lager supply --box X` with no subcommand, status / dry-run paths, etc.) do **not** acquire the auto-lock. The v0.12–0.13.3 design used a shared decorator on every command, and v0.13.4 reverted it. This implementation instead uses the same TTL + heartbeat + atexit infrastructure as `lager python`. That avoids the three corner cases that motivated the revert. See *Backward compatibility* below for the full history. The lock identity is **CI-aware** so concurrent test runs in CI mutually exclude correctly. Holder formats: | Environment | Holder string | | ------------------- | ------------------------------------------------------- | | Dev (your machine) | OS user (same as `lager defaults --user`) | | GitHub Actions | `ci:github:#-/@:` | | Drone | `ci:drone:#:@` | | GitLab CI | `ci:gitlab:#/:@` | | Bitbucket Pipelines | `ci:bitbucket:#:@` | | Jenkins | `ci:jenkins::@` | | Generic CI fallback | `ci:generic::` | The `:pid` (and `@runner` / `@host`) suffix guarantees that two parallel matrix items in the same workflow run get distinct holder strings. ### Collision behavior When `lager python` tries to acquire a lock that another holder owns: * **On dev**: prints an error and exits 1 immediately (no waiting). * **In CI**: waits up to `LAGER_LOCK_WAIT` seconds (default `1800`, i.e. 30 min), polling every 2s, and only fails if the wait elapses. This lets matrix jobs queue against the same self-hosted box. If you locked the box as yourself with `lager boxes lock` before you ran `lager python`, the CLI sees the lock as already-ours. It **does not release the lock on exit**, so your explicit reservation survives the test. ### TTL & heartbeat Each test lock is written with `ttl_seconds: 1800` and refreshed every 60 seconds by a background heartbeat thread inside the CLI. The TTL is **not** a cap on test runtime — as long as the heartbeat keeps refreshing `last_heartbeat`, the lock stays valid indefinitely. What the TTL actually bounds is the worst-case **stale-lock dwell time after a CLI crash**. If your laptop loses network, or the CI runner is hard-killed, the box reaps the lock once `last_heartbeat + ttl_seconds` falls in the past. Another caller therefore waits at most one TTL. ### `--detach` hands the lock to the box `lager python script.py --detach` has no CLI left to hold its lock -- the client is answered and goes away, which is the point. So the box takes the lock's lifetime over: it heartbeats while the detached job runs and releases when the job ends, however it ends. Nothing has to be unlocked by hand. ```bash theme={null} lager python long_test.py --box my-lager-box --detach # Box 'my-lager-box' is held for the detached run and released when it ends; # to free it sooner: lager boxes unlock --box my-lager-box ``` The box can only ever touch the lock the CLI handed it, and only a lock the run freshly acquired is handed over. A `lager boxes lock` reservation that a detached run merely resumed is never handed over and never released. That reservation is the whole point of taking one. Against a box too old to know about the handoff, the CLI keeps the previous behavior. That is an eternal hold, with the old "release with `lager boxes unlock`" message. The CLI arms the lapse TTL only after the box confirms that it heartbeats. So a newer CLI can never leave a lock that expires underneath a job that still runs. ### Escape hatches | Env var | Effect | | ---------------------------- | -------------------------------------------------------------------------------------------------------- | | `LAGER_AUTO_LOCK_DISABLE=1` | Skip auto-lock entirely. The command still checks for someone else's user lock but does not acquire. | | `LAGER_LOCK_WAIT=` | Override collision wait time. `0` = fail-fast (dev default), large value = patient queue (CI default). | | `LAGER_LOCK_HOLDER=` | Override the holder identity. Useful when you intentionally want two jobs to share a single reservation. | | `LAGER_LOCK_TTL=` | Override the TTL the CLI writes. `LAGER_LOCK_TTL=none` = eternal (caller must `lager boxes unlock`). | | `LAGER_LOCK_HEARTBEAT=` | Override the heartbeat refresh interval (default 60s). | ## User lock A **user lock** is an explicit, persistent reservation you place on a box. Unlike the automatic test lock, user locks **never expire** — you must manually unlock when you're done. Use cases: * Reserving a box for an extended debugging session. * Preventing others from using a box during maintenance. * Claiming a box when you're not actively running a command. ### `lager boxes lock` ```bash theme={null} lager boxes lock --box NAME ``` **Options**: * `--box` (required) — name of the box to lock. * `--user` — username to lock as (useful when running inside Docker where the user is otherwise `root`). **Example**: ```bash theme={null} lager boxes lock --box my-lager-box # Output: Box 'my-lager-box' is locked by alice ``` If the box is already locked by another user: ``` Error: Box 'my-lager-box' is already locked by bob (since 2026-03-20T13:00:00Z) ``` ### `lager boxes unlock` ```bash theme={null} lager boxes unlock --box NAME [--force] ``` **Options**: * `--box` (required) — name of the box to unlock. * `--force` — force unlock even if the box was locked by another user (use this to clear a stale `lager boxes lock` left by a teammate). **Examples**: ```bash theme={null} # Unlock your own lock lager boxes unlock --box my-lager-box # Force unlock a box locked by someone else lager boxes unlock --box my-lager-box --force ``` ## Management operations skip the lock The following sub-commands of `lager python` are *management operations* on already-running processes and intentionally skip both lock checks and auto-acquire: * `lager python --kill ` * `lager python --kill-all` * `lager python --reattach ` * `lager python --continue ` * `lager python --console ` This is what lets you Ctrl+C a hung detached script and immediately `--kill` it without first having to fight an unrelated user lock. ## `lager boxes` shows lock holders When boxes are locked, `lager boxes` shows an extra column: ``` name ip version status locked by ===================================================================== my-lager-box 100.x.x.1 0.24.0 current alice staging-box 100.x.x.2 0.24.0 current github lager run 9182 job test on runner-3 pi-box 100.x.x.3 0.24.0 current ``` CI holders are formatted human-readably (e.g. `github lager run 9182 job test on runner-3`) rather than printed as raw colon-delimited strings. ## CI workflow example The always-on auto-lock + CI auto-wait combination means a CI matrix job needs no special invocation: ```yaml theme={null} # .github/workflows/integration-tests.yml jobs: hardware-tests: strategy: matrix: suite: [power, communication, debug] runs-on: [self-hosted, lager-bench] steps: - uses: actions/checkout@v4 - run: pip install lager-cli - run: lager python test/api/${{ matrix.suite }} --box my-lager-box ``` The three matrix items each get a unique holder, because `...GITHUB_JOB=hardware-tests/:` differs per item. Each item then POSTs `/lock`. Whichever item loses the race waits up to 30 minutes for the winner to finish, and then retries. No `lager boxes lock` call needed. ## Backward compatibility * `lager boxes lock` and `lager boxes unlock` behave exactly as before. The CLI now sends `holder_type: "user"` + `ttl_seconds: null` on the wire. A legacy client — an older CLI against the new box server — still gets the same eternal-lock behavior. The server treats a payload with neither field as legacy, and applies the same defaults. * `_check_box_lock` (the read-only lock check that already gates every command in resolve\_and\_validate\_box) is unchanged. ### How this differs from v0.13.0 – v0.13.3 (removed in v0.13.4) v0.13.0 added an ephemeral "command-in-progress" lock that fired on **every** CLI command via a shared decorator, gated by a `--force-command` flag. v0.13.4 removed it because three corner cases were unfixable in that design: | v0.13.4 corner case | How the current design avoids it | | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *"Supply commands never released the lock"* | Every auto-lock uses `auto_lock_acquire_for_command` which registers an `atexit` handler + heartbeat thread. The lock is released on normal exit, exception, SIGINT, or (worst case) reaped by the server TTL after SIGKILL. The v0.13 bug was a decorator that swallowed exceptions before release ran — the atexit safety net makes that impossible. | | *"Long-running commands blocked all other commands on the same box"* | Read-only / listing paths (`lager supply --box X` with no subcommand, `lager gpi --box X` with no netname, `lager boxes`, `lager hello`) use plain `resolve_box()` which only checks the lock passively. Only hardware-*interacting* subcommands acquire. For concurrent hardware use, dev gets fail-fast in \<1s and CI gets a queue (default 30min, configurable via `LAGER_LOCK_WAIT`). | | *"Detached processes left stale locks"* | All ephemeral locks have `ttl_seconds=1800` + heartbeat every 60s. If the CLI dies, the box reaps the lock within one TTL. `--detach` (on `lager python` only) has no CLI left to heartbeat, so the box holds that lock for exactly as long as the job it launched and releases it when the job ends — including a job that fails to start, which used to leave the box locked with nothing running on it. | `--force-command` is **gone**. Collision policy is structured (fail-fast in dev, queue in CI) and the existing `lager boxes lock --force` is the escape hatch when you genuinely need to override. # Logic Analyzer (Preview) Source: https://docs.lagerdata.com/source/reference/cli/logic Control logic analyzer channels and triggers Control logic analyzer Nets through the Lager CLI for digital signal capture, protocol decoding, and trigger configuration. **Not Yet Available:** The Logic Analyzer feature for Rigol MSO5000 series is currently under development. The commands below are documented for preview purposes only. The underlying device methods are not yet implemented and the CLI command is disabled. Attempting to use these commands will result in an error. Check back in a future release for full functionality. ## Syntax ```bash theme={null} lager logic [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 | | -------------- | ------------------------------ | | `enable` | Enable logic analyzer channel | | `disable` | Disable logic analyzer channel | | `start` | Start waveform capture | | `start-single` | Start single waveform capture | | `stop` | Stop waveform capture | | `measure` | Measure signal characteristics | | `trigger` | Configure trigger settings | | `cursor` | Control cursor position | ## Command Reference ### `enable` Enable logic analyzer channel for the specified net. ```bash theme={null} lager logic NET_NAME enable [--box BOX] [--mcu MCU] ``` ### `disable` Disable logic analyzer channel. ```bash theme={null} lager logic NET_NAME disable [--box BOX] [--mcu MCU] ``` ### `start` Start continuous waveform capture. ```bash theme={null} lager logic NET_NAME start [--box BOX] [--mcu MCU] ``` ### `start-single` Start single waveform capture (one-shot). ```bash theme={null} lager logic NET_NAME start-single [--box BOX] [--mcu MCU] ``` ### `stop` Stop waveform capture. ```bash theme={null} lager logic NET_NAME stop [--box BOX] [--mcu MCU] ``` *** ## Measure Subcommands ### `measure period` Measure signal period. ```bash theme={null} lager logic NET_NAME measure period [--display BOOL] [--cursor BOOL] ``` ### `measure freq` Measure signal frequency. ```bash theme={null} lager logic NET_NAME measure freq [--display BOOL] [--cursor BOOL] ``` ### `measure dc-pos` / `measure dc-neg` Measure positive or negative duty cycle. ```bash theme={null} lager logic NET_NAME measure dc-pos [--display BOOL] [--cursor BOOL] lager logic NET_NAME measure dc-neg [--display BOOL] [--cursor BOOL] ``` ### `measure pw-pos` / `measure pw-neg` Measure positive or negative pulse width. ```bash theme={null} lager logic NET_NAME measure pw-pos [--display BOOL] [--cursor BOOL] lager logic NET_NAME measure pw-neg [--display BOOL] [--cursor BOOL] ``` *** ## Trigger Subcommands ### `trigger edge` Set edge trigger configuration. ```bash theme={null} lager logic NET_NAME trigger edge [OPTIONS] ``` **Options:** * `--mode` - Trigger mode: `normal`, `auto`, `single` (default: normal) * `--coupling` - Coupling mode: `dc`, `ac`, `low_freq_rej`, `high_freq_rej` (default: dc) * `--source NET` - Trigger source net * `--slope` - Trigger slope: `rising`, `falling`, `both` * `--level FLOAT` - Trigger level in volts ### `trigger pulse` Set pulse trigger configuration. ```bash theme={null} lager logic NET_NAME trigger pulse [OPTIONS] ``` **Options:** * `--mode` - Trigger mode * `--coupling` - Coupling mode * `--source NET` - Trigger source * `--level FLOAT` - Trigger level * `--trigger-on` - Trigger on: `gt`, `lt`, `gtlt` * `--upper FLOAT` - Upper pulse width * `--lower FLOAT` - Lower pulse width ### `trigger i2c` Set I2C protocol trigger. ```bash theme={null} lager logic NET_NAME trigger i2c [OPTIONS] ``` **Options:** * `--mode` - Trigger mode * `--coupling` - Coupling mode * `--source-scl NET` - SCL trigger source * `--source-sda NET` - SDA trigger source * `--level-scl FLOAT` - SCL trigger level * `--level-sda FLOAT` - SDA trigger level * `--trigger-on` - Trigger on: `start`, `restart`, `stop`, `nack`, `address`, `data`, `addr_data` * `--address INT` - Address value (for address trigger) * `--addr-width` - Address width: `7`, `8`, `9`, `10` bits * `--data INT` - Data value (for data trigger) * `--data-width` - Data width: `1`-`5` bytes * `--direction` - Direction: `write`, `read`, `rw` ### `trigger uart` Set UART protocol trigger. ```bash theme={null} lager logic NET_NAME trigger uart [OPTIONS] ``` **Options:** * `--mode` - Trigger mode * `--coupling` - Coupling mode * `--source NET` - Trigger source * `--level FLOAT` - Trigger level * `--trigger-on` - Trigger on: `start`, `error`, `cerror`, `data` * `--parity` - Parity: `even`, `odd`, `none` * `--stop-bits` - Stop bits: `1`, `1.5`, `2` * `--baud INT` - Baud rate * `--data-width INT` - Data width in bits * `--data INT` - Data value to trigger on ### `trigger spi` Set SPI protocol trigger. ```bash theme={null} lager logic NET_NAME trigger spi [OPTIONS] ``` **Options:** * `--mode` - Trigger mode * `--coupling` - Coupling mode * `--source-mosi-miso NET` - MOSI/MISO source * `--source-sck NET` - SCK source * `--source-cs NET` - CS source * `--level-mosi-miso FLOAT` - MOSI/MISO level * `--level-sck FLOAT` - SCK level * `--level-cs FLOAT` - CS level * `--data INT` - Trigger data value * `--data-width INT` - Data width in bits * `--clk-slope` - Clock slope: `positive`, `negative` * `--trigger-on` - Trigger on: `timeout`, `cs` * `--cs-idle` - CS idle state: `high`, `low` * `--timeout FLOAT` - Timeout length *** ## Cursor Subcommands ### `cursor set-a` / `cursor set-b` Set cursor A or B position. ```bash theme={null} lager logic NET_NAME cursor set-a [--x FLOAT] [--y FLOAT] lager logic NET_NAME cursor set-b [--x FLOAT] [--y FLOAT] ``` ### `cursor move-a` / `cursor move-b` Shift cursor position. ```bash theme={null} lager logic NET_NAME cursor move-a [--del-x FLOAT] [--del-y FLOAT] lager logic NET_NAME cursor move-b [--del-x FLOAT] [--del-y FLOAT] ``` ### `cursor hide` Hide the cursor. ```bash theme={null} lager logic NET_NAME cursor hide ``` *** ## Examples ```bash theme={null} # Enable logic channel lager logic SPI_CLK enable --box my-lager-box # Start capture lager logic SPI_CLK start # Measure frequency lager logic SPI_CLK measure freq # Set edge trigger on rising edge at 1.5V lager logic SPI_CLK trigger edge --slope rising --level 1.5 # Set I2C trigger on address match lager logic I2C_SDA trigger i2c --trigger-on address --address 0x50 --direction write # Set UART trigger on data match lager logic UART_TX trigger uart --trigger-on data --baud 115200 --data 0x55 # Set SPI trigger lager logic SPI_MOSI trigger spi --data 0xFF --data-width 8 ``` *** ## Supported Hardware | Manufacturer | Model Series | Features | | ------------ | ------------ | ----------------------------- | | Rigol | MSO5000 | Mixed-signal, protocol decode | | Saleae | Logic Pro | High-speed capture | *** ## Notes * Logic nets capture digital signals (high/low states) * Protocol triggers (I2C, UART, SPI) require proper level configuration * Use `lager nets` to see available logic nets * Analog and Logic nets can be combined for mixed-signal analysis # Signing In Source: https://docs.lagerdata.com/source/reference/cli/login Using lager login for access-controlled boxes Most Lager Boxes need no sign-in — you run commands and they work. Some boxes, though, sit behind an **access gateway**: an authenticating proxy that only lets assigned users reach the box. Against one of those, Lager asks you to sign in once, then authenticates every command automatically. A plain Lager Box never prompts for this. You only see it when someone deliberately places a box behind a gateway. ## Signing in The first time you run a command against a gated box, it tells you exactly what to do. The message fills in the URL for you: ```bash theme={null} lager login https://your-control-plane.example.com ``` You'll be asked for your account email and password (and an MFA code if your account uses one). Your session is stored in `~/.lager_gateway_auth` (readable only by you) and refreshes on its own, so you rarely sign in more than once. From then on, `lager hello`, `lager python`, net commands, and everything else just work against that box. ### Non-interactive sign-in Both credentials can be supplied as options instead of being prompted for, which is what you want in a CI job: | Option | Description | | ----------------- | ---------------- | | `--email TEXT` | Account email | | `--password TEXT` | Account password | ```bash theme={null} lager login https://your-control-plane.example.com --email "$LAGER_EMAIL" --password "$LAGER_PASSWORD" ``` A password passed on the command line is visible to other users via the process list and is written to your shell history. Read it from a secret store or an environment variable, as above, rather than typing the literal value. If the account has MFA enabled, these two options are not enough on their own. The CLI still prompts for the MFA code, so the sign-in is not fully unattended. Use an account without MFA for automation. ```bash theme={null} lager logout # forget every stored session lager logout # forget one server's session ``` ## Checking your status When something looks off, `lager whoami` is the first thing to run: ```bash theme={null} lager whoami ``` It shows four things: * which servers you are signed in to * who you are signed in as * when each session expires * which gated boxes the CLI saw That separates three problems at a glance: "not signed in", "signed in as the wrong account", and "signed in but no access". ## Common messages and what they mean | Message | What it means | What to do | | -------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | | **This box requires sign-in** | The box is gated and you have no stored session. | Run the `lager login ` it prints, then retry. | | **requires sign-in… now linked to this box** | First contact after signing in — the box was just linked to your session. | Re-run the command; it authenticates automatically. | | **Your session… was rejected** | Your session expired or was revoked. | Run `lager login ` again. | | **signed in but not authorized** | Your account is valid but has no access grant for this box. | Ask an org admin to grant you access. | | **could not verify your access right now** | The box couldn't reach its auth server. | Try again shortly; if it persists, contact your admin. | If you hit any of these on an **old** Lager version, upgrade first — sign-in support needs a current CLI: ```bash theme={null} pip install --upgrade lager-cli ``` ## For administrators Your control-plane dashboard, not the CLI, controls whether a box requires sign-in and who can use it. Assign users to a box, then turn its access guard on; denied attempts are logged so you can see who needs access. # Logs Source: https://docs.lagerdata.com/source/reference/cli/logs Manage Lager Box logs View, clean, and manage log files on Lager Boxes. ## Syntax ```bash theme={null} lager logs COMMAND [OPTIONS] ``` ## Commands | Command | Description | | -------- | ------------------------------------- | | `size` | Check log file sizes on Lager Box(es) | | `clean` | Clean old log files from Lager Box | | `docker` | Check Docker container log sizes | *** ## Command Reference ### `size` Check log file sizes on one or all Lager Boxes. ```bash theme={null} lager logs size [--box BOX] [--verbose] ``` **Options:** * `--box BOX` - Specific Lager Box (if not specified, checks all) * `--verbose` / `-v` - Show individual log files **Examples:** ```bash theme={null} # Check all Lager Boxes lager logs size # Check specific Lager Box lager logs size --box my-lager-box # Show individual files lager logs size --box my-lager-box --verbose ``` Output: ``` Log sizes on my-lager-box: Total: 1.2 GB [WARNING] Log size exceeds 500 MB Details (--verbose): /var/log/syslog: 450 MB /var/log/docker.log: 320 MB /var/log/lager/*.log: 430 MB ``` ### `clean` Remove old log files from a Lager Box. ```bash theme={null} lager logs clean --box BOX [--older-than DAYS] [--yes] ``` **Options:** * `--box BOX` (required) - Lager Box to clean * `--older-than DAYS` - Remove logs older than N days (default: 1) * `--yes` - Skip confirmation prompt **Examples:** ```bash theme={null} # Clean logs older than 1 day (default) lager logs clean --box my-lager-box # Clean logs older than 7 days lager logs clean --box my-lager-box --older-than 7 # Clean without confirmation lager logs clean --box my-lager-box --yes ``` Output: ``` Cleaning logs older than 1 day on my-lager-box... Removed 15 files Space freed: 856 MB ``` ### `docker` Check Docker container log sizes. ```bash theme={null} lager logs docker --box BOX [--container NAME] ``` **Options:** * `--box BOX` (required) - Lager Box to check * `--container NAME` - Specific container (default: all) **Examples:** ```bash theme={null} # Check all containers lager logs docker --box my-lager-box # Check specific container lager logs docker --box my-lager-box --container controller ``` Output: ``` Docker log sizes on my-lager-box: controller: 8.2 MB python: 12.4 MB hardware_rtc: 3.1 MB Docker log rotation: max-size=10m, max-file=3 ``` *** ## Log Rotation Docker containers use automatic log rotation: * **Maximum size**: 10 MB per file * **Maximum files**: 3 (rotates oldest) This means Docker logs are self-managing and shouldn't grow unbounded. *** ## Size Thresholds The `size` command uses warning thresholds: | Size | Status | | ------------- | -------- | | \< 500 MB | Normal | | 500 MB - 1 GB | Warning | | > 1 GB | Critical | *** ## What Gets Cleaned The `clean` command removes: * System logs in `/var/log/` * Lager application logs * Old rotated log files (`.log.1`, `.log.gz`, etc.) It does **not** remove: * Current log files * Docker container logs (managed separately) * Files newer than `--older-than` threshold *** ## Automation Schedule regular log cleaning: ```bash theme={null} # In crontab or CI/CD lager logs clean --box my-lager-box --older-than 7 --yes ``` *** ## Examples ```bash theme={null} # Daily maintenance workflow for box in gw1 gw2 gw3; do echo "Checking $box..." lager logs size --box $box lager logs clean --box $box --older-than 3 --yes done # Monitor log growth lager logs size # Check all Lager Boxes lager logs docker --box my-lager-box # Check Docker logs ``` *** ## Notes * Log cleaning requires SSH access to the Lager Box * Docker logs are automatically rotated * Use `--verbose` to identify large log files * Regular cleaning prevents disk space issues # Nets Source: https://docs.lagerdata.com/source/reference/cli/nets Create and manage nets (test points) on your Lager Box Nets are the core abstraction in Lager for representing physical test points, signals, or buses on your device under test. Each net maps a friendly name to a specific instrument channel. ## Syntax ```bash theme={null} lager nets [OPTIONS] [COMMAND] ``` ## Global Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | --------------- | ---------------------------------------------------------------------------------------- | | (none) | List all saved nets (default) | | `delete` | Delete a specific net by name and type | | `delete-all` | Delete all saved nets (dangerous) | | `rename` | Rename an existing net | | `add` | Add a new net | | `add-all` | Auto-create all available nets from connected instruments | | `add-batch` | Create multiple nets from a JSON file | | `assign` | Assign a USB-serial cable to an RS-232 instrument the box can't auto-detect | | `show` | Show full details of a saved net, including metadata | | `describe` | Set metadata on a saved net (purpose, notes, tags) for agent-assisted testing | | `tui` | Launch interactive Net Manager TUI | | `set-script` | Attach a J-Link script *or* OpenOCD `.cfg`/`.tcl` to a debug net (backend auto-detected) | | `remove-script` | Remove the debug script (J-Link or OpenOCD) attached to a debug net | | `show-script` | Display the debug script attached to a debug net | ## Command Reference ### List Nets (Default) List all saved nets on a Lager Box. This is the default behavior when no subcommand is provided. ```bash theme={null} lager nets --box my-lager-box ``` **Output Columns:** | Column | Description | | ------------ | ----------------------------------------------------------------- | | `Name` | User-friendly net identifier | | `Net Type` | Role/type of net (power-supply, debug, adc, gpio, i2c, spi, etc.) | | `Instrument` | Physical equipment (Rigol\_DP811, Keithley\_2281S, etc.) | | `Channel` | Specific channel on the instrument | | `Address` | VISA or USB address of the instrument | | `Script` | Whether a J-Link script is attached (debug nets only) | The `Script` column only appears if any debug net has a J-Link script attached. **Example Output:** ``` Name Net Type Instrument Channel Address ================================================================================ supply1 power-supply Rigol_DP811 1 TCPIP::192.168.1.100::INSTR battery1 battery Keithley_2281S 1 TCPIP::192.168.1.101::INSTR debug1 debug J-Link STM32F4 USB::001::002 adc1 adc LabJack_T7 AIN0 USB::470026574 gpio1 gpio LabJack_T7 FIO0 USB::470026574 i2c1 i2c LabJack_T7 0 USB::470026574 spi1 spi Aardvark 0 USB::2238595116 uart1 uart Prolific_USB 0 /dev/ttyUSB0 ``` ### `add` Create a new net by specifying its name, type, channel, and instrument address. ```bash theme={null} lager nets add NAME ROLE CHANNEL ADDRESS [OPTIONS] ``` **Arguments:** * `NAME` - Unique name for the net (e.g., `supply1`, `debug_main`) * `ROLE` - Type of net: `power-supply`, `battery`, `solar`, `debug`, `adc`, `dac`, `gpio`, `scope`, `eload`, `uart`, `usb`, `camera`, `arm`, `watt-meter`, `thermocouple`, `i2c`, `spi`. The legacy tokens `supply` and `batt` are accepted as input aliases and normalized to `power-supply` / `battery` — saved nets always carry the canonical role. * `CHANNEL` - Channel identifier (e.g., `1`, `AIN0`, `FIO0`, `STM32F4`, `0`) * `ADDRESS` - VISA address or device path (e.g., `TCPIP::192.168.1.100::INSTR`) **Options:** * `--box TEXT` - Lager Box name or IP * `--jlink-script FILE` - J-Link script file for debug nets (stored on box) * `--sda PIN` / `--scl PIN` - Custom LabJack pins for `i2c` nets * `--cs PIN` / `--sck PIN` / `--mosi PIN` / `--miso PIN` - Custom LabJack pins for `spi` nets Pin values accept LabJack DIO names (`FIO0`-`FIO7`, `EIO0`-`EIO7`, `CIO0`-`CIO3`, `MIO0`-`MIO2`) or raw DIO numbers (`0`-`22`). When pin options are given, the `CHANNEL` argument is ignored — pass `custom`. If a chosen pin overlaps another saved LabJack net, a warning is printed but the net is still created. `--cs` is optional: omit it for 3-pin SPI with manual chip select. **Examples:** ```bash theme={null} # Create a power supply net lager nets add supply1 power-supply 1 TCPIP::192.168.1.100::INSTR --box my-lager-box # Create a debug net for STM32 lager nets add debug1 debug STM32F407VG USB::001::002 --box my-lager-box # Create a debug net with J-Link script lager nets add debug1 debug STM32F407VG USB::001::002 --jlink-script ./my_device.JLinkScript --box my-lager-box # Create an ADC net on LabJack lager nets add temp_sensor adc AIN0 USB::470026574 --box my-lager-box # Create an I2C net on LabJack (default pins: SDA=FIO4, SCL=FIO5) lager nets add i2c_bus i2c FIO4-FIO5 USB::470026574 --box my-lager-box # Create an I2C net on LabJack with custom pins lager nets add i2c_bus i2c custom USB::470026574 --sda EIO0 --scl EIO1 --box my-lager-box # Create an I2C net on Aardvark lager nets add i2c_aardvark i2c 0 USB::2238595116 --box my-lager-box # Create an SPI net on LabJack (default pins: CS=FIO0, SCK=FIO1, MOSI=FIO2, MISO=FIO3) lager nets add spi_bus spi FIO0-FIO3 USB::470026574 --box my-lager-box # Create an SPI net on LabJack with custom pins lager nets add spi_flash spi custom USB::470026574 --cs FIO6 --sck FIO7 --mosi EIO0 --miso EIO1 --box my-lager-box # Custom-pin SPI without chip select (3-pin SPI, manual CS via gpio) lager nets add spi_flash spi custom USB::470026574 --sck FIO7 --mosi EIO0 --miso EIO1 --box my-lager-box # Create an SPI net on Aardvark lager nets add spi_aardvark spi 0 USB::2238595116 --box my-lager-box # Create a UART net lager nets add serial1 uart 0 /dev/ttyUSB0 --box my-lager-box ``` The `--jlink-script` option is only applicable for debug nets. If used with other net types, a warning is printed and the option is ignored. **Validation Rules:** * Net names must be globally unique across all types * The (role, instrument, channel, address) tuple must match a connected instrument * Channel binding follows the per-instrument rules described in **Channel & Role Constraints** below ### Channel & Role Constraints Different instrument families bind nets to channels differently. Lager classifies every supported instrument into one of three categories and enforces the rules consistently across `add`, `add-all`, and the TUI. #### 1. Multi-channel instruments Instruments with physically independent outputs / inputs. Each channel is its own circuit and can host its own net. | Instrument | Channels | Notes | | ------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `Rigol_DP811` / `DP821` / `DP831` / `DP832` | `1`, `2`, `3` | One `power-supply` net per output | | `KEYSIGHT_E36233A` | `1`, `2` | Dual-output supply | | `KEYSIGHT_E36313A` / `E36312A` | `1`, `2`, `3` | Triple-output supply | | `LabJack_T7` | `AIN0`–`AIN13`, `FIO0`–`FIO7`, `DAC0`–`DAC1`, etc. | One net per pin | | `LabJack_U3` | `AIN0`–`AIN15`, `DAC0`–`DAC1`; `gpio` on `FIO4`–`FIO7`, `EIO0`–`EIO7`, `CIO0`–`CIO3` | `FIO0`–`FIO3` are fixed high-voltage analog inputs — read them with an `adc` net on `AIN0`–`AIN3`, never as `gpio`. A U3 has no `i2c` or `spi` | | `Aardvark` | `SPI0`, `I2C0`, GPIO pins | Mixed-mode multi-channel | | `MCC_USB-202` | `CH0`–`CH7`, `DIO0`–`DIO7`, `DAC0`–`DAC1` | One net per channel | | `Phidget` | `0`–`3` | One `thermocouple` net per channel | | `Acroname_8Port` / `4Port`, `YKUSH_Hub` | Port indices | One `usb` net per port | **Rule:** at most one net per `(instrument, address, role, channel)` tuple. Two nets that share `(instrument, address, role)` but differ in `channel` are fine — that's exactly what multi-channel is for. #### 2. Single-channel, multi-mode instruments Instruments with one physical channel that can run in one of several **modes** but not multiple modes at once. The role tells the box which firmware mode to flip the chip into. | Instrument | Allowed roles | Why exclusive | | ----------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------- | | `Keithley_2281S` | `battery` or `power-supply` | One channel: battery-simulator firmware OR power-supply firmware | | `EA_PSB_10080_60` | `solar` or `power-supply` | One channel: solar-array simulator OR straight supply | | `EA_PSB_10060_60` | `solar` or `power-supply` | Same as above | | `FTDI_FT232H` | `spi` or `i2c` or `gpio` or `debug` or `uart` | One channel hardware-multiplexed between MPSSE (libftdi) and async-serial (`ftdi_sio`) modes | **Rule:** at most one net per `(instrument, address)`. Once any role is saved on the chip, every other role disappears from the add list. To switch modes, delete the existing net first. These chips are tracked in `_SINGLE_CHANNEL_INST` (Keithley, EA) and `_MODE_EXCLUSIVE_INST` (FTDI\_FT232H) in `cli/commands/box/nets.py` and `cli/commands/box/net_tui.py`. #### 3. Single-role debug probes Standalone debugger boxes — one probe drives one target MCU. | Instrument | Backend | Role | | --------------------------------------------------------------- | ------------------- | ------- | | `J-Link` / `J-Link_Plus` / `Flasher_ARM` / `J-Link_Flasher_Pro` | J-Link (SEGGER) | `debug` | | `STLink_v2` / `v2_1` / `v3` / `v3_Mini` / `v3_2VCP` | OpenOCD | `debug` | | `RP2040_Picoprobe` | OpenOCD (CMSIS-DAP) | `debug` | | `Atmel_EDBG` | OpenOCD (CMSIS-DAP) | `debug` | | `DAPLink` | OpenOCD (CMSIS-DAP) | `debug` | **Rule:** at most one `debug` net per `(instrument, address)`. #### 4. Multi-channel FTDI debug adapters `FT2232H` (2 channels: A, B) and `FT4232H` (4 channels: A, B, C, D) physically expose multiple USB interfaces. Channels A and B have an MPSSE engine; on the FT4232H, C and D do not. That matters per *net type*, not per chip. `debug`, `spi` and `i2c` are MPSSE protocols, so they are limited to A and B. `gpio` runs as asynchronous bitbang, needs no MPSSE, and works on all four. The user picks an interface per net. **Debug nets** encode the channel in the device field: ```bash theme={null} # Channel A (interface 0) — by far the most common lager nets add debug_a debug STM32F4x@A USB0::0x0403::0x6010::ABCDEF::INSTR # Channel B (interface 1) — second target on the same FT2232H lager nets add debug_b debug NRF52840_XXAA@B USB0::0x0403::0x6010::ABCDEF::INSTR ``` Equivalent forms: `@A`/`@0`, `@B`/`@1`, `@C`/`@2`, `@D`/`@3`. Devices without an `@` suffix default to the interface OpenOCD's interface config picks (typically channel A). **GPIO, I2C and SPI nets** take the channel as `params.interface` on the net record, accepting the same vocabulary as the `@` suffix: `A`-`D` or `0`-`3`. A net with no `interface` uses channel A, the only choice on a single-channel FT232H. Lager enforces which channels are legal per net, not per instrument. If you ask for C or D on an `i2c` or `spi` net, net construction fails and names the channel. The failure does not come from somewhere inside pyftdi. A `gpio` net can use any channel the part has. Before v0.43.0 all three drivers hardcoded interface A. A board that wires comms to one channel and control lines to another was therefore impossible to drive. **UART nets** distinguish channels by their tty path. The USB scanner enumerates every `/dev/ttyUSB` bound to the chip's USB serial. Each one shows up as a separate add-list entry, so on an FT4232H you will see up to four UART options. **Rule:** a debug net is unique per `(instrument, address, channel-suffix)`. So a single FT2232H can host: * one `debug` net on `@A` * one `debug` net on `@B` * one `uart` net on a `/dev/ttyUSB` belonging to whichever channels you didn't claim for MPSSE * one each of `spi` / `i2c` / `gpio`, each on a channel you name — see below The user is responsible for not double-booking a channel (e.g. `debug@A` *and* a `spi` net on interface A); the box doesn't validate that today. #### Quick decision table | You want to add a second net on the same chip | Allowed? | | --------------------------------------------------------------------------------- | ---------------------------- | | `power-supply` on `Rigol_DP811` CH1 + `power-supply` on Rigol\_DP811 CH2 | Yes | | `battery` on `Keithley_2281S` + `power-supply` on the same Keithley | **No** — pick one | | `solar` on `EA_PSB_10080_60` + `power-supply` on the same EA | **No** — pick one | | `spi` on `FTDI_FT232H` + `uart` on the same FT232H | **No** — pick one | | `debug@A` on `FTDI_FT2232H` + `uart` on a different interface of the same FT2232H | Yes | | Two `debug@A` nets on the same `FTDI_FT2232H` | **No** — same channel | | Two `debug` nets on the same J-Link | **No** — single-target probe | ### `add-all` Automatically create nets for all available channels on all connected instruments. This is useful for quickly setting up a new Lager Box. ```bash theme={null} lager nets add-all [OPTIONS] ``` **Options:** * `--box TEXT` - Lager Box name or IP * `--yes` - Skip confirmation prompt **Example:** ```bash theme={null} # Preview what nets would be created lager nets add-all --box my-lager-box # Create all nets without prompting lager nets add-all --box my-lager-box --yes ``` **Output:** ``` Found 8 nets that can be created: - supply1 (supply) on Rigol_DP811 channel 1 - adc1 (adc) on LabJack_T7 channel AIN0 - adc2 (adc) on LabJack_T7 channel AIN1 - gpio1 (gpio) on LabJack_T7 channel FIO0 - i2c1 (i2c) on LabJack_T7 channel 0 - spi1 (spi) on LabJack_T7 channel 0 - debug1 (debug) on J-Link channel STM32F4 Create all 8 nets on box ? [y/N]: ``` ### `add-batch` Create multiple nets from a JSON file for efficient bulk setup. ```bash theme={null} lager nets add-batch JSON_FILE [OPTIONS] ``` **Arguments:** * `JSON_FILE` - Path to JSON file containing net definitions **Options:** * `--box TEXT` - Lager Box name or IP **JSON Format:** ```json theme={null} [ { "name": "supply1", "role": "supply", "channel": "1", "address": "TCPIP::192.168.1.100::INSTR" }, { "name": "i2c_bus", "role": "i2c", "channel": "0", "address": "USB::470026574" }, { "name": "spi_bus", "role": "spi", "channel": "0", "address": "USB::2238595116" } ] ``` **Example:** ```bash theme={null} lager nets add-batch nets.json --box my-lager-box ``` ### `assign` Assign a USB-serial cable to a known instrument the box cannot auto-detect. Some instruments have no USB control port. You reach such an instrument over RS-232 through a generic USB-serial adapter — a **Rigol DP711** power supply behind a Prolific cable, for example. The box sees only the adapter (a `uart` device), not the instrument behind it. `assign` records "this cable is the DP711's serial line" on the box. From then on, the scanner reports the instrument itself. It appears in `lager instruments` and in the TUI, and you can create nets for it with `lager nets add` like any auto-detected device. Assign **once per cable**; the assignment is stored on the box and survives reboots and replugs. Creating nets stays the normal, repeatable step. ```bash theme={null} lager nets assign --list [OPTIONS] # discover lager nets assign DEVICE --serial|--port [OPTIONS] # assign lager nets assign --remove --serial|--port [OPTIONS] # unassign ``` **Options:** | Option | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `--list` | List assignable devices, current assignments, and unassigned USB-serial cables | | `--serial TEXT` | USB serial number of the cable (durable; the assignment follows the cable across ports) | | `--port TEXT` | USB port path (sysfs name, e.g. `1-1.2`); pins the assignment to a physical box port — for cables without a usable serial number | | `--baud INTEGER` | Baud-rate override; must match the instrument's front-panel setting (DP711 factory default: 9600) | | `--remove` | Remove the assignment matching `--serial`/`--port` | | `--as-net [NAME]` | Also create a net for the instrument right away (name defaults to the device name) | | `--box TEXT` | Lager Box name or IP | **End-to-end example (Rigol DP711):** ```bash theme={null} # 1. Plug the instrument's USB-serial cable into the box, then find it: lager nets assign --list --box my-lager-box # Unassigned USB-serial cables: # serial 00000006 port 1-1.2 [067b:23a3] /dev/ttyUSB0 # 2. Assign the cable — and create a supply net in the same step: lager nets assign Rigol_DP711 --serial 00000006 --as-net main_supply --box my-lager-box # 3. Drive it like any other supply net: lager supply main_supply voltage 3.3 --box my-lager-box ``` Without `--as-net`, the command prints the exact `lager nets add` invocation for the new instrument: ```bash theme={null} lager nets add power-supply 1 'serial://067b:23a3/serial/00000006' ``` **How it works:** * The cable **must be plugged in** to assign it — its USB identity (vendor/product ID) is captured from the live device. * Nets for assigned instruments use a durable `serial://:/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, // 0 connected, 1 failed, 2 connecting, 3 no IP pub sta_conn_name: Option, pub soft_ap_conn: Option, } pub struct BlufiProvisionResult { pub device_name: Option, pub ssid: String, pub sta_conn: Option, // 0 means connected pub sta_conn_name: Option, } pub struct BlufiNetwork { pub ssid: String, pub rssi: Option, // dBm, as seen by the target } ``` ## Method Reference ### `scan(timeout: f64) -> Result>` Scan for BluFi-capable devices advertising nearby. ### `connect(device_name: &str) -> Result` Connect by advertised BLE **name**, not address. Returns firmware version and WiFi state. ### `provision(device_name: &str, ssid: &str, password: &str) -> Result` Provision the target onto a network. Fails as `Error::Box` when the target does not reach the connected state. ```rust theme={null} let r = blufi.provision("ESP-DUT", "bench-wifi", "hunter2")?; assert_eq!(r.sta_conn, Some(0), "not connected: {:?}", r.sta_conn_name); ``` ### `wifi_scan(device_name: &str) -> Result>` Ask the target to scan for networks **it** can see. This is the target's radio reporting, not the box's — which is the point when testing antenna placement. ### `status(device_name: &str) -> Result` and `version(device_name: &str) -> Result>` The target's current WiFi state, and its BluFi firmware version. ## Examples ### Provision a fresh device and confirm it joined ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let blufi = lager.blufi(); let devices = blufi.scan(10.0)?; let dut = devices.iter().find(|d| d.name.starts_with("ESP-")) .expect("no BluFi device advertising"); let info = blufi.connect(&dut.name)?; println!("BluFi firmware {:?}", info.version); let visible = blufi.wifi_scan(&dut.name)?; assert!(visible.iter().any(|n| n.ssid == "bench-wifi"), "target cannot see the bench AP; check antenna placement"); let result = blufi.provision(&dut.name, "bench-wifi", "hunter2")?; assert_eq!(result.sta_conn, Some(0), "provisioning failed: {:?}", result.sta_conn_name); let after = blufi.status(&dut.name)?; println!("op mode {:?}, station {:?}", after.op_mode_name, after.sta_conn_name); ``` ## Notes * **`sta_conn == 0` means connected.** Zero is success here, not failure, and the values run 0 connected, 1 failed, 2 connecting, 3 connected but no IP. A target at 3 is associated but has no DHCP lease, which is a different bug from a wrong password. * Every action except `scan` connects over BLE and negotiates BluFi security first, so the budgets are wide. The box-side connect timeout is 20 seconds, and the client adds its own allowance on top of that: * Most actions add 40 seconds. * `provision` adds 60 seconds, because end-to-end provisioning routinely takes over 30 seconds. * `wifi_scan` adds 30 seconds, because the target runs its own scan. * BluFi shares the box's one Bluetooth adapter with BLE, and the box serializes them. * Devices are addressed by advertised **name**, unlike BLE, which uses the address. # Client and Box Source: https://docs.lagerdata.com/source/reference/rust/client Constructing LagerBox, discovering nets, locking the box, and safety limits `LagerBox` is the entry point. It is cheap to construct and does no network traffic until you call something, so building one in a test helper costs nothing. ## Constructing ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; // reads LAGER_BOX_HOST let lager = LagerBox::connect("192.168.1.42")?; // or an explicit host ``` `connect()` accepts a host name, an IP, a `host:port`, or a full URL. The scheme defaults to `http` and the port to 9000. ### The builder ```rust theme={null} use lager::LagerBox; use std::time::Duration; let lager = LagerBox::builder("192.168.1.42") .timeout(Duration::from_secs(30)) .debug_service_url("http://127.0.0.1:8765") .bearer_token("...") .build()?; ``` | Method | Description | | --------------------- | ----------------------------------------------------------------- | | `timeout()` | Default HTTP budget for quick commands; 10 s unless changed | | `debug_service_url()` | Override the debug service base; defaults to the box on port 8765 | | `bearer_token()` | Pin a gateway token instead of resolving one | | `build()` | Construct the client | `timeout()` sets the budget for **quick** commands only. Long-running actions compute their own wider budgets regardless — a `wait_for_level` of 60 seconds is not cut short by a 10-second default. ### Environment variables | Variable | Meaning | | ------------------------- | ---------------------------------------------------------------------- | | `LAGER_BOX_HOST` | The box `from_env()` connects to | | `LAGER_DEBUG_SERVICE_URL` | Override the debug service base URL, e.g. when tunneling | | `LAGER_GATEWAY_TOKEN` | Pin a bearer token for a gated box | | `LAGER_GATEWAY_AUTH_FILE` | Override the CLI token store path; defaults to `~/.lager_gateway_auth` | ## Discovery | Method | Description | | ------------------------ | -------------------------------------------------------- | | `base_url()` | The normalized base URL, e.g. `http://192.168.1.42:9000` | | `health()` | Box health | | `status()` | Version, configured nets, and endpoint capabilities | | `nets()` | Every saved net record | | `usb_devices()` | Every USB device on the box's bus | | `usb_devices_matching()` | The same, filtered box-side by vid, pid or serial | ```rust theme={null} let status = lager.status()?; println!("box {} with {} nets", status.version, status.nets.len()); for net in lager.nets()? { println!("{} ({}) {:?}", net.name, net.role, net.instrument); } ``` ### `BoxCapabilities` `status().capabilities` says which endpoints the box serves. | Field | Wire key | Meaning | | ------------------- | ----------------- | ----------------------------------------------------------------- | | `net_command` | `netCommand` | The box serves `POST /net/command` | | `net_command_roles` | `netCommandRoles` | Which roles it serves; empty on images predating role advertising | | `ble_command` | `bleCommand` | BLE route registered | | `wifi_command` | `wifiCommand` | WiFi route registered | | `blufi_command` | `blufiCommand` | BluFi route registered | | `custom_devices` | `customDevices` | Not used by this crate | | `binaries` | `binaries` | Not used by this crate | | `safety_limits` | `safetyLimits` | `PUT /nets//safety-limits`, box 0.35.0+ | **A capability flag mirrors route registration, not the box's ability to do the work.** `ble_command: true` on a box whose container has no BlueZ still fails every BLE call with `Error::Box` and HTTP 502. `wifi_command: true` without `nmcli` installed fails the same way. Use these flags to decide whether an endpoint exists, not whether it will succeed. ### `usb_devices()` Enumerates the box's USB bus from sysfs. It takes a few milliseconds, needs no exclusive access to anything, and is therefore safe to poll while waiting for a DUT to re-enumerate. ```rust theme={null} use lager::UsbDeviceFilter; let stm = lager.usb_devices_matching(&UsbDeviceFilter { vid: Some("0483".into()), ..Default::default() })?; ``` Requires box 0.33.0 or newer. `devnum` changes every time a device re-enumerates. Match on `serial`, or on vid/pid, when checking that a device came back after a power cycle. ## Locking the box A shared bench needs a reservation, or two CI jobs will drive the same instruments at once. | Method | Description | | ------------------ | ------------------------------------------------ | | `lock_status()` | Who holds the box, if anyone | | `lock()` | Take an eternal lock as a user | | `lock_with()` | Take a lock with an explicit holder type and TTL | | `lock_heartbeat()` | Refresh a TTL lock | | `unlock()` | Release your lock | | `unlock_force()` | Release someone else's | | `lock_guard()` | RAII claim that releases on drop | ```rust theme={null} { let _guard = lager.lock_guard("ci-job-4711")?; // the box is yours for this scope run_the_suite(&lager)?; } // released here, even on an early return or a panic unwind ``` Contention is loud. Locking a box someone else holds is `Error::Box` with HTTP 409 and `Box is locked by `; unlocking as a non-holder is HTTP 403 with the same message. Unlocking a box that is already free succeeds. `lock_guard()` exists only on the blocking client. The async client has every other lock method, but no RAII guard. ## Safety limits Per-net ceilings, enforced by the box's hardware service rather than by your test — so they hold even when the test misbehaves. Requires box 0.35.0 or newer. ```rust theme={null} use lager::SafetyLimits; lager.set_safety_limits("supply1", &SafetyLimits { max_voltage: Some(3.6), max_current: Some(0.5), allow_destructive: Some(false), })?; ``` | Field | Meaning | | ------------------- | -------------------------------------------------------------- | | `max_voltage` | Volts. Must be positive. | | `max_current` | Amps. Must be positive. | | `allow_destructive` | `Some(false)` makes the box refuse erase and flash on this net | | Method | Description | | ----------------------- | ------------------------------------------------------ | | `set_safety_limits()` | Write the limits record; returns what the box applied | | `safety_limits()` | Read the current limits; `Ok(None)` means unrestricted | | `clear_safety_limits()` | Remove all limits | A refused setpoint arrives as `Error::Box`: ```text theme={null} Refused voltage(5.0) on net 'supply2': exceeds max_voltage of 3.6 configured for this net. ``` and a refused erase as HTTP 403: ```text theme={null} Refused erase on net 'debug1': this net is configured with allow_destructive: false. ``` **A PUT replaces the whole record.** Fields you leave as `None` are removed, not preserved. Setting only `max_voltage` on a net that already had a `max_current` ceiling drops the current ceiling. Read the current limits first and modify what you read. **Ceilings cap setpoints, not protection trips.** With a 3.6 V ceiling, `set_voltage(5.0)` is refused — but `set_ovp(12.0)` is accepted and applied. A refused `set_ocp` on such a net comes from the instrument's own hardware limit, not from the ceiling. Do not rely on a safety limit to bound an OVP or OCP setting. There is deliberately no `max_power`. One setter call establishes either a voltage or a current, never both, so the box cannot evaluate a power ceiling honestly. It therefore refuses the key outright. ## Notes * Handles borrow the client, so keep the `LagerBox` alive as long as any handle derived from it. * The box serializes access per physical instrument, so parallel tests cannot interleave I/O on one instrument. Tests sharing a *net* still see each other's state changes. * `nets()` falls back to the older `{"nets": [...]}` response shape automatically, so it works against boxes that predate the bare-array form. # DAC Source: https://docs.lagerdata.com/source/reference/rust/dac Set and read back an analog output voltage Drive an analog output voltage into your DUT from a DAC net. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let dac = lager.dac("dac1"); ``` ## Methods | Method | Description | | -------- | ------------------------------------ | | `name()` | The net name this handle addresses | | `set()` | Set the output voltage in volts | | `read()` | Read back the current output voltage | ## Method Reference ### `name() -> &str` The net name this handle was created with. ### `set(volts: f64) -> Result<()>` Set the output voltage. ```rust theme={null} dac.set(1.8)?; ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | ------------------------------ | | `volts` | `f64` | Target output voltage in volts | ### `read() -> Result` Read back the voltage that the DAC currently outputs. ```rust theme={null} let v = dac.read()?; ``` **Returns:** `f64` — the output voltage in volts. Not every DAC supports readback. On an MCC USB-202 this call fails with `Error::Box` and the message `USB-202 does not support DAC output readback`. Where you need a verified output, measure it with an ADC net instead of trusting `read()` to exist. ## Examples ### Sweep a reference and check the DUT tracks it ```rust theme={null} let dac = lager.dac("dac1"); let sense = lager.adc("adc1"); for target in [0.0_f64, 0.5, 1.0, 1.5, 2.0] { dac.set(target)?; std::thread::sleep(std::time::Duration::from_millis(50)); let measured = sense.read()?; assert!((measured - target).abs() < 0.05, "set {target:.2} V, measured {measured:.4} V"); } dac.set(0.0)?; ``` ## Supported Hardware | Instrument | Channels | Readback | | ----------- | ---------- | ------------- | | LabJack T7 | DAC0, DAC1 | Supported | | MCC USB-202 | DAC0, DAC1 | Not supported | ## Notes * Leave the output where the next test expects it. A DAC holds its last value after your test ends; setting it back to `0.0` in teardown is usually right. * Output accuracy is the instrument's, not the API's. A LabJack T7 asked for 1.0 V reads back around 0.99995 V, which is the DAC's resolution rather than an error. # Debug Probes Source: https://docs.lagerdata.com/source/reference/rust/debug Flash, erase, reset and read memory through a J-Link or OpenOCD probe Drive a debug probe from a cargo test: connect, flash firmware, reset the target and read its memory. Debug nets are the one net type that does not talk to the box's API on port 9000. They reach the box's **debug service on port 8765** instead. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let dbg = lager.debug("debug1"); ``` The net's saved record is fetched once on first use and cached on the handle. The cache is dropped whenever an operation fails, so a re-saved net is picked up on the next attempt rather than needing a new handle. ## Methods | Method | Description | | ---------------- | ---------------------------------------------------- | | `name()` | The net name this handle addresses | | `connect()` | Connect with defaults, starting a GDB server | | `connect_with()` | Connect with explicit options | | `disconnect()` | Disconnect, optionally leaving the gdbserver running | | `reset()` | Reset the target, optionally halting it | | `erase()` | Mass-erase the target's flash | | `flash()` | Flash a file, inferring its type from the extension | | `flash_bin()` | Flash a raw binary at an explicit base address | | `flash_bytes()` | Flash bytes already in memory | | `read_memory()` | Read target memory | | `info()` | Device, architecture, probe, serial, backend | | `status()` | Whether a gdbserver is running for this probe | RTT streaming lives on the same handle and is covered in [RTT](/source/reference/rust/rtt). ## Types ### `ConnectOptions` ```rust theme={null} pub struct ConnectOptions { pub speed: Option, // "4000" (kHz) or "adaptive" pub force: bool, // start a fresh backend even if one is running pub halt: bool, // halt the target immediately after connecting pub gdb: bool, // start a GDB server } ``` `Default` sets `gdb: true` and everything else off, so `connect()` starts a GDB server. This is the one field where the default is not `false`. ### `FirmwareKind` ```rust theme={null} pub enum FirmwareKind { Hex, Elf, Bin } ``` ## Method Reference ### `connect() -> Result` Connect using `ConnectOptions::default()`. ```rust theme={null} let conn = dbg.connect()?; if let Some(gdb) = &conn.gdb_server { println!("gdb on {:?}, RTT telnet on {:?}", gdb.gdb_port, gdb.rtt_telnet_port); } ``` **Returns:** `DebugConnection`, carrying the device, probe, serial, backend, and a `GdbServer` with the ports that were opened. On a J-Link these come back as gdb 2331, SWO 2332, telnet 2333 and RTT telnet 9090. `tcl_port` is OpenOCD-only and is `None` on a J-Link, just as `swo_port` is J-Link-only. ### `connect_with(opts: &ConnectOptions) -> Result` Connect with explicit options. ```rust theme={null} use lager::ConnectOptions; dbg.connect_with(&ConnectOptions { speed: Some("4000".into()), halt: true, ..Default::default() })?; ``` ### `disconnect(keep_running: bool) -> Result<()>` Disconnect. Pass `true` to leave the gdbserver up for an external GDB client to attach to; pass `false` to tear it down. ### `reset(halt: bool) -> Result<()>` Reset the target. `halt: true` leaves it stopped at the reset vector. ### `erase() -> Result<()>` Mass-erase the target's flash. **`erase()` drops the debugger connection.** The next `read_memory()` fails with `Error::Box` and `No debugger connection found`. `flash` re-establishes on its own, so an erase-then-flash sequence works, but an erase-then-read does not — call `connect()` again first. ### `flash(firmware_path) -> Result<()>` Flash a file, inferring the type from its extension: `.hex`, `.elf` or `.bin`. An unrecognized extension is `Error::Config`. **A `.bin` is flashed at `0x08000000`**, the STM32 application base. On any other family that address is wrong, and the call still returns `Ok(())`. This was verified on an nRF5340: `flash()` of a `.bin` succeeded while flash at `0x0` stayed erased. This is a silent wrong result, not an error. Off STM32, use `flash_bin()` with the correct base address. ### `flash_bin(firmware_path, address: u32) -> Result<()>` Flash a raw binary at an explicit base address. This is the correct call for any target whose application does not start at `0x08000000`. ```rust theme={null} dbg.flash_bin("target/app.bin", 0x0000_0000)?; // nRF5340 application core ``` ### `flash_bytes(contents: &[u8], kind: FirmwareKind, address: Option) -> Result<()>` Flash bytes you already hold, without writing them to a file first. `address` is used only for `FirmwareKind::Bin` and defaults to `0x08000000`. ### `read_memory(address: u64, length: usize) -> Result>` Read `length` bytes from the target starting at `address`. ```rust theme={null} let vectors = dbg.read_memory(0x0000_0000, 8)?; let initial_sp = u32::from_le_bytes(vectors[0..4].try_into().unwrap()); ``` Requires a live connection: without one it fails with `Error::Box` and `No debugger connection found`. ### `info() -> Result` and `status() -> Result` `info()` reports the device, architecture, probe, serial and backend, plus whether a connection is live. `status()` reports just the gdbserver: whether one is active, its pid, and the probe serial. Both work without connecting first. ```rust theme={null} let i = dbg.info()?; println!("{:?} ({:?}) via {:?}", i.device, i.arch, i.backend); // nRF5340_xxAA_APP (armv8-m.main) via jlink ``` ## Examples ### Flash and verify ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let dbg = lager.debug("debug1"); dbg.connect()?; dbg.erase()?; dbg.flash_bin("target/app.bin", 0x0)?; // erase() dropped the connection; flash() brought it back, but be explicit. dbg.connect()?; let image = std::fs::read("target/app.bin").expect("firmware image"); let readback = dbg.read_memory(0x0, image.len().min(1024))?; assert_eq!(&readback[..], &image[..readback.len()], "flash verify failed"); dbg.reset(false)?; dbg.disconnect(false)?; ``` ### Leave a gdbserver up for an interactive session ```rust theme={null} dbg.connect()?; let conn = dbg.info()?; println!("attach with: target remote localhost:2331 ({:?})", conn.device); dbg.disconnect(true)?; // keep_running: the server survives ``` ## Supported Hardware | Probe | Backend | Notes | | ------------------------- | ------- | ------------------------------- | | SEGGER J-Link and Flasher | jlink | SWO port available; no TCL port | | ST-LINK v2 / v2-1 / v3 | openocd | TCL port available; no SWO port | | RP2040 Picoprobe | openocd | | | Atmel EDBG, DAPLink | openocd | | ## Notes * Timeout budgets are per operation and match the CLI: * connect 30 s * flash 180 s * erase 120 s * memory read 30 s * 10 s for `info`, `status` and `disconnect` * Net resolution requires the `debug` role. A net that exists with another role gives `net 'adc1' exists but is not a debug net`. A name that does not exist gives `debug net 'nosuch' not found on this box`. * The debug service does not use the same success envelope as the port-9000 API. A 200 is success; anything else is `Error::Box` carrying the service's message. * Async note: `AsyncDebugNet` provides everything here, but no RTT. * A net configured with `allow_destructive: false` refuses `erase()` and `flash()` with `Error::Box` and HTTP 403. See [Client and Box](/source/reference/rust/client). # USB DFU Source: https://docs.lagerdata.com/source/reference/rust/dfu Flash a device over USB DFU, without a debug probe Flash firmware through USB DFU using `dfu-util` on the box. This is the path for a DUT with no debug probe attached, or one that exposes a DFU bootloader. Requires box software 0.33.0 or newer, and `dfu-util` installed on the box. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let dfu = lager.dfu(); ``` DFU is a **box-level** capability, not a net. There is no net name and no `name()`. ## Methods | Method | Description | | ------------ | -------------------------------------------------- | | `list()` | Enumerate DFU-capable devices on the box's USB bus | | `download()` | Upload firmware and flash it | | `detach()` | Detach a device from DFU mode | ## Types ### `DfuOptions` ```rust theme={null} pub struct DfuOptions { pub vid_pid: Option, // "0483:df11" pub serial: Option, pub alt: Option, // alternate setting pub dfuse_address: Option, // "0x08000000:leave" pub reset: bool, // reset after download } ``` `Default` selects whatever single DFU device is on the bus. With more than one attached, `dfu-util` errors on the ambiguity rather than guessing, so name one. ### `DfuDevice` ```rust theme={null} pub struct DfuDevice { pub mode: String, // "DFU" or "Runtime" pub vid: String, pub pid: String, pub devnum: Option, pub cfg: Option, pub intf: Option, pub alt: Option, pub name: Option, // e.g. "@Internal Flash /0x08000000/..." pub serial: Option, pub path: Option, // hub port path, e.g. "1-1.4" } ``` `mode` is `"DFU"` for a device already in its bootloader, and `"Runtime"` for one running an application that advertises DFU capability. ### `DfuOutput` ```rust theme={null} pub struct DfuOutput { pub exit_code: Option, pub stdout: String, pub stderr: String, } ``` ## Method Reference ### `list() -> Result>` Enumerate DFU-capable devices, parsed from `dfu-util -l` into typed records. ```rust theme={null} for d in dfu.list()? { println!("{}:{} ({}) {:?}", d.vid, d.pid, d.mode, d.name); } ``` ### `download(firmware: &[u8], opts: &DfuOptions) -> Result` Upload firmware to the box and flash it. The bytes are base64-encoded into the request, so nothing needs to exist on the box's filesystem first. ```rust theme={null} use lager::DfuOptions; let image = std::fs::read("target/app.bin").expect("firmware image"); let out = dfu.download(&image, &DfuOptions { vid_pid: Some("0483:df11".into()), dfuse_address: Some("0x08000000:leave".into()), reset: true, ..Default::default() })?; assert_eq!(out.exit_code, Some(0), "{}", out.stderr); ``` An STM32 system bootloader needs `dfuse_address`. Without it, `dfu-util` has no base address to write to and the download fails. ### `detach(opts: &DfuOptions) -> Result` Detach a device from DFU mode, so it leaves the bootloader and runs the application. ## Examples ### Flash a DUT that has no debug probe ```rust theme={null} use lager::{DfuOptions, LagerBox}; let lager = LagerBox::from_env()?; let dfu = lager.dfu(); // The DUT must already be in its bootloader. let devices = dfu.list()?; let target = devices.iter().find(|d| d.mode == "DFU") .expect("no device in DFU mode; hold BOOT0 and power-cycle"); println!("flashing {}:{}", target.vid, target.pid); let image = std::fs::read("target/app.bin").expect("firmware image"); let out = dfu.download(&image, &DfuOptions { vid_pid: Some(format!("{}:{}", target.vid, target.pid)), dfuse_address: Some("0x08000000:leave".into()), reset: true, ..Default::default() })?; assert_eq!(out.exit_code, Some(0), "dfu-util failed:\n{}", out.stderr); ``` ### Enter DFU by power-cycling a hub port ```rust theme={null} let port = lager.usb("usb3"); port.disable()?; std::thread::sleep(std::time::Duration::from_millis(500)); port.enable()?; std::thread::sleep(std::time::Duration::from_secs(2)); assert!(dfu.list()?.iter().any(|d| d.mode == "DFU"), "did not enter DFU"); ``` ## Notes * **`dfu-util` writes its progress to stderr**, not stdout. A successful download still fills `DfuOutput::stderr`; check `exit_code`, not whether stderr is empty. * A box without `dfu-util` installed answers with `Error::Box` and HTTP 500 carrying `dfu-util is not installed on this box. Install it with 'lager box-config apt add dfu-util'`. This is deliberately **not** `UnsupportedByBox` — the route exists, the tool does not. * A box older than 0.33.0 does not serve the route at all, and that **is** `Error::UnsupportedByBox`. * The client allows 180 seconds for a download, above the box's own 120-second `dfu-util` budget, to leave room for the upload and for queueing. # Electronic Load Source: https://docs.lagerdata.com/source/reference/rust/eload Draw a controlled current, voltage, resistance or power from your DUT Present a programmable load to a DUT's output: draw a fixed current, hold a fixed voltage, or emulate a resistance or a constant power draw. ## Handle ```rust theme={null} use lager::{EloadMode, LagerBox}; let lager = LagerBox::from_env()?; let load = lager.eload("eload1"); ``` ## Methods | Method | Description | | ------------ | ------------------------------------------------------ | | `name()` | The net name this handle addresses | | `state()` | Mode, input enable and measurements in one transaction | | `set()` | Switch mode and apply its setpoint atomically | | `setpoint()` | Read the stored setpoint for a mode | ## Types ### `EloadMode` ```rust theme={null} pub enum EloadMode { Cc, Cv, Cr, Cp } ``` | Variant | Meaning | Setpoint unit | | ------- | ------------------- | ------------- | | `Cc` | Constant current | amps | | `Cv` | Constant voltage | volts | | `Cr` | Constant resistance | ohms | | `Cp` | Constant power | watts | ### `EloadState` ```rust theme={null} pub struct EloadState { pub mode: Option, // "cc" / "cv" / "cr" / "cp" pub input_enabled: Option, pub measured_voltage: Option, // V pub measured_current: Option, // A pub measured_power: Option, // W } ``` ## Method Reference ### `set(mode: EloadMode, value: f64) -> Result<()>` Switch to `mode` and apply `value` as its setpoint, **atomically**. The box performs both as one composite operation under the instrument lock. The load therefore never spends a moment in the new mode carrying the old mode's setpoint. ```rust theme={null} load.set(EloadMode::Cc, 0.5)?; // draw 500 mA ``` **Parameters:** | Parameter | Type | Description | | --------- | ----------- | --------------------------------- | | `mode` | `EloadMode` | The regulation mode to switch to | | `value` | `f64` | The setpoint, in that mode's unit | ### `setpoint(mode: EloadMode) -> Result` Read the stored setpoint for a mode. Each mode keeps its own, so reading `Cv` after setting `Cc` gives you the constant-voltage setpoint, not the current you just set. ### `state() -> Result` Current mode, whether the input is enabled, and the measurements. ## Examples ### Step the load and watch the rail hold up ```rust theme={null} use lager::{EloadMode, LagerBox}; let lager = LagerBox::from_env()?; let supply = lager.supply("supply1"); let load = lager.eload("eload1"); supply.set_voltage(5.0)?; supply.set_current(2.0)?; supply.enable()?; for amps in [0.1_f64, 0.5, 1.0, 1.5] { load.set(EloadMode::Cc, amps)?; std::thread::sleep(std::time::Duration::from_millis(300)); let s = load.state()?; let v = s.measured_voltage.unwrap_or(0.0); assert!(v > 4.75, "rail sagged to {v:.3} V at {amps} A"); println!("{amps:>4.1} A -> {v:.3} V"); } load.set(EloadMode::Cc, 0.0)?; supply.disable()?; ``` ## Supported Hardware | Instrument | Modes | | ------------------- | -------------- | | Rigol DL3000 series | CC, CV, CR, CP | ## Notes * Prefer `set()` over changing mode and setpoint separately — that is the whole reason it takes both. * Return the load to zero in teardown. A load left drawing current keeps drawing it after your test exits. * `measured_voltage` is the voltage at the load's terminals, which includes any drop in the wiring between it and the DUT. For a rail measurement at the DUT, use an ADC net wired there. # Energy Analyzer Source: https://docs.lagerdata.com/source/reference/rust/energy Integrate energy and charge, or gather statistics, over a window Measure how much energy and charge a DUT consumed over a window, or gather current/voltage/power statistics across it. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let energy = lager.energy_analyzer("energy1"); ``` ## Methods | Method | Description | | --------------- | ------------------------------------------------------------- | | `name()` | The net name this handle addresses | | `read_energy()` | Integrate energy (joules) and charge (coulombs) over a window | | `read_stats()` | Current, voltage and power statistics over a window | ## Types ### `EnergyReading` ```rust theme={null} pub struct EnergyReading { pub energy_j: Option, // joules pub charge_c: Option, // coulombs pub duration_s: Option, // the window actually used } ``` ### `EnergyStats` and `StatSummary` ```rust theme={null} pub struct EnergyStats { pub current: Option, // amps pub voltage: Option, // volts pub power: Option, // watts } pub struct StatSummary { pub mean: Option, pub min: Option, pub max: Option, pub std: Option, } ``` ## Method Reference ### `read_energy(duration: f64) -> Result` Integrate energy and charge over `duration` seconds. ```rust theme={null} let r = energy.read_energy(10.0)?; println!("{:?} J, {:?} C", r.energy_j, r.charge_c); ``` ### `read_stats(duration: f64) -> Result` Statistics over the window, rather than an integral. ```rust theme={null} let s = energy.read_stats(5.0)?; if let Some(c) = s.current { println!("mean {:?} A, peak {:?} A", c.mean, c.max); } ``` ## Examples ### Budget a duty cycle ```rust theme={null} let energy = lager.energy_analyzer("energy1"); // One full wake/transmit/sleep cycle. let cycle = energy.read_energy(30.0)?; let joules = cycle.energy_j.expect("analyzer reports energy"); // 2000 mAh at 3.7 V, in joules. let battery_j = 2.0 * 3.7 * 3600.0; let days = (battery_j / joules) * 30.0 / 86_400.0; println!("projected battery life: {days:.1} days"); assert!(days > 180.0, "only {days:.1} days of battery life"); ``` ### Catch a current spike a mean would hide ```rust theme={null} let s = energy.read_stats(10.0)?; let c = s.current.expect("analyzer reports current"); let (mean, peak) = (c.mean.unwrap_or(0.0), c.max.unwrap_or(0.0)); assert!(peak < mean * 20.0, "peak {peak:.4} A is {:.0}x the mean", peak / mean); ``` ## Supported Hardware | Instrument | Notes | | ---------------- | -------------------------------------------------- | | Joulescope JS220 | Wide dynamic range; suits sleep-to-transmit ratios | | Nordic PPK2 | Source-meter and ampere-meter modes | ## Notes * **The box clamps the window to 0.1-120 seconds.** A request outside that range is refused rather than silently adjusted. * The client widens its HTTP budget to `max(30, duration + 30)` seconds. * `duration_s` on the reply is the window the instrument actually used, which can differ slightly from what you asked for. Divide by it, not by your request, when converting an integral to an average. * Energy is joules and charge is coulombs. Convert to mWh or mAh in your test if that is the unit your budget is written in. # Errors Source: https://docs.lagerdata.com/source/reference/rust/errors Every Error variant, when it fires, and how to match on it Every fallible call returns `lager::Result`, an alias for `std::result::Result`. ```rust theme={null} pub type Result = std::result::Result; ``` `Error` is `#[non_exhaustive]`, so a `match` on it needs a catch-all arm and new variants will not break your build. ## Variants | Variant | Fires when | | ---------------------------------------------- | ------------------------------------------------------------ | | `Connection(String)` | The box is unreachable — DNS, TCP connect, transport failure | | `Timeout(String)` | A client-side deadline expired | | `Box { status, message }` | The box accepted the request and reported a failure | | `UnsupportedByBox { message }` | This box's software is too old for an endpoint that exists | | `NotSupportedByBox { feature, details }` | The crate has no route for this at all | | `Decode(String)` | The response could not be parsed | | `AuthRequired { box_host, auth_url, message }` | A gated box, and no usable credential | | `Config(String)` | A client-side configuration problem | | `Stream(String)` | A streaming session (UART or RTT) failed | ## Variant Reference ### `Connection(String)` The crate did not reach the box at all. This variant also covers a failed Socket.IO connect. ```text theme={null} cannot reach the Lager box: {msg}. Check network/Tailscale and that the box is online and updated ``` ### `Timeout(String)` A **client-side** deadline expired: the HTTP request, a streaming session's connect confirmation, or a `wait_for` needle that never arrived. ```text theme={null} request to the Lager box timed out: {msg} ``` A hardware wait that expires is **not** this variant. `wait_for_level` timing out on the box comes back as `Error::Box` with HTTP 502 and `GPIO 'gpio24' did not reach level 1 within 2.0s`, because the box completed the request and reported the outcome. `Error::Timeout` means the transport gave up. ### `Box { status: u16, message: String }` The box accepted the request and reported a failure. This is the variant you will see most, and the `message` is the box's own text. ```text theme={null} box error (HTTP {status}): {message} ``` Common shapes: | Situation | Status | Message | | ------------------------------ | ------ | ------------------------------------------------------------------- | | Net does not exist | 404 | `[Supply] Net 'x' not found. Create it with 'lager nets add'.` | | Net exists, wrong role | 404 | `Net 'gpio24 (role adc)' not found` | | Debug net, wrong role | 404 | `net 'adc1' exists but is not a debug net` | | Debug net missing | 404 | `debug net 'nosuch' not found on this box` | | Setpoint over a hardware limit | 400 | `Voltage 999.0V exceeds hardware limit 60.0V` | | Setpoint over a safety ceiling | 502 | `Refused voltage(5.0) on net 'supply2': exceeds max_voltage of 3.6` | | Erase on a protected net | 403 | `Refused erase on net 'debug1': ... allow_destructive: false.` | | Box lock held by someone else | 409 | `Box is locked by ` | | Instrument absent from the bus | 502 | `Could not open instrument at
: No device found.` | A router net's "not found" reads `Net 'router1' not found` with no role, because router nets deliberately send no role hint. The box can also report failure with **HTTP 200** and `success: false` — a cross-role instrument conflict does this. That is still `Error::Box`. ### `UnsupportedByBox { message }` The endpoint exists in the API, but *this box* is too old to serve it. The message names the version needed. ```text theme={null} {message}. This box image does not support this endpoint; update the box ``` Examples: `usb_devices()` and `dfu()` need box 0.33.0; safety limits and interactive RTT need 0.35.0; `UsbPort::state()` needs 0.29.0. ### `NotSupportedByBox { feature, details }` The crate has no route for this feature on any box. Today this is only `Scope`. ```text theme={null} '{feature}' is not yet available over the box HTTP API: {details} ``` `UnsupportedByBox` and `NotSupportedByBox` are different variants with confusingly similar names. **Unsupported** means *update the box*. **Not supported** means *no box can do this yet*. ### `Decode(String)` The crate did not parse the response into the expected shape. The cause is a truncated body, an unexpected field type, or a missing required field. ```text theme={null} could not decode box response: {msg} ``` ### `AuthRequired { box_host, auth_url, message }` A gated box, and no usable credential. ```text theme={null} {message}. Sign in with `lager login {auth_url}` (this crate reuses the CLI's session), or set LAGER_GATEWAY_TOKEN / use LagerBoxBuilder::bearer_token ``` See [Authentication](/source/reference/rust/auth). ### `Config(String)` A client-side problem, detected before anything is sent: `LAGER_BOX_HOST` unset, an unparseable host, a firmware path whose type cannot be inferred, an unreadable file. ```text theme={null} configuration error: {msg} ``` ### `Stream(String)` A UART or RTT session failed: the net is already in use by another session, the device disappeared, or the session closed unexpectedly. ```text theme={null} streaming session error: {msg} ``` ## Matching ```rust theme={null} use lager::Error; match supply.set_voltage(5.0) { Ok(()) => {} Err(Error::UnsupportedByBox { message }) => { eprintln!("box too old, skipping: {message}"); return Ok(()); } Err(Error::Box { status: 403, message }) => { panic!("refused by a safety limit: {message}"); } Err(Error::AuthRequired { auth_url, .. }) => { panic!("run `lager login {auth_url}` first"); } Err(e) => return Err(e), // Error is #[non_exhaustive] } ``` ### Skipping a test on an old box ```rust theme={null} fn skip_if_unsupported(r: lager::Result) -> Option { match r { Ok(v) => Some(v), Err(lager::Error::UnsupportedByBox { message }) => { eprintln!("skipping: {message}"); None } Err(e) => panic!("{e}"), } } ``` ## Notes * `Error` implements `std::error::Error` and `Display`, so `?` works into `Box` and `anyhow::Error` without a conversion. * `From` maps into `Error::Decode`. * The messages are written to be read by a person in CI output. Print the error rather than only its variant. # GPIO Source: https://docs.lagerdata.com/source/reference/rust/gpio Read, drive and wait on digital pins Drive digital signals into your DUT and read signals back out. This includes a hardware-timed wait that blocks on the box rather than polling over the network. ## Handle ```rust theme={null} use lager::{LagerBox, Level}; let lager = LagerBox::from_env()?; let pin = lager.gpio("gpio1"); ``` ## Methods | Method | Description | | ----------------------- | -------------------------------------------------------------- | | `name()` | The net name this handle addresses | | `input()` | Read the current input level | | `output()` | Drive the output to a given level | | `output_high()` | Drive the output high | | `output_low()` | Drive the output low | | `toggle()` | Invert the output and return the new level | | `wait_for_level()` | Block until the input reaches a level; returns elapsed seconds | | `wait_for_level_with()` | As above, with full control including an unbounded wait | ## Types ### `Level` ```rust theme={null} pub enum Level { High, Low } ``` `Level::as_str()` gives `"high"` or `"low"`; `Level::is_high()` gives a `bool`. ### `WaitForLevelOptions` ```rust theme={null} pub struct WaitForLevelOptions { pub timeout: Option, // None waits forever pub scan_rate: Option, // LabJack streaming sample rate, Hz pub scans_per_read: Option, // LabJack scans per read batch pub poll_interval: Option, // poll interval for non-streaming drivers } ``` Every field defaults to `None`, meaning "use the box's default". Unset fields are omitted from the request entirely rather than sent as null. ## Method Reference ### `input() -> Result` Read the current level on the pin. ```rust theme={null} if pin.input()?.is_high() { println!("asserted"); } ``` **Returns:** `Level::High` or `Level::Low`. ### `output(level: Level) -> Result<()>` Drive the output to `level`. ```rust theme={null} pin.output(Level::High)?; ``` ### `output_high() -> Result<()>` and `output_low() -> Result<()>` Convenience wrappers over `output()`. ### `toggle() -> Result` Invert the output. **Returns:** the level the pin is at **after** toggling, not before. ```rust theme={null} pin.output_high()?; let now = pin.toggle()?; assert_eq!(now, Level::Low); ``` ### `wait_for_level(level: Level, timeout_s: f64) -> Result` Block until the input reaches `level`, or `timeout_s` elapses. ```rust theme={null} let elapsed = boot_ok.wait_for_level(Level::High, 5.0)?; println!("asserted after {elapsed:.3}s"); ``` **Parameters:** | Parameter | Type | Description | | ----------- | ------- | --------------------------------------- | | `level` | `Level` | The level to wait for | | `timeout_s` | `f64` | Seconds to wait before the box gives up | **Returns:** `f64` — seconds elapsed before the level was reached. A pin already at the requested level returns almost immediately (order of a millisecond). The wait happens **on the box**, so the timing is not distorted by network latency and a short pulse is not missed between polls. The client widens its own HTTP budget to `timeout_s + 20s` so the device, not the transport, decides when to give up. A wait that times out comes back as `Error::Box`, not `Error::Timeout` — the box completed the request and reported that the level was never reached. The message reads `GPIO 'gpio24' did not reach level 1 within 2.0s`. `Error::Timeout` means the HTTP request itself expired, which is a different problem. ### `wait_for_level_with(level: Level, opts: &WaitForLevelOptions) -> Result` Full control over the wait, including waiting forever. ```rust theme={null} use lager::WaitForLevelOptions; // No timeout at all: the client drops its HTTP deadline too. let elapsed = pin.wait_for_level_with( Level::High, &WaitForLevelOptions { timeout: None, ..Default::default() }, )?; ``` ## Examples ### Simulate a button press and check the DUT reacts ```rust theme={null} use lager::{LagerBox, Level}; let lager = LagerBox::from_env()?; let button = lager.gpio("button1"); let led = lager.gpio("led1"); button.output(Level::High)?; std::thread::sleep(std::time::Duration::from_millis(100)); assert_eq!(led.input()?, Level::High, "LED did not follow the button"); button.output(Level::Low)?; ``` ### Measure boot time ```rust theme={null} let supply = lager.supply("supply1"); let boot_ok = lager.gpio("boot_ok"); supply.disable()?; std::thread::sleep(std::time::Duration::from_millis(500)); supply.enable()?; let boot_ms = boot_ok.wait_for_level(Level::High, 10.0)? * 1000.0; assert!(boot_ms < 800.0, "boot took {boot_ms:.0} ms"); ``` ## Supported Hardware | Instrument | Pins | | ------------------------------- | ------------------------------------------ | | LabJack T7 | FIO0-FIO7, EIO0-EIO7, CIO0-CIO3, MIO0-MIO2 | | MCC USB-202 | DIO0-DIO7 | | FTDI FT232H / FT2232H / FT4232H | Async bitbang on all channels | ## Notes * One net is one pin. Driving `gpio1` says nothing about `gpio2`. * A pin is an input or an output depending on what you last asked of it. Calling `input()` on a pin that you drive reads back your own output. * GPIO nets are **box-side** pins: they drive signals into the DUT or read signals out of it. They are not DUT pins. * On FTDI parts, gpio is async bitbang and works on all four channels. By contrast, the MPSSE protocols (debug, spi, i2c) are limited to channels A and B. * Bench pins sometimes gate instrument power rather than DUT signals. Read a pin's net name before driving it. # I2C Source: https://docs.lagerdata.com/source/reference/rust/i2c Scan an I2C bus and transfer bytes to devices on it Drive an I2C bus from the box: discover devices, read and write registers, and run write-then-read transactions with a repeated start. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let bus = lager.i2c("i2c1"); ``` ## Methods | Method | Description | | -------------- | --------------------------------------------------------- | | `name()` | The net name this handle addresses | | `configure()` | Apply bus overrides and return the effective config | | `scan()` | Scan the default address range for devices that ACK | | `scan_range()` | Scan an explicit inclusive address range | | `read()` | Read bytes from a device | | `write()` | Write bytes to a device | | `write_read()` | Write then read in one transaction, with a repeated start | ## Types ### `I2cEffectiveConfig` ```rust theme={null} pub struct I2cEffectiveConfig { pub frequency_hz: Option, pub pull_ups: Option, } ``` ## Method Reference ### `configure(frequency_hz: Option, pull_ups: Option) -> Result` Apply bus settings and read back what actually took effect. A `None` argument keeps the net's saved value; anything you pass is applied live **and persisted on the box**. ```rust theme={null} let cfg = bus.configure(Some(400_000), Some(true))?; println!("{:?} Hz, pull-ups {:?}", cfg.frequency_hz, cfg.pull_ups); ``` **Parameters:** | Parameter | Type | Description | | -------------- | -------------- | -------------------------------------------- | | `frequency_hz` | `Option` | Bus clock in Hz, e.g. `100_000` or `400_000` | | `pull_ups` | `Option` | Enable the controller's internal pull-ups | ### `scan() -> Result>` Scan the default address range. ```rust theme={null} for addr in bus.scan()? { println!("device at 0x{addr:02x}"); } ``` **Returns:** `Vec` of **7-bit** addresses that acknowledged, in ascending order. ### `scan_range(start_addr: u16, end_addr: u16) -> Result>` Scan an explicit inclusive range. ```rust theme={null} let found = bus.scan_range(0x40, 0x50)?; ``` ### `read(address: u16, num_bytes: u32) -> Result>` Read `num_bytes` from the device at `address`. ```rust theme={null} let bytes = bus.read(0x48, 2)?; ``` ### `write(address: u16, data: &[u8]) -> Result<()>` Write `data` to the device at `address`. ```rust theme={null} bus.write(0x48, &[0x01, 0x60])?; ``` ### `write_read(address: u16, data: &[u8], num_bytes: u32) -> Result>` Write, then read, in a single transaction using a repeated start rather than a stop and a fresh start. This is what a register read on most parts requires. ```rust theme={null} // Point at register 0x00, then read two bytes from it. let temp = bus.write_read(0x48, &[0x00], 2)?; ``` ## Examples ### Discover what is on the bus, then read a sensor register ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let bus = lager.i2c("i2c1"); bus.configure(Some(100_000), Some(true))?; let addrs = bus.scan()?; assert!(addrs.contains(&0x48), "temperature sensor missing; bus has {addrs:02x?}"); let raw = bus.write_read(0x48, &[0x00], 2)?; let celsius = i16::from_be_bytes([raw[0], raw[1]]) as f64 / 256.0; println!("{celsius:.2} C"); ``` ## Supported Hardware | Instrument | Pins | Notes | | ------------------------------- | ---------------------- | ------------------------------------------------------------- | | LabJack T7 | Any two FIO/EIO pins | Configured as an SDA/SCL pair when the net is created | | FTDI FT232H / FT2232H / FT4232H | MPSSE channels A and B | I2C is an MPSSE protocol, so channels C and D cannot serve it | | Total Phase Aardvark | Dedicated | | ## Notes * Addresses are **7-bit**. Pass `0x48`, not the 8-bit read/write-shifted forms. * **A scan hit is not a promise.** A scan reports which addresses acknowledged an address byte; a subsequent `read()` can still fail with `No ACK from device at 0x48`. This is common on a bus with the controller's internal pull-ups disabled, where the line can float convincingly enough to look like an ACK. Treat `scan()` as discovery, not verification. * Bus transactions run on the box under the physical device's lock, so a write-then-read cannot be interleaved by another request on the same device. * `configure()` persists. A frequency you set in one test is still set in the next. # Net Types Source: https://docs.lagerdata.com/source/reference/rust/net-types Every net handle and box-level capability the Rust crate exposes Each net type has a handle obtained from `LagerBox` and a page of its own. Handles are cheap and do no network traffic until you call a method. The box resolves and validates the net name on every request, so a typo fails loudly. ## Instrument nets Addressed by the net name you gave them with `lager nets add`. | Handle | Obtain with | Reference | | ---------------- | ----------------------------- | ----------------------------------------------------- | | `Supply` | `lager.supply(name)` | [Power Supply](/source/reference/rust/supply) | | `Battery` | `lager.battery(name)` | [Battery Simulation](/source/reference/rust/battery) | | `Solar` | `lager.solar(name)` | [Solar Simulation](/source/reference/rust/solar) | | `Eload` | `lager.eload(name)` | [Electronic Load](/source/reference/rust/eload) | | `WattMeter` | `lager.watt_meter(name)` | [Watt Meter](/source/reference/rust/watt) | | `EnergyAnalyzer` | `lager.energy_analyzer(name)` | [Energy Analyzer](/source/reference/rust/energy) | | `Adc` | `lager.adc(name)` | [ADC](/source/reference/rust/adc) | | `Dac` | `lager.dac(name)` | [DAC](/source/reference/rust/dac) | | `Thermocouple` | `lager.thermocouple(name)` | [Thermocouple](/source/reference/rust/thermocouple) | | `Gpio` | `lager.gpio(name)` | [GPIO](/source/reference/rust/gpio) | | `I2c` | `lager.i2c(name)` | [I2C](/source/reference/rust/i2c) | | `Spi` | `lager.spi(name)` | [SPI](/source/reference/rust/spi) | | `UsbPort` | `lager.usb(name)` | [USB Hub Ports](/source/reference/rust/usb) | | `Uart` | `lager.uart(name)?` | [UART](/source/reference/rust/uart) | | `DebugNet` | `lager.debug(name)` | [Debug Probes](/source/reference/rust/debug) | | `Arm` | `lager.arm(name)` | [Robot Arm](/source/reference/rust/arm) | | `Webcam` | `lager.webcam(name)` | [Webcam](/source/reference/rust/webcam) | | `Router` | `lager.router(name)` | [Router](/source/reference/rust/router) | | `Scope` | `lager.scope(name)` | [Oscilloscope](/source/reference/rust/scope) — a stub | ## Box-level capabilities These drive the box's own hardware, so they take no net name and have no `name()`. | Handle | Obtain with | Reference | | ------- | --------------- | ------------------------------------- | | `Ble` | `lager.ble()` | [BLE](/source/reference/rust/ble) | | `Wifi` | `lager.wifi()` | [WiFi](/source/reference/rust/wifi) | | `Blufi` | `lager.blufi()` | [BluFi](/source/reference/rust/blufi) | | `Dfu` | `lager.dfu()` | [USB DFU](/source/reference/rust/dfu) | ## Two handles that behave differently Almost every constructor above returns a handle immediately and infallibly. Two do not: * **`lager.uart(name)?` returns a `Result`** — it opens a Socket.IO session and starts streaming right away, so it can fail at construction. * **`lager.scope(name)` returns a value, not a borrow.** `Scope` has no lifetime and no async twin, because the box has no endpoint for it to talk to yet. ## Discovery ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; for net in lager.nets()? { println!("{:<12} {:<14} {:?}", net.name, net.role, net.instrument); } ``` `NetRecord` carries: * the name * the role * the instrument * the pin or channel * the VISA or device address * any role-specific saved params * the net's safety limits See [Client and Box](/source/reference/rust/client) for the rest of the discovery surface. ## Not yet available `Scope` is a documented stub, and there is no logic-analyzer handle at all: the box has no HTTP endpoint for scope or logic capture yet. `Rotation` and `Actuate` nets serve provisioning flows rather than test-time net access, and the crate deliberately leaves them out. The work list is in [MISSING\_ENDPOINTS.md](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md). # Rust SDK Overview Source: https://docs.lagerdata.com/source/reference/rust/overview Write your entire hardware-in-the-loop test suite in Rust and run it with cargo test The Lager Rust crate gives embedded developers first-class access to Lager nets. A hardware-in-the-loop (HIL) test suite can live next to your firmware and run with `cargo test`. No Python is required. The crate is a pure HTTP/JSON client of the Lager Box API. It covers: * power supplies, battery simulators, e-loads and solar simulators * GPIO, ADC, DAC, thermocouples, watt meters and energy analyzers * SPI, I2C, USB hub ports, robot arms, webcams and routers * streaming UART * the box-level capabilities: its own BLE adapter, WiFi interface, and BluFi ESP32 provisioning Debug-probe nets (flash / erase / reset / memory reads / RTT) talk to the box's debug service. The package publishes on crates.io as [**`lager-net`**](https://crates.io/crates/lager-net), because the bare `lager` name is taken by an unrelated crate. The library target is still named `lager`, so your code reads `use lager::LagerBox;`. Full API reference lives on [docs.rs/lager-net](https://docs.rs/lager-net). ## Quickstart Add the crate to your firmware project's dev-dependencies: ```toml theme={null} # Cargo.toml [dev-dependencies] lager = { package = "lager-net", version = "0.4" } ``` The crate requires Rust **1.75** or newer and builds on edition 2021. Write a test: ```rust theme={null} // tests/boot.rs use lager::{LagerBox, Level}; #[test] fn dut_boots_at_3v3() -> lager::Result<()> { let lager = LagerBox::from_env()?; // reads LAGER_BOX_HOST let supply = lager.supply("supply1"); let boot_ok = lager.gpio("boot_ok"); supply.set_voltage(3.3)?; supply.enable()?; // Hardware-timed wait on the box; returns elapsed seconds. let t = boot_ok.wait_for_level(Level::High, 5.0)?; println!("booted in {t:.3}s"); supply.disable() } ``` Run it: ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test ``` `LagerBox::connect("hostname-or-ip")` also works, with an optional `host:port` or full URL. ## Features | Feature | Default | What you get | | ---------- | ------- | ------------------------------------------------------------------------------------------------------ | | `blocking` | yes | `LagerBox` on [`ureq`](https://crates.io/crates/ureq) — tiny dependency tree, no tokio | | `async` | no | `AsyncLagerBox` on [`reqwest`](https://crates.io/crates/reqwest)/tokio; same methods, `.await`ed | | `uart` | no | `Uart` streaming sessions over the box's Socket.IO `/uart` namespace | | `rtt` | no | RTT log streaming over the box's Socket.IO namespace, for reading `defmt` output from a running target | Both clients execute the exact same request builders and response parsers, so the two transports cannot drift apart. ```toml theme={null} lager = { package = "lager-net", version = "0.4", features = ["async"] } ``` ## Errors Everything returns `lager::Result` with a single `Error` enum: | Variant | Meaning | | ------------------------- | -------------------------------------------------------------- | | `Connection` | Box unreachable (network/Tailscale/box offline) | | `Timeout` | The box stalled past the (already widened) budget | | `Box { status, message }` | The box refused or the hardware failed | | `UnsupportedByBox` | HTTP 501: the box image predates this endpoint; update the box | | `AuthRequired` | The box's gateway wants a bearer token and none is available | | `NotSupportedByBox` | The net type is a documented stub (see the note below) | ## Requirements * A Lager Box with software new enough to serve `POST /net/command` — check `lager.status()?.capabilities.net_command`, or run `lager update`. * Rust 1.75+. Oscilloscope / logic-analyzer workflows are not exposed on the box HTTP API yet, so `Scope` ships as a documented stub whose methods return `Error::NotSupportedByBox`. Support lands when the box API does; the endpoint sketch is tracked in the crate's [`MISSING_ENDPOINTS.md`](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md). ## Reference ### Getting started * [Client and Box](/source/reference/rust/client) — constructing `LagerBox`, discovery, box locks, safety limits * [Net Types](/source/reference/rust/net-types) — the index of every handle * [Errors](/source/reference/rust/errors) — every `Error` variant and when it fires ### Firmware and debug * [Debug Probes](/source/reference/rust/debug) — flash, erase, reset, read memory * [RTT](/source/reference/rust/rtt) — firmware logs, and driving an RTT console * [USB DFU](/source/reference/rust/dfu) — flashing without a debug probe ### Power and simulation * [Power Supply](/source/reference/rust/supply) * [Battery Simulation](/source/reference/rust/battery) * [Solar Simulation](/source/reference/rust/solar) * [Electronic Load](/source/reference/rust/eload) * [Watt Meter](/source/reference/rust/watt) * [Energy Analyzer](/source/reference/rust/energy) ### Measurement * [Oscilloscope](/source/reference/rust/scope) — a documented stub today * [ADC](/source/reference/rust/adc) * [Thermocouple](/source/reference/rust/thermocouple) ### I/O and communication * [GPIO](/source/reference/rust/gpio) * [DAC](/source/reference/rust/dac) * [I2C](/source/reference/rust/i2c) * [SPI](/source/reference/rust/spi) * [USB Hub Ports](/source/reference/rust/usb) * [UART](/source/reference/rust/uart) * [BLE](/source/reference/rust/ble) * [WiFi](/source/reference/rust/wifi) * [BluFi](/source/reference/rust/blufi) * [Router](/source/reference/rust/router) ### Utilities * [Robot Arm](/source/reference/rust/arm) * [Webcam](/source/reference/rust/webcam) ### Guides * [Testing with cargo test](/source/reference/rust/testing) — structuring a suite, parallelism, CI * [Authentication](/source/reference/rust/auth) — boxes behind an authenticating gateway * [Async Client](/source/reference/rust/async) — `AsyncLagerBox`, and where it differs # Router Source: https://docs.lagerdata.com/source/reference/rust/router Degrade the network your DUT is on, rather than removing it Drive a MikroTik router as a Lager net. This is the bench's network fault-injection tool. It lets a test assert what firmware does when the network *degrades* — loses the internet, loses DNS, gets slow — rather than simply disappearing. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let router = lager.router("router1"); ``` ## Methods | Method | Description | | ------------------------- | ------------------------------------------------------------- | | `name()` | The net name this handle addresses | | `connect()` | Verify connectivity; returns identity, version, board, uptime | | `system_info()` | Structured system resource information | | `interfaces()` | All network interfaces, as raw RouterOS records | | `wireless_interfaces()` | Wireless interfaces, raw | | `wireless_clients()` | Currently associated wireless clients | | `dhcp_leases()` | Active DHCP leases | | `enable_interface()` | Enable an interface by name | | `disable_interface()` | Disable an interface by name | | `set_wireless_ssid()` | Change the broadcast SSID | | `block_internet()` | Drop all forwarded traffic | | `remove_firewall_rules()` | Remove every Lager-added firewall rule | | `reboot()` | Reboot the router | | `command()` | Generic escape hatch for any other router action | ## Types ### `RouterSystemInfo` ```rust theme={null} pub struct RouterSystemInfo { pub name: Option, // identity pub version: Option, // RouterOS version pub board: Option, pub architecture: Option, pub uptime: Option, // e.g. "1w2d3h" pub cpu_load: Option, // percent pub free_memory: Option, // bytes pub total_memory: Option, pub free_hdd_space: Option, } ``` ## Method Reference ### `connect() -> Result` Verify connectivity by fetching the router's identity. ### `system_info() -> Result` Structured resource information. ### `block_internet() -> Result<()>` Drop all forwarded traffic, simulating an internet outage while leaving the local network — and the DUT's association to it — intact. ### `remove_firewall_rules() -> Result<()>` Remove every firewall rule Lager added. This is the undo for `block_internet` and anything added through `command()`. Rules persist on the router until removed. A test that blocks the internet and then fails without cleaning up leaves the next test running on a broken network. Put `remove_firewall_rules()` in a teardown path that runs even on failure. ### `enable_interface(interface: &str)` and `disable_interface(interface: &str)` Enable or disable an interface by name — use these when you want the access point to vanish entirely, rather than to lose routing. ### `set_wireless_ssid(interface: &str, ssid: &str) -> Result` Change the broadcast SSID. ### `command(action: &str, params: serde_json::Value) -> Result` The generic escape hatch: invoke any other router action by name with raw JSON parameters. This is how you reach DNS blocking, port blocking, bandwidth limits, DHCP control, security profiles and access lists. None of these have a typed wrapper in this crate yet. ```rust theme={null} use serde_json::json; router.command("block_dns", json!({}))?; router.command("block_port", json!({ "port": 8883, "protocol": "tcp" }))?; router.command("add_bandwidth_limit", json!({ "target": "192.168.88.0/24", "max_limit": "1M/1M", }))?; ``` ## Examples ### Assert firmware retries instead of rebooting when the internet goes away ```rust theme={null} use lager::LagerBox; use std::time::Duration; let lager = LagerBox::from_env()?; let router = lager.router("router1"); let mut uart = lager.uart("uart1")?; router.connect()?; let outcome = (|| -> lager::Result<()> { router.block_internet()?; // The DUT stays on the AP; only routing is gone. let log = uart.wait_for(b"retry", Duration::from_secs(60))?; assert!(!String::from_utf8_lossy(&log).contains("rebooting"), "firmware rebooted instead of retrying"); Ok(()) })(); router.remove_firewall_rules()?; // always, even if the assertion failed outcome?; ``` ### Squeeze the bandwidth and check an OTA still completes ```rust theme={null} use serde_json::json; router.command("add_bandwidth_limit", json!({ "target": "192.168.88.50", "max_limit": "256k/256k", }))?; // ... trigger the OTA and wait for it ... router.command("remove_bandwidth_limits", json!({}))?; ``` ## Supported Hardware | Device | Notes | | ------------------------- | --------------------------------- | | MikroTik RouterOS devices | Driven over the RouterOS REST API | ## Notes * **This net sends no role hint.** A router net can be saved with role `router` or the legacy `mikrotik`. The box verifies a supplied hint exactly, so a hint will fail against whichever spelling the net does not use. A consequence you will see: a missing router net reports `Net 'router1' not found` without naming a role, where other net types report `Net 'x (role adc)' not found`. * Every router action gets a flat 60-second budget, because reboots and reset actions are slow and a busy router lags. * `reboot()` returns as soon as the router accepts the command, not when it is back. * Blocking the internet does **not** disconnect the DUT from WiFi. That distinction is the entire point of this net type. # RTT Source: https://docs.lagerdata.com/source/reference/rust/rtt Stream firmware log output, and drive an RTT console from a test Read what your firmware prints over SEGGER RTT. With the `rtt` feature you can also write back into its down-channel, so a cargo test can drive an interactive console. There are two paths, and they behave differently. | Path | Feature | Direction | Transport | | ------------------------- | ------- | -------------- | ---------------------- | | `debug.rtt()` | none | Read only | HTTP stream | | `debug.rtt_interactive()` | `rtt` | Read and write | Socket.IO, box 0.35.0+ | Prefer the interactive session. The one-way stream yields raw HTTP chunked-transfer framing rather than clean payload — see the warning below. ## Interactive sessions ### Enabling the feature ```toml theme={null} [dev-dependencies] lager = { package = "lager-net", version = "0.4", features = ["rtt"] } ``` The `rtt` feature implies `blocking`. There is no async equivalent. ### Opening a session ```rust theme={null} use lager::{LagerBox, RttOptions}; use std::time::Duration; let lager = LagerBox::from_env()?; let dbg = lager.debug("debug1"); dbg.connect()?; // required first, see below let mut rtt = dbg.rtt_interactive()?; ``` **The gdbserver must already be running.** Calling `rtt_interactive()` without connecting first fails with `Error::Stream` and the message `No debugger connection found for net 'debug1'. Start one first`. ### `RttOptions` ```rust theme={null} pub struct RttOptions { pub channel: u32, // default 0 pub search_addr: Option, // RAM start for the control-block search pub search_size: Option, // size of the RAM region to search pub chunk_size: Option, // box-side read chunk, J-Link only } ``` `channel` selects the channel in **both** directions. `chunk_size` applies only to interactive sessions; the one-way HTTP stream ignores it. ## Methods | Method | Description | | ------------- | ------------------------------------------------- | | `netname()` | The debug net this session streams | | `channel()` | The RTT channel, both directions | | `backend()` | The debug backend, `"jlink"` or `"openocd"` | | `read()` | Bytes arriving within a timeout; may be empty | | `try_read()` | Bytes already buffered, without waiting | | `wait_for()` | Accumulate until a needle appears | | `write()` | Write raw bytes to the target's down-channel | | `write_str()` | Write a string to the down-channel | | `stop()` | Stop cleanly, releasing the box's RTT telnet port | ## Method Reference ### `read(timeout: Duration) -> Result>` Whatever up-channel bytes arrive within `timeout`. ```rust theme={null} let chunk = rtt.read(Duration::from_secs(3))?; print!("{}", String::from_utf8_lossy(&chunk)); ``` `read()` waits out the **full** timeout when the target is idle. In a poll loop use `try_read()` instead, or every iteration costs the whole timeout. ### `try_read() -> Result>` Bytes already received, without waiting. Returns an empty vector when nothing is buffered. ### `wait_for(needle: &[u8], timeout: Duration) -> Result>` Accumulate output until `needle` appears. **Returns:** everything up to **and including** the needle. Bytes after it stay buffered for the next read. An empty needle returns immediately; a miss is `Error::Timeout`. ```rust theme={null} let banner = rtt.wait_for(b"boot complete", Duration::from_secs(10))?; ``` ### `write(data: &[u8]) -> Result<()>` and `write_str(s: &str) -> Result<()>` Write into the target's RTT **down**-channel. Writing needs a firmware-declared down buffer on that channel. `defmt-rtt` alone provides only the up buffer, and without a down buffer the target **silently discards** what you write — the call still returns `Ok(())`. That is a target-side fact, not a transport failure, so the crate has no error to report. ### `stop() -> Result<()>` Stop cleanly. Consumes the session. Dropping it also stops the session, but `stop()` surfaces errors rather than swallowing them. ## The one-way stream `debug.rtt()` and `debug.rtt_with(&RttOptions)` return an `RttStream`, which implements `std::io::Read`. It needs no cargo feature and no Socket.IO. **`RttStream` yields raw HTTP chunked-transfer framing, not clean payload.** A read returns bytes like `384\r\nblink 46823 period=500ms\n...`, where `384` is a hex chunk length. Wrapping it in a `BufReader` and iterating lines produces `"64"`, `"384"` and empty strings interleaved with real firmware output. A hex chunk length is indistinguishable from a line that your firmware printed. This is tracked as [lager-rs#5](https://github.com/lagerdata/lager-rs/issues/5). Until it is fixed, use `rtt_interactive()` where you need parseable output. The interactive path is clean. ## Examples ### Drive a firmware console and assert on the reply ```rust theme={null} use lager::LagerBox; use std::time::Duration; let lager = LagerBox::from_env()?; let dbg = lager.debug("debug1"); dbg.connect()?; let mut rtt = dbg.rtt_interactive()?; rtt.wait_for(b"ready", Duration::from_secs(10))?; rtt.write_str("version\n")?; let reply = rtt.wait_for(b"\n", Duration::from_secs(2))?; assert!(String::from_utf8_lossy(&reply).contains("v1.")); rtt.stop()?; dbg.disconnect(false)?; ``` ### Collect boot output without blocking on an idle target ```rust theme={null} use std::time::{Duration, Instant}; let mut log = Vec::new(); let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { log.extend_from_slice(&rtt.try_read()?); std::thread::sleep(Duration::from_millis(50)); } println!("{}", String::from_utf8_lossy(&log)); ``` ## Notes * **The box's RTT telnet port takes a single client.** A second session on the same probe and channel is refused with `Error::Stream` and `RTT port 9090 is already in use by another session`. Two different channels on one probe are separate ports and can run at once. * Bytes are **raw**. `defmt` output is compressed binary and must be piped through `defmt-print -e `; `wait_for` only helps against a plain-text console. * The connect confirmation timeout is 30 seconds, wider than UART's 15. The extra time covers a search of RAM for the RTT control block, and a retry of the telnet attach while the gdbserver settles. * Sessions carry the gateway bearer token on the Socket.IO handshake, so a gated box works with no extra setup. * Requires box software 0.35.0 or newer. An older box gives `Error::UnsupportedByBox`. # Oscilloscope Source: https://docs.lagerdata.com/source/reference/rust/scope Why scope and logic capture are not yet available from Rust `Scope` is a **documented stub**. Every method except `name()` returns `Error::NotSupportedByBox`. The type exists so that code written against it compiles today and keeps compiling when the box grows the endpoints behind it. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let scope = lager.scope("scope1"); // returns Scope by value, not a borrow ``` `Scope` is one of the few handles with no lifetime and no async twin: there is no client work for it to do yet. ## Methods | Method | Behavior | | ----------- | ---------------------------- | | `name()` | Works. Returns the net name. | | `enable()` | `Error::NotSupportedByBox` | | `disable()` | `Error::NotSupportedByBox` | | `capture()` | `Error::NotSupportedByBox` | | `measure()` | `Error::NotSupportedByBox` | The error carries the reason verbatim: ```text theme={null} 'scope' is not yet available over the box HTTP API: the box needs `POST :9000/net/command` scope/logic roles (or a dedicated capture endpoint) for trigger config, single capture, and measurement queries; see MISSING_ENDPOINTS.md ``` ## Why The crate is a pure HTTP/JSON client of the box's API on port 9000. Every other net type has a route there. Scope and logic-analyzer work does not. `lager scope` and `lager logic` still run over the legacy exec path on port 5000, and over a dedicated oscilloscope streaming daemon on ports 8082-8085. Neither of those is an HTTP/JSON API that this crate can speak. Closing the gap needs `analog` and `logic` roles in the box's `/net/command` `ROLE_ACTIONS`. Those roles must cover trigger configuration, a single capture that returns the trace as JSON, and scalar measurements. Streaming capture can stay on the dedicated daemon. The work list lives in [MISSING\_ENDPOINTS.md](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md). ## What to do instead Use the Python API or the CLI for scope and logic work, and Rust for everything else. They drive the same box and the same nets. ```rust theme={null} // Detect the stub explicitly rather than letting it surprise you. match scope.measure("vpp") { Err(lager::Error::NotSupportedByBox { .. }) => { eprintln!("scope capture is not on the HTTP API yet; skipping"); } Ok(v) => println!("vpp = {v}"), Err(e) => return Err(e), } ``` For a scalar an assertion actually needs, an ADC net often suffices and is available today. Where a real waveform is required, drive `lager scope` from the test's shell step, or write that portion in Python. ## Notes * The stub returns `Error::NotSupportedByBox`, which is a **different variant** from `Error::UnsupportedByBox`. `NotSupportedByBox` means the crate has no route for this at all. `UnsupportedByBox` means this particular box is too old for a route that does exist. See [Errors](/source/reference/rust/errors). * A logic net has no Rust handle whatsoever — there is no `lager.logic(...)`. * Nothing here depends on box version. A box running the newest software still returns the stub error. # Solar Simulation Source: https://docs.lagerdata.com/source/reference/rust/solar Present a photovoltaic source with programmable irradiance Drive an EA PSB supply in photovoltaic mode so a DUT sees a real solar panel's current-voltage curve rather than a stiff supply. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let panel = lager.solar("solar1"); ``` ## Methods | Method | Description | | ------------------ | ----------------------------------------------------------- | | `name()` | The net name this handle addresses | | `set()` | Initialize the instrument and start PV simulation | | `stop()` | Stop PV simulation and release the instrument's remote lock | | `irradiance()` | Read the configured irradiance | | `set_irradiance()` | Set irradiance in W/m2 | | `mpp_current()` | Maximum-power-point current | | `mpp_voltage()` | Maximum-power-point voltage | | `resistance()` | Read dynamic panel resistance | | `set_resistance()` | Set dynamic panel resistance | | `temperature()` | Cell temperature | | `voc()` | Open-circuit voltage | ## Method Reference ### `set() -> Result<()>` Initialize the instrument and enter photovoltaic mode. Call this before anything else. ### `stop() -> Result<()>` Leave PV mode and release the instrument's remote lock, handing the front panel back. ### `set_irradiance(watts_per_m2: f64) -> Result` Set irradiance, in W/m2, over the range 0 to 1500. ```rust theme={null} panel.set_irradiance(800.0)?; ``` **Returns:** `f64` — the value that the instrument actually applied. The instrument can clamp the requested value. ### `set_resistance(ohms: f64) -> Result` Set the dynamic panel resistance, the ratio that shapes the knee of the curve. **Returns:** the applied value. ### `irradiance()`, `mpp_current()`, `mpp_voltage()`, `resistance()`, `temperature()`, `voc()` Reads, returning `f64` in W/m2, amps, volts, ohms, degrees Celsius and volts respectively. ## Examples ### Sweep irradiance across a day and check the charger tracks it ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let panel = lager.solar("solar1"); panel.set()?; for irradiance in [200.0_f64, 500.0, 800.0, 1000.0, 400.0] { let applied = panel.set_irradiance(irradiance)?; std::thread::sleep(std::time::Duration::from_secs(2)); let (v, i) = (panel.mpp_voltage()?, panel.mpp_current()?); println!("{applied:>6.0} W/m2 -> MPP {v:.2} V, {i:.3} A, {:.2} W", v * i); } panel.stop()?; ``` ## Supported Hardware | Instrument | Notes | | --------------- | ----------------- | | EA PSB 10080-60 | Photovoltaic mode | | EA PSB 10060-60 | Photovoltaic mode | ## Notes * **Every solar action re-asserts PV mode on the instrument before doing its work**, including the reads. That makes even `voc()` slow. This net type therefore has the widest timeout budgets in the crate: 120 seconds for `set()` and `stop()`, and 90 seconds for everything else. * A solar net drives the same physical instrument a power-supply net can point at. The box serializes both under one per-address lock, so their SCPI traffic cannot interleave, but they are the same hardware. * `stop()` releases the instrument's remote lock. Skipping it leaves the front panel locked out for whoever walks up to the bench next. # SPI Source: https://docs.lagerdata.com/source/reference/rust/spi Configure a SPI bus and run full-duplex transfers Clock data in and out of a SPI peripheral from the box, with full control over mode, bit order, word size and chip select. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let spi = lager.spi("spi1"); ``` ## Methods | Method | Description | | -------------- | ------------------------------------------------------ | | `name()` | The net name this handle addresses | | `configure()` | Apply bus overrides and return the effective config | | `read()` | Clock in a number of words, clocking out the fill word | | `write()` | Clock out words; the words clocked in come back | | `read_write()` | Full-duplex transfer of exactly the words given | | `transfer()` | Transfer a word count, padding or truncating the data | ## Types ### `SpiConfig` ```rust theme={null} pub struct SpiConfig { pub mode: Option, // 0-3 (CPOL/CPHA) pub bit_order: Option, pub frequency_hz: Option, pub word_size: Option, // 8, 16 or 32 pub cs_active: Option, pub cs_mode: Option, } ``` A `None` field keeps the net's saved value. Anything set is applied live **and persisted on the box**. ```rust theme={null} pub enum BitOrder { Msb, Lsb } pub enum CsActive { Low, High } // Low is typical pub enum CsMode { Auto, Manual } ``` ### `SpiOptions` ```rust theme={null} pub struct SpiOptions { pub fill: u32, // word clocked out while reading; default 0xFF pub keep_cs: bool, // keep CS asserted after the transaction; default false } ``` ### `SpiTransfer` ```rust theme={null} pub struct SpiTransfer { pub words: Vec, // one entry per word, whatever the word size pub word_size: u32, // bits the transaction actually ran at } ``` ## Method Reference ### `configure(config: &SpiConfig) -> Result` Apply settings and read back what took effect. ```rust theme={null} use lager::{BitOrder, SpiConfig}; let cfg = spi.configure(&SpiConfig { mode: Some(0), frequency_hz: Some(1_000_000), word_size: Some(8), bit_order: Some(BitOrder::Msb), ..Default::default() })?; ``` ### `read(n_words: u32, opts: &SpiOptions) -> Result` Clock in `n_words`, clocking out `opts.fill` for each. ### `write(data: &[u32], opts: &SpiOptions) -> Result` Clock out exactly `data`. SPI is full duplex, so the words clocked in during the same transaction come back in the result — a write is also a read. ### `read_write(data: &[u32], opts: &SpiOptions) -> Result` Full-duplex transfer of exactly `data`. ### `transfer(data: &[u32], n_words: u32, opts: &SpiOptions) -> Result` Transfer exactly `n_words`, padding `data` with the fill word or truncating it. ## Examples ### Read a flash chip's JEDEC ID ```rust theme={null} use lager::{LagerBox, SpiConfig, SpiOptions}; let lager = LagerBox::from_env()?; let spi = lager.spi("spi1"); spi.configure(&SpiConfig { mode: Some(0), frequency_hz: Some(1_000_000), word_size: Some(8), ..Default::default() })?; // 0x9F then three bytes of ID, in one CS assertion. let t = spi.transfer(&[0x9F], 4, &SpiOptions::default())?; let (manufacturer, memtype, capacity) = (t.words[1], t.words[2], t.words[3]); println!("JEDEC {manufacturer:02x} {memtype:02x} {capacity:02x}"); assert_ne!(manufacturer, 0xFF, "no device responded"); ``` ### Hold CS across two transactions ```rust theme={null} let held = SpiOptions { keep_cs: true, ..Default::default() }; spi.write(&[0x03, 0x00, 0x00, 0x00], &held)?; // read command + address let data = spi.read(256, &SpiOptions::default())?; // releases CS at the end ``` ## Supported Hardware | Instrument | Channels | Notes | | ---------------------- | ---------------------- | ------------------------------------------------- | | FTDI FT232H | MPSSE, single channel | Mode-exclusive with UART on this part | | FTDI FT2232H / FT4232H | MPSSE channels A and B | SPI is MPSSE, so channels C and D cannot serve it | | LabJack T7 | Configured pins | | | Total Phase Aardvark | Dedicated | | ## Notes * `opts.fill` is only sent for `read()` and `transfer()`. `write()` and `read_write()` clock out exactly the data given, so a fill word is meaningless. * `word_size` comes back on every transfer, because a bus configured for 16-bit words returns one entry per word rather than one entry per byte. * Transactions run on the box under the device's lock, so a multi-word transfer cannot be interleaved by another request on the same device. * `configure()` persists across tests. Set what you depend on rather than assuming. # Power Supply Source: https://docs.lagerdata.com/source/reference/rust/supply Drive a programmable power supply and read its full state Set voltage and current, arm protection trips, and read a supply's complete state in one instrument transaction. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let supply = lager.supply("supply1"); ``` ## Methods | Method | Description | | --------------- | --------------------------------------------- | | `name()` | The net name this handle addresses | | `set_voltage()` | Set the output voltage setpoint | | `set_current()` | Set the output current limit | | `enable()` | Turn the output on | | `disable()` | Turn the output off | | `set_ovp()` | Set and enable over-voltage protection | | `set_ocp()` | Set and enable over-current protection | | `clear_ovp()` | Clear an over-voltage trip | | `clear_ocp()` | Clear an over-current trip | | `state()` | Full structured state in a single transaction | ## Types ### `SupplyState` Every field is `Option` because the box returns a full-shaped record even when an individual read fails, and because not every supply reports every quantity. ```rust theme={null} pub struct SupplyState { pub netname: Option, pub channel: Option, pub error: Option, // set when the whole gather failed pub voltage: Option, // measured, V pub current: Option, // measured, A pub power: Option, // measured, W pub enabled: Option, pub mode: Option, // "CV" or "CC" pub voltage_set: Option, // setpoint, V pub current_set: Option, // setpoint, A pub voltage_max: Option, // instrument hardware limit, V pub current_max: Option, // instrument hardware limit, A pub ocp_limit: Option, pub ocp_tripped: Option, pub ovp_limit: Option, pub ovp_tripped: Option, } ``` ## Method Reference ### `set_voltage(volts: f64) -> Result<()>` Set the output voltage setpoint. ```rust theme={null} supply.set_voltage(3.3)?; ``` The box refuses a value above the instrument's hardware limit before it reaches the instrument, as `Error::Box` with a message naming the limit. The message reads `Voltage 999.0V exceeds hardware limit 60.0V`. ### `set_current(amps: f64) -> Result<()>` Set the current limit. Reaching it is what moves a supply from CV into CC mode. ### `enable() -> Result<()>` and `disable() -> Result<()>` Turn the output on or off. Call `disable()` in teardown. A supply left enabled stays enabled after your test process exits, and the next test starts with the DUT already powered. ### `set_ovp(volts: f64) -> Result<()>` and `set_ocp(amps: f64) -> Result<()>` Set **and enable** the protection trip. These are not just threshold writes. ### `clear_ovp() -> Result<()>` and `clear_ocp() -> Result<()>` Clear a trip that fired. Until you clear it, the output stays down. ### `state() -> Result` Read everything in one instrument transaction. ```rust theme={null} let s = supply.state()?; println!("{:?} V at {:?} A, mode {:?}", s.voltage, s.current, s.mode); ``` There are no individual getters. `state()` is the read path, and gathering everything at once means the fields describe one moment rather than several. ## Examples ### Power up, verify, tear down ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let supply = lager.supply("supply1"); supply.set_voltage(3.3)?; supply.set_current(0.5)?; supply.enable()?; std::thread::sleep(std::time::Duration::from_millis(200)); let s = supply.state()?; assert_eq!(s.enabled, Some(true)); let v = s.voltage.expect("output is on, so a measurement exists"); assert!((v - 3.3).abs() < 0.05, "rail at {v:.4} V"); supply.disable()?; ``` ### Assert the DUT stays inside its current budget ```rust theme={null} supply.set_voltage(3.3)?; supply.set_ocp(0.25)?; // trip rather than let it draw more supply.enable()?; std::thread::sleep(std::time::Duration::from_secs(5)); let s = supply.state()?; assert_eq!(s.ocp_tripped, Some(false), "DUT tripped the 250 mA budget"); supply.disable()?; ``` ## Supported Hardware | Instrument | Channels | Hardware limits | | ---------------------- | -------- | ------------------------------------------ | | Rigol DP821 | 2 | Channel 1 60 V / 1 A, channel 2 8 V / 10 A | | Rigol DP800 series | 2-3 | Per model | | Keithley 2281S | 1 | 20 V / 6 A; also serves a battery net | | Keysight E36000 series | 1-3 | Per model | ## Notes * **A measurement can be `None` while the output is off.** A Keithley 2281S reports `voltage`, `current` and `power` as `None` with the output disabled, and real numbers once enabled. A Rigol DP821 reports zeros in both states. This is why the fields are `Option` — treat `None` as "not measured", not as zero. * `state()` returning `Ok` does not mean the instrument answered. When the box cannot reach the hardware service, you get a fully-shaped `SupplyState` with every measurement `None` and `error` set to a string explaining why. Check `error` before trusting a field. * **Safety limits cap setpoints, not trips.** With a `max_voltage` ceiling configured on the net, `set_voltage()` above it is refused, but `set_ovp()` above it is accepted and applied. See [Client and Box](/source/reference/rust/client). * Supply and battery nets can point at the same physical instrument. The box serializes them under one per-instrument lock, so they cannot interleave, but they do share the instrument's mode. # Testing with cargo test Source: https://docs.lagerdata.com/source/reference/rust/testing Structuring a Rust hardware-in-the-loop suite, parallelism, and CI The crate is designed so that your HIL suite is just ordinary Rust integration tests. These are files under `tests/`, run by `cargo test`, that live in the same repository as your firmware. ## Structure Point tests at a box with the `LAGER_BOX_HOST` environment variable and construct the client with `LagerBox::from_env()`: ```rust theme={null} // tests/power.rs use lager::LagerBox; #[test] fn dut_draws_less_than_100ma_idle() -> lager::Result<()> { let lager = LagerBox::from_env()?; let supply = lager.supply("DUT_POWER"); supply.set_voltage(3.3)?; supply.enable()?; let current = supply.state()?.current.expect("supply reports current"); assert!(current < 0.1, "idle draw {current} A"); supply.disable() } ``` ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test ``` Group tests by net or by DUT feature — one file per concern (`boot.rs`, `power.rs`, `sensors.rs`) keeps `cargo test ` filtering useful. ## Parallel tests and instrument safety `cargo test` runs tests on multiple threads by default. That is safe at the instrument level. The box serializes access per physical instrument: every net command runs under a per-device lock in the box's single-owner hardware service. Because of that per-device lock, parallel tests can never interleave I/O on one instrument. Examples are a LabJack shared across GPIO/ADC/SPI nets, or a Keithley shared by supply and battery roles. Tests sharing a *net* still observe each other's state changes (one test's `disable()` is visible to another test reading the same supply). Either partition nets across tests, or serialize: ```sh theme={null} cargo test -- --test-threads=1 ``` ## Timeouts Timeout budgets mirror the Lager CLI. Quick commands use 10 s. Some operations block on the box for a caller-controlled duration: the watt and energy integration windows, and `wait_for_level`. Those operations widen or drop the client timeout automatically, so a healthy long measurement is never aborted mid-flight. ## Hermetic tests vs. hardware tests Mark tests that always need real hardware with `#[ignore]`. A plain `cargo test` then stays green on a laptop with no box, or in a PR check: ```rust theme={null} #[test] #[ignore = "requires a Lager box"] fn flash_and_boot() -> lager::Result<()> { // ... } ``` Then opt in explicitly where a box is available: ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test -- --ignored ``` This is the convention that the crate itself uses. Its own suite is hermetic, and `cargo test` runs against a mock box. Its hardware smoke tests run with `cargo test --test hardware -- --ignored`. ## CI A minimal GitHub Actions job, assuming the runner can reach the box (e.g. a self-hosted runner on the lab network or a Tailscale-connected runner): ```yaml theme={null} jobs: hil: runs-on: [self-hosted, lab] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Run HIL suite env: LAGER_BOX_HOST: ${{ vars.LAGER_BOX_HOST }} # Only needed for boxes behind an authenticating gateway: LAGER_GATEWAY_TOKEN: ${{ secrets.LAGER_GATEWAY_TOKEN }} run: cargo test -- --ignored ``` For gateway-fronted boxes, see **[Authentication](/source/reference/rust/auth)** for how the token is attached and refreshed. ## A harness that runs against any box The crate's own hardware suite uses a pattern worth copying. Every test is `#[ignore]`d, so `cargo test` stays hermetic. Each test reads its net name from an environment variable, and skips with a note when that variable is unset. The same suite then runs against whatever nets a given bench configures. ```rust theme={null} #![cfg(feature = "blocking")] use lager::{LagerBox, Level}; fn lager() -> LagerBox { LagerBox::from_env().expect("set LAGER_BOX_HOST to a reachable box") } /// The net name in `var`, or None with a printed skip note. fn net_from_env(var: &str) -> Option { match std::env::var(var) { Ok(name) if !name.is_empty() => Some(name), _ => { eprintln!("skipping: set {var} to a configured net name to run this test"); None } } } #[test] #[ignore = "requires a live box + LAGER_TEST_GPIO_NET"] fn gpio_toggles() { let Some(name) = net_from_env("LAGER_TEST_GPIO_NET") else { return }; let lager = lager(); let pin = lager.gpio(&name); pin.output(Level::High).unwrap(); assert_eq!(pin.input().unwrap(), Level::High); pin.output(Level::Low).unwrap(); } ``` Run it by opting in: ```bash theme={null} LAGER_BOX_HOST=192.168.1.42 \ LAGER_TEST_GPIO_NET=gpio1 \ LAGER_TEST_SUPPLY_NET=supply1 \ cargo test --test hardware -- --ignored --nocapture ``` The environment variables are a convention, not a crate feature — name them whatever suits your bench. What matters is that a test which cannot run says so and passes, rather than failing on a bench that simply lacks that instrument. ## Skipping on an older box A box too old for an endpoint answers with `Error::UnsupportedByBox`, which is a better signal than a panic when a fleet is mid-upgrade. ```rust theme={null} match lager.usb_devices() { Ok(devices) => assert!(!devices.is_empty()), Err(lager::Error::UnsupportedByBox { message }) => { eprintln!("skipping: {message}"); } Err(e) => panic!("{e}"), } ``` ## Teardown that actually runs A test that panics mid-way skips everything after the panic, which on a bench means leaving a supply enabled or a firewall rule in place. Put the restore on a path that survives a failure. ```rust theme={null} fn with_power(lager: &lager::LagerBox, body: impl FnOnce() -> T) -> lager::Result { let supply = lager.supply("supply1"); supply.set_voltage(3.3)?; supply.enable()?; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)); supply.disable()?; // runs even if `body` panicked Ok(result.unwrap_or_else(|e| std::panic::resume_unwind(e))) } ``` Reserve the whole box for the duration when a suite must not be interleaved with another job: ```rust theme={null} let _guard = lager.lock_guard("ci-job-4711")?; ``` `BoxLockGuard` releases on drop, including during a panic unwind. ## Verifying against real hardware Some behaviors only show up on a bench, and are worth knowing before you write an assertion around them: * A supply's measurements can be `None` while its output is disabled, depending on the instrument. Assert on `enabled` and setpoints when the output is off. * `state()` returning `Ok` does not mean the instrument answered — check the `error` field. * An `i2c.scan()` hit does not guarantee a subsequent read will ACK. * `erase()` drops the debugger connection; reconnect before reading memory. # Thermocouple Source: https://docs.lagerdata.com/source/reference/rust/thermocouple Read temperature from a thermocouple net Read a temperature in degrees Celsius from a thermocouple net. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let tc = lager.thermocouple("tc1"); ``` ## Methods | Method | Description | | -------- | --------------------------------------- | | `name()` | The net name this handle addresses | | `read()` | Read the temperature in degrees Celsius | ## Method Reference ### `name() -> &str` The net name this handle was created with. ### `read() -> Result` Read the junction temperature in degrees Celsius. ```rust theme={null} let celsius = tc.read()?; println!("{celsius:.1} C"); ``` **Returns:** `f64` — degrees Celsius. There is no Fahrenheit or Kelvin variant; convert in your test if you need one. ## Examples ### Assert a part stays inside its thermal envelope under load ```rust theme={null} use lager::{EloadMode, LagerBox}; use std::time::{Duration, Instant}; let lager = LagerBox::from_env()?; let load = lager.eload("eload1"); let tc = lager.thermocouple("tc1"); let ambient = tc.read()?; load.set(EloadMode::Cc, 1.5)?; let deadline = Instant::now() + Duration::from_secs(60); let mut peak = ambient; while Instant::now() < deadline { peak = peak.max(tc.read()?); std::thread::sleep(Duration::from_secs(1)); } assert!(peak - ambient < 40.0, "rose {:.1} C above ambient", peak - ambient); ``` ## Supported Hardware Any thermocouple front-end the box exposes as a `thermocouple` role net. The amplifier and junction type are fixed when the net is created, not chosen per call. ## Notes * Thermocouples settle slowly. A reading taken immediately after a thermal step is the sensor's history, not the part's temperature — sample over time. * Cold-junction compensation happens on the instrument. The value returned is the compensated temperature. # UART Source: https://docs.lagerdata.com/source/reference/rust/uart Stream a serial port from a cargo test Open a streaming session against a serial port on the box and drive your DUT's console from a test. ## Enabling the feature UART sessions ride on Socket.IO, so they are behind a cargo feature: ```toml theme={null} [dev-dependencies] lager = { package = "lager-net", version = "0.4", features = ["uart"] } ``` The `uart` feature implies `blocking`. There is no async equivalent. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let mut uart = lager.uart("uart1")?; ``` `uart()` is the only handle constructor that returns a `Result`. Every other net handle is inert until you call a method. `uart()` connects the session and starts streaming immediately, so it can fail right here. ## Methods | Method | Description | | --------------- | ----------------------------------------------- | | `netname()` | The net this session streams | | `device_path()` | The device path on the box, e.g. `/dev/ttyUSB0` | | `baudrate()` | The baud rate the port was opened at | | `last_status()` | Most recent session status notification | | `read()` | Bytes arriving within a timeout; may be empty | | `try_read()` | Bytes already buffered, without waiting | | `wait_for()` | Accumulate until a needle appears | | `write()` | Write raw bytes to the device | | `write_str()` | Write a string to the device | | `stop()` | Stop cleanly, closing the port on the box | ## Method Reference ### `read(timeout: Duration) -> Result>` Bytes arriving within `timeout`. The call can return an empty vector. `read()` waits out the **full** timeout when the device is idle. Use `try_read()` in a poll loop, or every iteration costs the whole timeout. ### `try_read() -> Result>` Bytes already received, without waiting. ### `wait_for(needle: &[u8], timeout: Duration) -> Result>` Accumulate until `needle` appears. **Returns:** everything up to **and including** the needle; the remainder stays buffered. An empty needle returns immediately; a miss is `Error::Timeout`. ### `write(data: &[u8]) -> Result<()>` and `write_str(s: &str) -> Result<()>` Write to the device. ### `last_status() -> Option<&str>` The most recent session status notification, updated during reads. While a USB serial adapter re-enumerates — because a hub port was cycled, or the DUT was reflashed — this reads `"reconnecting"` and then `"reconnected"`. Checking it distinguishes "the DUT is quiet" from "the adapter went away". ### `stop() -> Result<()>` Stop cleanly and close the port on the box. Consumes the session. Dropping it also stops the session, but `stop()` surfaces errors. ## Examples ### Wait for a boot banner, then drive the console ```rust theme={null} use lager::LagerBox; use std::time::Duration; let lager = LagerBox::from_env()?; let supply = lager.supply("supply1"); let mut uart = lager.uart("uart1")?; supply.set_voltage(3.3)?; supply.enable()?; uart.wait_for(b"boot complete", Duration::from_secs(10))?; uart.write_str("status\r\n")?; let reply = uart.wait_for(b"OK", Duration::from_secs(2))?; println!("{}", String::from_utf8_lossy(&reply)); uart.stop()?; supply.disable()?; ``` ### Survive a reflash without losing the session ```rust theme={null} use std::time::Duration; dbg.flash_bin("target/app.bin", 0x0)?; // the adapter may re-enumerate // The session reconnects on its own; watch the status while it does. let banner = uart.wait_for(b"ready", Duration::from_secs(30))?; println!("status during reflash: {:?}", uart.last_status()); assert!(String::from_utf8_lossy(&banner).contains("ready")); ``` ## Supported Hardware | Adapter | Notes | | ---------------------------------------- | -------------------------------------------------------- | | SiLabs CP210x | | | FTDI FT232R / FT232H / FT2232H / FT4232H | On multi-channel parts, all four channels can serve UART | | Prolific USB serial | | | ESP32 JTAG/serial | | ## Notes * **The box owns the port exclusively, and only one session per net or device is allowed.** A second opener gets `Error::Stream` saying the port is already in use. That includes a session your own previous test leaked — call `stop()`. * The connect confirmation timeout is 15 seconds; failing it is `Error::Timeout`. * Bytes are raw. There is no line discipline, no echo handling and no encoding conversion; `wait_for` works on bytes. * Sessions carry the gateway bearer token on the Socket.IO handshake, so a gated box needs no extra setup. * On FTDI multi-channel parts, UART works on all four channels, unlike the MPSSE protocols (debug, spi, i2c) which are limited to channels A and B. On a single-channel FT232H, claiming UART makes the MPSSE roles unavailable and vice versa. # USB Hub Ports Source: https://docs.lagerdata.com/source/reference/rust/usb Power-cycle a programmable USB hub port Cut and restore power to one port of a programmable USB hub. A test uses this to force a DUT to re-enumerate, or to simulate a cable pull. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let port = lager.usb("usb1"); ``` ## Methods | Method | Description | | ----------- | ------------------------------------------ | | `name()` | The net name this handle addresses | | `enable()` | Power the port on | | `disable()` | Power the port off | | `toggle()` | Invert the port and report the new state | | `state()` | Read whether the port is currently powered | ## Method Reference ### `enable() -> Result<()>` and `disable() -> Result<()>` Power the port on or off. ```rust theme={null} port.disable()?; std::thread::sleep(std::time::Duration::from_secs(1)); port.enable()?; ``` ### `toggle() -> Result` Invert the port's power state. **Returns:** `true` if the port is now **enabled**, `false` if now disabled — the state after the toggle, not before. ### `state() -> Result` Read whether the port is powered. ```rust theme={null} assert!(port.state()?, "port should be up"); ``` **Returns:** `true` when enabled. `state()` needs box software 0.29.0 or newer. Older boxes reject the action, and the crate turns that specific rejection into `Error::UnsupportedByBox` with the message `the 'state' action on /usb/command requires box software >= 0.29.0; update the box or use toggle/enable/disable`. ## Examples ### Force a DUT to re-enumerate and wait for it to come back ```rust theme={null} use lager::{LagerBox, UsbDeviceFilter}; use std::time::{Duration, Instant}; let lager = LagerBox::from_env()?; let port = lager.usb("usb3"); port.disable()?; std::thread::sleep(Duration::from_millis(500)); port.enable()?; // Poll the bus until the DUT reappears. usb_devices() is cheap and takes no // exclusive access, so this loop is safe. let filter = UsbDeviceFilter { vid: Some("0483".into()), ..Default::default() }; let deadline = Instant::now() + Duration::from_secs(10); loop { if !lager.usb_devices_matching(&filter)?.is_empty() { break; } assert!(Instant::now() < deadline, "DUT did not re-enumerate"); std::thread::sleep(Duration::from_millis(200)); } ``` ## Supported Hardware | Instrument | Ports | | ----------------- | ---------------------------------------- | | Acroname USBHub3+ | 8 or 4, depending on model | | YKUSH hub | Per model | | Plugable USB hub | Addressed by topology rather than serial | ## Notes * One net is one port. Cycling `usb1` leaves the hub's other ports alone. * The port's `devnum` on the bus changes across a re-enumeration. Match a device by serial or vid/pid, never by `devnum`, when checking that it came back. * Powering a port down can remove a device that the box uses for another net. A debug probe or serial adapter behind that port will disappear mid-test. # Watt Meter Source: https://docs.lagerdata.com/source/reference/rust/watt Measure mean power, current and voltage over a window Integrate power, current and voltage over a caller-supplied window. Every reading is an average over time, not an instant sample. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let meter = lager.watt_meter("watt1"); ``` ## Methods | Method | Description | | ----------- | ------------------------------------------------------ | | `name()` | The net name this handle addresses | | `power()` | Mean power in watts over a window | | `current()` | Mean current in amps over a window | | `voltage()` | Mean voltage in volts over a window | | `all()` | Current, voltage and power together in one transaction | ## Types ### `WattReading` ```rust theme={null} pub struct WattReading { pub current: Option, // amps, mean pub voltage: Option, // volts, mean pub power: Option, // watts, mean pub duration_s: Option, // the window actually used } ``` ## Method Reference ### `power(duration: f64) -> Result` Mean power over `duration` seconds. ```rust theme={null} let watts = meter.power(0.5)?; ``` **Parameters:** | Parameter | Type | Description | | ---------- | ----- | ----------------------------- | | `duration` | `f64` | Integration window in seconds | **Returns:** `f64` — mean power in watts. ### `current(duration: f64) -> Result` and `voltage(duration: f64) -> Result` Mean current in amps and mean voltage in volts over the same kind of window. Both need a meter that actually reports that quantity — a Joulescope or PPK2 does, a power-only meter does not. ### `all(duration: f64) -> Result` Current, voltage and power gathered in a single instrument transaction. Prefer this over three separate calls: it is one window rather than three, so the numbers describe the same moment. ```rust theme={null} let r = meter.all(1.0)?; println!("{:?} A, {:?} V, {:?} W", r.current, r.voltage, r.power); ``` ## Examples ### Compare sleep current against a budget ```rust theme={null} let meter = lager.watt_meter("watt1"); let dut = lager.gpio("sleep_req"); dut.output_high()?; std::thread::sleep(std::time::Duration::from_secs(2)); // let it settle let sleeping = meter.all(5.0)?; let amps = sleeping.current.expect("meter reports current"); assert!(amps < 50e-6, "sleep current {:.1} uA over budget", amps * 1e6); ``` ## Supported Hardware | Instrument | Current | Voltage | Power | | -------------------- | --------------- | --------------- | --------- | | Joulescope JS220 | Supported | Supported | Supported | | Nordic PPK2 | Supported | Supported | Supported | | Yoctopuce watt meter | Varies by model | Varies by model | Supported | ## Notes * **There is no default window.** The Rust API requires an explicit `duration`, unlike the CLI, which defaults to `0.1`. * The client widens its HTTP budget to `max(30, duration + 20)` seconds, so a long window does not trip the ordinary 10-second timeout. * A window is a real wait. `power(5.0)` blocks your test for five seconds. * Fields on `WattReading` are `Option` because some meters cannot report all three; a `None` is "this instrument does not measure that", not a failure. # Webcam Source: https://docs.lagerdata.com/source/reference/rust/webcam Start an MJPEG stream from a camera on the box Start and stop an MJPEG video stream from a USB camera attached to the box. A person or a vision tool can then watch the DUT while a test runs. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let cam = lager.webcam("cam1"); ``` ## Methods | Method | Description | | ---------- | ------------------------------------------------- | | `name()` | The net name this handle addresses | | `start()` | Start the stream and return its URL | | `stop()` | Stop the stream | | `status()` | Whether a stream is running, and where | | `url()` | The stream URL, or `None` when nothing is running | ## Types ```rust theme={null} pub struct WebcamStream { pub url: String, pub port: Option, pub already_running: bool, // true when an existing stream was reused } pub struct WebcamStatus { pub running: bool, pub url: Option, pub port: Option, pub video_device: Option, // e.g. "/dev/video0" } ``` ## Method Reference ### `start() -> Result` Start the MJPEG stream. **Idempotent**: called while a stream is already up, it reports the existing one with `already_running: true` rather than starting a second. ```rust theme={null} let s = cam.start()?; println!("watch at {} (reused: {})", s.url, s.already_running); ``` This call gets a 30-second budget, because starting the stream subprocess takes a few seconds. ### `stop() -> Result` Stop the stream. **Returns:** `false` if no stream was active. ### `status() -> Result` and `url() -> Result>` Whether a stream is active and where, or just the URL. ## Examples ### Record the bench while a long test runs ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let cam = lager.webcam("cam1"); let stream = cam.start()?; println!("::notice::watch the DUT at {}", stream.url); let result = run_the_long_test(&lager); cam.stop()?; result?; ``` ## Supported Hardware | Camera | Notes | | -------------------------------------------------------------- | --------------------------------- | | Logitech BRIO, C920, C922, C925e, C930e, C270, C615, StreamCam | Any UVC camera the box enumerates | ## Notes * **The stream URL points at the box as the client reached it.** A URL captured on one network does not always resolve from another, so do not persist it across environments. * This is a video stream, not a frame grab. There is no assert-on-image here — point an MJPEG consumer (a browser, VLC, OpenCV) at the URL. * Always `stop()` in teardown. A running stream holds the camera open and the next test cannot start one. # WiFi Source: https://docs.lagerdata.com/source/reference/rust/wifi Manage the box's own wireless interface Scan for access points and join networks with the box's own wireless interface. This is useful when the DUT hosts an access point. It is also useful when the box must move between networks during a test. ## Handle ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let wifi = lager.wifi(); ``` WiFi is a **box-level** capability, not a net. There is no net name and no `name()`. This is the box's own interface. It is not the `NetType.Wifi` net available in the Python API, which gates one DUT's internet access through a router's parental controls. It is not a router net either. ## Methods | Method | Description | | ----------- | --------------------------------------------- | | `status()` | Status of every wireless interface on the box | | `scan()` | Scan for access points on an interface | | `connect()` | Join a network | | `delete()` | Delete a saved connection profile | ## Types ```rust theme={null} pub struct WifiInterface { pub interface: String, // e.g. "wlan0" pub ssid: String, // or a placeholder like "Not Connected" pub state: String, // "Connected" / "Disconnected" } pub struct WifiAccessPoint { pub ssid: Option, // "Hidden" for hidden networks pub address: Option, // BSSID pub strength: Option, // approximate percent, 0-100 pub security: Option, // "Open" / "Secured" } pub struct WifiConnection { pub ssid: String, pub connected: bool, pub interface: Option, pub method: Option, // "nmcli" / "wpa_supplicant" } ``` ## Method Reference ### `status() -> Result>` Status of every wireless interface on the box. ### `scan(interface: &str) -> Result>` Scan for access points, strongest first. ```rust theme={null} for ap in wifi.scan("wlan0")? { println!("{:?} {:?}% {:?}", ap.ssid, ap.strength, ap.security); } ``` ### `connect(ssid: &str, password: &str) -> Result` Join a network. Pass an empty password for an open network. ### `delete(ssid: &str) -> Result<()>` Delete a saved connection profile by SSID. ## Examples ### Assert the DUT brought up its access point ```rust theme={null} use lager::LagerBox; let lager = LagerBox::from_env()?; let wifi = lager.wifi(); let supply = lager.supply("supply1"); supply.set_voltage(3.3)?; supply.enable()?; std::thread::sleep(std::time::Duration::from_secs(15)); let aps = wifi.scan("wlan0")?; let dut_ap = aps.iter() .find(|ap| ap.ssid.as_deref() == Some("DUT-SETUP")) .expect("DUT did not bring up its setup AP"); println!("AP up at {:?}% signal", dut_ap.strength); supply.disable()?; ``` ## Notes * `capabilities.wifi_command` being `true` means the box **serves the route**, not that it can do the work. A box whose container has no `nmcli` installed answers `scan()` with `Error::Box` and HTTP 502 carrying `[Errno 2] No such file or directory: 'nmcli'`. On that same box, `status()` does not error at all: it returns an interface reporting `Interface detection failed`. * Every action gets a flat 90-second budget. The nmcli and iwlist calls block for seconds, a connect can retry through wpa\_supplicant, and they queue on the box's wifi lock. * `strength` is an approximate percentage, not dBm. * Moving the box between networks can move the address you reach it at. Be careful connecting the box to a DUT-hosted AP if that is also the path that your test uses. # Version 0.10.0 Source: https://docs.lagerdata.com/source/release-notes/v0.10.0 March 17, 2026 ## Features * **`lager router` command group** — manage routers as Lager nets * **`lager router add-net`** — register a router (MikroTik hAP or compatible) as a net on a Lager Box * **`lager router connect`** — verify connectivity to a router net * **`lager router interfaces`** / **`lager router wireless-interfaces`** — inspect network and wireless interfaces * **`lager router wireless-clients`** — list currently connected wireless clients * **`lager router dhcp-leases`** — list devices that have received IP addresses from the router * **`lager router system-info`** — query router CPU, memory, and uptime * **`lager router reboot`** — reboot a router net * **`lager router enable-interface`** / **`lager router disable-interface`** — toggle wireless interfaces on/off * **`lager router block-internet`** — drop all forwarded traffic for network isolation testing * **`lager router reset`** — restore a router to a clean baseline (removes test firewall rules, bandwidth limits, and access list entries; optionally re-applies a baseline SSID and WPA2 password) * **`lager router run`** — make arbitrary REST API GET calls against the router ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.10.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.10.0/) # Version 0.11.0 Source: https://docs.lagerdata.com/source/release-notes/v0.11.0 March 18, 2026 ## Features * **Nordic PPK2 instrument support** — The Nordic Semiconductor Power Profiler Kit II is now a supported watt-meter and energy-analyzer instrument, alongside the Joulescope JS220 and Yocto-Watt * `lager watt --box ` reads instantaneous power (watts) from a PPK2 * `lager energy read --box --duration ` integrates energy (J, Wh) and charge (C, Ah) over a configurable duration * `lager energy stats --box --duration ` computes mean/min/max/std statistics for current, voltage, and power * PPK2 devices are auto-detected by `lager instruments` and can be added with `lager nets add-all` * Full Python API via `Net.get(name, type=NetType.WattMeter)` and `Net.get(name, type=NetType.EnergyAnalyzer)` ## Bug Fixes * Fixed webcam MJPEG stream returning 404 for dashboard `/stream/{netName}` requests * Fixed `Net.get()` not resolving instrument location when saved net config uses `address` instead of `location` ## Improvements * `lager energy` command now uses consistent argument order: `lager energy --options` * Cleaner formatted output for energy read and stats commands ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.11.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.11.0/) # Version 0.12.0 Source: https://docs.lagerdata.com/source/release-notes/v0.12.0 March 20, 2026 > Historical note: `lager boxes connect` was later removed from open-source Lager once downstream control planes became the canonical box bootstrap and orchestration layer. ## Features * **Command-in-progress lock** — When a `lager` command is running on a Lager Box, all other commands are automatically blocked with a clear error message, including from the same user. Locks auto-expire after 30 minutes to handle crashed CLI processes * **User lock (`lager boxes lock/unlock`)** — Explicitly lock a Lager Box so only you can run commands on it. Other users see a lock error until you unlock. The user who locked it can still run commands * `--force-command` global flag to bypass command-in-progress locks * `lager boxes` list now shows "locked by" and "busy" columns when applicable * `lager python --kill`, `--kill-all`, and `--reattach` skip lock checks so you can always manage running processes ## Improvements * Hardcoded control plane URL for the historical `lager boxes connect` flow, removing the `--url` flag ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.12.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.12.0/) # Version 0.13.0 Source: https://docs.lagerdata.com/source/release-notes/v0.13.0 March 20, 2026 > Historical note: `boxes connect` was a temporary migration seam. Current open-source Lager no longer exposes that command; downstream control planes own box install/bootstrap. ## Features * `--force-command` flag is now available on all subcommands that target a Lager Box (not just as a global flag). Place it anywhere after the subcommand name: `lager python script.py --box lab-box --force-command` * `--force-command` added to `hello`, `install`, `uninstall`, and the historical `boxes connect` command ## Improvements * `lager python --detach` now keeps the command lock until the detached process finishes on the Lager Box, preventing others from accidentally interfering with running scripts. The lock is automatically released when the script completes * All commands that acquire locks now automatically support `--force-command` via shared decorator ## Bug Fixes * Updated locking documentation to reflect current behavior (detach keeps lock, local flag syntax, correct `lager boxes` output format) ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.0/) # Version 0.13.2 Source: https://docs.lagerdata.com/source/release-notes/v0.13.2 March 21, 2026 ## Improvements * Updated `.gitignore` to exclude local AI assistant configuration directories ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.2/) # Version 0.13.3 Source: https://docs.lagerdata.com/source/release-notes/v0.13.3 March 21, 2026 ## Bug Fixes * **`lager python --detach` now holds command lock**: Detached Python scripts now correctly keep the Lager Box marked as busy while the script runs. Previously, the command lock was released immediately after detaching, allowing other commands (e.g. `lager hello`) to run against a busy box. The lock is automatically released when the detached process finishes or is killed. ## Installation ```bash theme={null} pip install lager-cli==0.13.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.3/) # Version 0.13.4 Source: https://docs.lagerdata.com/source/release-notes/v0.13.4 March 23, 2026 ## Bug Fixes * Removed the automatic command-in-progress lock (ephemeral lock) that fired on every CLI command. The feature had multiple corner cases — supply commands never released the lock, long-running commands blocked all other commands on the same box, and detached processes left stale locks * Removed `--force-command` flag from all commands (no longer needed) ## Improvements * User lock (`lager boxes lock` / `lager boxes unlock`) is unchanged and remains the recommended way to reserve a Lager Box ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.4/) # Version 0.14.0 Source: https://docs.lagerdata.com/source/release-notes/v0.14.0 March 24, 2026 ## Features * `lager install-wheel path/to/wheel --box X` installs a local Python wheel file on a Lager Box. Automatically uninstalls any previously installed version of the package before installing, so the version number does not need to be bumped on every rebuild. The package name is parsed from the wheel filename per the wheel specification. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.0/) # Version 0.14.1 Source: https://docs.lagerdata.com/source/release-notes/v0.14.1 March 24, 2026 ## Bug Fixes * `lager update --version v0.14.0` (and any version tag) now works correctly. Previously, version tags were incorrectly resolved as remote branch refs (`origin/v0.14.0`), causing the update to fail. Tags are now resolved directly. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.1/) # Version 0.14.2 Source: https://docs.lagerdata.com/source/release-notes/v0.14.2 March 30, 2026 ## Bug Fixes * `lager debug erase` and `lager debug flash` now correctly pass the JLinkScript to J-Link during the connect step. Previously only `gdbserver` passed the script, causing erase/flash to fail on MCUs that require a JLinkScript to load the correct flash algorithm (e.g. DA1469x with external QSPI flash) * For DA1469x targets, erase now uses address-range erase instead of chip erase, and no longer halts after erase when flashing * Fixed crash when running RTT after flashing * Improved J-Link process management: stale PID files are now cleaned up, and JLinkGDBServer is stopped before chip erase operations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.2/) # Version 0.14.3 Source: https://docs.lagerdata.com/source/release-notes/v0.14.3 March 31, 2026 ## Bug Fixes * Supply net current limit no longer gets automatically reset to 1A on TUI startup or any CLI command * OVP value now correctly displays in `lager supply state` output ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.3/) # Version 0.14.4 Source: https://docs.lagerdata.com/source/release-notes/v0.14.4 March 31, 2026 ## Changes * `lager debug flash` now erases flash by default before programming, ensuring a clean boot state. Use `--no-erase` to skip. The `--erase` flag is retained for backwards compatibility. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.4/) # Version 0.15.0 Source: https://docs.lagerdata.com/source/release-notes/v0.15.0 April 2, 2026 ## Features * `lager boxes lock` now accepts a `--user` flag to lock as a specific username, useful when running inside a Docker container where the effective user would otherwise be `root` ## Improvements * `lager boxes` now shows a warning when any Lager Box is locked as `root`, with instructions to use `--user` or `lager defaults add --user` * `LAGER_USER` environment variable is now the highest-priority source when determining the lager user for lock operations (before `~/.lager` config and the OS username) * Lock output and error messages now display the user's email address when available. External tools that lock boxes using the `::` lock format will have the email extracted and shown rather than the raw lock string * `lager update` SSH operations now use `StrictHostKeyChecking=accept-new` to avoid host-key prompts on first connection to a new Lager Box * `lager update` Docker rebuild step now correctly passes the explicit SSH key file when one is in use * `lager update` stop/remove step now targets the `lager` and `pigpio` containers by name instead of stopping all running containers ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.0/) # Version 0.15.1 Source: https://docs.lagerdata.com/source/release-notes/v0.15.1 April 7, 2026 ## Bug Fixes * DA1469x post-flash reset now uses GDB-based reset instead of J-Link Commander register writes, fixing unreliable behavior on DA1469x targets after flashing ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.1/) # Version 0.15.2 Source: https://docs.lagerdata.com/source/release-notes/v0.15.2 April 8, 2026 ## New Features * `lager install --version` now accepts a release tag (e.g. `v0.15.0`) in addition to a git branch, so you can install a box at a pinned version directly: `lager install --ip --version v0.15.0`. This replaces the old `--branch` flag, which only accepted branches. ## Bug Fixes * Reverted DA1469x post-flash reset to use J-Link Commander register writes (restores 0.15.0 behavior). The GDB-based reset introduced in 0.15.1 caused regressions on DA1469x targets. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.2/) # Version 0.16.0 Source: https://docs.lagerdata.com/source/release-notes/v0.16.0 April 13, 2026 ## Features * **Lager MCP server** — a Model Context Protocol server now runs on the Lager Box on port 8100 (FastMCP, streamable-http), allowing AI agents to discover a bench and understand how its nets are wired to the DUT. The server has moved from the CLI to the box and is started automatically inside the Docker container. * **Net metadata** — nets now support `description`, `dut_connection`, `test_hints`, and `tags` fields. New `lager nets` CLI commands and TUI flows let you edit this metadata interactively so AI agents (and humans) can reason about what each net is for. * **Capability graph and heuristic engine** — a new engine maps test types to the nets available on a bench, giving agents a principled way to pick the right instrument for a given task. * **Auto-generated MCP API reference** — the MCP API reference is now generated from driver introspection at Docker image build time. The build fails fast if a driver is renamed, so the agent-facing surface stays in sync with the code. ## Improvements * Every MCP tool call is wired through an `@audited` decorator that records the call via `audit.log_tool_call`, giving downstream control planes a consistent audit trail. * `quick_io` writes now pass through a `preflight_check` that enforces voltage, current, and dangerous-action constraints before touching hardware. * The `bench.json` parser is now defensive: a single malformed entry no longer breaks `discover_bench`. * MCP errors no longer return raw tracebacks to agents. `NetType()` inputs are validated against the enum. * `plan_firmware_test` uses a regex-based pattern split instead of the previous unsafe `get_pattern` split. * New integration test `test_agent_loop` plus unit tests for the bench loader, capability graph, heuristic engine, safety preflight, and MCP schemas. ## Security * The `run_lager` MCP passthrough tool is now gated behind the `LAGER_MCP_ALLOW_RUN_LAGER` environment flag and is **off by default**. Operators must opt in explicitly before agents can invoke arbitrary `lager` commands on a box. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.0/) # Version 0.16.1 Source: https://docs.lagerdata.com/source/release-notes/v0.16.1 April 13, 2026 ## Bug Fixes * **`bench_loader` null-value crash** — the MCP bench loader no longer crashes when `bench.json` or `saved_nets.json` contains explicit `null` values for list or dict fields such as `test_hints`, `tags`, `aliases`, `params`, `net_overrides`, `dut_slots`, `interfaces`, or `channels`. Previously, `dict.get(key, default)` only substituted the default when the key was absent, so a literal `"test_hints": null` would return `None` and break downstream iteration. All affected sites now coerce an explicit `null` to the same empty default as an absent key. Regression tests added in `test/mcp/unit/test_bench_loader.py::TestNullTolerance`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.1/) # Version 0.16.10 Source: https://docs.lagerdata.com/source/release-notes/v0.16.10 May 1, 2026 ## Bug Fixes * **`lager debug connect` no longer hides the real Segger error behind an `AttributeError` when J-Link cannot reach the target.** When J-Link's multi-speed retry loop in `box/lager/debug/api.py:connect_jlink` exhausted without ever reaching the target, `status['logfile']` could be set to `None` instead of being absent — so `status.get('logfile', 'No log available')` returned `None`, which was then passed into `clean_logfile_content` and crashed with `AttributeError: 'NoneType' object has no attribute 'replace'`. The crash masked the real Segger "Connecting to target failed" message that operators need to see in the dashboard. Two changes: `connect_jlink` now uses `status.get('logfile') or 'No log available'` so the literal fallback fires for both missing and `None` values, and `clean_logfile_content` itself returns `''` when given `None` as defense in depth for any future caller. ## Internal * Bumped seven transitive Rust dependencies in `box/oscilloscope-daemon/Cargo.lock` (`quinn-proto`, `rustls-webpki`, `time`, `bytes`, `tracing-subscriber`, `rand` 0.8 and 0.9 lines) to clear ten Dependabot security advisories on the daemon's QUIC/TLS stack. Lockfile-only change; the daemon binary is built and deployed separately from the lager-cli pip package, so this has no runtime effect on existing boxes until the daemon is rebuilt. Verified with a full release build + libps2000 link on a Picoscope-equipped box. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.10 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.10/) # Version 0.16.2 Source: https://docs.lagerdata.com/source/release-notes/v0.16.2 April 17, 2026 ## Bug Fixes * **Keysight E36313A USB PID** — corrected the USB product ID for the Keysight E36313A power supply (`2a8d:1202`) in the `SUPPORTED_USB` tables used by the box's USB scanner and the CLI's `query_instruments` path. The PID was previously a placeholder (`????`), so the instrument was not recognized on plug-in. A matching udev rule was added so PyVISA can open the device directly via libusb (`MODE=0666`, with the `usbtmc` driver unbound on `bind` to prevent "Resource busy" errors). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.2/) # Version 0.16.3 Source: https://docs.lagerdata.com/source/release-notes/v0.16.3 April 24, 2026 ## Improvements * **`user` column in `lager boxes`** — the table output for `lager boxes` (and `lager boxes list`) now includes a `user` column between `ip` and `version`. Lager Boxes added with `--user` show the configured SSH username; Lager Boxes added without `--user` show the default (`lagerdata`). Makes it easy to see, at a glance, which Lager Boxes are configured for a non-default SSH user. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.3/) # Version 0.16.4 Source: https://docs.lagerdata.com/source/release-notes/v0.16.4 April 27, 2026 ## Bug Fixes * **`/instruments/list` returning empty from worker threads** — when the box's HTTP server handled `/instruments/list` on a `ThreadingHTTPServer` worker, the USB scan's `signal.SIGALRM`-based timeout raised "signal only works in main thread of the main interpreter". The error was silently swallowed and the endpoint returned `[]`, so connected devices (e.g. LabJack T7) appeared to be missing. The scanner now falls back to a no-timeout direct call when it isn't on the main thread; the underlying serial and sysfs reads already have their own I/O timeouts. The CLI path was not affected because it runs `query_instruments.py` as a subprocess. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.4/) # Version 0.16.5 Source: https://docs.lagerdata.com/source/release-notes/v0.16.5 April 27, 2026 ## Bug Fixes * **Keysight E36xxx supplies reporting `Enabled: OFF` after `enable`** — running `lager supply state` (or any other read-only supply command) immediately after a successful `lager supply enable` would report the output as `OFF` on Keysight E36200/E36300 series supplies. The `KeysightE36000` driver constructor was unconditionally calling `disable_output()` as a "safe default" on every connect, so each fresh CLI invocation silently turned the output off before running its query. The disable is now gated behind the explicit `reset=True` flag, so constructing a driver for a read (or for `enable`) no longer mutates output state. * **EA PSB supplies briefly dropping output on re-enable** — `lager supply enable` on an EA PSB 10060-60 / 10080-60 caused a brief (\~500ms) output drop when the output was already on. `EA.enable()` always ran `_clear_latched_events()` first, which writes `OUTPut OFF` and waits 200ms before turning the output back on. `enable()` is now idempotent: if `OUTPut?` reports the output is already on, the call returns immediately without toggling. The off→on path that needs latched-protection clearing is unchanged. * **`lager supply tui` closing silently on Rigol DP821** — the supply TUI was closing after \~5 seconds with no visible error whenever a direct supply command (e.g. `state`) had been run beforehand. The WebSocket supply monitor on the box was opening its own pyvisa session, conflicting with the cached VISA session held by `hardware_service.py` on port 8080. Instruments that don't tolerate concurrent USB sessions (Rigol DP821 reproduces this) hung silently and the TUI's 15-second wait for `supply_driver_ready` always timed out. The monitor thread now releases the cached handle (via `localhost:8080/cache/clear`) before opening its own session. As a defensive bonus, `get_channel_limits()` and the session-store block now emit a visible `error` event on any failure during init, and the CLI captures the TUI's exit reason and prints it red to stderr after Textual's alt-screen tears down — so failures no longer disappear with the screen restore. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.5/) # Version 0.16.6 Source: https://docs.lagerdata.com/source/release-notes/v0.16.6 April 27, 2026 ## Bug Fixes * **`lager battery tui` now works for the first time.** The OLD WebSocket battery monitor crashed at module load with `ImportError: cannot import name '_resolve_net_and_driver' from 'lager.power.battery.dispatcher'` — that symbol existed at module level only in the supply dispatcher, never the battery one. Nobody had reported the bug because nobody had tested the battery TUI before. Incidentally fixed by the VISA-ownership unification below; the battery monitor now also emits a `battery_driver_ready` event mirroring `supply_driver_ready` for client symmetry. * **Concurrent SCPI access on the same instrument now serializes correctly.** Two `/invoke` requests against the same cached driver in `box/lager/hardware_service.py` could race on the SCPI bus and produce `Query INTERRUPTED` pyvisa errors. Added a per-`(device_name, address)` lock that wraps every driver call. Multi-channel devices (e.g., Rigol DP821) correctly share one lock since they share one VISA session. ## Improvements * **VISA session ownership unified.** The supply and battery WebSocket monitors no longer open their own pyvisa sessions in monitor threads. They now route every driver call through `hardware_service.py:/invoke` via the existing `Device` HTTP proxy. `hardware_service.py` (port 8080) is the sole owner of pyvisa sessions per `(device_name, address)`. The v0.16.5 `POST /cache/clear` band-aid is removed — this is the architectural fix that replaces it. Net effect: TUIs are more robust, no longer trip on stale cached sessions, and `Query INTERRUPTED` errors during simultaneous TUI + CLI activity are gone. * **Battery handlers consolidated.** \~670 lines of duplicate battery-handler code in `box/lager/box_http_server.py` (parallel to the modular `box/lager/http_handlers/battery.py`) were deleted. `box_http_server.py` now imports and registers the modular versions, matching what was already done for supply. * **Test hygiene.** Two unit tests in `test/unit/cli/test_performance_improvements.py` had been silently failing since the `.lager` config format was migrated to JSON-only. Tempfiles now use `{"LAGER": {...}}` JSON; full unit suite is back to 141/141 passing. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.6 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.6/) # Version 0.16.7 Source: https://docs.lagerdata.com/source/release-notes/v0.16.7 April 28, 2026 ## Bug Fixes * **`lager uart ` no longer returns `404 — UART net not found`.** The v0.16.6 battery-handler consolidation (commit `f277402`) deleted the two-line UART handler registration in `box/lager/box_http_server.py` as collateral damage. The imports stayed in place, so the file still parsed cleanly; the `/uart/nets/list` Flask route just was never registered, so every UART CLI command 404'd. Restored the `register_uart_routes(app)` / `register_uart_socketio(socketio)` calls alongside the supply and battery registrations. * **`lager supply state` (and other one-shot supply/battery commands) no longer fail with `[Errno 16] Resource busy` immediately after exiting the TUI.** Root cause: `/supply/command` and `/battery/command` returned 404 when no active WebSocket session was found, forcing the CLI's `_run_backend` into a direct-pyvisa subprocess fallback (`cli/impl/power/supply.py` → dispatcher) that opened its own pyvisa session against the same USB device `hardware_service.py` was still caching. Both endpoints now build a transient `Device` proxy via `resolve_net_proxy()` when no active WS session exists, routing through `hardware_service.py:/invoke` like the WS monitor already does. There is now exactly one pyvisa session per `(device_name, address)` regardless of TUI lifecycle. This completes v0.16.6's "VISA session ownership unified" promise. * **Concurrent TUI + CLI access on the same supply no longer cascades `Resource busy` errors across subsequent commands.** Previously, a single transient kernel-level USB-claim collision (the kind that can momentarily occur when two pyvisa-issued USB transfers overlap on the same device) would be mis-classified as a stale pyvisa session: `_is_visa_session_error()` matched the substring `'resource'` inside `'Resource busy'`, the retry path then popped the live cache entry and called `module.create_device()` on the same address, and the new open hit `Resource busy` again because the original session was still alive in the same process. The result was that an isolated USB-busy error turned into a chain of failures across every following command. Removed `'resource'` from `_VISA_SESSION_ERROR_KEYWORDS` in `box/lager/hardware_service.py`; retry now fires only for genuine stale-session signals (`'session'`, `'closed'`, `'invalid'`). An isolated USB-busy collision is still possible on heavily-contended USB transfers but is now returned to the caller cleanly without disturbing the cache, so the next command immediately succeeds. ## Known Limitations * **Keithley 2281S dual-role nets must be used one at a time.** When the same physical Keithley 2281S has both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`) configured, `box/lager/hardware_service.py` caches them under two different keys (`("keithley", address)` vs `("keithley_battery", address)`) and tries to open two pyvisa sessions on the same USB device — the second hits `[Errno 16] Resource busy`. Workaround: configure only the role you need on the Keithley 2281S, or restart the box's lager container between switching roles. The proper fix (shared pyvisa Resource between supply and battery driver instances, or a merged dual-role driver class) is targeted for v0.16.8. * **Concurrent battery TUI + CLI on the Keithley 2281S can surface `[Errno 16] Resource busy`.** Running `lager battery tui` in one terminal while running `lager battery state` (or any other one-shot battery CLI command against the same net) in another terminal can fail with `Resource busy`, even when only the battery role is configured on the Keithley (so this is distinct from the dual-role limitation above). v0.16.7's Bug-B retry-classification fix prevents this from cascading across subsequent commands but does not eliminate the initial collision; the underlying contention appears to live in the Keithley pyvisa session itself rather than in `hardware_service.py`'s lock. Workaround: do not invoke battery CLI commands while a battery TUI is open against the Keithley 2281S — close the TUI first, or run TUI-only or CLI-only on the Keithley battery net. Root-cause investigation tracked for v0.16.8. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.7 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.7/) # Version 0.16.8 Source: https://docs.lagerdata.com/source/release-notes/v0.16.8 April 28, 2026 ## Features * **SEGGER J-Link Flasher PRO support** — the J-Link Flasher PRO (USB `1366:0105`) is now recognized as a supported `debug` instrument. Plugging one into a Lager Box and running `lager instruments` will list it as `J-Link_Flasher_Pro`. Both the box-side scanner (`box/lager/http_handlers/usb_scanner.py`) and the CLI-side scanner (`cli/impl/query_instruments.py`) were updated. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.8 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.8/) # Version 0.16.9 Source: https://docs.lagerdata.com/source/release-notes/v0.16.9 April 29, 2026 ## Bug Fixes * **Keithley 2281S dual-role nets now switch cleanly between supply and battery roles.** When the same physical Keithley 2281S is configured with both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`), the box now opens exactly one pyvisa session per VISA address and both driver classes wrap that one session — instead of each driver opening its own session and the second hitting `[Errno 16] Resource busy`. Implemented as a process-wide shared-resource cache (`_visa_resources` keyed by VISA address) in `box/lager/hardware_service.py`, plus a `raw_resource=` factory kwarg on `box/lager/power/supply/keithley.py:create_device` and `box/lager/power/battery/keithley.py:create_device`. Both Keithley driver constructors track an `_owns_resource` flag so `close()` does not release the underlying USB claim while the sibling driver still needs it. SCPI serialization moved to a per-address lock (was per `(device_name, address)` cache key) so a supply command followed by a battery command against the same Keithley serialize correctly on the USB bus. This resolves the **sequential** half of the dual-role known limitation in v0.16.7 — a script can now alternate `lager supply ` and `lager battery ` commands against the same Keithley without restarting the box service. The instrument's two operating modes (Power Supply via `:ENTR:FUNC POW`, Battery Simulator via `:ENTR:FUNC BATT`) remain mutually exclusive in firmware, so genuinely *concurrent* supply + battery operation against one Keithley is still not supported by the hardware itself; see Known Limitations. * **Concurrent battery TUI + CLI on the Keithley 2281S no longer fails with `[Errno 16] Resource busy`.** The stale-VISA-session retry path in `box/lager/hardware_service.py:/invoke` now calls `_close_device(old_device, cache_key)` before invoking `module.create_device(net_info)`. Previously the popped driver instance stayed alive in the Python process and kept libusb's USB claim, so the recreated session's `pyvisa.ResourceManager().open_resource(addr)` failed with `Resource busy` — surfaced as `Could not open instrument at ...`. Closing the old session before opening a new one fixes this for any driver whose retry path fires; for Keithley shared-resource drivers the underlying pyvisa session is also reopened so both supply and battery drivers get a fresh handle. This resolves the concurrent battery TUI + CLI known limitation documented in v0.16.7. * **Keithley 2281S supply commands no longer crash with `TypeError`.** `box/lager/http_handlers/supply.py` is modeled on multi-channel drivers (Rigol DP800) and calls supply-driver methods with a `channel=` kwarg or positional channel. The Keithley 2281S supply driver follows the `SupplyNet` abstract (no `channel` parameter — the 2281S is single-channel), so the very first call hit `TypeError: Keithley2281S.output_is_enabled() got an unexpected keyword argument 'channel'`. The handler treated that as a hardware failure and triggered `/cache/clear`, which tore down the shared pyvisa session this release had just opened for dual-role mode. `Keithley2281S.output_is_enabled` now accepts (and ignores) a `channel=None` kwarg, and six new public OCP/OVP wrapper methods (`set_overcurrent_protection_value`, `enable_overcurrent_protection`, `set_overvoltage_protection_value`, `enable_overvoltage_protection`, `clear_overcurrent_protection_trip`, `clear_overvoltage_protection_trip`) delegate to the existing private `_set_ocp` / `_set_ovp` and public `clear_ocp` / `clear_ovp` methods so the supply handler can call them without `AttributeError`. No new SCPI logic — the wrappers exist purely so a single-channel driver can satisfy the multi-channel calling convention used elsewhere. * **`lager battery state` no longer collides with the shared pyvisa session.** The battery CLI sends `action='print_state'` (matching the dispatcher function name), but `/battery/command` previously only recognized `action='state'` (matching the supply handler). The mismatched action returned HTTP 400, the CLI's `_run_backend` fell through to the python:5000 dispatcher path, and that subprocess opened a *second* pyvisa session against the same Keithley — colliding with the shared session that hardware\_service had just opened in the previous `lager supply` command and surfacing as `Could not open instrument at USB0::...: failed to set configuration [Errno 16] Resource busy`. `/battery/command` now accepts both `'state'` and `'print_state'`, keeping the CLI on the WebSocket → hardware\_service path so the shared pyvisa session this release introduces is actually reused for sequential supply→battery CLI workflows. * **`lager python` no longer wipes hardware\_service's cache on every script exit.** `cli/commands/development/python.py` was POSTing `/cache/clear` on script normal exit, Ctrl+C, and BrokenPipeError — a v0.16.5 band-aid that pre-dates Phase 2's per-address shared session. With v0.16.9, hardware\_service is the single owner of the pyvisa session for each USB device and that session is *meant* to persist for the container's lifetime. Clearing it on every script exit defeated the design and re-introduced the very `[Errno 16] Resource busy` race that Phase 2 set out to eliminate. The clears are removed; if you really need to force a reload (e.g., a script that opens its own pyvisa session out-of-band), you can still `curl -X POST http://:8080/cache/clear` manually. * **Resilient first open against libusb's release-interface timing race.** `hardware_service._get_or_open_visa_resource` now retries `open_resource()` on `[Errno 16] Resource busy` with an exponential backoff (`0.2, 0.5, 1.0, 2.0` s) before giving up. pyvisa-py + libusb on Linux releases the USB interface asynchronously, so opening the same device too quickly after a close (e.g. after a manual `/cache/clear` or a TUI exit) could fail the first time and succeed the second. The retries hide the kernel's catch-up window without masking genuine "device unplugged" failures. * **`POST /cache/clear` preserves shared pyvisa sessions; new `POST /cache/clear_all` for the old behavior.** The endpoint still drops cached driver wrappers from `device_cache` (so a wedged driver gets a fresh load on the next `/invoke`), but the per-VISA-address shared session that this release relies on is no longer torn down. This was the missing piece that caused V.5/V.6 hardware verification to fail even after the script-exit clear was removed from `lager python` — older clients (`lager-cli` ≤ 0.16.7) still POST `/cache/clear` on every script exit, and that was nuking the shared session out from under hardware\_service. With the endpoint now safe under Phase 2, those older clients no longer break dual-role workflows. If you actually need to force-close a shared session (e.g. you unplugged the instrument), `POST /cache/clear_all` does what `/cache/clear` used to do. * **Cross-role concurrent use on a single Keithley 2281S now fails fast with a clear error.** Running `lager supply tui` and a concurrent `lager battery ` command (or vice-versa) against the **same** physical Keithley used to surface as cryptic SCPI timeouts or `[Errno 16] Resource busy` errors, because the 2281S's Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`) entry functions are mutually exclusive in firmware and the two clients were fighting over the entry function on every poll. The box now tracks the active monitoring sessions per role (`box/lager/http_handlers/state.py:conflicting_other_role_session`), records the resolved VISA address when a TUI starts, and refuses an opposite-role command at `/supply/command`, `/battery/command`, `start_supply_monitor`, and `start_battery_monitor` with a message that names the conflicting net and explains the hardware limitation. Sequential CLI cross-role workflows are unaffected and continue to work cleanly via Phase 2's shared pyvisa session. ## Known Limitations * **Concurrent supply and battery operation on the same Keithley 2281S is not supported by the instrument itself.** The 2281S has two mutually-exclusive entry functions — Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`). Each Lager driver flips the entry function to its preferred mode before every SCPI command, so running a supply TUI in one terminal while running a battery CLI command in another terminal causes the two clients to fight over the entry function on every poll, producing intermittent SCPI errors and Resource busy events. **Configure either the supply role or the battery role on the Keithley 2281S, not both — or use them strictly sequentially in a single workflow** (which v0.16.9's shared-pyvisa-session work makes fast and clean). This is a property of the instrument, not Lager. ## Internal * Drivers that share a single pyvisa session per VISA address are listed in `box/lager/hardware_service.py:_SHARED_VISA_DEVICE_NAMES` (currently `keithley`, `keithley_battery`). Adding a future dual-role instrument means adding its supply and battery `device_name` strings here and giving each `create_device` factory the `raw_resource=` kwarg pattern. * `Keithley2281S.__init__` and `KeithleyBattery.__init__` accept a new `_owns_resource` kwarg (default `True` for backward compatibility). When `False`, `close()` drops the wrapper reference without closing the underlying pyvisa session. * Single-role drivers (Rigol DP800/DP821, Keysight E36xxx, EA PSB, etc.) are unchanged — they continue to use the legacy per-driver-opens-its-own-session path. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.9 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.9/) # Version 0.17.0 Source: https://docs.lagerdata.com/source/release-notes/v0.17.0 May 5, 2026 ## Features * **Concurrent J-Link probes on a single Lager Box.** Two J-Link probes plugged into one Box can now run independent debug sessions side-by-side. The box-side service in `box/lager/debug/service.py` resolves each debug net's J-Link USB serial from its VISA address and allocates a deterministic per-probe slot (read from `saved_nets.json` via `NetsCache`). Slot N owns a three-port window — GDB `2331+3N`, SWO `2332+3N`, telnet `2333+3N` — plus RTT base `9090+2N`. The auxiliary `-swoport` and `-telnetport` are passed explicitly to `JLinkGDBServer` so its defaults of `2332`/`2333` can't collide with another slot's GDB port. The service passes `-select USB=` to `JLinkGDBServer` and `-SelectEmuBySN ` to `JLinkExe`, writes per-serial PID and log files, and narrows `pkill` so disconnecting one probe no longer tears down the other. The CLI's `--gdb-port` default is now `None` rather than `2331`, so the box's allocator is honored unless an explicit port is requested; the effective `gdb_port` returned by the box is printed on connect. `start_box.sh` publishes the widened `2331-2342` Docker port range and `secure_box_firewall.sh` admits the same range, so existing hardened boxes need a firewall refresh to use the new slots. Nets without a parseable serial (legacy single-probe setups) fall back to slot 0 / GDB 2331 / RTT 9090 / `/tmp/jlink_gdbserver.pid` and continue to work unchanged. * **RIGOL DP811 power supply detection.** `lager instruments` now lists the DP811 alongside the DP821 and DP832. The DP811 shares VID:PID `1ab1:0e11` with its siblings, so it is identified by USB serial prefix (`DP8H` or `DP81` → `Rigol_DP811`) in both `box/lager/http_handlers/usb_scanner.py` and `cli/impl/query_instruments.py`, and is added to the serial-disambiguated bucket so a generic VID:PID match cannot misclassify it as a DP821. * **Multiple concurrent viewers per webcam stream.** Each `/stream` connection used to open its own `cv2.VideoCapture` against `/dev/videoN`, which V4L2 serves exclusively — the second viewer either failed or got blank frames. The streamer subprocess in `box/lager/automation/webcam/service.py` now spins up a single daemon capture thread on the first viewer that owns the device and broadcasts encoded JPEG frames to a shared buffer guarded by a `threading.Condition`. Any number of viewers can now subscribe to the same stream concurrently. Stop and re-start each webcam to pick up the regenerated streamer script. ## Bug Fixes * **`LabJackADC.input()` no longer inherits sticky AIN register state from a previous tool.** ADC reads previously called `ljm.eReadName` with zero AIN register configuration, inheriting whatever the previous tool left in `AIN_RANGE`, `AIN_NEGATIVE_CH`, `AIN_RESOLUTION_INDEX`, and `AIN_SETTLING_US`. T7 register state persists in device RAM until USB power-cycle, so if a previous tool left an AIN in differential mode with a floating negative channel, every read saturated at \~10.10 V regardless of the actual signal — indistinguishable from a real wiring fault. Safe defaults (`RANGE=10.0`, `NEGATIVE_CH=199`, `RESOLUTION_INDEX=0`, `SETTLING_US=0`) are now written once per `(handle, channel)` tuple before the first `eReadName`, cached in a class-level set. Config-write failures are logged but do not raise. ## Improvements * **Webcam capture forces MJPEG so two cameras can share a USB 2.0 bus.** Default OpenCV negotiation picked YUYV (uncompressed, \~150 Mbps at 640×480 30fps), which doesn't leave room for a second camera on the same bus — the kernel rejected `VIDIOC_STREAMON` with "Not enough bandwidth for altsetting". MJPEG is roughly 5× smaller and fits two cameras comfortably; the FourCC is now set before width/height/fps so negotiation honors it. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.17.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.17.0/) # Version 0.18.0 Source: https://docs.lagerdata.com/source/release-notes/v0.18.0 May 12, 2026 ## Features * **`lager box config` — declarative per-box provisioning.** A new top-level command tree that replaces ad-hoc SSH-and-edit workflows with a single JSON manifest at `/etc/lager/box_config.json` per Lager Box. The file declares mounts, named Docker volumes, container environment variables, host apt packages, kernel sysctl settings, in-container pip packages, cargo crates, and npm packages — and `lager box config apply` reconciles the box to match. Re-applying the same config is a no-op via SHA-256 comparison against the last applied snapshot, so it's safe to wire into CI. The full operator surface: `init`, `show`, `validate`, `diff`, `apply` (with `--dry-run` and `--yes`), `audit`, `status`, `edit` (round-trips through `$EDITOR`/`nano`/`vi` with shim-side validation on save), `copy --from --to`, `import FILE`, `export FILE`, and `repair`. Multi-box fanout via `--box A,B,C` on `show` and `apply` for fleet operations. Every section has CRUD verbs: `mount add/remove/list`, `pip add/remove/list`, `apt add/remove/list`, `cargo add/remove/list`, `npm add/remove/list`, `sysctl set/unset/list`, `env set/unset/list`, `volume add/remove/list`. * **npm support inside the container.** A new `npm_packages` first-class field on `box_config.json` lets you declare Node.js global packages alongside the existing pip and cargo lists. Scoped packages (`@types/node`) and versioned packages (`lodash@4.17.21`) are both supported. The container Dockerfile now ships `nodejs npm` and sets `NPM_CONFIG_PREFIX=/home/www-data/.npm-global` (pre-created and chowned to the `www-data` runtime user) so `npm install -g` works without root. * **Rust toolchain baked into the container image.** rustup is now installed into `/opt/rust` (owned by `www-data`) with `RUSTUP_HOME`, `CARGO_HOME`, and `PATH` set in the Dockerfile, so `cargo install` runs cleanly from the post-bounce loop. No more manual rust installation per Lager Box. `cargo_packages` entries accept both `name` and `name@version`. * **Audit log of every config mutation.** Every `add`/`set`/`remove`/`unset`/`apply` operation is recorded to `/etc/lager/box_config.audit.log` (JSONL, append-only) with an ISO-8601 timestamp. `lager box config audit` reads it back. Filters compose: `--tail 20`, `--since 1h`, `--verb apt-add`, `--json`. Useful for "what changed today" or "every apt operation ever." * **Automatic rollback on failed bounces.** When `lager box config apply`'s container restart fails (for example, because docker rejected a malformed mount), the previously applied snapshot is restored to `/etc/lager/box_config.json` via SSH `sudo cp` and a re-bounce brings the box back up on the prior good config. Sysctl values are reverse-diffed to their previous state in the same pass. The restore goes through direct SSH file ops rather than the in-container shim because the container is necessarily dead by the time the rollback fires. `lager box config repair --box X` exposes the same recovery as a standalone command for situations that automatic rollback can't reach — for example, when an operator hand-edits the JSON to invalid syntax outside the CLI. * **Sudoers auto-bootstrap.** `lager install` (on new boxes) and `lager update` (on existing boxes) now install `/etc/sudoers.d/lager-box-config` with the narrow NOPASSWD grants `lager box config apply` needs: `apt-get` with `SETENV:` for `DEBIAN_FRONTEND`, path-scoped `tee`/`rm`/`sysctl --system` for the sysctl conf, `mkdir`/`chown` for mount auto-prep, and a path-scoped `cp` for the rollback snapshot restore. A marker file at `/etc/lager/.boxcfg-sudoers-v2` lets `lager update` skip re-bootstrapping once the current rule shape is in place. Operators never type a sudoers snippet by hand. ## Bug Fixes * **`lager update` container startup timeout raised from 5 to 10 minutes.** First-time docker builds with cargo and npm layers were timing out on slower Lager Boxes. `_bounce_container`'s SSH ceiling was also bumped from 300s to 900s for the same reason — covers cargo crate compilation + pip and npm install loops with headroom. * **SSH user resolution.** The new shared SSH runner used by `lager box config` was calling `get_box_user(box_ip)` even though that helper keys by box *name*, so every Lager Box with a stored custom SSH user silently fell back to `lagerdata`. The runner now reverse-resolves the name via `get_box_name_by_ip` before the lookup, and uses `~/.ssh/lager_box` via `-i` to match the rest of the CLI's SSH conventions. * **`DEBIAN_FRONTEND=noninteractive` actually propagates on apt installs.** Default Ubuntu sudoers' `env_reset` strips `DEBIAN_FRONTEND` set as a `sudo VAR=value cmd` argument unless `SETENV:` is granted. Packages with debconf prompts (`iptables-persistent` and similar) were hanging on a prompt that never showed. The new sudoers rule grants `SETENV:` only on `/usr/bin/apt-get` so the env var propagates. * **`cargo` found inside the container during apply.** `start_box.sh`'s cargo install loop used `bash -lc` (login shell), which re-sourced `/etc/profile` and reset `PATH` — wiping the Dockerfile's `ENV PATH=/opt/rust/cargo/bin:...`. Switched to `bash -c` (non-login) so the docker `ENV` is honored. Same fix applied to the npm install loop. * **Real exit codes captured from pip/cargo/npm install loops.** The previous `if ! cmd; then _rc=$?` pattern in `start_box.sh` captured `$?` *after* bash's `!` inversion — so `_rc` was always `0` even on real failures, and error messages reported `(rc=0)` for non-zero exits. Refactored to `if cmd; then : else _rc=$?` so error codes propagate accurately. * **Env values with whitespace, `$`, backticks, or single quotes survive the bounce.** The docker-args renderer used to emit `--env 'KEY=hello world'` to stdout, which `start_box.sh` interpolated unquoted into `docker run` — bash variable expansion does not re-parse quotes, so values got word-split and the literal quote characters leaked through. The renderer now writes a bash-sourceable file declaring `BOX_CONFIG_MOUNTS`, `BOX_CONFIG_ENV`, and `BOX_CONFIG_HOST_PATHS` arrays via `shlex.quote`; `start_box.sh` sources that file and uses `"${BOX_CONFIG_MOUNTS[@]}"` so each element preserves its content verbatim. * **`lager box config edit` no longer rejects valid saves with non-zero editor exit.** Some vim plugins return `1` from `:wq` even when the save succeeded. The command now compares tempfile contents before and after the editor exits — content changed AND non-zero rc means "user saved, proceed"; content unchanged AND non-zero rc means "abort." Bonus: `nano` is preferred over `vi` as the fallback when `$EDITOR` is unset. ## Improvements * **`lager box config show` reads as a tree.** Bold uppercase `HOST` / `CONTAINER` group headers with horizontal-rule underlines, bold section labels indented two spaces, and `├── /└── ` branches under each section. Mount paths align around `->`; env/sysctl keys align around `=`; empty sections render as `(none)` leaves so operators discover what's configurable. The header carries a color-coded `[Up To Date]` / `[Unapplied Changes!]` marker driven by a `hash` vs `applied-hash` comparison. * **`apply` shows the pending diff inline before confirming.** When `--yes` is not passed, the confirm prompt is preceded by a per-field diff of what's about to change — closes the most common pre-apply workflow ("run diff first, then apply") into a single command. * **Tightened sudoers rule.** `tee`, `rm`, and `sysctl --system` in the recommended sudoers grant are now path-locked to the exact files and flags `apply` invokes, so a compromised `lagerdata` account cannot escalate to root via those binaries. `apt-get` and `mkdir`/`chown` stay unscoped because the package list and host paths are user-defined. * **flock against the in-container shim.** Two concurrent `lager box config X` invocations against the same Lager Box used to do read-modify-write on `box_config.json` and silently drop one mutation. The shim now `flock`s `/etc/lager/box_config.lock` around the whole dispatch. * **Post-apply consistency check.** After the bounce + API-ready probe but before recording the new applied-hash, the apply path re-runs `validate` + `show` against the box. If either drifts from what was bounced (the JSON was hand-edited mid-apply, say), `applied-hash` is left untouched and the operator is told to re-run apply. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` After upgrading, run `lager update --box ` on each existing Lager Box to deploy the matching box-side code and pick up the sudoers rule. ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.0/) # Version 0.18.1 Source: https://docs.lagerdata.com/source/release-notes/v0.18.1 May 13, 2026 ## Bug Fixes * **J-Link GDB attach no longer halts the target.** Dropped the `-ir` flag from `JLinkGDBServer` and switched GDB into non-stop async mode before target attach, so attaching gdbserver/rtt no longer halts the target CPU on \~15% of attempts. ## Improvements * **`lager usb` enable / disable / toggle \~2.6x faster.** Routes through a new Flask handler on the box's port 9000 instead of spawning a fresh Python subprocess per call (mirrors the supply/battery fast-path from 0.17.x). Backward compatible — falls back to the slow path against older box images. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.1/) # Version 0.18.2 Source: https://docs.lagerdata.com/source/release-notes/v0.18.2 May 13, 2026 ## Features * **`lager box update` is now the canonical update command.** The top-level `lager update` keeps working as a hidden alias for existing scripts but prints a deprecation notice. Same flag set; runs through the same flow. * **`--check` flag for dry-run updates.** `lager box update --box X --check` reports what would change (code, deps, container) without touching the box. Useful for CI gating and pre-deploy checks. * **Docker cache auto-invalidates when `Dockerfile` or `requirements.txt` change.** No more remembering to pass `--force` after a deps bump — the next update detects the drift and rebuilds. ## Bug Fixes * **Updates now take effect on the first run.** Fixes a cluster of bugs behind the recurring "had to run `lager update` 2–3 times before it stuck" reports: stale `/etc/lager/version` after the early-exit branch, the flatten heuristic re-fetching on every run, the 5-second post-restart sleep racing against an unready service, silent flatten failures producing broken images, and the cache-invalidation early-exit skipping rebuilds when only deps had changed. * **Branch switches with conflicting root-level files no longer fail.** Adds `git checkout -f` so a previous flatten step that clobbered a tracked file (e.g. `README.md`) doesn't block the switch with "local changes would be overwritten." * **Git errors are now shown.** `git checkout` and `git reset --hard` failures used to surface only "Failed to checkout version X" without git's underlying error message. ## Improvements * **Consecutive no-op `lager box update` runs are \~10× faster.** SSH calls multiplex through a single OpenSSH ControlMaster connection — \~20s → \~1.6s. * **Cleaner output.** Single green summary line, progress bar adapts to terminal width, elapsed time appears on the bar itself. * **`--all`, `--force`, `--skip-restart` flags removed.** `--all` will return as its own command if needed; `--force` is obsoleted by auto cache-invalidation; `--skip-restart` had no real workflow. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.2/) # Version 0.18.3 Source: https://docs.lagerdata.com/source/release-notes/v0.18.3 May 15, 2026 ## Features * **`lager box update --version ` rolls back.** Previously the update flow only counted commits the box was *behind* the target and treated any "ahead" state as in-sync, so you could not downgrade a box to an earlier branch or tag without manually `git reset --hard`-ing on the box. Now diverges in both directions; an explicit second confirmation prompt (skippable via `--yes`) gates the destructive direction so a typo'd `--version` argument can't silently downgrade. `--check` reports "will roll back N commit(s)" / "will switch (N ahead / M behind)". ## Bug Fixes * **Update no longer aborts on git ≥2.36 with `fatal: 'cli/__init__.py' is not a directory`.** Cone-mode sparse-checkout (default since git 2.36) rejects single-file patterns; the pre-batching version of the sparse-checkout step ran in a separate SSH call whose exit was never checked, so the failure was silently swallowed. The new batched pull script chained it with `&&`, propagating the failure and aborting the whole pull. Now treats the `cli/__init__.py` add as best-effort. Affects boxes on newer git (e.g. git 2.43 on Debian bookworm-backports). * **`/etc/lager/version` and the end-of-run summary report the box's actual code, not the CLI's version.** Previously a rollback or branch switch could write the running CLI's version (e.g. `0.18.3`) into the version file even when the code on the box was older (e.g. `v0.18.2`). The CLI now reads `__version__` from `cli/__init__.py` at the box's post-pull HEAD via `git show`, which works even on cone-mode boxes where the file isn't in the working tree. ## Improvements * **No-op `lager box update` runs \~3× faster (\~5s → \~1.6s).** A single SSH probe collapses \~11 separate `test`/`cat`/`git`/`diff`/`stat` round-trips (git-repo check, remote URL, layout, current commit, build-cache hashes, udev rule state, sudoers ownership, box-config sudoers state, `/etc/lager/version`) into one structured shell script. Combined with merging fetch+rev-list, sparse-checkout+checkout+reset, flatten+verify, post-build directory setup, and verify+J-Link presence into single calls. * **Typical `lager box update` \~6× faster on boxes with cargo/npm packages in `box_config.json` (\~1:40 → \~17s).** Adds two named Docker volumes — `lager-cargo` at `/opt/rust/cargo` and `lager-npm-global` at `/home/www-data/.npm-global` — to `start_box.sh`'s `docker run`, so user-installed cargo crates and global npm packages survive container recreation. Without them, every update reinstalled them from scratch (`cargo install` recompiled from source, \~50–60s per update). With them, the second-and-onward run sees "already installed" and finishes in seconds. The CLI wipes both volumes alongside `docker rmi lager` whenever the build-hash changes, so a Dockerfile rustup/node bump can't leave a stale toolchain in the volume. First update on each existing box is the same speed as today (the volumes seed themselves); the win shows up from the second update on. * **`--verbose` output cleanup.** Probe results print as one tidy block instead of a dozen "Checking X... OK" lines; consistent step labels between the progress bar and verbose log; noise lines dropped (e.g. "Checking remote URL" only prints when it actually migrates SSH→HTTPS); a single label for the build step instead of two. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.3/) # Version 0.18.4 Source: https://docs.lagerdata.com/source/release-notes/v0.18.4 May 20, 2026 ## Bug Fixes * **`lager python` scripts no longer miss tight response deadlines under streaming back-pressure.** A running script's stdout/stderr were drained from their kernel pipes *inline* on the same generator that forwards bytes back to the CLI over HTTP, so any stall on that socket (slow link, Nagle, retransmit) stopped pipe drainage. Once the 64 KiB kernel pipe filled, the script blocked on its next `print()`. For scripts with tight timing budgets — for example a DA14695 ROM-bootloader handshake that must reply within 50–120 ms of each received byte — this stretched response windows enough to fail roughly 90% of the time, even though the same script run directly on the host succeeded every time. Output is now drained on background threads into a bounded queue so HTTP-write latency can no longer back-pressure the script, stdout/stderr pipe buffers are enlarged to 1 MiB, and the interpreter runs unbuffered (`python -u`). The wire format and public API are unchanged. * **Removed a potential deadlock when launching a `lager python` script.** The per-script scheduling-priority boost was applied inside the `fork()`/`exec()` window via a `preexec_fn`, which Python documents as unsafe in a multithreaded service. It is now applied from the parent process after the script starts, with identical effect and no window in which a concurrent request could deadlock the launch. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.4/) # Version 0.18.5 Source: https://docs.lagerdata.com/source/release-notes/v0.18.5 May 22, 2026 ## Bug Fixes * **`lager debug ... flash` and `... erase` no longer fail with a 500 (`filedescriptor out of range in select()`) on a Lager Box that has been running for a long time.** The box debug service is a long-lived process. Its GDB controller helper rebuilds a fresh `gdb-multiarch` connection on every retry attempt — retries that happen routinely while a debugger connection is coming up during flash and RTT — but a *failed* attempt was never registered for cleanup, so each one leaked the `gdb-multiarch` subprocess and the pipe handles to it. After enough leaks the service crossed the operating system's 1024 file-descriptor limit for `select()`, at which point the tool that performs erase and flash crashed instead of running. Failed attempts are now closed immediately, and the erase/flash path uses `poll()` instead of `select()` so it is no longer bound by the 1024 limit even if descriptors run high. Recovering a box that has already hit this no longer needs anything beyond restarting the debug service. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.5/) # Version 0.19.0 Source: https://docs.lagerdata.com/source/release-notes/v0.19.0 May 23, 2026 ## Features * **OpenOCD debug backend.** Non-Segger debug probes are now first-class peers of J-Link under `lager debug` — same `connect` / `gdbserver` / `flash` / `erase` / `reset` / `memrd` / RTT command surface, same multi-probe slot allocator, same Net Manager TUI. The Lager Box dispatches each debug net to the right backend automatically based on the probe's USB vendor ID. Auto-detected OpenOCD probes: * **ST-Link V2 / V2-1 / V3** (STMicroelectronics, VID `0483`) * **Raspberry Pi Debug Probe** (RP2040 Picoprobe / CMSIS-DAP, VID `2e8a`) * **FTDI FT232H** (`0403:6014`, mapped to `c232hm.cfg`) * **FTDI FT2232H** (`0403:6010`, mapped to `olimex-arm-usb-ocd-h.cfg`) * **ARM DAPLink / NXP MK20 CMSIS-DAP** (VID `0d28`) * **Atmel EDBG / mEDBG** (VID `03eb`) * **Olimex ARM-USB-OCD-H** (VID `15ba`) **FTDI FT4232H** is supported via a user-supplied OpenOCD config — the chip exposes four channels and Lager can't guess which one carries SWD without it. Other open-hardware probes whose VIDs aren't on the auto-list (Black Magic Probe, Glasgow, etc.) can also be used by setting `debug_backend: openocd` on the net and supplying an `openocd_config`. Probes already on a J-Link USB ID stay on the J-Link backend, so existing nets are unaffected. OpenOCD nets can run concurrently with J-Link nets on the same Lager Box; the existing J-Link multi-probe slot stride is reused, and OpenOCD adds its own per-slot telnet (`4444 + slot`) and TCL/RPC (`6666 + slot`) ports. * **DA1469x flash programming over OpenOCD via the Apache Mynewt RAM-resident flash\_loader.** Mainline OpenOCD has no QSPI flash driver for the Dialog/Renesas DA1469x family, so before this release `lager debug SWD flash` against an FT4232H rig connected to a DA1469x silently did nothing despite a green `Flashed!` log line. The Lager Box now ports the upstream GDB-script protocol (`flash.gdb` / `erase.gdb` / `flash_loader.gdb`) to a pure OpenOCD TCL/RPC implementation: it brings the loader up in RAM, drives the `fl_cmd` command struct, programs in chunks, and software-resets on success. The CLI side is unchanged — `lager debug SWD flash --bin ,0x16000000` and `lager debug SWD erase` just work — and absolute XIP addresses are accepted with a clear error if the user passes a flash-relative offset by mistake. The two loader artefacts (`flash_loader.elf` + `flash_loader.elf.bin`) are dropped into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` once per box; `lager update` no longer wipes them out, and `start_box.sh` creates the directory tree on every container start so an operator can `scp` the pair in without first `mkdir -p`. Validated end-to-end on hardware after a few bring-up fixes for the loader's double-buffer pointer rotation, the `fl_cmd_rc` post-loop handshake, and the CLI's absolute-XIP-to-flash-offset translation. The J-Link DA1469x flash path is unchanged; this release adds a working OpenOCD path for the same target. * **Concurrent multi-probe slots extended to OpenOCD.** A Lager Box can now run up to four debug probes simultaneously across any mix of J-Link and OpenOCD adapters. Each probe gets a deterministic per-slot port window — GDB on `2331+3·slot`, RTT base on `9090+2·slot`, OpenOCD telnet on `4444+slot`, OpenOCD TCL on `6666+slot`. The legacy single-probe configuration (slot 0: GDB 2331, RTT 9090, OpenOCD telnet 4444, OpenOCD TCL 6666) is preserved exactly as before. `lager python` scripts that resolve a debug net via `Net.get(name, NetType.Debug).connect()` now share the same slot pool as the HTTP debug service, so concurrent scripts no longer collide on slot-0 ports. * **`lager nets add --openocd-config ` and an `openocd_config` field on `nets add-batch`.** Parallels the existing `--jlink-script` flag — the user's `.cfg` is stored on the saved net and materialised on the box before each `openocd` spawn. Required for FT4232H, supported on every other adapter as an escape hatch for vendor-supplied configs. * **`lager nets set-script` / `show-script` / `remove-script` now work for both backends.** The script-routing trio is backend-agnostic: it detects the target backend from the probe VID + the file's extension and content, and writes to the right slot on the saved net (`jlink_script` for J-Link probes, `openocd_config` for OpenOCD). Pass `--backend jlink|openocd` to override; ambiguous cases are refused with a clear hint instead of silently guessing. `SCRIPT_PATH='-'` reads from stdin. A debug net carries either field but never both, and any switch clears the other slot with a yellow stderr notice. * **`--jlink-version ` on `setup_and_deploy_box.sh`.** Pin the J-Link tools version installed on a new Lager Box at deploy time, instead of taking whatever Segger ships at the moment of the box build. The deployment options table in the README is also corrected to match the current flag set. * **Documentation: ST-Link, RP2040, and FTDI listed under Debug & Flashing**, the OpenOCD RTT `chunk_size` knob is documented in the `lager nets` reference, and `lager nets` documents the new `--openocd-config` flag and the unified `set-script` / `show-script` / `remove-script` commands. ## Bug Fixes * **The Net Manager TUI no longer lets you assign two roles to a single Keithley 2281S (or EA PSB).** A single physical Keithley 2281S can run as a `power-supply` *or* a `battery` but never both — its two firmware entry functions are mutually exclusive. The Add Net wizard's duplicate-detection was checking per-role, so once a `supply` net was saved on a Keithley the wizard kept offering a `battery` row for the *same* VISA address (and vice-versa). The two saved nets fought for the entry function on every command, surfacing as `[Errno 16] Resource busy` — the same hardware constraint that the v0.16.7 known-limitations entry and the v0.16.9 hardware-service shared-session work were spent papering over. The TUI now treats `_SINGLE_CHANNEL_INST` chips as one-net-per-(instrument, address) regardless of role, hiding the second-role row entirely once any role binds the chip; the user-visible message tightens from "Only one net per role may be added per ..." to "Only one net may be added per ...". The same hardening applies to EA PSB (`solar` / `supply`). Direct CLI paths (`lager nets add` / `add-batch`) are unaffected, so power users keep an escape hatch. * **FTDI adapters whose EEPROM was never programmed now work end-to-end.** A FT4232H with no readable USB serial caused a chain of silent failures on the prior release: the UART scanner emitted bare interface indices (`"0"`/`"1"`/`"2"`/`"3"`) into the saved net's `pin` field, and the box-side UART dispatcher then failed at first use with `UART bridge with serial 2 not found`; the debug-probe regex rejected the empty serial slot in the VISA address and silently fell back to J-Link for what was actually an OpenOCD-backed FTDI, so `lager debug gdbserver` came back as the canned "Failed to connect to debugger" checklist with no real cause; and the `nets show` output labelled the overloaded `pin` field as "Channel:" regardless of role, hiding misconfigurations. Fixed across the whole stack: the scanner now matches `/dev/ttyUSB*` paths by sysfs node instead of by USB serial, the legacy `["0","1","2","3"]` static channel fallback is removed, the VISA regex tolerates the empty serial slot, the TUI refuses to persist a UART net with an unprogrammed-EEPROM placeholder pin (with an actionable message pointing at the EEPROM), and `nets show` is now role-aware (`Pin/serial:` for UART, `Device:` for debug). On `lager debug gdbserver` failures the CLI surfaces the box's structured error directly instead of falling back to the generic checklist. * **OpenOCD flash failures are no longer reported as success.** OpenOCD's TCL/RPC channel returns `program ...`'s stdout as plain text even when the underlying flash write or verify failed, so a bad flash looked successful to callers — hence the long-standing "Flashed!" line that didn't actually flash. The box debug service now scans the response for `program_error` markers (`** Programming Failed **`, `** Verify Failed **`, etc.) and any `Error:` lines, and surfaces them up to the CLI. Side effect: `Erase complete!` / `Flashed!` no longer print on rigs whose `target.cfg` declares no flash bank — those calls now fail fast with the underlying error. * **Custom OpenOCD configs uploaded via `lager nets set-script` were silently stored in the wrong slot.** `set-script` previously routed every upload to the `jlink_script` field regardless of which backend the probe used, so OpenOCD configs uploaded that way were ignored at run time. The new backend-detection in `set-script` writes to the correct slot, and the in-box `DebugNet` Python API also picks up `openocd_config` correctly — it was being looked up under the wrong key (`openocd_config_path`) and never decoded to disk, so custom OpenOCD configs had no effect when scripts ran `Net.get(name, NetType.Debug).connect()`. Same shape of bug for `jlink_script` on the in-box API path. * **Custom OpenOCD configs failed to start with "adapter driver is not configured".** Lager was emitting `-c "adapter serial "` and `-c "transport select swd"` *before* `-f `, but those `-c` commands require an adapter driver that only gets set inside the user's cfg, so OpenOCD bailed out before the cfg ever loaded. The user cfg now occupies the same command-line slot the auto-detected interface cfg would, and the auto `transport select` is suppressed when a user cfg is supplied (vendor cfgs almost always call it themselves, and OpenOCD errors on duplicate sets). * **Off-box GDB clients can now reach OpenOCD's gdb / telnet / TCL ports.** OpenOCD ≥ 0.11 defaults `bindto` to `127.0.0.1`, so `docker run -p 2331-2342:2331-2342` forwarded traffic to a listener that wasn't accepting it and clients timed out without an error. OpenOCD now binds all interfaces by default, matching `JLinkGDBServer`. The TCL/RPC channel remains 127.0.0.1-only on the wire because the box-side service drives it locally. * **The Net Manager TUI's "script attached" indicator covers OpenOCD configs.** `has_script` was computed only from `jlink_script`, so debug nets carrying only an `openocd_config` (the new normal for FT4232H rigs) showed no indicator even though one was attached. Now checks both fields. * **Hardened Lager Boxes admit OpenOCD telnet and TCL traffic.** `secure_box_firewall.sh`'s `LAGER_PORTS` allowlist was scoped to the J-Link-only port window, so on a box hardened with this script any external client reaching OpenOCD's telnet (`4444-4447`) or TCL (`6666-6669`) port was silently dropped while J-Link sessions kept working. The allowlist now mirrors the slot pool published by `start_box.sh`. ## Improvements * **`DebugNet.connect()` and `.status()` are now symmetric across J-Link and OpenOCD.** `connect()` accepts `force=False` (restart the daemon if already running) and `ignore_if_connected=False` (return the existing status instead of raising) on both backends. `status()` always returns a dict containing at minimum `running`, `pid`, and `backend` keys regardless of which backend handled the probe — backend-specific extras pass through unchanged, so consumers writing portable code can rely on the three guaranteed fields. * **OpenOCD speed-fallback ladder.** `connect_jlink` already walked `[requested, 4000, 1000, 500, 100]` kHz when the requested speed didn't take; OpenOCD's `adapter speed` is set once at daemon startup with no built-in retry, so a vendor cfg expecting 500 kHz against Lager's 4 MHz default would die silently at the first SWD transaction. The same ladder is now applied at the `DebugNet` layer for the OpenOCD branch. * **VID/PID-based FTDI dispatch.** The original VID-only FTDI mapping fell over the moment a Lager Box had both an FT232H and an FT2232H plugged in. The dispatcher now keys on the full VID/PID pair and refuses ambiguous cases with a hint pointing at `lager nets set-script --backend openocd` for the FT4232H path. * **Cleaner debug-script command surface.** The short-lived `set-openocd-config` / `show-openocd-config` / `remove-openocd-config` aliases (which existed only on this branch and never shipped in a tagged release) are removed in favour of the unified `set-script` / `show-script` / `remove-script` trio with `--backend openocd`. There's nothing to migrate; existing CLI usage is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` After upgrading, run `lager update --box ` on each existing Lager Box to deploy the OpenOCD backend, the new firewall ports, and the matching box-side code. To use the new DA1469x OpenOCD flash path, drop the `flash_loader.elf` and `flash_loader.elf.bin` pair into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` on the box. ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.0/) # Version 0.19.1 Source: https://docs.lagerdata.com/source/release-notes/v0.19.1 May 25, 2026 ## Bug Fixes * **`lager debug ... flash` and the DA1469x flash loader now quote the firmware path before handing it to OpenOCD.** OpenOCD parses its TCL commands word-by-word, so a `program` or `load_image` argument with a space in it was being chopped into two TCL words and the underlying flash op either failed loudly with `wrong # args` or hit the wrong file. In practice the path comes from `tempfile.NamedTemporaryFile()` or the fixed `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` tree (no spaces), so the bug never bit in normal operation; the fix is defensive and aligns `box/lager/debug/openocd.py`'s `OpenOcdRpc.program()` and `OpenOcdRpc.load_image()` with the existing quoting pattern in `OpenOcdRpc.rtt_setup()`. Notable for operators who relocate the flash-loader tree via `LAGER_FLASH_LOADERS_DIR=/path/with spaces/`. * **DA1469x flash\_loader ELF parser now reports a clear error on a truncated symbol-table name instead of a Python `ValueError` traceback.** `box/lager/debug/da1469x_loader.py`'s ELF32 symbol walker used `bytes.index(b'\x00', ...)` to locate the null terminator for each name in the string table, which raised an unwrapped `ValueError` if the strtab itself was truncated. Switched to `bytes.find()` with an explicit error message that names the offending offset; `_resolve_loader_symbols()` still rewraps it as `Da1469xLoaderError` so the call site error type is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.1/) # Version 0.19.2 Source: https://docs.lagerdata.com/source/release-notes/v0.19.2 May 25, 2026 ## Features * **`--ip` now accepts DNS hostnames in addition to IP addresses** on `lager boxes add`, `lager boxes edit`, `lager install`, and `lager uninstall`. Lets a Lager Box sit behind a DNS name (e.g. `box.example.com`) or a Tailscale MagicDNS short name (e.g. `box-1.tailXYZ.ts.net`) instead of requiring the operator to look up and pin a numeric address. Validation is purely syntactic — IPv4/IPv6 (incl. Tailscale `100.x.x.x`) take the existing `ipaddress.ip_address` fast path; everything else is checked against RFC 1123 hostname rules (1–63 char alphanumeric/hyphen labels, ≤253 chars total, single-label allowed for MagicDNS), with actual resolution deferred to SSH/HTTP. The shared validator lives in the new `cli/address_utils.py` (covered by 34 unit tests in `test/unit/cli/test_address_utils.py`); the four call sites all share one error path that prints a "Valid formats:" cheatsheet on failure (`install` / `uninstall` previously printed only the bare error). Inputs that already carry a scheme, port, or path (e.g. `http://...`, `host:5000`, `host/api`) are rejected with a specific message instead of the previous generic "not a valid IP" — the rest of the CLI composes `http://{addr}:port/...` itself, so an embedded one of those would conflict. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.2/) # Version 0.2.17 Source: https://docs.lagerdata.com/source/release-notes/v0.2.17 November 21, 2025 ## Features ### Concurrent CLI Commands * Enabled concurrent CLI commands while supply TUI is running * Improved multi-tasking capabilities with TUI interfaces ## Bug Fixes * Fixed `--line-ending` flag in UART WebSocket client * UART communication patches and improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.17 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.17/) # Version 0.2.18 Source: https://docs.lagerdata.com/source/release-notes/v0.2.18 November 24, 2025 ## Features ### Automatic Security Configuration * Added automatic security configuration to `lager update` * Enhanced Lager Box security during update process * Automated firewall and security settings ## Improvements * Updated Keysight E36300 device support * Various testing and stability improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.18 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.18/) # Version 0.2.19 Source: https://docs.lagerdata.com/source/release-notes/v0.2.19 November 24, 2025 ## Features ### Lager Binaries Command * Added `lager binaries` command for managing binary files * Enhanced binary file handling capabilities ## Bug Fixes * Fixed `lager update` functionality ## Improvements * Added progress bar to `lager update` for better visibility * Added verbose flag to `lager update` for detailed output * Proper firewall configuration during updates * Cleaned up UART implementation ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.19 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.19/) # Version 0.2.20 Source: https://docs.lagerdata.com/source/release-notes/v0.2.20 November 26, 2025 ## Features ### Python Script Enhancements * Nets now working inside `lager python` scripts * Enhanced Python script execution environment * Added support for UART in Python scripts ### Oscilloscope Web UI * Added HTTP server for oscilloscope web UI on port 8080 * Added oscilloscope-streamer with WebSocket support * Exposed oscilloscope visualization interface ## Bug Fixes * Fixed `python --add-file` command functionality * Fixed `lager update` udev rules path * Fixed LabJack device open timeout causing indefinite hangs ## Improvements * Improved Lager Box deployment scripts * Enhanced `lager update` to include all necessary files in sparse checkout * Moved oscilloscope-streamer to gateway/oscilloscope-daemon/ * Code cleanup and organization improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.20 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.20/) # Version 0.2.21 Source: https://docs.lagerdata.com/source/release-notes/v0.2.21 December 2, 2025 ## Features ### Phidget Thermocouple Expansion * Phidget thermocouple now supports 4 channels (previously limited to fewer channels) * Enhanced multi-channel temperature measurement capabilities ### Keysight Python Support * Added Keysight device support to `lager python` scripts * Enabled Keysight instruments in Python execution environment ## Improvements * Added dependencies for BLE test suite integration * Enhanced `lager update` to include BLE test dependencies ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.21 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.21/) # Version 0.2.22 Source: https://docs.lagerdata.com/source/release-notes/v0.2.22 December 2, 2025 ## Features ### Hardware Invocation Service * Added hardware invocation service for Device proxy pattern * Enabled remote device method calls through proxy interface * Improved hardware abstraction layer ### Keysight Python Support * Added Keysight power supply support to `lager python` scripts * Enhanced Python script execution with Keysight devices ## Bug Fixes * Fixed hardware service to extract low-level device from SupplyNet wrappers * Fixed hardware service: added import paths for supply/battery/eload modules * Fixed hardware service: handle unhashable types in net\_info cache key * Fixed PowerSupply Net initialization in get\_from\_saved\_json * Fixed Net class: changed self.net.channel to self.channel ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.22 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.22/) # Version 0.2.23 Source: https://docs.lagerdata.com/source/release-notes/v0.2.23 December 2, 2025 ## Bug Fixes * Fixed multi-channel USB resource sharing for Keysight devices * Fixed Keysight device compatibility issues ## Improvements * Enhanced Keysight device support * Improved resource management for multi-channel instruments ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.23 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.23/) # Version 0.2.24 Source: https://docs.lagerdata.com/source/release-notes/v0.2.24 December 4, 2025 ## Features ### New Command * Added `lager boxes add-all` command for bulk Lager Box management ## Bug Fixes * Fixed UART data corruption issues * Fixed NetType.Analog handling for Rigol oscilloscopes in get\_from\_saved\_json ## Improvements * Updated CLI commands for better usability * Enhanced oscilloscope integration ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.24 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.24/) # Version 0.2.25 Source: https://docs.lagerdata.com/source/release-notes/v0.2.25 December 4, 2025 ## Improvements * Updated `lager devenv` command functionality * Enhanced development environment setup ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.25 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.25/) # Version 0.2.26 Source: https://docs.lagerdata.com/source/release-notes/v0.2.26 December 5, 2025 ## Features ### Webcam Improvements * Improved webcam interface with enhanced controls * Reduced zoom latency for better responsiveness * Cleaned up webcam sidebar interface ## Bug Fixes * Fixed `lager exec` command * Fixed net.py module functionality * Added usb\_net\_wrapper.py for better USB network handling ## Improvements * Enhanced webcam user experience * Streamlined webcam code organization * Optimized zoom operations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.26 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.26/) # Version 0.2.27 Source: https://docs.lagerdata.com/source/release-notes/v0.2.27 December 5, 2025 ## Improvements * Internal maintenance release * Minor bug fixes and stability improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.27 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.27/) # Version 0.2.28 Source: https://docs.lagerdata.com/source/release-notes/v0.2.28 December 5, 2025 ## Bug Fixes * Fixed `lager exec` command execution ## Improvements * Updated exec command functionality * Enhanced remote execution capabilities ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.28 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.28/) # Version 0.2.29 Source: https://docs.lagerdata.com/source/release-notes/v0.2.29 December 6, 2025 ## Bug Fixes * Fixed `lager python download` command functionality * Fixed `lager exec` command execution * Fixed `lager devenv` environment setup ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.29 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.29/) # Version 0.2.30 Source: https://docs.lagerdata.com/source/release-notes/v0.2.30 December 8, 2025 ## Features ### MCC USB-202 DAQ Support * Added support for Measurement Computing USB-202 DAQ device * Implemented ADC, DAC, and GPIO functionality for USB-202 * Added USB-202 to instrument detection system * Enabled USB-202 configuration through lager nets ## Bug Fixes * Fixed USB-202 VISA address parsing * Fixed USB-202 GPIO toggle functionality * Updated USB-202 channel naming for consistency ## Improvements * Improved `lager update` interface and performance * Streamlined update process for faster execution * Cleaned up GPIO interface code * Enhanced update interface user experience ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.30 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.30/) # Version 0.2.31 Source: https://docs.lagerdata.com/source/release-notes/v0.2.31 December 10, 2025 ## Features ### Oscilloscope Support * Added support for PicoScope and Rigol oscilloscopes * Implemented voltage measurements for oscilloscopes * Added cursor measurement modes and autoscale functionality ## Bug Fixes * Fixed critical bug in Device proxy: properly handle serialized enum dictionaries * Fixed critical channel parameter bug when channel=None in Device proxy wrapper * Fixed autoscale infinite recursion bug in RigolMSO5000 mapper * Fixed asyncio deprecation warning in PicoScope commands * Fixed oscilloscope measurement channel parameter bugs * Fixed cursor timeout issues in oscilloscope operations * Fixed trigger validation in oscilloscope commands * Fixed mux.connect() error in oscilloscope interface ## Improvements * Removed accidentally committed Rust build artifacts from repository * Added missing clear\_measurement and disable\_cursor\_measure\_mode methods to RigolMso5000 * Improved get\_measure\_item error handling with better logging * Enhanced oscilloscope command reliability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.31 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.31/) # Version 0.2.32 Source: https://docs.lagerdata.com/source/release-notes/v0.2.32 December 11, 2025 ## Features ### J-Link Debugger Integration * J-Link debugger installation now automated during Lager Box deployment * J-Link automatically installed and configured during `lager update` operations * Improved debug workflow reliability across all Lager Boxes ### Flexible Deployment * Lager Boxes can now be deployed with custom usernames instead of requiring `lagerdata` username * Enhanced deployment flexibility for different organizational setups ## Bug Fixes * Fixed webcam documentation links in Mintlify docs ## Improvements * Cleaned up webcam functionality and code organization * Streamlined deployment process with better error handling * Reorganized deployment documentation for improved clarity * Enhanced J-Link integration stability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.32 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.32/) # Version 0.2.33 Source: https://docs.lagerdata.com/source/release-notes/v0.2.33 December 15, 2025 ## Features ### Improved Hello Command * `lager hello` now displays the actual Lager Box hostname instead of the Docker container ID * Format changed from "Hello from DUT " to "Hello from ()" * Mounted host's `/etc/hostname` file into container for hostname access ### Documentation * Added Release Notes section to Mintlify documentation * Created release notes pages for versions v0.2.32 through v0.2.17 * Updated RELEASE\_PROCESS.md with comprehensive release notes instructions ## Bug Fixes * Fixed eload Net API and multi-channel USB caching issues * Fixed cache clearing to use inline requests instead of test\_utils import * Fixed Keithley mapper voltage handling when output is disabled * Fixed OVP conflicts in voltage tests by raising OVP to 10V * Fixed Keithley enable state issues with proper state reset ## Improvements ### Lager Update Enhancements * Reduced password prompts to one input per `lager update` * Improved third\_party directory mounting to container * Streamlined update process with better error handling * Cleaned up update code and output ### Testing & Caching * Auto-clear hardware service cache after `lager python` scripts finish * Added cache clearing utilities to all test files * Increased voltage settling time from 0.3s to 1.0s for more accurate readings * Removed unnecessary clear cache terminal output ### Power Supply & E-Load API * Updated Python API for power supplies and e-loads * Fixed multi-channel USB resource caching * Improved Keithley device handling ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.33 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.33/) # Version 0.2.35 Source: https://docs.lagerdata.com/source/release-notes/v0.2.35 December 17, 2025 ## Features ### Python API Function Renaming * Renamed 14 Python API functions for improved clarity and consistency * Updated documentation to reflect new function names ## Bug Fixes ### ARM/Robot API * Fixed serial port hangs by adding proper timeout handling * Fixed position polling with buffer clearing and reduced frequency * Fixed CLI to close serial port after commands complete * Fixed movement commands to not wait for 'ok' response * Removed problematic reset\_input\_buffer() calls that caused hangs ### Battery API * Fixed Battery API mapper to properly delegate to Keithley methods * Fixed device name conflict issues * Fixed class alias placement ### Other Fixes * Fixed Webcam API * Fixed `lager update` command * Fixed GDB `read_memory` response parsing ## Improvements ### Code Cleanup * Removed unused modules and deprecated code * Updated documentation and Python API ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.35 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.35/) # Version 0.2.36 Source: https://docs.lagerdata.com/source/release-notes/v0.2.36 December 18, 2025 ## Breaking Changes ### Terminology Restructure: Gateway/DUT to Box This release standardizes terminology across the entire codebase, replacing "gateway" and "DUT" (Device Under Test) with the unified term "box". **CLI Changes:** * The `--dut` option is now `--box` (hidden alias kept for backward compatibility) * The `--gateway` option is now `--box` where applicable * All help text and error messages now use "box" terminology **Configuration Changes:** * The `DUTS` key in `.lager` configuration files is now `BOXES` (backward compatible - old format is still read) * Box storage functions renamed (e.g., `load_duts()` → `load_boxes()`) **Directory Structure:** * `gateway/` directory renamed to `box/` * `gateway/lager/lager/` flattened to `box/lager/` * `gateway_http_server.py` renamed to `box_http_server.py` * `start_lager.sh` renamed to `start_box.sh` * Deployment scripts renamed: * `setup_and_deploy_gateway.sh` → `setup_and_deploy_box.sh` * `secure_gateway_firewall.sh` → `secure_box_firewall.sh` * `verify_gateway_security.sh` → `verify_box_security.sh` **Migration:** * Existing `.lager` configuration files will continue to work * The `--dut` CLI option works as a hidden alias for `--box` * Update any scripts or automation to use the new `--box` option ## Features ### Unified Box Terminology * Consistent "box" terminology throughout CLI, Python API, and documentation * Simplified mental model for users - one term for all hardware targets * Cleaner codebase with consistent naming conventions ### Flattened Directory Structure * Removed redundant `gateway/lager/lager/` nesting * More intuitive project navigation * Cleaner import paths ## Improvements ### Documentation * All documentation updated with "box" terminology * Unified help text and error messages * Updated training data for AI assistants ### Code Quality * Removed deprecated modules and unused code * Standardized naming conventions throughout codebase * Improved code organization and readability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.36 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Migration Guide ### For CLI Users Replace `--dut` with `--box` in your commands: ```bash theme={null} # Before lager hello --dut my-device lager supply voltage --dut my-device 3.3 # After lager hello --box my-device lager supply voltage --box my-device 3.3 ``` ### For Script Authors Update any automation scripts to use the new flag names: ```bash theme={null} # Before BOX_NAME="my-device" lager hello --dut $BOX_NAME # After BOX_NAME="my-device" lager hello --box $BOX_NAME ``` ### For Box Administrators Update deployment commands: ```bash theme={null} # Before cd deployment ./setup_and_deploy_gateway.sh # After cd deployment ./setup_and_deploy_box.sh ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.36/) # Version 0.20.0 Source: https://docs.lagerdata.com/source/release-notes/v0.20.0 May 27, 2026 This release is a direct response to the 2026-05-26 "battery net not responding" incident on a Keithley 2281S, where root-causing one EBUSY took \~2 hours across `lsof`, `dmesg`, bare `pyvisa` probes, and hardware-service introspection. The biggest items below — `lager diagnose`, the `usbtmc` blacklist, automatic ENODEV recovery, and cross-process device locks — collectively eliminate the most common failure modes that drove that session, and surface the rest (e.g. wedged instrument firmware that only mains-power-cycling can fix) with a single one-line diagnosis. ## Features * **`lager diagnose --box [--type ]` — single-shot net diagnosis.** Polls three box-side endpoints in parallel (USB enumeration + USB-TMC interface-class detection + holder detection + `dmesg` + `lsmod` for usbtmc, bare `pyvisa` `*IDN?` probe, hardware-service in-process session cache) and classifies the net into one actionable bucket with the next step the user should take: `HOST-SIDE: usbtmc kernel module loaded` (→ `lager box update`), `HOST-SIDE: USB device claimed by multiple processes` (→ names the PIDs), `HOST-SIDE: USB device busy`, `TRANSIENT: device disappeared from USB`, `TRANSIENT: device enumerated as USB-TMC but pyvisa probe couldn't reach it` (→ stale libusb context recovery hint), `INSTRUMENT WEDGED` (→ mains-side power-cycle), `NOT ENUMERATED`, `NOT USB-TMC` (LabJack/Picoscope/Acroname use vendor SDKs), or `HEALTHY` (with the IDN string). `--type` is auto-detected from the box's saved nets if omitted. Backwards-compatible against pre-0.20 boxes (per-endpoint 404 fallbacks). * **`usbtmc` kernel-module blacklist shipped with the box image** at `/etc/modprobe.d/blacklist-usbtmc.conf`. Without this, the kernel auto-binds the `usbtmc` driver to USB-TMC-class instruments (Keithley 2281S, Keysight, Rigol scopes) and claims interface 0; pyvisa-py's libusb backend then can't `set_configuration()` and returns `[Errno 16] Resource busy`. The blacklist is the only durable fix. Deployed by `setup_and_deploy_box.sh` (new boxes) and refreshed by `lager box update` (existing boxes). * **Cross-process device locks for USB-TMC drivers** via the new `lager.util.device_lock` module. Generalizes the long-standing EA-solar/supply `DeviceLockManager` pattern (`fcntl.flock` on a lockfile keyed by VISA address) and adopts it in the Keithley battery + supply, Rigol DP800, Rigol DL3021 eload, Keysight E36000, and Rigol MSO5000 scope drivers. Guards against a second box-side `pyvisa` client racing the hardware service for the libusb interface-0 claim. Fails open if the locking infrastructure itself errors, so a transient filesystem hiccup can't take legitimate work offline. * **Version-skew warning** prints once per CLI session to stderr when the CLI's minor version is ahead of the box's by one or more. The 2026-05-26 session started with a 0.19.2 CLI talking to a 0.18.3 box and the first error was opaque — this single line would have cut diagnosis time by hours. Cached per-process by box IP; fails open on any error so a flaky network can never break a working command. * **Actionable error messages for `[Errno 16/19/110]`** in `lager battery` and `lager supply` commands. Errno 16 EBUSY → "USB device busy — another process holds the libusb interface" with a `Try: lager diagnose ` hint. Errno 19 ENODEV → "Instrument disappeared from USB (re-enumeration)" with a `Hw service should auto-recover; if not: sudo docker restart lager` hint. Errno 110 ETIMEDOUT → "Instrument did not respond to SCPI — firmware may be wedged" with a "mains-side power-cycle required" hint. Raw error remains available via `LAGER_DEBUG=1`. * **`lager update` verbose status block now includes `modprobe.d:`** alongside the existing `udev rules:` line. * **`lager diagnose` command-specific docs** at `docs/diagnose.md` covering the three endpoints, the classification decision tree, sample sessions for each bucket, and the `--type` semantics. ## Bug Fixes * **`lager battery ` and `lager supply ` no longer return `[Errno 19] No such device` until `docker restart lager`** after a USB re-enumeration of the instrument (mains power-cycle, accidental unplug, USB hub port toggle). The hardware-service retry path was gated on a keyword tuple that did not match libusb's ENODEV signature — the existing retry never fired. The tuple is extended, a dedicated `_is_enodev_error()` helper is added, and on ENODEV the `/invoke` retry now evicts every sibling `device_cache` entry on the same VISA address and force-closes the shared `pyvisa` session pool entry. Live-verified on a Keithley 2281S via a USB driver unbind/bind sequence. * **`lager diagnose` host-side holder detection now works on the actual box image.** The original `/diagnose/usb` endpoint shelled out to `sudo lsof /dev/bus/usb/` to find competing libusb claims, but neither `sudo` nor `lsof` ship in the lager container; the subprocess silently exited 127 and the endpoint always returned `lsof: []`. As a result the `HOST-SIDE: USB device claimed by multiple processes` and `HOST-SIDE: USB device busy` classifications could never fire in production. Replaced with a `/proc/*/fd/*` walk that reads `/proc//comm` for the process name. No external tools, no permission gymnastics. * **`lager diagnose` classifier no longer misclassifies a healthy USB-TMC instrument as `NOT USB-TMC`** when pyvisa's fresh-probe path can't reach it (most common cause: a stale libusb context inside `box_http_server` after a USB re-enumeration; hw\_service runs in a separate process and recovers transparently). `/diagnose/usb` now reads the device's sysfs interface descriptors and surfaces `is_usbtmc` for USB-TMC class 0xFE / subclass 0x03 devices. The classifier disambiguates: enumerated USB-TMC + fresh-probe failure → new `TRANSIENT` bucket with a concrete recovery hint; enumerated non-USB-TMC → existing `NOT USB-TMC` hint preserved. * **`lager diagnose` VISA-side error mapping catches all three libusb "device not reachable" message variants.** pyvisa-py emits `[Errno 19] No such device` (libusb's standard ENODEV after a re-enumeration), `[Errno 2] Entity not found` (authorized=0 or denied open), and `No device found.` (generic vendor-not-matched-or-stale path). All three now map to `error_class: nodev` so the classifier consistently returns `TRANSIENT` instead of falling through to `UNCLEAR`. * **`lager diagnose` VISA section renders all five fields on endpoint-returned errors.** The pre-fix renderer short-circuited on any `error` key in the dict, collapsing the section to a single `error:` line and dropping the `error_class` and `elapsed_ms` context the user needs to interpret the failure. * **`lager diagnose` prints an actionable message when the box is unreachable** instead of wrapping the raw urllib3 traceback. Now reads `Box '' unreachable at :5000 (connection refused). The lager container may be stopped. Check with: lager ssh --box -- "sudo docker ps"`. Connection-refused and timeout cases are tailored separately. * **`/diagnose/visa` correctly consults hw\_service's session pool across processes.** `box_http_server` (port 9000) and `hardware_service` (port 8080) are separate processes; the original implementation imported `_visa_resources` from `lager.hardware_service` and saw its own empty copy of the dict rather than hw\_service's live state. The fresh probe then always ran and hit EBUSY on healthy boxes with a cached session. Now consulted via HTTP at `localhost:8080/diagnose/dispatcher`. * **`device_lock` no longer truncates the lock file before acquiring.** The pre-fix `open(path, 'w')` erased the existing holder's PID at open time, leaving the file empty under contention even when our own acquire later timed out. Now opens via `os.open(O_RDWR|O_CREAT)` and only truncates + writes the PID after a successful flock acquisition. * **`_dmesg_usb_tail` is robust against missing passwordless sudo.** The pre-fix shell pipeline used `sudo dmesg` (could hang on password prompt), `2>&1 | grep` (merged stderr into stdout where grep filtered it), and a final `tail` (whose rc masked upstream failures). Now uses `sudo -n dmesg` (fails fast on password prompt), does the filtering in Python, and the rc reflects what actually happened. * **`lager update` Step 5b (new) re-detects the `modprobe_d/` source dir post-pull.** The update probe runs before the `git pull`; on the very first deploy that introduces the directory, the pre-pull probe correctly reports the source path empty and the install step would short-circuit. Re-detects via a fresh SSH round-trip if the pre-pull probe came up empty. ## Improvements * **TUI WebSocket-failure messages call out the specific next step** instead of `WebSocket connection failed: Failed to connect to WebSocket server`. `lager battery tui` and `lager supply tui` now probe `http://:9000/health` on connect failure and emit one of four actionable messages depending on the response (box reachable but pre-0.20, services partially up, connect-timeout via Tailscale, container not running). Original WS error preserved in parentheses. * **Documented "TUIs are laptop-only"** in `box/lager/README.md`. Running TUIs directly on the box was the suspected culprit of that incident (a second `pyvisa-py` client competing with hardware-service for interface 0). The OS-level `device_lock` makes this case detect-and-fail-clean instead of silent EBUSY, but the right answer is still to launch TUIs from the laptop CLI. * **`lager diagnose` output labels clarified.** The header line reads `NetType: ` instead of `resolved role: ` to align with terminology elsewhere in the CLI. The USB section prints `usb-tmc class: yes/no` (newly surfaced from `/diagnose/usb`) so the user can see whether the classifier is treating the device as USB-TMC. The existing kernel-module-status line is renamed from the ambiguous `usbtmc:` to `usbtmc kmod:` so the two related fields are visually distinct. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.20.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.20.0/) # Version 0.20.1 Source: https://docs.lagerdata.com/source/release-notes/v0.20.1 May 27, 2026 ## Features * **New `--force` option on `lager update`.** Re-runs an update even when Lager thinks your box is already up to date, and rebuilds the box cleanly from scratch. Reach for this if a previous update didn't finish and the box is acting strangely — `lager update --box --force`. ## Bug Fixes * **Updates no longer hang or fail on some networks.** On certain setups a box couldn't reach GitHub while updating, which made `lager update` appear to freeze for around 15 minutes and then fail. Boxes now connect reliably during an update, and a brief network hiccup is retried automatically instead of stopping the whole update. ## Improvements * **Updating a box is much faster — about 30 seconds instead of \~15 minutes.** Lager now reuses the work from your last update instead of rebuilding everything from scratch each time. (A longer update still happens when the box's software dependencies actually change.) * **Use `lager update` to update a box.** This is now the one command to remember. The older `lager box update` has been removed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.20.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.20.1/) # Version 0.21.0 Source: https://docs.lagerdata.com/source/release-notes/v0.21.0 May 28, 2026 ## Features * **Pause a running script with `lager.pause()`.** Drop `lager.pause("why")` anywhere in a `lager python` script and it stops at that line mid-run, so you can check the bench from another terminal before it continues — useful for a long test that reaches a known trouble spot. A paused script doesn't lock the box, so your other `lager` commands (read a supply, toggle a GPIO, check a net) keep working while it waits. * **Resume however suits you.** Press **Enter** in the script's terminal, run **`lager python --continue --box `** from anywhere, or just walk away — it auto-resumes after 5 minutes by default so an unattended run never hangs. The pause prints the `id` and the exact resume commands. * **Inspect the paused script with a live Python console.** Add `pause(interactive=True)` and connect with **`lager python --console --box `** to get a Python prompt running inside the paused script — read any of its variables, evaluate expressions, or call its functions. This is also how you read a device the script is holding open (e.g. a LabJack), since the console runs in the same process. ## Improvements * **The built-in `breakpoint()` now works in `lager python` scripts.** It previously errored out; calling `breakpoint()` now triggers the same interactive pause as `lager.pause()`. * **Tune or disable the auto-resume.** Set a longer (or shorter) wait with `pause("...", timeout=1800)` or `lager python ... --env LAGER_BREAKPOINT_TIMEOUT=1800`; use `timeout=0` to wait indefinitely; set `LAGER_BREAKPOINTS=off` to turn every breakpoint into a no-op for a clean run. See the [Breakpoints guide](/source/reference/python/breakpoints) for the full reference and a worked example. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.0/) # Version 0.21.1 Source: https://docs.lagerdata.com/source/release-notes/v0.21.1 May 29, 2026 ## Improvements * **Help that actually shows you what to type.** Every net command now displays the real `lager [NET_NAME] [COMMAND] --box [BOX_NAME]` usage pattern instead of a generic placeholder, and each one comes with copy-pasteable examples. `lager --help` is now grouped into sections instead of one long alphabetical list, so it's far easier to find the command you want. * **Clearer errors that tell you the fix.** When something goes wrong — a Box you can't reach, a bad config, an SSH or login failure, an instrument that's busy or unplugged, or a command missing its net name — Lager now prints a short message describing the problem and what to do about it, instead of a raw Python traceback. (Need the full technical detail? Re-run with `--debug`.) ## Bug Fixes * **Fixed a couple of rough edges** in the command-line tooling: a broken internal entry point and an incorrect "defaults set" hint. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.1/) # Version 0.21.2 Source: https://docs.lagerdata.com/source/release-notes/v0.21.2 May 29, 2026 ## Bug Fixes * **Fixed erratic input in `lager nets tui`.** Since 0.21.0, the interactive Net-Manager TUI could drop keystrokes and feel unresponsive — navigation and the rename/edit dialogs would lag or miss input. The TUI now correctly opts out of the `lager.pause()` stdin watcher that 0.21.0 added, so it no longer competes with the terminal for your keypresses. Breakpoint resume in `lager python` is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.2/) # Version 0.21.3 Source: https://docs.lagerdata.com/source/release-notes/v0.21.3 May 29, 2026 ## Bug Fixes * **Fixed erratic input in `lager supply tui` and `lager battery tui`.** This completes the 0.21.2 fix. Since 0.21.0, the `lager.pause()` breakpoint feature added a background thread that watches `stdin` so you can press Enter to resume a paused `lager python` script. 0.21.2 stopped `lager nets tui` from competing with that thread, but the power TUIs (and the `lager supply`/`lager battery`/`lager arm` confirmation prompts) hit the same problem through a different path — validating the net before launch left a stray `stdin` reader that then stole keypresses. Both TUIs now feel responsive again, and `y`/Enter confirmations are no longer intermittently swallowed. * **Hardened the breakpoint watcher against future regressions.** The stdin watcher now only starts for a genuine interactive foreground run, so any command that captures script output internally can no longer leak a competing reader. Breakpoint resume in `lager python` is unchanged, including when piping output (e.g. `lager python script.py | tee log.txt`). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.3/) # Version 0.22.0 Source: https://docs.lagerdata.com/source/release-notes/v0.22.0 June 1, 2026 ## Changes * **Pin to a release with its tag.** `lager update --version X.Y.Z` and `lager install --version X.Y.Z` now resolve a release number to the matching `vX.Y.Z` tag instead of a same-named git branch. You can pass the bare number (`0.21.3`) or the tag form (`v0.21.3`) — both resolve to the tag, including pre-releases like `v0.22.0-rc1`. Branch targets such as `main`, `staging`, or a feature branch are unchanged and still pin to that branch. Existing `--version X.Y.Z` pins keep working unchanged. ## Bug Fixes * **Tag pins fetch reliably on every box.** Updating to a tag now fetches it with an explicit refspec so the tag is created as a local ref on the box. Previously, on a box that didn't already have the tag, `lager update --check` could report "update state unknown" and the checkout could fail. ## Deprecations * **Per-release version branches are deprecated.** Releases no longer publish a `X.Y.Z` branch alongside the `vX.Y.Z` tag — the tag is the single source of truth for pinning. Pin with the tag going forward. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.0/) # Version 0.22.1 Source: https://docs.lagerdata.com/source/release-notes/v0.22.1 June 2, 2026 ## Improvements * **Standardized Lager Box references to placeholders across the repository.** All `--help` output, command docstrings, source comments, the CHANGELOG, release notes, and documentation now use the `` placeholder (and ``) for box names and addresses; test fixtures use a neutral `test-box` token. This is a documentation/metadata change only — no functional or API changes. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.1/) # Version 0.22.2 Source: https://docs.lagerdata.com/source/release-notes/v0.22.2 June 3, 2026 ## Bug Fixes * **Multi-output power supplies now apply every command to the selected channel.** On Keysight E363xx and Rigol DP800 series supplies, the channels of a single instrument share one USB session, and the shared driver stayed bound to whichever channel was opened first. Commands that don't name a channel — setting voltage or current, enabling/disabling the output, and reading state — were applied to that first channel instead of the one you selected. On the Keysight E36312A this looked like a limits problem: a voltage setpoint above 6V on CH2 or CH3 (25V channels) was rejected, because the write was actually reaching CH1 (6V max). Each command now targets the correct channel. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.2/) # Version 0.23.0 Source: https://docs.lagerdata.com/source/release-notes/v0.23.0 June 4, 2026 ## Features * **`lager box config udev add/list/remove` — add your own USB device rules.** Grant a USB device read/write access from inside the Lager Box container by vid:pid, e.g. `lager box config udev add 1209:0001 --box ` followed by `lager box config apply`. This fixes the common case where a freshly-plugged device is owned by root, so tools like `dfu-util` fail to open it ("No DFU capable USB device available"). Pass `--usbtmc` for SCPI/USBTMC instruments to also unbind the kernel `usbtmc` driver (needed for PyVISA/libusb access). Rules persist in the box config and are installed on the host on every `apply` — no more waiting for a new release to support a device. * **`lager box config reset` — erase the box config to empty.** A single command that clears the config to a clean slate (unlike `init`, which seeds the default `box-tools` volume). Pass `--apply` to erase **and** restart the container in one step — handy before a test run. * **`lager box config restart` — restart the container without changing config.** Brings up a fresh container with the same configuration, useful for per-test isolation. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.23.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.23.0/) # Version 0.24.0 Source: https://docs.lagerdata.com/source/release-notes/v0.24.0 June 5, 2026 ## Features * **DUT context for AI agents — the MCP server now understands the board, not just the bench.** New `DUTContext`, `SubSystem`, and `DocRef` models capture a device-under-test's purpose, summary, MCU, key peripherals, and schematic/datasheet references (by URL or synced `repo_path`). New `discover_dut()` and `cite_schematic()` tools and `lager://dut/overview.md` resources surface it, and `discover_bench(net)` now returns the parent subsystem and relevant doc refs. Schematics are referenced, never stored — the agent fetches and analyzes them with its own tools. * **`lager box dut show | edit | add-doc`** — author and inspect DUT context (subsystems, documentation references) stored in the Lager Box's `bench.json`. * **`DebugNet.session()` and `DebugNet.rtt_defmt(elf=...)`.** `session()` is a context manager that scopes connect-on-entry and guaranteed teardown so the safe connect/disconnect ordering is encoded once. `rtt_defmt()` opens an RTT session and pipes it through `defmt-print`, yielding decoded log lines instead of raw bytes — so on-box `lager python` tests can assert directly on defmt-encoded firmware logs. `defmt-print` is now bundled in the Lager Box image. * **MCP slash-command prompts** (`write_lager_test`, `explore_bench`, `assess_test_feasibility`) and a new "AI Agents (MCP)" documentation tab with a server overview and DUT-context guide. ## Improvements * **The MCP server is now a read-only discovery & planning surface.** Its purpose is to let an agent learn the bench and DUT well enough to write and run a test; execution happens in the test script via `lager python … --box `. `discover_bench`/`discover_dut` echo the real address the client connected on and return a ready-to-run command, and `discover_bench` now reports instrument channels, capabilities, firmware, and authored specs/ranges. * **Reconnect-aware RTT and self-healing reset/read\_memory on both J-Link and OpenOCD backends.** The RTT reader transparently re-attaches to the same port after a socket drop (it only re-attaches to an already-running server, never starts one), and `reset`/`erase`/`read_memory` reconnect automatically when no server is running — so scripted flash → attach → reset loops no longer thrash. A DA1469x guard avoids auto-starting an unhalted server. * **Simplified agent-facing net metadata to `purpose` / `notes` / `tags`**, replacing the overlapping `description` / `dut_connection` / `test_hints` fields. `lager nets describe` now takes `--purpose` / `--notes` / `--tag`, and the Net Manager TUI edit dialog matches. * **The MCP server auto-reloads bench config on change** (it watches the mtimes of `bench.json`, `saved_nets.json`, and `box_id`), so DUT/net edits are picked up on an agent's next request without a reload call or service restart. It also warns when a subsystem references a net that doesn't exist. ## Bug Fixes * **Debug connect no longer burns its retries when the GDB remote rejects non-stop mode.** JLinkGDBServer rejects `set non-stop on`, which previously exhausted all connect retries and skipped target verification and RTT control-block auto-detection. The connect now detects that specific rejection and retries once with an all-stop controller; OpenOCD keeps non-stop, and any other target error still fails loudly. * **`lager box dut add-doc` / `edit` now save `bench.json` without requiring passwordless sudo**, and no longer raise a `KeyError` on a Lager Box whose `bench.json` has no DUT block. * **`lager box dut edit` and `lager box config edit` now honor `$EDITOR`/`$VISUAL` flags** (e.g. `subl -w`, `code -w`, `vim -p`) by parsing the editor string with `shlex.split()` instead of treating it as a single program name. ## Breaking Changes * **The MCP server no longer exposes hardware I/O or mutation tools.** The `quick_io`, `install_dependency`, `run_python`, `pip`, `logs`, `defaults`, and `binaries` tools, the safety/preflight engine, the `run_lager` CLI passthrough, and the audit subsystem have been removed. Agents now execute tests via `lager python path/to/test.py --box ` rather than over MCP. Per-bench safety constraints are now advisory metadata surfaced in `discover_bench`, not enforced. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.24.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.24.0/) # Version 0.25.0 Source: https://docs.lagerdata.com/source/release-notes/v0.25.0 June 11, 2026 ## Features * **Custom serial-device assignment — RS-232 instruments become first-class.** Instruments the Lager Box cannot identify by USB enumeration (first case: the Rigol DP711 power supply, reached through a generic Prolific USB-serial cable that enumerates as the cable, not the PSU) can now be assigned to their cable once with `lager nets assign`. From then on the instrument scans, nets, and drives exactly like an auto-detected device. * **`lager nets assign`** — `--list` shows assignable devices, current assignments, and unassigned USB-serial cables; `lager nets assign Rigol_DP711 --serial ` (or `--port ` for serial-less clone cables) stores the assignment on the Lager Box, durable across reboots and replugs. `--baud` overrides the catalog default when the instrument's front panel differs; `--as-net [NAME]` creates the net in the same step; `--remove` unassigns. * **Assign Device flow in the Net Manager TUI** — the interactive twin of `nets assign`: pick a cable, pick the instrument (with optional baud override), and name its net in a follow-up dialog. Assignments can be removed from the same screen. * **Rigol DP711 (DP700-series) support** — single-channel RS-232 power supply driver with the DP800-compatible method surface, addressed by a durable `serial://:/serial/` (or `/port/

`) identity that re-resolves to the live tty at open time — surviving tty renumbering, port moves, and replugs — with stale-session self-healing. * **Generic `POST /net/command` endpoint** on the Lager Box HTTP server for Tier-1 instruments (GPIO, ADC, DAC, thermocouple, watt-meter, e-load), giving them the same warm in-process path the supply/battery endpoints use instead of a subprocess per call. The `netCommand` capability is advertised in `/status` only when the route actually registers. ## Improvements * **Nets live and die with their assignment.** Removing (or replacing) a cable assignment deletes the saved nets bound to its address and reports them; pre-existing generic-UART nets on the cable are retired at assign time so a terminal session can never fight the instrument driver for one tty. A baud-only re-assign keeps existing nets. * **The scanner reports assigned instruments, not their cables.** `lager instruments`, the TUI, and the Lager Box's `/instruments/list` show the catalog instrument at its durable `serial://` address; the cable's generic UART record is suppressed while assigned, and assigned ttys are excluded from the Dexarm G-code handshake probe. * **Backend JSON parsing is robust to doubled output for objects, not just arrays** — both the CLI and TUI parsers now take the first complete JSON value, fixing a latent recovery bug for the known "double execution" Lager Box output. ## Bug Fixes * **Manually-added supply and battery nets are driveable again.** `lager nets add`/`delete`/`add-batch` now normalize the legacy role tokens `supply` → `power-supply` and `batt` → `battery`; the short tokens were previously saved verbatim, producing nets that listed fine but that no supply/battery command could drive. The tokens remain accepted as input aliases, `delete` reaches legacy nets saved under either spelling, and channel validation for supplies now actually runs on `nets add`. * **Documentation and error hints no longer reference the nonexistent `lager nets create` family** — renamed to the real `add`/`add-all`/`add-batch` commands across the docs, READMEs, and four runtime error messages, and the documented role vocabulary now matches what saved nets actually carry. * **The DP700 driver reports a missing cable as a device-not-found error** when unplugged mid-session, instead of a raw Python traceback. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.25.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.25.0/) # Version 0.26.0 Source: https://docs.lagerdata.com/source/release-notes/v0.26.0 June 11, 2026 ## Security * **Key authorization is rate limited.** Bad-token attempts against the Lager Box `/authorize-key` endpoint are limited per client IP (5 attempts per 60-second window, then HTTP 429); the window resets on a successful authorization. * **The Lager Box web service `SECRET_KEY` persists across restarts.** Generated once and stored at `/etc/lager/secret_key` (mode 0600) instead of regenerated on every boot, so sessions survive container restarts. * **`org_secrets.json` is held at owner-only permissions.** The on-box secrets file is tightened to mode 0600 at load time, and the boot-time permission fix is best-effort so an unexpected owner can no longer abort container startup. * **Instrument device nodes are scoped to a dedicated `lager` group.** The shipped udev rules grant `MODE="0660", GROUP="lager"` instead of world-writable 0666; `lager update` creates the group on the Lager Box host when missing and the container joins it automatically. User-added udev rules (`lager box config udev add`) default to the same scoping — run `lager update` on a Lager Box before applying new user udev rules so the group exists. ## Features * **Per-connect J-Link script override — `DebugNet.connect(script=...)`.** Pass a path on the Lager Box or a base64 blob to swap the J-Link script for one session phase (for example, a halt-in-place reset script for a memory read-back, then the stock script to reboot the target). The bytes are copied to the shared script path so `flash`/`reset`/`read_memory` pick the new script up immediately; an already-running gdbserver adopts it on relaunch (`force=True`). Invalid input is ignored and the net's saved script stays in effect. * **Opt-in cache-coherent post-program verify for DA1469x QSPI images (experimental).** Set `LAGER_DA1469_UNCACHED_VERIFY=1` to read programmed `.bin` bytes back through the uncached QSPI mirror after a cache-controller flush: a matching image suppresses J-Link's stale-cache false "verification failed" report from no-reset attaches, a real mismatch is reported with its first differing address, and an inconclusive read-back leaves the original output untouched. `LAGER_DA1469_UNCACHED_VERIFY_BYTES` caps the compare (0 = whole file). Default off; flash output is unchanged when unset. ## Bug Fixes * **`lager debug ... gdbserver --rtt` no longer leaves the target halted** on probes whose J-Link GDB server rejects non-stop mode: the RTT control-block scan implicitly halts the core in the all-stop fallback, and the core is now resumed after the scan. Non-stop and OpenOCD paths are unchanged. * **`lager box config` host-side operations no longer dead-end on Lager Boxes with customer-managed SSH users.** The dedicated `~/.ssh/lager_box` key previously replaced ssh's default identity list, so a user whose own key was authorized (via `ssh-copy-id`) failed every host-side call with `Permission denied (publickey,password)` even though `lager ssh` worked. The SSH runner now retries once without the dedicated key on an auth failure, so default identities get their chance. * **SSH transport failures are reported as SSH failures.** An unreachable or hung Lager Box host during mount pre-flight was misread as "path missing" (producing a wrong manual fix) or crashed with a raw traceback; it is now classified separately with the real user\@ip and actionable fixes, `mount add` persists the mount, and `apply` warns and continues. * **Mount pre-flight runs after the confirm prompt and after apt/sysctl/udev provisioning**, so a mount of a file installed by an apt package in the same config (for example `/usr/bin/dfu-util`) works in a single `apply`, and the host is no longer mutated before the operator confirms. `apply --skip-restart` no longer runs the pre-flight at all. * **Leaked file handles closed** in project packaging (`zip_dir`) and the gdb `--debugfile` read; **bare `except:` clauses replaced with specific exceptions** across the CLI so interrupts and unexpected errors surface instead of being silently swallowed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.26.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.26.0/) # Version 0.27.0 Source: https://docs.lagerdata.com/source/release-notes/v0.27.0 June 12, 2026 ## Features * **Custom LabJack pin selection for i2c/spi nets.** `lager nets add` now accepts `--sda`/`--scl` (i2c) and `--cs`/`--sck`/`--mosi`/`--miso` (spi). Any DIO pin (FIO0-FIO7, EIO0-EIO7, CIO0-CIO3, MIO0-MIO2) or raw DIO number can be assigned per signal; omit `--cs` for 3-pin SPI with manual chip select. Defaults are unchanged, and pins already used by another saved LabJack net warn without blocking. * **Net TUI pin-picker dialog.** Adding a LabJack i2c/spi net in the TUI opens a pin dialog with the historical defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI: CS=FIO0/SCK=FIO1/MOSI=FIO2/MISO=FIO3). Duplicate pins block the save; pins claimed by saved nets warn live. ## Bug Fixes * **Net TUI buttons no longer need multiple clicks** (a 0.25.0 regression): box round-trips ran on the UI thread and froze the interface for seconds per call, worst right after launch and on Assign Device. All box calls — assign flows, add/save, delete, rename, delete-all, edit details — now run in the background with busy indicators and disabled controls while in flight. * **Fixed a `signal only works in main thread` crash** when TUI actions ran Lager Box scripts from worker threads; the Ctrl+C handler is now only installed for interactive command-line runs. ## Improvements * `lager i2c` and `lager spi` display custom LabJack pin assignments with canonical pin names (for example `EIO0` instead of a raw DIO number). * Net TUI startup is faster: the saved-nets list is no longer fetched a second time while the first screen paints. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.27.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.27.0/) # Version 0.27.1 Source: https://docs.lagerdata.com/source/release-notes/v0.27.1 June 12, 2026 ## Features * **`lager authorize --box [BOX]`** sets up passwordless SSH to a Lager Box in one step: it creates `~/.ssh/lager_box` if needed, copies the key to the box (one password prompt), and verifies it works. Running it again against an already-authorized box just confirms it. When a command fails with `Permission denied (publickey,password)`, the error now points you straight at `lager authorize`. ## Bug Fixes * **`lager nets` and `lager instruments` no longer cut off data.** UART serial-port paths (for example `/dev/ttyUSB0`) and full VISA/USB addresses now display in full instead of being truncated. * **Clearer SSH error reporting in `lager box dut` and `lager box config`.** A failed connection now shows the real cause and the fix to run, instead of a raw error line or a misleading "no snapshot" message. ## Improvements * **Cleaner `--help` usage lines.** Command groups now read `COMMAND [OPTIONS]`, and `lager nets` / `lager authorize` show `--box [BOX_NAME]`, consistent with commands like `lager supply`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.27.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.27.1/) # Version 0.28.0 Source: https://docs.lagerdata.com/source/release-notes/v0.28.0 June 13, 2026 ## Features * **`lager python` automatically reserves the box while it runs.** Every run acquires the box lock at start and releases it on exit, `Ctrl+C`, crash, or kill, with a server-side TTL + heartbeat reap as the backstop. Lock identity is CI-aware, so parallel CI matrix jobs queue against a shared box instead of colliding: collisions fail fast on a dev machine and wait (up to `LAGER_LOCK_WAIT`) in CI. `--detach` holds an eternal lock you release with `lager boxes unlock`. The box-mutating admin commands — `lager install`, `uninstall`, `update`, `install-wheel` — also hold the lock across their destructive steps so a concurrent test is never killed mid-run. Tune or disable via `LAGER_AUTO_LOCK_DISABLE`, `LAGER_LOCK_WAIT`, `LAGER_LOCK_TTL`, `LAGER_LOCK_HEARTBEAT`, and `LAGER_LOCK_HOLDER`. * **Explicit `lager boxes lock` reservations are never disturbed** by any auto-locking command — they keep their no-expiry semantics on new and old box servers alike. ## Bug Fixes * **The supply TUI now works on slow instruments (e.g. Keithley 2281S).** Previously its live readout could stay stuck at `00.000` with repeated "Hardware service unreachable" errors and timed-out commands. The box-side monitor now reads the full display state in a single call per update and paces itself to the instrument, so readings populate and commands respond promptly — even when the instrument is shared with the battery simulator. * **TUI error messages now say what actually failed** (a timeout, a refused connection, or a device error) instead of an empty message, and a reachable box with a failed supply/battery session points you at the instrument (check it is on and shows in `lager instruments`) instead of suggesting an outdated box image. * **`lager battery tui` with a non-battery net now exits with an error code** instead of reporting failure but exiting successfully. ## Improvements * **Pinned `textual` and `python-socketio` to compatible ranges** so a fresh `pip install lager-cli` can't pull in a newer, breaking version of either library. Existing installs are unaffected. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.0/) # Version 0.28.1 Source: https://docs.lagerdata.com/source/release-notes/v0.28.1 June 15, 2026 ## Features * **`spi`, `i2c`, and `energy-analyzer` instruments now run on the Lager Box's warm path.** These roles previously went through the box's per-command Python executor; they now run in the long-lived box server alongside gpio/adc/dac/eload/thermocouple/watt-meter, removing an interpreter spawn and device re-open on every command. Behavior and dashboard logs are unchanged, and energy-analyzer read durations are clamped to 0.1–30s. The rollout is back-compatible — an older Lager Box automatically falls back to the previous path. * **WebSocket transport support added to the Lager Box.** The box image now bundles `simple-websocket`, so clients can use a native WebSocket connection instead of long-polling, negotiated automatically. ## Bug Fixes * **`lager ssh` now uses the key set up by `lager authorize`.** Authorized Lager Boxes were still dropping to a password prompt because `lager ssh` didn't offer `~/.ssh/lager_box`; it now does when that key exists, while leaving the password fallback intact for boxes that haven't been authorized. * **UART devices are now opened exclusively.** A second user of a serial port — another dashboard session, or `lager uart` while a Workbench session is live — now fails fast with a clear "device in use" message instead of silently interleaving reads on the same port. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.1/) # Version 0.28.2 Source: https://docs.lagerdata.com/source/release-notes/v0.28.2 June 17, 2026 ## Features * **Save your devenv container setup in the project.** You can now store container settings in your project's `.lager` file instead of retyping them or keeping shell aliases. Use `devenv set`/`unset`/`show` for basic settings (image, shell, user, group, ports, and more), `devenv mount add`/`remove`/`list` for folders to share into the container, and `devenv env set`/`unset`/`list` for environment variables. Both `devenv terminal` and `lager exec` use these settings automatically, so they travel with the repo for everyone on the team. * **Add settings for a single run.** `devenv terminal` and `lager exec` take `-v HOST:CONTAINER` to share a folder. `devenv terminal` also takes `-e FOO=BAR` to set a variable and `--passenv NAME` to forward one from your shell. Paths can use `~` and `${PROJECT_ROOT}`, so saved settings work on any machine. * **Preview a session without launching it.** `devenv terminal --info` prints the exact `docker` command it would run, then exits without starting anything. * **Skip the reset before a memory read.** `lager debug memrd --no-reset` skips the reset-and-halt the Lager Box normally does before reading a DA1469x — useful on a blank chip where you don't want to reboot it. ## Bug Fixes * **Reading memory from a running DA1469x now works.** Live firmware turns off the debug port, so reads used to fail. The Lager Box now resets and halts the chip first. This reboots the device under test — pass `--no-reset` to skip it. * **Some memory reads returned wrong values.** Reads of certain chip registers now return the correct values. * **`devenv terminal --group` now works.** The group setting was being ignored before; it is now applied. ## Improvements * **More predictable devenv settings.** When the same setting is given both on the command line and in `.lager`, the command line now wins. The container entrypoint can also be saved in `.lager`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.2/) # Version 0.28.3 Source: https://docs.lagerdata.com/source/release-notes/v0.28.3 June 18, 2026 ## Features * **`lager diagnose` now works on J-Link debug nets.** Point it at a `debug` net — `lager diagnose --box ` — and it walks the whole debug-probe stack and tells you exactly what's wrong and what to do about it: the probe isn't on USB (cable/power/hub), the J-Link software isn't installed on the Lager Box, the probe is held by another process or its firmware is wedged (power-cycle it), a debug server is wedged, the target board is unpowered, the target is locked by readout/IDCODE protection, the net's device/MCU name is wrong, or the probe is fine but can't reach the target over SWD/JTAG (wiring, reset, or speed). When a debug session is already running for the probe, diagnose reports from that session instead of interrupting it. OpenOCD/ST-Link probes get basic coverage (probe detection and debug-server state). Previously `lager diagnose` only covered USB-TMC instruments such as power supplies, electronic loads, and scopes. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.3/) # Version 0.28.4 Source: https://docs.lagerdata.com/source/release-notes/v0.28.4 June 22, 2026 ## Improvements * **`lager update` is much faster.** Box image rebuilds now reuse a build cache for the Rust (`defmt-print`) and Python package layers, so a from-scratch rebuild reuses already-downloaded packages and compiled artifacts instead of redoing them — a cold rebuild that used to take around 20 minutes now finishes in a few minutes, and a warm rebuild takes seconds. `lager update` also checks up front that the box's Docker supports BuildKit and tells you how to upgrade if it doesn't. * **`lager update` asks for the sudo password at most once.** The udev, modprobe, sudoers, and box-config setup steps used to each prompt separately, so a box that needed several of them could ask for the password multiple times in one run. They now run in a single step — at most one prompt, and none at all on a fully set-up Lager Box. ## Bug Fixes * **A repeat `lager update` no longer rebuilds when nothing changed.** The box keeps a record of its build inputs so it can skip an unnecessary rebuild, but that record couldn't be updated and went stale, so every update rebuilt the image and restarted the box (about 30 seconds). It is now written reliably, so an unchanged box finishes in about a second. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.4/) # Version 0.28.5 Source: https://docs.lagerdata.com/source/release-notes/v0.28.5 June 24, 2026 ## Bug Fixes * **`lager update` fails fast with a clear fix when a box is missing buildx.** The BuildKit work in 0.28.4 made the box image require Docker's `buildx` plugin, which a stock `docker.io` install (for example on Ubuntu) doesn't bundle. The up-front check used to pass on the Docker version alone and then the build died minutes later with a confusing "buildx component is missing or broken". `lager update` now verifies buildx is actually present, checks it before stopping the box's container (so a box that can't build is never taken offline), and tells you the exact command to install it. * **A stale SSH connection no longer breaks `lager update`.** A leftover SSH control socket from an earlier interrupted run could be silently reused and surface as "Permission denied (publickey,password)" on the box state check, even when the key worked fine. `lager update` now clears any leftover connection before it starts. * **`lager update` accepts a first-seen box host key.** Its SSH calls now auto-trust a brand-new box's host key the same way the key-setup step does, so updating a box that isn't yet in `known_hosts` no longer fails with "Host key verification failed". ## Improvements * **Provisioning a box installs buildx automatically.** New box setup now installs the Docker `buildx` plugin (falling back to the official buildx binary when distro packages don't provide a working one), so a freshly provisioned box is ready to build the box image and never hits the update preflight error. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.5/) # Version 0.29.0 Source: https://docs.lagerdata.com/source/release-notes/v0.29.0 June 29, 2026 ## Features * **`lager boxes add` now requires `--user` (breaking change).** The implicit `lagerdata` default has been removed — you must specify the box's login user explicitly when adding a box. * **Read a USB net's state without changing it.** The new `lager usb state` reports whether a port is on or off, read-only. * **Run a one-off command on a Lager Box over SSH.** `lager ssh --box -- ` runs a single command on the box and returns its output, like `ssh user@host `, instead of only opening an interactive shell. * **Keithley 2281S battery-simulator internal resistance.** You can now set the simulated ESR in battery-sim mode (`:BATT:SIM:RES:OFFSet`). * **Keithley 2281S two-quadrant current readback.** Charger and sink testing now reads back negative (sink) current correctly instead of reporting 0. ## Bug Fixes * **Multi-hub Acroname boxes address the right hub.** A Lager Box with more than one Acroname USB hub now binds each USB net to its own hub by serial number, so commands no longer land on the wrong hub. * **YKUSH USB hubs recover automatically.** A stale or transient YKUSH handle is now auto-recovered, and the hardware service self-restarts after a power-cycle instead of staying wedged. * **The Keithley battery/supply monitor self-heals after a power-cycle.** A non-intrusive liveness probe detects a dropped VISA session and restarts the hardware service automatically, so the supply/battery TUI keeps working without manual intervention. * **A wedged USB hub no longer takes down USB control.** `box_http_server` now self-restarts to recover a wedged hub. ## Improvements * **`lager usb toggle` reports the resulting state.** Toggling a port now tells you whether it ended up on or off. * **Documentation refresh.** Added a `devenv` reference page and a J-Link section to `lager diagnose`, documented the DP711 crossover-cable requirement, refreshed the `debug`/`boxes` docs, and removed dead pages. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.29.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.29.0/) # Version 0.3.1 Source: https://docs.lagerdata.com/source/release-notes/v0.3.1 January 5, 2026 ## Features ### Major Codebase Restructure * Reorganized CLI commands into logical groups: power, measurement, communication, development, box, and utility * Consolidated shared utilities into `cli/core/` package * Reorganized Lager Box code into grouped modules: power, io, measurement, protocols, and automation ### New Hardware Support * Added support for Logitech C930e webcam ## Bug Fixes * Fixed Dockerfile build: corrected pcb -> nets reference * Fixed webcam import path and updated test results * Fixed arm `move-by` command: use `move_relative` instead of delta * Fixed UART nets list: use `get_saved_nets` from `lager.core` * Fixed UART import path: `lager.uart` -> `lager.protocols.uart` * Fixed documentation: changed 'command above' to 'command below' in adding-first-lager-box guide ## Improvements * Removed legacy OpenOCD code (J-Link is now the only supported debug backend) * Removed backward compatibility import stubs from CLI * Removed \~500 lines of commented-out legacy Keithley battery code * Added confirmation output when setting voltage/current on power supplies * Updated documentation overview and guides ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.1/) # Version 0.3.10 Source: https://docs.lagerdata.com/source/release-notes/v0.3.10 January 15, 2026 ## Features ### Lager Terminal Integrated into CLI * The Lager Terminal is now built directly into lager-cli * No separate installation required - just run `lager` with no arguments * Three ways to launch: * `lager` - Launches terminal when no subcommand given * `lager terminal` - Explicit terminal command * `lager-terminal` - Direct entry point * Tab completion for all commands and subcommands * Command history navigation with up/down arrows * Auto-suggest from history * Colored output with success/error indicators ### Interactive Command Protection * TUI commands (supply tui, battery tui, nets tui) are now blocked inside Lager Terminal * Clear warning message directs users to run TUI commands directly from their shell * Prevents hanging/stuck terminal sessions ## Improvements ### Package Consolidation * Removed separate `lager_terminal` package * All terminal code now lives in `cli/terminal/` * Cleaner installation with single `pip install lager-cli` ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.10 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.10/) # Version 0.3.11 Source: https://docs.lagerdata.com/source/release-notes/v0.3.11 January 15, 2026 ## Bug Fixes ### Supply TUI Force-Close Lock Release * Fixed VISA resource lock not being released when terminal window is force-closed * Added `on_unmount()` lifecycle hook to ensure WebSocket cleanup on any exit * Prevents "Resource busy" errors after ungraceful TUI termination ### Keysight Power Supply Output State * Fixed bug where changing voltage on Keysight E36200 power supplies would disable the output * Removed unnecessary `disable_output()` call from driver initialization * Output state is now preserved when changing voltage or current setpoints ### Voltage/Current Command Feedback * Fixed missing confirmation output for `lager supply voltage` and `lager supply current` commands * Commands now display "\[OK] Voltage set to X.XV" or "\[OK] Current set to X.XA" on success ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.11 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.11/) # Version 0.3.12 Source: https://docs.lagerdata.com/source/release-notes/v0.3.12 January 15, 2026 ## Bug Fixes ### Keysight E36233A Power Supply TUI Support * Fixed Supply TUI monitoring for Keysight E36233A power supplies on Lager Boxes * Resolved "cannot import name '\_resolve\_net\_and\_driver'" error that prevented TUI from starting * Fixed SCPI measurement commands to use correct syntax for voltage and current readings * Fixed channel validation to properly accept channel numbers * Fixed OCP and OVP protection setting commands in TUI * Fixed negative zero display issue (no longer shows "-0.000") ## Improvements ### Power Supply Driver Enhancements * Added retry logic for device identification queries to improve connection reliability * Improved cache management to prevent stale USB/VISA connections * Updated hardware maximum specifications display for E36233A (30V/20A per channel) ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.12 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.12/) # Version 0.3.13 Source: https://docs.lagerdata.com/source/release-notes/v0.3.13 January 16, 2026 ## Features ### Lager Terminal - Interactive REPL * New `lager terminal` command launches an interactive shell for running Lager commands * Tab completion for commands, subcommands, and options * Command history with up/down arrow navigation (persisted between sessions) * Auto-suggestions from command history as you type * Clean ASCII art welcome banner * Type `help` for built-in commands, `exit` or `quit` to leave ### Update All Lager Boxes * New `lager update --all` flag updates all saved Lager Boxes sequentially * New `lager update --all --needs-update` flag only updates Lager Boxes with versions older than your CLI * Visual progress bar with elapsed time during updates * Summary report showing successful and failed updates ### Live Lager Box Status * `lager boxes` now queries all saved Lager Boxes and displays live version status * Shows whether each Lager Box is current, needs update, or has a newer version * Loading spinner while querying multiple Lager Boxes * Summary counts for Lager Boxes needing updates ## Improvements ### Keysight Power Supply Driver Consolidation * Merged Keysight E36200 and E36300 series drivers into a unified `keysight_e36000.py` driver * Reduced code duplication and simplified maintenance * No changes to user-facing commands ### Code Quality * Refactored boxes command for better code organization * Refactored update command with improved progress tracking * Test suite formatting and cleanup ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.13 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.13/) # Version 0.3.14 Source: https://docs.lagerdata.com/source/release-notes/v0.3.14 January 20, 2026 ## Improvements ### Enhanced Error Messages * All CLI commands now provide clearer, more actionable error messages * Error messages include specific guidance on how to fix issues * Consistent error formatting across all commands ### Input Validation * Added range validation for numeric parameters (voltages, currents, percentages, timeouts) * Added format validation for BLE addresses, IP addresses, and package names * Invalid inputs are rejected early with helpful error messages showing valid ranges ### Connection Error Handling * Improved distinction between timeout, DNS errors, connection refused, and unreachable host * SSH commands now include platform-specific troubleshooting hints * Better handling of authentication failures with guidance ### Net Validation * Commands now validate that nets exist before attempting operations * Wrong net type errors now list available nets of the correct type * Validation happens before confirmation prompts to avoid wasted time ## Bug Fixes * Fixed spurious "WebSocket connection failed: 0" message when disconnecting from UART with Ctrl+C * Fixed webcam net creation failing with "Unexpected result format" error * Fixed webcam net type mapping (was using "camera" instead of "webcam") ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.14 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.14/) # Version 0.3.15 Source: https://docs.lagerdata.com/source/release-notes/v0.3.15 January 20, 2026 ## Bug Fixes * Fixed `lager update` command to correctly update Lager Box status after updates * Fixed `lager update --all` to correctly sync all Lager Boxes ## Improvements * Cleaned up `--help` output across CLI commands for better readability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.15 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.15/) # Version 0.3.16 Source: https://docs.lagerdata.com/source/release-notes/v0.3.16 January 26, 2026 ## Features ### J-Link Script File Support * Added support for custom J-Link script files (`.JLinkScript`) in debug commands * Scripts can be passed to enable advanced initialization sequences for custom hardware configurations * Useful for enabling trace clocks, custom target initialization, and specialized debug setups ### Expanded Device Support for J-Link Debugging * Dramatically expanded ARM architecture detection to support 70+ device families * Now supports: Nordic (nRF51/52/53/91), STM32 (all families), NXP (LPC, Kinetis, i.MX RT), TI (Stellaris, Tiva-C, CC26xx), Microchip/Atmel (SAM), Silicon Labs (EFM32/EFR32), Renesas (RA), Dialog, Infineon, and more * Unknown devices now gracefully fall back to a default architecture instead of failing ## Bug Fixes ### Resource Busy Error Fix * Fixed "Resource Busy" errors when reconnecting to VISA/USB instruments * Devices are now properly closed before being removed from the cache * Added cleanup handler on process exit to release hardware resources ### Debug Reset Reliability * Improved debug reset reliability for Cortex-M33 devices (e.g., nRF5340) * Reset and memory read operations now use J-Link Commander directly, avoiding GDB register mismatch issues ## Improvements ### Robotic Arm Enhancements * Increased default arm move timeout from 5 seconds to 15 seconds for more reliable operation * Improved out-of-bounds error messages now show which coordinates are invalid and display workspace limits * Updated Rotrics Dexarm workspace bounds to accurate values ### Silent USB Hub Operations * USB hub enable/disable/toggle operations now complete silently for cleaner automation output ### Python Compatibility * Added Python 3.14 support * Updated dependencies for newer Python version compatibility ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.16 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.16/) # Version 0.3.17 Source: https://docs.lagerdata.com/source/release-notes/v0.3.17 January 29, 2026 ## Features ### SPI Communication Support * Added new `lager spi` command for SPI (Serial Peripheral Interface) communication via LabJack T7 * Subcommands: `read`, `write`, `transfer`, `config` * Supports all standard SPI parameters: mode (0-3), bit order (MSB/LSB), frequency, chip select polarity, word size (8/16/32-bit) * Multiple output formats: hex, bytes, JSON * Data input via hex string (`--data`) or binary file (`--data-file`) * Automatic padding and truncation for transfer operations ### SPI Python API * New `SPINet` class accessible via `Net.get('my_spi', NetType.SPI)` * Methods: `config()`, `read()`, `read_write()`, `transfer()`, `write()` * Full-duplex read/write support with configurable chip select behavior ## Bug Fixes ### LabJack Handle Sharing * Fixed an issue where GPIO operations would close the LabJack device and kill active SPI connections * ADC, DAC, and GPIO modules now use a global shared handle manager instead of opening and closing per operation * Added atexit cleanup handler to properly release hardware resources on process exit ## Improvements ### LabJack SPI Hardware Limitation Workaround * Automatically forces 800kHz clock speed for SPI transactions of 3 or more bytes when a lower frequency is requested, due to a LabJack T7 hardware limitation * Prints a warning when this override occurs so users are aware of the adjusted frequency * LabJack T7 firmware 1.0332 or later is required for SPI support ### Deployment * Improved Lager Box deployment scripts ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.17 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.17/) # Version 0.3.18 Source: https://docs.lagerdata.com/source/release-notes/v0.3.18 January 30, 2026 ## Features ### JLinkScript Stored with Debug Nets * JLinkScript files can now be stored directly with debug nets on the Lager Box, eliminating the need to configure the `.lager` DEBUG section or re-send the script on every connect * Once attached via `--jlink-script` on `lager nets create` or the new `lager nets set-script` command, the script is used automatically for connect, flash, erase, and reset operations * New `lager nets set-script ` command to attach a JLinkScript to an existing debug net * New `lager nets remove-script ` command to remove a JLinkScript from a debug net ## Bug Fixes ### Power Supply Python API Reliability * Fixed an issue where VISA sessions would go stale after extended use, causing `DeviceError` when calling power supply methods (e.g., `disable()`, `voltage()`) via the Python API * The Keysight E36000 resource cache now validates connections before reuse and automatically reconnects on stale sessions * ResourceManager references are now preserved across all power supply drivers (Keysight, Rigol, Keithley, EA) to prevent garbage collection from invalidating active session handles * The hardware service now automatically detects and retries on stale VISA session errors instead of returning failures ## Improvements ### Lager Box Update Timeouts * Increased SSH timeout during `lager update` to prevent timeouts when entering passwords on slower connections ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.18 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.18/) # Version 0.3.19 Source: https://docs.lagerdata.com/source/release-notes/v0.3.19 February 9, 2026 ## Features ### I2C Protocol Support * New `lager i2c` commands for I2C bus communication, including `config`, `scan`, and `transfer` subcommands * Supports Aardvark USB-I2C and LabJack T7 hardware adapters * `lager i2c scan` detects all devices on the I2C bus and reports their addresses * `lager i2c transfer` performs read, write, and write-then-read transactions * `lager i2c config` sets bus frequency and pull-up resistor options ### FT232H Adapter Support * Added the FTDI FT232H USB adapter as a backend for SPI and GPIO protocols * Provides an affordable, widely-available option for SPI communication with target devices ### Joulescope JS220 Support * Added support for the Joulescope JS220 precision power analyzer * The JS220 is now automatically detected during instrument discovery * Use existing `lager watt` commands to read power measurements from the Joulescope ### Net TUI Enhancements * The `lager nets` interactive TUI now includes Rename and Delete buttons for managing nets directly * Arrow key navigation works for all TUI buttons and selections * Updated color scheme with Lager branding * The `lager nets` list output format now matches the TUI layout for consistency ## Bug Fixes * Fixed SPI transactions to use separate TX and RX data arrays, resolving data corruption on simultaneous read/write operations * Fixed SPI Slave Select polarity handling to work across different versions of the aardvark\_py library * Fixed Aardvark adapter initialization to use the correct SPI+GPIO mode instead of I2C mode * Fixed `aa_spi_configure` to pass clock polarity and phase as separate arguments * Fixed Aardvark adapter to open by port number instead of serial number matching, improving reliability when multiple adapters are connected ## Improvements * SPI and GPIO nets can no longer be created on the same pins, preventing configuration conflicts * Improved help messages for SPI commands * Added validation for debug net types, preventing invalid net type configurations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.19 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.19/) # Version 0.3.2 Source: https://docs.lagerdata.com/source/release-notes/v0.3.2 January 07, 2026 ## Features ### Box Flag Support for Install and Uninstall * Added `--box` flag to `lager install` and `lager uninstall` commands * Allows using box names from `.lager` config instead of IP addresses * Simplifies box management workflows ## Bug Fixes * Fixed `lager update` command issues that prevented proper updates * Fixed `lager install` command to ensure reliable installation ## Improvements ### Preserve User Configuration on Uninstall * Changed `lager uninstall` default behavior to preserve `/etc/lager` directory * Saved nets and user packages are now kept by default * Use `--all` flag to remove all configuration (previous behavior) * Prevents accidental loss of hardware configuration ### Performance Optimization * Removed USB 202 library to reduce installation time * Faster deployment and update operations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.2/) # Version 0.3.20 Source: https://docs.lagerdata.com/source/release-notes/v0.3.20 February 13, 2026 ## Features ### Aardvark GPIO Support * Added GPIO driver for the Aardvark USB adapter, enabling digital I/O control via `lager gpi` and `lager gpo` commands * Supports reading and writing individual GPIO pins on the Aardvark adapter ### SPI Chip Select Control * SPI commands now support manual vs automatic chip select (CS) assertion for both Aardvark and LabJack adapters * Allows fine-grained control over CS pin behavior during multi-byte SPI transactions ### GPI Command Enhancements * Added GPIO direction configuration support to `lager gpi` commands * GPIO dispatcher now supports Aardvark and LabJack backends with direction control ## Bug Fixes * Fixed SPI protocol files to use separate TX and RX data handling across all backends * Fixed LabJack SPI driver transaction handling ## Improvements * Improved natural sorting for CLI list outputs (nets, boxes, instruments, defaults, logs, and status views) * Updated SPI base class and net abstractions for better multi-backend consistency * Temporarily disabled FT232H backend code pending further testing * Added comprehensive test scripts for Aardvark and LabJack I2C, SPI, and GPIO ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.20 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.20/) # Version 0.3.21 Source: https://docs.lagerdata.com/source/release-notes/v0.3.21 February 15, 2026 ## Bug Fixes * Fixed `lager update` version file write timing: version is now written to `/etc/lager/version` before container restart instead of after, preventing version info loss if the restart disrupts SSH * Added retry logic (3 attempts) for version file write during `lager update` to improve reliability ## Improvements * Updated SPI Aardvark test scripts with incremental config tests and improved manual test coverage ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.21 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.21/) # Version 0.3.22 Source: https://docs.lagerdata.com/source/release-notes/v0.3.22 February 16, 2026 ## Improvements ### Remove LabJack Pin Conflict Restrictions * Removed SPI/GPIO and I2C/GPIO pin conflict restrictions from `lager nets add`, `lager nets add-all`, and the interactive TUI * Users can now freely create SPI, I2C, and GPIO nets on the same LabJack T7 FIO pins without warnings, prompts, or blocking validation * The LabJack T7 configures pins dynamically at transaction time, so multiple net types on the same physical pins work correctly (e.g., SPI on FIO0-FIO3 alongside GPIO on FIO0) * `lager nets add-all` no longer prompts "Choose \[spi, gpio]" or "Choose \[i2c, gpio]" and creates all net types automatically * The TUI Add Nets screen no longer shows yellow pin conflict warnings ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.22 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.22/) # Version 0.3.23 Source: https://docs.lagerdata.com/source/release-notes/v0.3.23 February 16, 2026 ## Features ### LabJack Pin Conflict Detection * Added runtime pin conflict detection for LabJack T7 when multiple subsystems (SPI, I2C, GPIO) use the same physical pin within a single `lager python` script * A warning is printed to stderr when overlapping pin usage is detected, helping catch wiring or configuration mistakes early * Conflict tracking resets automatically between separate CLI commands ### I2C and SPI Documentation * Added new CLI reference pages for `lager i2c` and `lager spi` with full subcommand documentation, hex data formats, frequency formats, and troubleshooting guides * Added new Python API reference pages for I2C and SPI with method references, output formats, and usage examples ## Improvements ### Documentation Overhaul * Rewrote CLI reference pages for power supply, oscilloscope, ADC, GPI, GPO, watt meter, debug, python, nets, boxes, hello, defaults, and update commands with detailed options, examples, and supported hardware tables * Rewrote Python API reference pages for power supply, ADC, DAC, GPIO, battery, electronic load, watt meter, oscilloscope, and robot arm with full method references and examples * Updated all Python API examples to use the `from lager import Net, NetType` import pattern * Added new CLI reference pages for terminal, status, install, uninstall, and exec commands * Updated getting started guides with improved overview and instrument setup instructions ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.23 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.23/) # Version 0.3.24 Source: https://docs.lagerdata.com/source/release-notes/v0.3.24 February 17, 2026 ## Features ### CLI Update Notifications * The CLI now checks PyPI in the background for newer versions of `lager-cli` and displays a notification after each command when an update is available * Checks are cached for 24 hours to avoid unnecessary network requests * Can be disabled by setting `LAGER_NO_UPDATE_CHECK=1` or in CI environments ## Bug Fixes * Fixed duplicate SPI channel entry in LabJack T7 instrument query (`FIO0-FIO3` listed twice) ## Improvements * Updated SPI and GPIO test scripts to use correct net names matching current Lager Box configuration ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.24 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.24/) # Version 0.3.25 Source: https://docs.lagerdata.com/source/release-notes/v0.3.25 February 18, 2026 ## Features ### FT232H Instrument Support (SPI, I2C, GPIO) Full support for the FTDI C232HM-DDHSL-0 cable as a Lager instrument, including: * **SPI**: All 4 modes, frequencies 100kHz-10MHz, word sizes 8/16/32, LSB/MSB bit order, configurable CS polarity, and manual CS via external GPIO * **I2C**: Scan, read, write, and transfer at standard (100kHz) and fast (400kHz) modes with NACK detection * **GPIO**: Digital output, input, toggle on pins AD4-AD7 with file-based state caching across CLI commands * Auto-discovery via `lager instruments` and net creation via `lager nets add-all` ### GPIO Hold Mode * New `--hold` flag for `lager gpo` maintains the output state until Ctrl+C: ```bash theme={null} lager gpo gpio1 high --hold --box ``` ## Bug Fixes * Fixed FT232H GPIO USB "Resource busy" error after Ctrl+C: the USB interface is now properly released when exiting hold mode, preventing subsequent commands from failing * Fixed LabJack T7 SPI `SPI_OPTIONS` bit 0 (auto CS) not reliably driving the CS pin: switched to manual GPIO-based CS assert/deassert for all LabJack SPI transactions * Fixed SPI configuration not persisting between CLI commands: added `_persist_params()` to SPI dispatcher matching the I2C pattern ## Improvements * FT232H GPIO uses read-modify-write to avoid clobbering other pins' output state * FT232H GPIO output latch is written before enabling pin direction to prevent brief glitches after USB reset * FT232H SPI and I2C include USB disconnect recovery with exponential backoff retry logic * LabJack T7 SPI warm-up sequence no longer uses auto CS to avoid spurious CS assertions to connected devices ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.25 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.25/) # Version 0.3.26 Source: https://docs.lagerdata.com/source/release-notes/v0.3.26 February 19, 2026 ## Features ### MCP Server for AI Assistant Integration Full Model Context Protocol (MCP) server enabling AI assistants to control Lager hardware directly: * **165+ tools** across 21 modules covering all Lager CLI functionality: power supplies, batteries, solar simulation, electronic loads, I2C, SPI, UART, BLE, WiFi, USB, ADC, DAC, GPIO, oscilloscope, debug, robotic arm, webcam, and more * Run with `python -m cli.mcp` or `mcp dev cli/mcp/server.py` * Built on FastMCP with subprocess-based CLI wrapping for reliable operation * Power supply and battery tools auto-pass `--yes` to skip confirmation prompts for safe automated operation ### MCP Test Suite Comprehensive test coverage for the MCP server: * **254 unit tests** with mocked subprocess calls (no hardware required, runs in \~0.6s) * **64 integration tests** against real Lager Boxes covering power, battery, eload, I2C, SPI, ADC, DAC, GPIO, USB, and defaults * Safety fixtures auto-disable power output in test teardown ## Improvements * Cleaned up LabJack T7 SPI driver code ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.26 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.26/) # Version 0.3.27 Source: https://docs.lagerdata.com/source/release-notes/v0.3.27 February 19, 2026 ## Features ### `lager-mcp` Entry Point for MCP Server Added a `lager-mcp` console script entry point for easier MCP server setup with AI assistants: * Install with `pip install "lager-cli[mcp]"` and run with `lager-mcp` * Eliminates Python PATH resolution issues when configuring MCP clients * Setup is now a single command via your MCP client's standard configuration ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.27 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` To install with MCP support: ```bash theme={null} pip install "lager-cli[mcp]" ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.27/) # Version 0.3.3 Source: https://docs.lagerdata.com/source/release-notes/v0.3.3 January 07, 2026 ## Features ### PyPI Package Includes Deployment Scripts * `lager install` command now works when installed from PyPI * Deployment scripts are packaged with the CLI * Enables box deployment without cloning the lager repository * Users can now install directly with `pip install lager-cli` and deploy boxes ## Improvements ### Deployment Mode Restrictions * PyPI installations restricted to sparse checkout mode only * Rsync mode requires lager repository (uses local files) * Clear error messages guide users when restrictions apply * Ensures reliable deployments for all installation methods ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.3/) # Version 0.3.4 Source: https://docs.lagerdata.com/source/release-notes/v0.3.4 January 07, 2026 ## Features ### Custom User Support for Install and Uninstall * `lager install` and `lager uninstall` now support custom SSH usernames * Use `--user` flag to specify non-default users for box deployment * Enables deployment to boxes with different user configurations * Automatically uses configured user from `.lager` file when using `--box` flag ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.4/) # Version 0.3.5 Source: https://docs.lagerdata.com/source/release-notes/v0.3.5 January 09, 2026 ## Features ### Install Without lager Repository * `lager install` can now be executed without needing the lager repository present on the local machine * Simplifies deployment workflow for users who only need CLI functionality * Streamlines the installation process for end users ## Improvements * Added more informative error messages for invalid `.lager` JSON files * Documentation cleanup and improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.5/) # Version 0.3.6 Source: https://docs.lagerdata.com/source/release-notes/v0.3.6 January 15, 2025 ## Features ### Enhanced `lager boxes sync` Command * Added version comparison against local CLI version * Boxes with mismatched versions now display in yellow with "(needs update)" indicator * Shows local CLI version at the top of sync output * Comprehensive error messages for connection failures: * Connection refused (service not running) * Network unreachable (VPN/network issues) * Connection timeout (firewall/network issues) * Detailed HTTP error responses ### Improved Version Display * `lager hello` now displays the Lager Box version * Better version tracking and display across commands ## Bug Fixes ### Fixed `lager update` Sudoers Issues * Automatically detects and fixes incorrect sudoers file ownership * Resolves timeout issues when `/etc/sudoers.d/lagerdata-udev` is owned by wrong user * Updated version file write logic to work with both old and new sudoers configurations * Uses directory permissions instead of file-specific sudo commands ### Fixed Version File Updates * Correctly deletes old version file before writing new one * Prevents stale version information from persisting after updates ### Fixed Version Detection * Uses `git checkout` instead of `git restore` for better compatibility with sparse checkouts * Ensures accurate version reading during Lager Box updates ## Improvements ### Enhanced `lager update` Reliability * Increased container startup timeout from 3 minutes to 5 minutes for slower Lager Boxes * Added helpful troubleshooting tips when container startup times out * Better error handling with specific suggestions for debugging ### Simplified `lager boxes sync` Output * Removed unnecessary "Updated" and "Unchanged" counters * Streamlined summary to show only "Needs update" and "Failed" counts * Removed redundant command suggestions from output ### Hardware Database Updates * Updated Keysight E36233A USB VID/PID verification * Improved device identification consistency ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.6 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.6/) # Version 0.3.7 Source: https://docs.lagerdata.com/source/release-notes/v0.3.7 January 15, 2025 ## Bug Fixes ### Fixed Keysight E36233A Power Supply Detection * Resolved issue where Keysight E36233A power supplies were incorrectly identified as E36313A * `lager instruments` now correctly shows E36233A when this 2-channel power supply is connected * This fix ensures you can properly create power supply nets with the correct channel count ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.7 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.7/) # Version 0.3.8 Source: https://docs.lagerdata.com/source/release-notes/v0.3.8 January 15, 2026 ## Features ### Connection Manager for VISA Instruments * Added global connection manager that coordinates VISA instrument connections across dispatchers * Prevents "Resource busy" errors when the same physical device is accessed by multiple dispatchers * Enables seamless switching between power supply and battery simulator modes on devices like the Keithley 2281S ### Test Result Infrastructure * New `TestResult` schema for structured test data capture from `lager python` executions * Support for saving results to file in JSON, JSONL, or CSV formats * Webhook integration for posting test results to external services * Rich metadata support including device info, measurements, and execution context ## Improvements ### Power Module Return Values * Power supply and battery drivers now return numeric values when reading voltage/current * Enables programmatic access to measurement values in addition to console output * Updated Keithley, Keysight, EA, and Rigol drivers with consistent return value behavior ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.8 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.8/) # Version 0.3.9 Source: https://docs.lagerdata.com/source/release-notes/v0.3.9 January 15, 2026 ## Bug Fixes ### Supply TUI Import Error * Fixed an import error that prevented the Supply TUI from starting after the dispatcher refactoring * Added backward compatibility wrappers for `_resolve_net_and_driver` in supply and battery dispatchers * The TUI now starts correctly and connects to power supplies without errors ## Improvements ### Supply TUI Display Cleanup * Removed debug logging that was appearing in production output * Fixed cosmetic display issue where negative zero values (-0.000) appeared instead of 00.000 for current and power measurements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.3.9 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.3.9/) # Version 0.30.0 Source: https://docs.lagerdata.com/source/release-notes/v0.30.0 June 30, 2026 ## Features * **SEGGER J-Link Base Compact support.** The J-Link Base Compact (USB `1366:1020`) is now granted device access and auto-detected as a `debug` net, so it scans, nets, and drives a target exactly like any other J-Link. ## Bug Fixes * **Every J-Link variant is granted device access by vendor ID.** The bundled udev rules previously allow-listed only three J-Link product IDs (`0x1024`, `0x0101`, `0x0503`), so a J-Link enumerating under any other PID kept its default root-only ownership and was unusable from a Lager Box or its container — silently degrading debug/flash. The rule now matches the SEGGER vendor ID (`0x1366`), covering every current and future J-Link. * **The standard J-Link is no longer dropped from device discovery.** A duplicate dictionary key in the CLI's USB scanner silently overwrote the standard J-Link (`0x1024`) entry; each J-Link product now has its own key so both resolve. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.30.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.30.0/) # Version 0.31.0 Source: https://docs.lagerdata.com/source/release-notes/v0.31.0 July 2, 2026 ## Features * **Read current, voltage, or all three from a watt-meter net.** `lager watt current|voltage|all` reads current (A), voltage (V), or current/voltage/power together — not just power. Backed by the Joulescope JS220 and Nordic PPK2; a Yocto-Watt (power only) reports a clear "not supported" message. * **`--duration` averaging window on `lager watt`.** Average a reading over a longer capture for lower noise and higher effective resolution. On the JS220, long windows (e.g. `--duration 60`) are measured gaplessly via the on-device charge accumulator, so every transient is captured in constant memory. * **`--json` output for `lager watt`.** Emit a machine-readable object in base SI units (W/A/V) for HIL scripts. * **`lager nets add` now accepts Joulescope JS220, Nordic PPK2, and Yocto-Watt.** These watt-meter / energy-analyzer instruments can now be added from the command line instead of only through the Workbench UI. ## Bug Fixes * **Small loads no longer read `0.000 W`.** `lager watt` output is SI-scaled (µ/n units, e.g. `52.340 µW`), falling back to scientific notation for values too small for the nano prefix. * **`lager energy` reads no longer hang or crash on exit.** The reader now closes the Joulescope device when it finishes, so its USB streaming thread is torn down cleanly. * **`lager box dut edit` and `dut add-doc` succeed on a Lager Box's www-data-owned `/etc/lager`.** The updated `bench.json` is staged in `/tmp` and installed via a passwordless `sudo` fallback — with a clear message when the sudo grant is missing — instead of failing with "Permission denied". * **`lager install` deploys its udev and modprobe rules again**, and its box-code flatten step no longer clobbers the installed `lager` command. ## Improvements * **`lager install` prompts for the box password at most once.** SSH key setup now runs first, so the remaining install steps authenticate by key instead of re-prompting for the password on each one. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.0/) # Version 0.31.1 Source: https://docs.lagerdata.com/source/release-notes/v0.31.1 July 6, 2026 ## Features * **Structured state from the supply/battery HTTP command endpoints.** The `state` action on a Lager Box's `/supply/command` and `/battery/command` endpoints now returns the same structured state object the WebSocket monitors emit, so HTTP-only clients can render a live readout by polling. The supply endpoint also gains `clear_ocp`/`clear_ovp` actions, matching the WebSocket handler and the battery endpoint. * **Opt-in MCP box-control and command-execution tools.** The on-box MCP server (read-only by default) can now expose gated tools for probe/net status checks, USB-hub power-cycling, and command execution, enabling automated recovery workflows. These stay disabled unless explicitly enabled on the box. ## Bug Fixes * **One failing instrument query no longer blanks the whole supply/battery readout.** Every field in the monitor-state gather is guarded individually, so an unsupported SCPI query, a measurement overflow, or a transient bus error degrades that single field instead of dropping the entire state. The monitors report a clear error — and the TUI keeps its last good display — only when the instrument is entirely unreachable. * **Flashing recovers after a debug probe power-cycles mid-session.** A J-Link GDB server left defunct by a flash that ran while the probe was down was previously treated as still running, so the next flash failed. Zombie server processes are now detected and a clean server is restarted automatically. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.1/) # Version 0.31.10 Source: https://docs.lagerdata.com/source/release-notes/v0.31.10 July 13, 2026 ## Features * **8 more Logitech webcams supported.** The Logi 4K Pro, BRIO 4K Stream, C925e, C922 Pro, C920, C615, C270, and StreamCam now appear in `lager instruments` and can be added as webcam nets, joining the existing BRIO, BRIO HD, and C930e. Detection is catalog-driven — adding a future model is a one-line table entry — and each camera is mapped to its actual `/dev/video` capture node by walking sysfs, so setups mixing cameras with different node counts (a C920 exposes two, a BRIO four) resolve to the right device. * **`lager battery models` — list the battery models saved on the instrument.** Battery simulators store models in numbered memory slots, but until now there was no way to see which slots actually held a model without walking to the front panel. The new read-only `models` command (also available in the battery TUI and the box's `/battery/command` HTTP endpoint as `list_models`) prints each occupied slot plus the firmware's built-in models — all valid inputs to the existing `model` command. On the Keithley 2281S the catalog is assembled from query-only slot probes, so listing never changes instrument state. ## Bug Fixes * **Battery model readback now reports the actual loaded model.** The driver previously read the "current model" with `:BATT:STAT?`, which per the 2281S reference manual reports charge/discharge status — not the model — so `lager battery model`, `state`, and the TUI header showed "DISCHARGE" whenever the output was idle, regardless of what was loaded. The same misread made `model ` raise a false "slot is empty" error after every successful load. Readback and verification now use `:BATT:MOD:RCL?` (e.g. `Model: slot 5` or `Model: LI_ION4_2`), and because the 2281S fails empty-slot recalls silently, a recall that doesn't take effect is now reliably detected and reported with guidance instead of pretending to succeed. * **Built-in battery models are now loadable over SCPI.** The 2281S manual prints two built-in model names with hyphens (`LI-ION4_2`, `LEAD-ACID12`), but the instrument's SCPI parser rejects hyphens outright (error -102), so recalling those built-ins through Lager never worked. The driver now sends the underscore spellings the firmware actually accepts (`LI_ION4_2`, `LEAD_ACID12`) and accepts either spelling as input. * **Battery command errors no longer masquerade as "Resource busy".** When a battery driver raised a real error (like the empty-slot guidance above), the box's `/battery/command` endpoint returned it as a 5xx, which made the CLI treat the endpoint as unavailable and fall through to its legacy direct-USB path — always failing with `[Errno 16] Resource busy` and burying the actual message. Driver errors are now returned as ordinary command failures, so the CLI and TUI display the real diagnosis. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.10 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.10/) # Version 0.31.11 Source: https://docs.lagerdata.com/source/release-notes/v0.31.11 July 14, 2026 ## Bug Fixes * **`lager box config apply` no longer reports success while applying nothing.** `apply` runs `start_box.sh` on the box as the login user, and its four box-config renderers *create* files in `/etc/lager` (`box_config.docker.sh`, `user_requirements.txt`, `cargo_packages.txt`, `npm_packages.txt`). Creating a file needs write permission on the **directory**, and `lager install` left `/etc/lager` owned by the container user only (`33:33`, mode `755`) — so every render failed with `EACCES`. Renders are soft-failed by design (the container must always come up), which turned this into a silent no-op: the install steps read files that were never written, so `apply` skipped them, stamped the applied-hash, and printed "Applied box config". Every `pip`/`cargo`/`npm` package, mount, volume, and env var added through `lager box config` was quietly dropped on any box whose last provisioning step was an install. `/etc/lager` is now owned `33:` mode `2775` (setgid), so the container (owner) and `start_box.sh` (group) can both write it. * **`/etc/lager` is no longer world-writable.** `lager update` previously granted the box user write access by running `chmod 777` on the directory, which also gave it to every other local account — enough to replace `box_config.json`, `saved_nets.json`, or the org secrets. It now gets the same owner/group/setgid treatment as above: what the two writers actually need, and nothing more. * **A box-config render failure is now loud, and no longer poisons the retry.** A failed render used to print a one-line warning and a raw Python traceback, then let the run continue as if applied. It now reports which file could not be written, why, and how to fix it; `start_box.sh` exits 3 ("container up, config NOT applied") and `apply` no longer stamps the applied-hash (which had sealed the bug shut on retry) or rolls back a healthy container. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.11 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.11/) # Version 0.31.12 Source: https://docs.lagerdata.com/source/release-notes/v0.31.12 July 15, 2026 ## Features * **`lager battery model-create --csv [--force]` — create a custom battery model from a CSV file.** Writes a voltage/resistance curve into a Keithley 2281S memory slot (1-9); previously custom models could only be authored at the instrument's front panel. The CSV has two columns (`voc,resistance`, header optional) ordered from empty battery to full, with exactly 11 or 101 data rows — 11-row files are interpolated to 101 points by the instrument. Files are validated client-side with line-numbered errors (row count, VOC non-decreasing, resistance non-increasing, value ranges) before anything reaches the box. Saving overwrites the slot, and the instrument has no way to delete a saved model — a slot can only be overwritten — so occupied slots are refused unless `--force`. * **`lager battery model-export --csv ` — export a saved battery model's curve to CSV.** Read-only: writes the slot's 101 `voc,resistance` points in the exact format `model-create` accepts, enabling the export -> edit -> create round-trip. Exporting reads the saved slot directly and never changes the active model; exporting an empty slot is an error that points at `models`. ## Bug Fixes * **`lager battery model discharge` no longer fails with a misleading "slot appears to be empty" error.** Discharge mode is not selectable over SCPI on current 2281S firmware: the instrument rejects every recall form (numeric 0 is out of range — only slots 1-9 are valid recall arguments — and the DISCHARGE name and its quoted/abbreviated variants are syntax errors). The command now says so up front, pointing at the front panel and at `models`, and the model catalog no longer advertises a slot-0 DISCHARGE entry that was never actually loadable. A discharge selection made from the front panel still reads back as DISCHARGE. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.12 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.12/) # Version 0.31.13 Source: https://docs.lagerdata.com/source/release-notes/v0.31.13 July 16, 2026 ## Bug Fixes * **Joulescope JS220 watt reads no longer fail with "is not connected" after the first read on the warm `/net/command` path.** The handler closes the net after every read to release the USB device, but `close()` left the per-serial driver singleton cached as initialized — every later construction got the dead handle back, and one net's close also broke a sibling net sharing the same physical JS220, until the box runtime restarted. `close()` now evicts the instance so the next read reopens the device, `clear_cache()` can no longer deadlock, the JS220/PPK2 energy analyzers re-acquire the shared watt driver if the other net closed it, and dispatcher driver caches drop closed instances via a health check. * **Energy-analyzer reads work on nets addressed by a VISA resource string.** The energy dispatcher passes the net's VISA address (`USB0::0x16D0::0x10BA::::INSTR`) as the driver location, which was misparsed to serial `INSTR` — and even with the correct serial, the joulescope v1 API has no top-level `Device` class for the old re-wrap, so every such read failed with "Joulescope with serial 'INSTR' not found". VISA USB resource strings now parse to the serial field, devices are matched via their `serial_number`/`device_path` attributes and opened directly, and the not-found error lists device serial numbers. A specified serial that matches nothing still errors instead of silently opening the first device, which would measure the wrong unit on a multi-Joulescope bench. * **A warm-path energy read no longer blocks subsequent watt reads (and external tools) with `jsdrv IN_USE`.** Once the VISA fix let the in-process energy path actually open the JS220, it held the device's exclusive USB claim indefinitely. The energy handler now releases the device after every read, exactly like the watt handler, and the next read re-acquires it automatically. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.13 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.13/) # Version 0.31.14 Source: https://docs.lagerdata.com/source/release-notes/v0.31.14 July 17, 2026 ## Bug Fixes * **UART nets no longer get stuck reporting "already in use by another session" after a session's read loop wedges on a disconnected serial adapter.** The box tracks live UART sessions in an in-memory registry guarded per-connection, per-net, and per-device; an entry was only removed by a clean stop, a socket disconnect, or the read thread's exit path. If the read thread wedged inside a blocking serial read — a USB-serial adapter that vanished or re-enumerated without raising a device-gone error — none of those ran, so the net stayed reserved with no live reader behind it until the box restarted. Each session now carries a monotonic heartbeat, and a new connection reclaims a holder whose read thread has died or whose heartbeat has aged past 30s instead of refusing to start. A live or reconnecting session keeps its heartbeat fresh and is never reclaimed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.14 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.14/) # Version 0.31.15 Source: https://docs.lagerdata.com/source/release-notes/v0.31.15 July 20, 2026 ## Features * **`BlufiClient.scan()` — BLE advertisement presence checks from the box Python API.** `scan(timeout=10.0, name_prefix=None)` returns nearby BLE devices as `{name, address, rssi}` dicts sorted by RSSI descending, with an optional exact-prefix name filter. A test suite can now confirm its target device is advertising before attempting a BluFi connection and fail with a clear diagnostic when it is not, instead of driving a never-connected client into a confusing `NoneType` error. The `lager ble scan` and `lager blufi scan` commands already provide the equivalent from the CLI. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.15 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.15/) # Version 0.31.16 Source: https://docs.lagerdata.com/source/release-notes/v0.31.16 July 20, 2026 ## Bug Fixes * **A failed debug connect now shows the real J-Link reason instead of a Python traceback.** `validate_speed()` returned the caller's value unchanged, so an integer speed made the connect-error message's `', '.join(speeds_to_try)` raise `TypeError: sequence item 0: expected str instance, int found` — which replaced the actual diagnosis (e.g. "Failed to power up DAP", "Cannot connect to target") in the console. It now returns a normalized string, and the gdbserver argv stringifies the speed as well. * **A leftover GDB server can no longer wedge the next connect on its port.** Cleanup before starting a J-Link GDB server was anchored on the probe serial (`-select USB=`), so a server left running under a different `-select` tag kept holding the GDB port and the two servers collided ("Failed to open listener port 2331" on one, "Failed to power up DAP" on the other), deadlocking the probe. The port itself is now swept before binding, matched on the exact `-port ` token so sibling probes on other ports are untouched. * **The connect-failure message now includes the J-Link server's real log.** The failure path previously always printed "No log available" and hid the server's actual complaint; the server's on-disk logfile is now read back on failure. * **An opaque "RTT auto-detection failed: 'LAGER\_BOX\_COMMANDS'" warning is now actionable.** `get_device()` raised a bare `KeyError` when that variable is absent — the state when a script is exec'd into the Lager Box container directly rather than run through lager. It now raises a clear message explaining the variable is unset and the device must be passed explicitly. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.16 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.16/) # Version 0.31.2 Source: https://docs.lagerdata.com/source/release-notes/v0.31.2 July 8, 2026 ## Bug Fixes * **USB-hub nets work from `lager python` scripts instead of raising `OSError: open failed`.** libusb access to a Yepkit YKUSH or Acroname hub is exclusive, and the drivers cached the open handle indefinitely — so after the first `lager usb` command the Lager Box server pinned the hub, every separate process (each `lager python` script runs in its own subprocess) failed to open it, and only a container restart recovered. Each operation is now a fresh open, operate, release cycle, serialized within and across processes by a per-hub lock; different hubs never block each other. Note: the per-operation reconnect adds roughly 2 seconds to each Acroname operation (YKUSH is unaffected at \~0.1 s). * **Keithley 2281S measurement parsing.** Current/voltage reads that come back as multi-field or unit-suffixed responses are now parsed robustly instead of raising or returning an incorrect value. * **VISA-resource net mapping.** Nets backed by a VISA resource now resolve to the correct instrument backend, fixing misrouted access to VISA-connected supplies and meters. * **Windows-safe `lager update`.** SSH-key setup and the update flow no longer crash on Windows hosts (broadened error handling around `ssh-copy-id` and the container update steps). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.2/) # Version 0.31.3 Source: https://docs.lagerdata.com/source/release-notes/v0.31.3 July 10, 2026 ## Bug Fixes * **`lager install` deploys instrument udev rules from every CLI install method.** The rules were copied from the host's repo checkout, which only exists for editable/source installs — a pip-installed `lager-cli` (the common case) has no `box/` directory, so installs completed with a scroll-by warning and fresh Lager Boxes came up with no instrument udev rules or usbtmc blacklist. Both now install from the Lager Box's own checkout at exactly the deployed version, the `lager` group is created if missing, a failed deploy aborts the install instead of warning, and post-deployment verification checks the rules, group, and blacklist explicitly. * **Box-config passwordless sudo works on Lager Boxes whose login user isn't `lagerdata`.** The sudoers rule written by `lager install`/`lager update` hardcoded the `lagerdata` username, so on boxes with a different login user the grant never matched — install ended with "Sudoers file installed but `sudo -n apt-get` still fails" and `lager box config apply` required manual setup. The rule now names the box's actual login user (validated before being interpolated into sudoers content), already-provisioned boxes re-bootstrap automatically on their next `lager update`, and the manual-fix snippets shown on failure name the right user too. * **Fresh-box installs no longer fail at container start with "permission denied ... docker.sock".** When the install itself installs docker, the new group membership only takes effect on a new SSH login; the script now cycles the SSH connection automatically and continues. The docker install is also hardened for boxes where docker was ever removed (stale systemd socket units made the reinstall fail with "Device or resource busy"). * **SSH key setup is no longer silently skipped for clients with connection multiplexing.** The "Passwordless SSH already configured" check could ride an existing authenticated connection and false-positive, leaving the box unusable for `lager update`. The check now forces a genuinely fresh connection. ## Improvements * **The end-of-install sudo prompt no longer times out on a slow (or absent) operator.** Install now checks whether the passwordless-sudo grant is already live and skips the prompt entirely on re-installs; genuine first-time setups get a 10-minute window instead of 2 minutes. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.3/) # Version 0.31.4 Source: https://docs.lagerdata.com/source/release-notes/v0.31.4 July 10, 2026 ## Bug Fixes * **LabJack I2C nets honor the requested bus frequency.** The LabJack's `I2C_SPEED_THROTTLE` register counts *down* from 65536 toward slower speeds, but the old conversion assumed the opposite scale, produced invalid register values, and had been papered over by clamping every request to maximum speed (\~450 kHz) — so `frequency_hz` in a net's params or `i2c.config(frequency_hz=...)` was silently ignored. The throttle is now computed correctly from the requested frequency, clamped to the firmware floor, and degrades to maximum speed only if the firmware rejects the value. * **LabJack I2C auto-recovers from a wedged bus (error 2720).** A slave whose internal bus timeout fires mid-transaction — e.g. at very slow clock speeds — can hold SDA low, failing every subsequent transaction with `I2C_BUS_BUSY`. Transactions now retry once with the firmware's bus-reset option enabled, clearing the stuck slave transparently. * **LabJack I2C scan no longer returns empty on a wedged bus.** The address sweep swallowed per-probe errors, so a bus stuck in `BUS_BUSY` made every probe fail silently and the scan reported no devices. The sweep now enables the firmware bus reset as soon as one probe reports `BUS_BUSY` and keeps it on for the remainder of the sweep. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.4/) # Version 0.31.5 Source: https://docs.lagerdata.com/source/release-notes/v0.31.5 July 10, 2026 ## Bug Fixes * **UART nets survive USB re-enumeration.** When a UART adapter re-enumerated mid-session (hub power-cycle, DUT reflash, accidental replug), the Lager Box kept a stale open file descriptor on the vanished tty — which killed the stream and pinned the old `/dev/ttyUSB*` number so the device came back under a new one — and the "session already active" guards then refused clean reconnects. The Lager Box now closes the port and releases the session the moment a read fails, and transparently re-resolves and reopens the adapter with backoff (up to 60s): by USB serial when the adapter has one, otherwise by vendor/product + physical USB port + interface — so serial-less adapters and multi-port chips (FT4232H channels) heal in place. Applies to `lager uart` sessions, the HTTP stream endpoint, and on-box monitor modes. The CLI shows `[reconnecting...]` / `[reconnected]` notices during the gap; older CLIs simply resume streaming. * **New UART nets are saved with a durable USB identity.** Creating or re-saving a UART net (TUI or `lager nets add`) now records a `usb_identity` snapshot of the adapter alongside the existing `pin`, so the net keeps resolving across replugs and reboots even when it was created from a raw `/dev/ttyUSB*` path. Existing saved nets are untouched and keep working exactly as before — re-save a net once to upgrade it. ## Improvements * **`lager nets` shows where a UART device actually is.** The Channel column now displays the node the device owns right now (resolved live from its durable identity), so it stays truthful after a re-enumeration shuffles tty numbers; unplugged devices are marked `(disconnected)`. The stored record is never modified by listing. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.5/) # Version 0.31.6 Source: https://docs.lagerdata.com/source/release-notes/v0.31.6 July 13, 2026 ## Improvements * **Uniform help pages across all net-style commands.** Every usage line now reads positionals-first with the box target last — `lager uart [NET_NAME] --box [BOX_NAME]`, `lager supply [NET_NAME] voltage [VALUE] --box [BOX_NAME]` — matching the examples each help page prints. Previously, standalone commands showed Click's stock `lager uart [OPTIONS] [NET_NAME] [ACTION]` ordering, which contradicted how the commands are actually written. Applies to standalone net commands (`uart`, `adc`, `gpi`, `gpo`, `dac`, `thermocouple`), all net-group subcommands (`supply`, `scope`, `i2c`, `spi`, `debug`, `usb`, `nets`, ...), and the box-scoped `hello`/`instruments`. * **`lager uart`'s `serial-port` action is documented and validated.** The help body now explains it (prints the `/dev` path backing the net instead of connecting), and an invalid action fails with a clear error naming the valid value. * **Box lock holder types are open-ended.** The Lager Box lock endpoint no longer reclassifies unrecognized `holder_type` values as auto-expiring `ephemeral` locks, so reservations written by newer or third-party services can never be silently reaped. `lager boxes` displays the holder email for any `::` reservation string. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.6 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.6/) # Version 0.31.7 Source: https://docs.lagerdata.com/source/release-notes/v0.31.7 July 13, 2026 ## Bug Fixes * **`lager install` no longer writes a DNS server Docker cannot parse.** The installer copies the box's upstream resolvers into `/etc/docker/daemon.json` so image builds resolve reliably, but it previously trusted every value systemd-resolved reported. On a network that advertises DNS over IPv6 router advertisement, that includes a link-local resolver with a zone id (`fe80::1%3`) — and Docker **refuses to start** when any `dns` entry is not a bare IP address, rather than skipping it. Because `daemon.json` persists, the daemon stayed down across reboots, and re-running the installer undid any manual repair. Resolvers are now validated before they are written; link-local, loopback and unparseable values are dropped and named in the install log. * **A Docker DNS change that doesn't take is rolled back.** `daemon.json` is backed up first, and if Docker will not start with the new configuration, the previous file is restored and Docker is restarted on it. Pointing Docker at the box's resolvers is an optimization, and it can no longer leave a box worse off than it found it. * **The installer stops when the box's Docker daemon is down.** Previously it continued for six more steps and failed with a bare "Cannot connect to the Docker daemon" from `start_box.sh`, far from the actual cause. It now checks the daemon before deploying and reports the commands needed to diagnose it. ## Improvements * The Docker DNS logic moved into `configure_docker_dns.sh` / `configure_docker_dns.py` and is covered by unit tests. * The install step counter no longer prints `[8/7]`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.7 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.7/) # Version 0.31.8 Source: https://docs.lagerdata.com/source/release-notes/v0.31.8 July 13, 2026 ## Bug Fixes * **`lager uninstall --all` removes the artifacts today's install creates.** The old udev glob never matched the shipped `99-instrument.rules`, and the usbtmc modprobe blacklist, the `lager-box-config` sudoers file, the firewall helper script, the lager sysctl config, and the `lager` group were never removed at all. The removal list is now a single specification shared by the confirmation listing, `--dry-run`, the removal session, and the unit tests, so it cannot silently drift from what install creates. Deliberately left in place: docker itself (packages, buildx, the daemon.json DNS entry) and pip/apt packages. * **Privileged removals actually happen (and report honestly) on Lager Boxes without passwordless sudo.** Each sudo step used to fail silently and print "done" — a plain uninstall could leave `/etc/lager` behind while claiming success. All privileged steps now run in one interactive session (at most one sudo password prompt) with per-step results, and failures are summarized instead of hidden. * **`--all` removes this machine's key from the Lager Box's `authorized_keys`.** Previously the "deploy keys" cleanup deleted box-side private keys that modern installs never create, while the actual access grant survived. The output calls out that the next SSH connection will require a password. * **`--keep-config` is honored together with `--all`**, preserving `/etc/lager` (saved nets) through an otherwise complete removal. * **`--dry-run` inspects the real artifact list** and no longer reports `/etc/lager` as "(not found)" on Lager Boxes where reading it required sudo. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.8 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.8/) # Version 0.31.9 Source: https://docs.lagerdata.com/source/release-notes/v0.31.9 July 13, 2026 ## Bug Fixes * **UART reconnect can no longer land on a look-alike adapter with a clone serial.** Many USB-serial adapters ship with a non-unique programmed serial (e.g. several CP210x units all reading "0001"). If such a device dropped mid-session, the v0.31.5 reconnect could match a sibling adapter with the same serial while the real device was still off the bus — attaching the session to the wrong hardware. Identity resolution now treats a serial shared by multiple live devices as untrusted: the physical port must match, and reconnection keeps retrying until the real device returns. New identity snapshots record a bus-duplicated serial as null, pinning the net to its physical port outright. Nets on clone-serial adapters that were enriched under v0.31.5 pick up the corrected snapshot on their next re-save — re-save only while every adapter sits on the tty its net expects, since enrichment snapshots whatever device the stored pin currently points at. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.31.9 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.31.9/) # Version 0.32.0 Source: https://docs.lagerdata.com/source/release-notes/v0.32.0 July 20, 2026 ## Features * **CLI-to-box communication now runs on the box's :9000 hardware-service API.** Net commands (gpio, uart, watt, energy, battery, supply, usb, and more), plus ble, webcam, arm, wifi, router, blufi, box management, solar, net management, and binaries, all use dedicated HTTP handlers with in-process drivers — replacing the legacy :5000 script-upload model and its per-call subprocess spawn. Commands against a box running an older image now warn clearly ("run: lager box update") instead of degrading silently. * **Instrument claims are coordinated with `lager python`.** The box releases its direct-USB claims (LabJack, FT232H, Aardvark, Joulescope/PPK2, Phidget, Dexarm) before a user script runs and re-claims afterward, so scripts that open instruments directly no longer fight the warm device cache. * **`lager login` — authentication for gateway-fronted boxes.** Deployments that place an authenticating reverse proxy in front of a box are now fully supported: the CLI discovers the auth server from the box's 401 response, `lager login` stores a session (0600 on disk, transparent refresh, MFA supported), every CLI-to-box request attaches the session automatically, and denials explain exactly what to run. Boxes without a gateway are completely unaffected — no prompts, no stored tokens, no behavior change. * **`start_box.sh --no-publish` for reverse-proxy deployments.** Runs the box container reachable only on the internal Docker network, and the chosen mode persists across restarts so an update can't republish ports out from under a proxy that owns them. `--publish` restores the default. Default behavior without either flag is unchanged. ## Bug Fixes * **Webcam start/url/stop commands crashed with a `TypeError`** after the :9000 migration (an internal parameter collision); all three work again, and `webcam start` on an access-gated box now notes that the stream URL is not directly reachable there. * The CLI test suite's SIGPIPE crash and several pre-existing test failures. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.0/) # Version 0.32.1 Source: https://docs.lagerdata.com/source/release-notes/v0.32.1 July 21, 2026 ## Bug Fixes * **`lager adc` / `lager dac` failed on every named channel.** The migrated dispatchers' channel resolver only accepted integers, but adc/dac nets are saved with named pins (`AIN0`, `CH0`, `DAC0`) on LabJack T7 and MCC USB-202 — so every read/write on those nets failed with "Invalid channel pin". Named pins now pass through to the drivers, which already parse them. Hardware-verified on both instrument families. * **`lager supply set` failed with "Unknown action: set\_mode"** for every supply model, and **`--ocp`/`--ovp` on `supply voltage`/`supply current` were silently discarded** — the command reported success but protection limits never reached the instrument. Both work now, with hardware-limit validation; a protections-only call (e.g. `voltage --ovp 6` with no value) applies the protection. `clear-ocp`/`clear-ovp` no longer 502 on EA PSB supplies. * **`set_model('discharge')` on the Keithley 2281S raised an error in 0.32.0**, breaking HIL flows that select discharge battery simulation over SCPI. Discharge is the instrument's always-available idle default, not a stored model — the request is now treated as satisfied, and strict empty-slot detection for numbered slots is unchanged. * **A Joulescope JS220 could be lost until a container restart after a `lager python` claim handoff.** The open path now retries transient post-handoff failures with a short backoff, and a wedged USB context recovers with an automatic \~2s service respawn instead of requiring manual intervention. * **Hardware errors printed a raw Python dict containing the full box-side traceback**, and an internal proxy failure printed a literally empty "Hardware error: ". Driver errors now surface as their one-line message (the traceback stays in box logs), and connection failures name their cause. * **A slow box-side operation was misreported as "cannot reach box"**. The CLI now distinguishes a read timeout (box reachable, operation still running) from a genuine connection failure, and USB commands get a 30s first-contact budget for slow hub discovery. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.1/) # Version 0.32.2 Source: https://docs.lagerdata.com/source/release-notes/v0.32.2 July 22, 2026 ## Features * **The first command against a freshly-gated box now just works.** The CLI learns a box's auth server from the box's first 401 response, so the very first command against a newly-guarded box used to fail with a "re-run this command" message. That request is now retried once, transparently — the caller gets the authenticated response and never sees the round trip. A plain box never receives the token (only a gateway sends the discovery header), and genuine denials — revoked session, no access grant, auth server unreachable — still raise their actionable errors. * **`lager whoami` — access-gateway sign-in status at a glance.** Shows which auth servers you're signed in to, as whom, and whether each session is active, auto-renewing, or expired (with the exact `lager login` command to fix it). It's the first thing to run when a box reports an authorization problem. * **Clearer gateway auth errors, each linking to a new [Signing In](/source/reference/cli/login) docs page.** "Signed in but not authorized", "requires sign-in", and "session rejected" are now distinct messages with their own fixes, and the docs page walks through every gateway message and what to do about it. * **The Rust crate gets a first-class "Rust API" tab on the docs site** — overview, net types, cargo-test guide, debug/UART, and auth — with a side-by-side Rust example in the first-test guide. ## Changes * **`lager box config` is now `lager box-config`, `lager box dut` is now `lager dut`, and `lager authorize` is now `lager ssh-setup`.** The `box` group is flattened to top level, and the SSH-key setup command no longer reads like authentication now that `lager login` exists — it installs this machine's SSH key on a box (one-time passwordless-SSH setup), which the new name says plainly. All three old spellings keep working as hidden aliases that print a DEPRECATED warning on stderr; they will be removed in a future release. Help text, error hints, docs pages, and docs navigation all follow the new names. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.2/) # Version 0.32.3 Source: https://docs.lagerdata.com/source/release-notes/v0.32.3 July 22, 2026 ## Bug Fixes * **`lager update` could report "already at version X" on a Lager Box that was never actually updated.** The update stops and removes the box's containers before rebuilding the image, so a failed build left the box with no services at all — and the retry's early-exit check looked only at source state, so it printed a green success on a dead box. The check now also requires the lager container to be running and the last successfully deployed version to match the box's code — a box left dead, or left serving an older build by an interrupted update, gets a real rebuild and restart instead of a false success. `lager update --check` surfaces both states ("Container: NOT RUNNING" / "running a STALE build") and exits 1. ## Improvements * **A failed rebuild no longer strands the Lager Box with no services.** Unless the cached image was wiped (`--force` / a dependency change), the update restarts the previous image, waits for the box's health endpoint, and states plainly that the update FAILED and was not applied. Every build failure also invalidates the stored build cache marker so the next run always performs a clean rebuild. * **New build-failure hint for Docker BuildKit cache corruption.** When a build fails with "failed to prepare extraction snapshot ... parent snapshot does not exist", the update now suggests the remedy: clear the box's build cache with `docker builder prune -af` and re-run `lager update` (the next build runs cold and takes longer). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.3/) # Version 0.32.4 Source: https://docs.lagerdata.com/source/release-notes/v0.32.4 July 24, 2026 ## Bug Fixes * Fixed spurious "Box requires sign-in" failures on access-controlled Lager Boxes. The CLI refreshed its sign-in session far too eagerly when the server issued short-lived access tokens — every box command became a refresh round-trip, and rapid command sequences (for example `lager nets add-all` or a verbose `lager update`) could lose the session mid-command. The refresh schedule now adapts to the token lifetime, a failed refresh falls back to the still-valid stored session instead of failing the command, and refreshes are retried only when it is safe to do so. * `lager install` and `lager uninstall` no longer remove Docker containers or images they did not create. Previously the deploy stopped and force-removed every container on the Lager Box and pruned every unused image, which could destroy unrelated software running alongside Lager. Cleanup is now scoped to Lager's own containers and images. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.4/) # Version 0.32.5 Source: https://docs.lagerdata.com/source/release-notes/v0.32.5 July 27, 2026 ## Bug Fixes * First contact with an access-controlled Lager Box now signs in automatically everywhere. Previously, the first command to reach a gated box could fail with a raw `HTTP 401` — and `lager boxes` kept failing on every run — because most commands never completed the box discovery step. Every CLI path that talks to a box (HTTP and WebSocket, including the supply/battery/uart monitors) now links the box to its auth server on first contact and retries once with your existing session. When access is genuinely denied, `lager boxes` shows a clear per-box status (`sign-in required`, `no access`, `auth server down`) with the exact `lager login` command to run, instead of a raw status code — and one gated box no longer affects the rest of the table. Boxes without access control are completely unaffected. * `lager uart` no longer reports an access-denied box as having no instruments; it now shows the sign-in error instead. * `lager status` no longer raises `NameError` on Python 3.10 when a websocket failure occurs; the original error is now reported properly. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.5/) # Version 0.32.6 Source: https://docs.lagerdata.com/source/release-notes/v0.32.6 July 28, 2026 ## Features * `lager install` and `lager update` now also install the lager CLI onto the Lager Box's host system, version-matched to the box code — so tools running on the box itself, such as a self-hosted CI runner, can invoke `lager` locally. The CLI lands in a dedicated environment at `~/.lager/venv` with `lager` available at `~/.local/bin/lager`, works on hosts where `pip install --user` is blocked by the system Python policy (PEP 668), and stays current automatically: every `lager update` — including one that finds the box already up to date — checks the host CLI and repairs it if it is missing, broken, or on the wrong version. `lager update --check` shows the pending state on a new `Host CLI:` line. If the box host's Python is older than 3.10 the step is skipped with a clear warning and the update still succeeds. ## Bug Fixes * `lager debug` subcommands (`flash`, `erase`, `memrd`, `reset`, `gdbserver`, `status`, and friends) now sign in correctly against an access-controlled Lager Box. Previously they sent no credentials at all, so every debug command failed on a gated box with an authorization error that no user action could work around. * `lager status` no longer depends on an undeclared package: `pymongo` is now installed with the CLI, removing an `ImportError` that suggested installing the wrong similarly-named package. * `lager uart` no longer fails to load on Windows. Interactive terminal mode reports clearly that it is not supported on that platform instead of crashing with an `ImportError`. ## Improvements * Security: a dependency of the Lager Box's oscilloscope daemon was updated in the source tree to close a high-severity advisory (remote memory exhaustion). Deployed daemon binaries are distributed separately from box updates; contact Lager if you use the oscilloscope daemon. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.32.6 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.32.6/) # Version 0.33.0 Source: https://docs.lagerdata.com/source/release-notes/v0.33.0 July 28, 2026 ## Features * **`GET /usb/devices` on the Lager Box** enumerates every USB device on the bus from sysfs (vendor/product id, serial, product, manufacturer, bus/dev numbers, speed) with optional `vid`/`pid`/`serial` filters. The scan is a few milliseconds and takes no exclusive device access, so it is safe to poll while waiting for a DUT to re-enumerate after a hub power-cycle or DFU detach. Consumed by `lager-rs` as `usb_devices()`. * **`POST /usb/dfu` on the Lager Box** runs `dfu-util` for USB-DFU flashing: `list`, `download` (base64 firmware, with optional vid:pid / serial / alt / DfuSe address / reset), and `detach`. A missing binary returns a clear install hint (`lager box-config apt add dfu-util`). Consumed by `lager-rs` as `dfu()`. ## Improvements * **USB hub drivers cache discovery metadata per physical hub.** Each Acroname or YKUSH operation previously re-ran a full device discovery scan. The drivers now cache the discovery result — the hub's link specification and hub class for Acroname, the resolved HID device path for YKUSH — and connect directly from it, while still releasing the hub after every operation so other processes (for example a `lager python` test) can claim it. This removes redundant discovery scans. It does not restore the \~80ms hub-port timings seen before 0.32.1: measured on a USBHub3p, a hub-port operation costs \~2.1s, of which \~1.8s is the per-operation disconnect required to leave the hub unclaimed and \~0.3s is discovery. Where the hub class is identified correctly on the first attempt, the cache saves no measurable time. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.33.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.33.0/) # Version 0.33.1 Source: https://docs.lagerdata.com/source/release-notes/v0.33.1 July 29, 2026 ## Bug Fixes * The Lager Box's MCP server no longer fails to start after version 2.0.0 of the MCP SDK was published. The box image requested that dependency without an upper bound, so any image built after the 2.0.0 release picked it up — and 2.0 moved the transport settings the server configures at startup, so the service raised immediately and nothing listened on port 8100. AI agents configured against `http://:8100/mcp` saw connection timeouts with no other symptom. The box image and the CLI's optional `mcp` extra now cap the dependency below 2.0. Boxes pick the fix up on the next `lager update --box `. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.33.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.33.1/) # Version 0.34.0 Source: https://docs.lagerdata.com/source/release-notes/v0.34.0 July 30, 2026 ## Features * **`lager nets state`** reports live hardware state for every saved net in one command. Power supplies show channel, output and measured voltage/current; USB ports show enabled or disabled; GPIO shows level; ADC and DAC show volts; an I2C bus shows the addresses that answered. Roles with no probe report nothing rather than guessing. Add `--json` for the same data unformatted. It is a separate subcommand rather than part of `lager nets` because it touches hardware: plain `lager nets` reads saved configuration only, while this takes the same instrument locks a running `lager python` test holds. Nets are probed per instrument rather than per net, so a hub with eight ports costs one connect cycle instead of eight, and the command always answers within its deadline — an instrument that has stopped responding reports nothing for its own nets instead of failing the whole bench. A Lager Box too old to support this reports a clear upgrade hint. ## Bug Fixes * **`lager debug flash` no longer leaves the target blank when the post-erase reconnect fails.** `flash` erases by default and used to reconnect the debugger between the erase and the flash. That reconnect sat inside the erase's own error handling, so when it failed the command reported `Flash erase failed`, exited non-zero, and never programmed the part it had just erased. The reconnect was not needed by either debug backend and has been removed. * **`lager debug flash` no longer reports success when nothing was programmed.** The command printed `Flashed!` and exited zero regardless of what the programmer actually did, so a run whose log read `Could not connect to target` still looked like a success — with the part left erased, because `flash` erases first. It now takes its result from the programmer's own output and reports which step failed. * **`lager nets state` no longer reconfigures the hardware it reports on.** Reading GPIO state could reset a pin's direction on a LabJack T7, releasing a line held for a target's reset, boot-mode or enable signal; and reading an analog input reset that channel's range and resolution, disturbing a measurement in progress. Both now read without writing. * **The LabJack batch probe now serialises correctly against `lager gpo`, `gpi`, `adc` and `dac`.** It took a different lock than those commands, so reading state could overlap with a command already using the same device. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.34.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.34.0/) # Version 0.34.1 Source: https://docs.lagerdata.com/source/release-notes/v0.34.1 August 2, 2026 ## Bug Fixes * **`lager update` no longer leaves a file on the box after it is deleted upstream.** The step that flattens the repository layout on the box copied additively: it overwrote changed files but never removed ones the new tree no longer contained, and it then deleted the tracked source, so the result was beyond the reach of `git checkout -f` or `git reset --hard`. A file removed upstream therefore stayed on every box indefinitely and was copied into the runtime image by the next build — boxes were found carrying a module deleted thirteen minor versions earlier. Each top-level entry is now removed and then moved into place, so a subtree is rebuilt rather than merged into and deletions take effect on the first update. Files installed at the repository root by any other route are untouched. No manual cleanup is needed. * **A source-only change now invalidates the cached Docker image.** The build hash covered only the Dockerfile and `requirements.txt`, so a pure-Python change relied entirely on the layer cache invalidating correctly. Every file under the box's source tree now feeds the hash; `__pycache__` and `.pyc` are excluded so regenerated artifacts do not force needless rebuilds. Because the stored hash on an existing box was computed with the old formula, the first update after this release rebuilds the image in full, once per box. * **A J-Link script no longer leaks onto debug operations that never asked for one.** The box kept a single script file and handed it to any operation that did not supply its own, so `reset`, `erase`, `memrd` and the gdbserver path silently inherited whichever script was written last — by a different net, by an earlier session, or by a test suite that had since finished. Scripts are now written per net, an operation with no net gets no script rather than an ambient one, and a net's script is cleared when its debug session ends. A net with a genuinely required custom `InitTarget` still gets it on every operation, and an older box's shared file is removed when the debug service next starts. * **A J-Link script no longer leaves a just-erased target unattachable.** A user `.JLinkScript` replaces J-Link's built-in per-device `InitTarget()`, and on an nRF5340 that built-in is what brings the DAP up on a blank part. When it was displaced, the attach following an erase failed with `Could not read CPUID register` — and because `flash` erases by default, one scripted flash could leave the part blank and the net failing every later attach. The attach path now retries without the script rather than wedging the net. * **`lager nets state` now says *why* a net has no state.** `state: null` meant three unrelated things with no way to tell them apart: the instrument had not answered before the request deadline, its probe failed, or the role has no probe at all. Null entries now carry a `reason` — `"deadline"`, `"no probe for role"`, or `"unreadable: "`. The command prints the unexpected ones in a footnote after the table, grouped by reason, and `--json` carries them as `live_state_reason`. A USB hub that cannot be opened now names itself and its cause instead of failing silently. A CLI newer than its box simply sees no `reason` and renders as before. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.34.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.34.1/) # Version 0.34.2 Source: https://docs.lagerdata.com/source/release-notes/v0.34.2 August 2, 2026 ## Removed * **The HTTP SSH key-authorization endpoint (`POST /authorize-key` on port 9000\) is gone**, along with its handler, its rate limiter, and the `/tmp/lager-authorized-keys.d` staging directory it wrote to. The endpoint let any caller holding the bearer token create an arbitrarily-labelled `.pub` file, and the keys it created could never be removed, because the old sync only appended. It was also the first link in a privilege-escalation chain: a container-side file write became host SSH access, and from there host root via the privileged runtime container. **If you provision boxes through this endpoint, switch to writing `.pub` into `/etc/lager/authorized_keys.d/` directly.** That directory is bind-mounted into the runtime container, so a control plane can still write it from inside the container before it has SSH access — the bootstrap path is unchanged, and keys still appear in `~/.ssh/authorized_keys` within about five seconds. No CLI command called this endpoint, so command-line workflows are unaffected. ## Changed * **`~/.ssh/authorized_keys` is now rebuilt from the key directory rather than appended to.** The box owns only the region between its `# BEGIN LAGER MANAGED KEYS` and `# END LAGER MANAGED KEYS` markers, and regenerates that region on every pass, writing a temp file and renaming so `sshd` never sees a partial file. **Deleting a `.pub` now revokes the key**, which was previously impossible, and the old check-then-append race can no longer duplicate lines — boxes have been found with five entries built from three key files. Keys installed by any other route — `lager ssh-setup`, `ssh-copy-id`, cloud-init — live outside the marked region and are preserved byte-for-byte. A key that is *also* published through the key directory becomes managed, though, so deleting its `.pub` later removes it outright, including the copy the other route installed. Use a distinct key per access path when the two must be revoked independently. Another system that manages this file must claim its own distinct marker pair; two managers sharing one pair would each rebuild the other's region on every pass. ## Bug Fixes * **`start_box.sh` is now single-instance.** Concurrent copies raced each other and accumulated across restarts — boxes have been found running ten or more at once, some months old, each having burned hours of CPU, with their key-sync loops appending over one another. The script now takes a non-blocking lock for its lifetime and exits with a clear message if another copy holds it. The background key-sync poller closes the inherited lock descriptor, so a long-lived poller cannot pin the lock against later runs. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.34.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.34.2/) # Version 0.34.3 Source: https://docs.lagerdata.com/source/release-notes/v0.34.3 August 4, 2026 ## Bug Fixes * **A wedged USB hub no longer takes every later USB command down with it.** A hub operation that hung — native hub-driver code blocking forever against a hub whose USB link is wedged, typically after a re-enumeration — held the box's USB lock for the life of the process. Every subsequent `lager usb` command and every state poll queued behind it with no timeout of its own, and the box's self-restart recovery could not help, because it only ran when an operation *raised*. A call that never returns raises nothing. Three bounds close that. A hub command that cannot get the lock within 10 seconds now answers `hub-busy` instead of queueing; a hub that cannot be claimed reports as unavailable rather than waiting forever; and each hub operation runs under a 30-second deadline. On expiry the caller gets `hub-op-timeout` and the box schedules the same supervised restart it already used for unreachable devices, which is the only thing that clears an orphaned USB context. Hubs are bounded independently, so a wedged hub no longer affects the others on the bench. * **The same treatment for hardware-service device calls.** Per-device locks are acquired with an 8-second timeout and answer `device-busy` instead of queueing forever behind a wedged instrument open or a hung driver call. The driver call itself runs under a 30-second deadline; expiry answers `invoke-timeout` and schedules the restart. A device whose operation hung keeps its lock on purpose — the stuck operation still owns the instrument, so later requests get a fast, honest "busy" rather than wedging in turn. * **`lager usb` now waits long enough to hear the box's answer.** Its timeout was 30 seconds, but the box's own limits are additive, so a wedged hub answered at around 40 seconds and the command had already given up. That surfaced as "cannot reach box", which reads as a network fault and hides both the real diagnosis and the fact that the box had already started recovering. * **Errors no longer tell you to update the box when a device is unplugged.** `lager usb`, `lager supply` and `lager battery` appended "This box image does not expose ``; update the box." to every not-found response — including the ones meaning "this net or its instrument was not found". An unplugged USB hub reported both at once, sending the diagnosis somewhere the fault never was. * **Secret files are now owned by the container user, not just locked down.** `/etc/lager/org_secrets.json` and `/etc/lager/secret_key` were tightened to mode 0600, but 0600 grants the *owner* alone — and everything that reads these files runs as the container user. A secrets file copied onto a box by hand belonged to the host login user, so tightening it locked the runtime out of its own secrets. Nothing failed loudly: secret injection simply returned empty, and scripts broke far from the cause. `lager update` now repairs ownership of both files automatically, including on boxes that are already up to date. The box also repairs what it can at boot and prints an unmissable warning, with the exact commands to run, when it finds a secrets file the runtime cannot read. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.34.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.34.3/) # Version 0.34.4 Source: https://docs.lagerdata.com/source/release-notes/v0.34.4 August 5, 2026 ## Features * **`lager diagnose` now covers USB hub nets.** They used to fall through to a generic "check the role command yourself", which is no help when the question is why the hub will not answer. The new section reports what host-side tools structurally cannot: the vendor SDK's own view of the hub. A hub can be enumerated by the kernel, held by no other process, and still invisible to hub discovery — and in that state every other signal looks healthy. It also reports the device's USB device number against the rest of the bench. The kernel assigns those in order, so a device far above its neighbours has re-enumerated many times since they did. On the bench that prompted this work, the failing hub sat at 93 while every other instrument was in the 60s; that one number was the most useful fact in the investigation and nothing surfaced it. ## Bug Fixes * **The box no longer restarts its HTTP service for a USB hub it cannot reach.** A self-restart repairs exactly one thing: a USB handle the process orphaned across a re-enumeration, which only a fresh process can reopen. The Acroname driver keeps no such handle — it opens, operates and disconnects on every call — so there was never anything on that path for a restart to repair. It fired anyway, because the check asked only whether the device was still enumerated, and the kernel keeps a device node for hardware that has stopped answering on the wire. Observed on a two-hub bench: a hub that would not open triggered the restart twice, and each respawned process failed identically seconds later. Nothing was fixed, and every other in-flight operation — UART sessions, running scripts, hardware calls — was dropped to do it. Drivers now declare whether they hold a USB context between operations, and those that do not are skipped. The pyvisa and HID paths, where a session really does persist, are unchanged. * **`lager diagnose` reported the wrong instrument on a bench with two of the same model.** The device lookup matched on vendor and product id and returned the first hit. It now prefers an exact serial match, falling back to vendor/product so a device with an unreadable serial is not lost. * **`lager diagnose`'s kernel-log section has never worked.** It shelled out to a command the box image does not ship, so the field has read "unavailable" on every box since it shipped. It now reads the kernel log directly and, where the container is not permitted to, says so and points at where the history actually lives. ## Improvements * **A USB hub that will not open now tells you what to do about it.** The box already worked out which of three faults it was looking at — nothing from this vendor on the bus, the hub's serial present but not answering, or other devices present and none of them the one addressed — then flattened all three into a single line with vendor return codes appended. The terminal showed a wall of codes and no sentence saying whether to check a cable, a power switch, or the net's address. Unknown entries in `lager nets state` now carry a machine-readable `reason_code` alongside the human reason, and the footnote adds one remedy per affected group — red when the fault is hardware, yellow when the bench is more likely in a normal state. `--json` output carries `live_state_reason_code`. The box always sends a complete, self-sufficient human reason, so an older CLI, or a newer one seeing a code it does not recognise, renders exactly as before. A hub the kernel has enumerated but that will not answer is now logged as an error rather than a warning. That case is always hardware and always worth acting on; a hub that is simply absent is a normal bench state. * **An Acroname hub open is retried once when the bus says the hub is there.** A hub caught mid-re-enumeration is on the bus a beat before discovery will return it, so an operation landing in that window failed outright. The YKUSH driver has always retried once for this reason; this one did not. The retry is gated on the bus check, so a hub that is genuinely absent still costs exactly one attempt, and it is suppressed on the polling path so the whole-bench state sweep's timing is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.34.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.34.4/) # Version 0.35.0 Source: https://docs.lagerdata.com/source/release-notes/v0.35.0 August 6, 2026 ## Features * **Nets can now carry voltage and current ceilings that the Lager Box enforces.** A setpoint above a net's ceiling is refused before it ever reaches the instrument, so a mistake in a test script cannot drive hardware past what the bench can survive. Ceilings are stored on the net, not the instrument, so they follow the net when you swap the supply behind it. Enforcement is two-tier, and the difference is worth knowing when you decide what to rely on. The hard tier runs inside the Lager Box's hardware service — a separate process, and the only route to the instrument for the nets it covers — so a test script cannot talk around it. A second, advisory tier catches honest mistakes earlier but can be bypassed by a script that reaches for a driver directly; treat it as a convenience rather than a guarantee. Ceilings are always re-read from the Lager Box's own saved nets, never taken from the incoming request, so a script on a shared bench cannot raise its own limit. Inline overvoltage and overcurrent trip settings are checked too, since those would otherwise lift the instrument's built-in guard above the net's ceiling. A net can also refuse erase and flash outright, and the call rate per net is capped so a runaway loop stays bounded. This is opt-in. A net with no limits configured is unrestricted, so existing benches behave exactly as they did before upgrading. * **RTT is now bi-directional, so firmware with an interactive console can be driven from the command line.** The debug probe's RTT connection was always full duplex, but the only remote transport was one-way: you could read a target's log output and had no way to answer it. Firmware that takes commands over RTT was reachable from a script running on the Lager Box and from nowhere else. `lager debug gdbserver --rtt --interactive` now sends what you type to the target while its output streams back as before. The output side is still raw bytes, so an existing defmt pipeline keeps working and composes with the new flag: ```bash theme={null} lager debug gdbserver --rtt --interactive 2>/dev/null | defmt-print -e app.elf ``` What you type is echoed by your terminal rather than mixed into that stream, so what reaches `defmt-print` is exactly what it was before. `--rtt-channel` selects a channel other than 0 in both directions, and plain `--rtt` is untouched. This needs firmware that declares an RTT **down** buffer on the channel in use. `defmt-rtt` on its own sets up only the outgoing buffer; without an incoming one the target quietly discards whatever you send, which looks like a problem on the host side and is not one. * **A `lager python` script can now command interactive firmware and read its decoded logs in the same session.** `rtt_defmt()` decodes a target's defmt logs into readable lines, but it could only listen — and opening a second, raw RTT session alongside it is not a way around that, because a target's RTT connection accepts one reader at a time. Decoding a target's logs therefore meant giving up the ability to talk to it. It now accepts writes, so a test can send a command and assert on the reply it decodes: ```python theme={null} with dbg.rtt_defmt(elf='build/app.elf') as logs: logs.write(b'self_test\n') line = logs.read_line(timeout=5.0) ``` As with the CLI flag above, this needs firmware that declares an RTT down buffer on the channel in use. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.35.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.35.0/) # Version 0.36.0 Source: https://docs.lagerdata.com/source/release-notes/v0.36.0 August 12, 2026 ## Bug Fixes * A `lager python` script whose client vanishes now gets to finish its teardown. When the CLI is hard-killed, the network drops, or a CI job is cancelled, the Lager Box previously terminated the script outright — no `finally` blocks, no context managers, no `atexit` — and only noticed the dead client the next time the script wrote output. Disconnects are now detected sub-second even for silent scripts, the script is interrupted with SIGINT first, and a progress-aware watchdog gives cleanup work a grace window before escalating. New jobs wait for a previous job's teardown to clear before starting. * `lager python` now survives more than Ctrl+C: SIGTERM and SIGHUP stop the job too. A killed terminal or a supervisor's TERM (including a cancelled CI job) previously ended the client silently — the script on the Lager Box kept running and the box lock stayed held. All three stop signals now trigger the same remote stop and lock release, and the handlers are restored afterwards so a second signal can always break out. * Killing a `lager python` job now stops every process the job started, not whichever one a scan happened to find first. All of a job's processes are collected before any is signalled, delivery is reduced to one signal per process group so a graceful interrupt is not cut short by its own duplicate, and one grace window is shared by the whole job — a multi-process job no longer holds the kill request open while each process is resolved in turn. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.36.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.36.0/) # Version 0.36.1 Source: https://docs.lagerdata.com/source/release-notes/v0.36.1 August 12, 2026 ## Bug Fixes * Two identical USB-UART adapters no longer collapse onto one tty. The Lager Box's USB scanner handed every device of the same model a reference to one shared channel table, so with two same-model adapters plugged in, both scan entries advertised whichever device was enumerated last — adding a net for the second adapter recorded the first adapter's tty and USB identity, and both nets silently drove the same physical port. Scan entries now carry their own channel lists, and the CLI's net-add flows prefer the per-device `tty_paths` field, so an updated CLI offers the right tty even against a Lager Box that predates this fix. A net saved with a cross-wired identity does not self-correct: re-add it after updating. * One slow USB hub no longer reports the rest of the bench as timed out. `/nets/state` probes every USB hub serially inside one work unit, so a hub that burned its driver timeout consumed the whole request budget and every hub after it came back `reason: "deadline"` while being perfectly healthy. The request deadline is now sub-budgeted per hub: each hub's probe is clamped to the time actually remaining, and a hub the budget cannot cover is skipped with its own reason and a `hub-skipped` code — "the budget ran out before this hub's turn" and "this hub is slow" are no longer the same message. * Every Lager Box now installs the same Acroname BrainStem SDK. The box image installed `brainstem` unpinned in a build-cached layer, so which SDK a box ran depended on when that layer was last invalidated, and two boxes built weeks apart could behave differently. The SDK is now pinned, and `lager diagnose` output for USB hubs reports the installed version as `sdk_version` so version skew between boxes is visible from the CLI. ## Improvements * The BluFi AES import follows CFB into cryptography's new module home, silencing the deprecation warning on cryptography 50.0 while remaining compatible with older installs. Ciphertext is byte-for-byte unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.36.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.36.1/) # Version 0.36.2 Source: https://docs.lagerdata.com/source/release-notes/v0.36.2 August 12, 2026 ## Bug Fixes * `lager install` could take Docker down on a fresh Lager Box and then blame the configuration. Docker's service ships a start-rate limit of three starts per minute, and the installer used four in eleven seconds — so systemd refused the last one and latched the service into a failed state where every later restart, including the installer's own retry, failed instantly without attempting a start. Docker itself was healthy throughout. The installer now performs one service start per step and clears the failure counter before each restart, so a Lager Box already stuck in that state recovers instead of staying wedged. The failure is also diagnosed correctly now: it previously reported a malformed `/etc/docker/daemon.json` simply because writing that file is the preceding step, which sent debugging down the wrong path. * `lager uninstall` warned about a lock heartbeat failure on every successful run. The command holds the Lager Box lock across its teardown so a concurrent `lager python` cannot be pulled out from under a running test — but its first step removes the container that serves the lock, so every later heartbeat was a request to a server the command had just deleted. The warning was guaranteed, and it read as a fault when it was the uninstall working. The lock session is now dissolved once the container is confirmed gone. If the removal fails, nothing is dissolved and a heartbeat failure is real signal again. * `lager install` warned that its lock was about to expire, on every install. A full image rebuild spends roughly a quarter of an hour with the container — and therefore the lock server — deliberately down, so the renewals during that window could not succeed. Install now declares that outage up front rather than reporting it as a fault, and holds its lock on a longer lease sized to the work. Relatedly, the heartbeat no longer warns on the first missed renewal; it waits until half the lease has gone unrenewed, and says how long it has been. * `lager uninstall --keep-config` left behind a lock that nothing could clear. The teardown wrote a lock record with no expiry into the configuration directory it was asked to keep, and a record with no expiry is never reclaimed — so the Lager Box stayed locked indefinitely and no later command could release it. The teardown now clears that state explicitly. * `lager install` warned that it could not reach the Lager Box on installs that had just succeeded. The connectivity check ran a single probe seconds after the container started, racing the services still coming up inside it, and failed often enough on healthy hardware that the warning was routinely ignored — which is the state in which it can no longer report a real failure. It now retries for thirty seconds and distinguishes a genuine failure from a slow start. * `lager uninstall --dry-run` could report a Lager Box as empty when it had simply failed to look. A query that timed out was indistinguishable in the output from one that found nothing, so a preview could describe a teardown as a no-op when it was not. A timed-out query now says so explicitly and does not claim absence. * `lager uninstall` reported removing a Lager Box from local configuration when the entry was still there. Only the global configuration is written, but a Lager Box defined in a project-level `.lager` file was reported as removed and then kept resolving. The command now names which file still defines it, and no longer reports a project-only Lager Box as "not found". * Provisioning could stop and wait for a keypress with nobody there to answer it, leaving automated installs hanging instead of finishing or failing. * The install summary pointed at `lager nets create`, which is not a command. It now shows `lager nets add`. ## Improvements * The Lager Box image pins the third-party MCC DAQ library it builds from source. It was previously built from whatever the upstream default branch held at build time, so two Lager Boxes built a week apart could carry different library code with no corresponding change on our side. It is now pinned to an upstream release tag. * The installer no longer installs pyOCD and no longer offers it as a debug backend. Debugging runs on J-Link and OpenOCD; pyOCD was installed on every Lager Box and advertised in help output despite not being a supported path. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.36.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.36.2/) # Version 0.37.0 Source: https://docs.lagerdata.com/source/release-notes/v0.37.0 August 14, 2026 ## Removed * The `lager-mcp` console script. It targeted Lager Box code that the `lager-cli` package has never shipped, so running it produced an immediate `ModuleNotFoundError` on every install — and installing the `mcp` extra did not help, because that extra supplies the MCP SDK rather than the Lager Box package. The script was never how the MCP server runs: the Lager Box starts it in-container and serves it on port 8100, which is what the MCP documentation describes and what clients connect to. Installs and updates previously created a `~/.local/bin` symlink to the script, so a Lager Box deployed from an older version carries a link that cannot work. `lager install` now removes that link instead of creating it, and those Lager Boxes self-heal on the next deploy. * The vendored `pyelftools` copy, whose ELF and DWARF parsing could not be imported in any environment. It had been copied in without one of its subpackages, so 34 of its 45 modules raised `ModuleNotFoundError` on installs and development checkouts alike — including every module that actually reads an ELF file. Its only consumer was a `lager debug gdb` command that was never registered and so could never be invoked; both are removed. This deletes roughly 13,000 lines and changes no behavior that worked. `PyCRC` is now the only third-party code bundled with the CLI, and `NOTICE` reflects that. ## Bug Fixes * `lager terminal` asked users to install one of its own dependencies by hand. `prompt_toolkit` is imported by the terminal UI but was never declared as a requirement, so on a clean install the command printed `Install with: pip install prompt_toolkit rich` and showed help instead of launching. It is now declared. A new packaging check installs the built package into a clean environment and imports every CLI module, so an undeclared import fails the build rather than reaching users. * Two CLI helper modules could not be imported from an installed copy of the CLI. Both imported Lager Box code that the CLI package does not ship, so both raised `ModuleNotFoundError` anywhere the Lager Box source was not already on the path — which is every clean install. Lager Box behavior was never affected, since these files are uploaded to the Lager Box and run there. Both now defer the import to the point of use. * BluFi provisioning no longer depends on a key-agreement primitive that current `cryptography` releases refuse outright. Release 50.0 drops finite-field Diffie-Hellman, and on it the group BluFi uses fails at parameter construction — so key generation, and therefore provisioning, could not complete at all. The exchange now uses direct modular exponentiation over the same published parameters, which works on both the pinned runtime version and 50.0. The wire format is unchanged and is pinned by known-answer tests, including the padding rule behind an intermittent failure that would otherwise appear in roughly one exchange in 256. * A second BluFi security negotiation on one client could derive a key from two peer public keys joined together, because the receive buffer was not cleared with the rest of the security state. Not reachable in current use, where each request builds a fresh client, but it failed silently rather than loudly. ## Improvements * Interactive USB hub commands are fast again — roughly 150 ms, rather than the 7 to 9 seconds each had been costing. A change in the 0.32 series moved the Acroname driver to connecting and disconnecting around every operation, and the cache meant to keep repeat connections scan-free had three paths where it silently ran a full USB scan anyway. Warm connections are now genuinely scan-free, a short idle window lets a burst of commands share one connection, and every hub cycle logs a per-phase timing breakdown that escalates in the log when a cycle runs long — so a slow connection path is visible from a Lager Box log rather than invisible. All existing timeouts and fail-fast behavior are preserved, and the 1 Hz state poll cannot hold a hub claimed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.37.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.37.0/) # Version 0.37.1 Source: https://docs.lagerdata.com/source/release-notes/v0.37.1 August 17, 2026 ## Bug Fixes * A short Python test that switched an Acroname USB hub net could abort at the very end, after its own work had already succeeded. The test printed its results, exited cleanly, and then died during interpreter shutdown, which the CLI reported as exit code 250 — enough to fail a test that had in fact passed. Version 0.37.0 introduced a short reuse window in which a hub stays connected after an operation, so that a burst of commands pays one connection instead of one per command. Nothing closed that connection if the process exited while the window was still open, and the hub vendor's library aborts when it finds its sockets still open at shutdown. A Lager Box now always closes a held hub connection before the process exits, and waits for a disconnection already under way rather than letting it be cut short. Scripts that exit more than a few seconds after their last hub command were never affected. The fix costs roughly a second at exit for a script that held a hub open, and the reuse window still more than pays for it: an eight-step hardware suite completes in 77.9 seconds against 92.6 seconds on the pre-0.37.0 connect-per-command driver. * Every successful `lager update` that rebuilt the Lager Box container warned that its own lock was failing. Update stops the container and rebuilds it, and that container serves the lock the command itself holds, so renewals across that window could not succeed and accumulated into a warning about a Lager Box that was busy doing exactly what it had been asked to do. Update now declares the outage for the duration of the rebuild and resumes once the Lager Box is confirmed healthy again. No lock semantics change, and a renewal failure outside that window is real signal again. * `lager update --check` could promise a cached build immediately before a ten-minute rebuild. On a Lager Box still using the older directory layout the preview reported a valid cache and an estimate of about ninety seconds; the run then reorganised the tree and rebuilt from scratch. The preview ignored the pending reorganisation, which necessarily invalidates the cache, and it also reported "cache valid" when it had simply been unable to measure. Both are fixed: an unmeasurable cache is now reported as unknown, and a Lager Box that needs the reorganisation no longer reports that there is nothing to do. * The sudoers snippet printed by `lager box-config mount` wrote a strict subset of the file it replaced. An operator who pasted it fixed mount preparation and silently removed the other grants that `lager box-config apply` needs, and left no marker, so the next update asked for the sudo password again. It now prints exactly what `lager install` and `lager update` write. * A change to the internal marker that decides whether the box-config sudoers file needs rewriting could not have taken effect, because the checks that read it were hardcoded rather than reading the marker itself. Changing it would have moved the file Lager writes without moving the file it looks for, so the rewrite would have been skipped on every Lager Box. ## Improvements * The three sudoers files Lager writes now say on the Lager Box that Lager rewrites them. Each is regenerated in full on every run, which is deliberate and unchanged — but there was previously no sign of that on the Lager Box, so an operator who added a grant inside one of those files got it silently erased by the next install with nothing to explain where it went. Each file now opens with a header naming the command that rewrites it and pointing operator grants at a separate file, which survives every Lager run. No existing Lager Box is touched and no Lager Box prompts for a sudo password. * The documentation now states that the Lager Box login account is root-equivalent by design. Provisioning requires root, and several of the grants Lager must install to do it are each a full path to root. Nothing about that posture changes here, but two places previously described those grants as a privilege boundary, which they are not. Treat anyone holding the Lager Box login account's SSH key as holding root on that Lager Box. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.37.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.37.1/) # Version 0.37.2 Source: https://docs.lagerdata.com/source/release-notes/v0.37.2 August 17, 2026 ## Features * Release tags now publish a pre-built Lager Box container image to GitHub Container Registry, as groundwork for faster updates. Nothing consumes these images yet: `lager update` still builds on the Lager Box on every path, and no Lager Box behavior changes in this release. Publishing ships first so that pulling can later be tested against real images. ## Improvements * Cold Lager Box container builds are faster. Node and npm now come from the official upstream tarball (verified against its published checksums) instead of Debian packages that pulled in roughly 400 packages the Lager Box never touches; the `cryptography` dependency moved to a version with a pre-built wheel for the image's Python, so it is no longer compiled from source on every cold build; and several build tools that nothing used were dropped. Note that Node's major version moves from 18 to 20: a Lager Box carrying globally-installed npm packages with compiled native modules should be updated once with `lager update --force`, which rebuilds the volume those packages live in. * The update progress bar now names what the container build is currently doing — for example `Building container... [pip install ...]` — instead of holding one unchanging label for the several minutes a cold build can take. `--verbose` output is unchanged. * A USB hub disconnect that fails during teardown is now logged rather than silently discarded. Teardown remains best-effort — a failed disconnect never turns a passing script into a failing one — but the failure is no longer invisible. An opt-in exit trace (set `LAGER_HUB_EXIT_DEBUG`) reports each held hub session's close outcome and timing at process exit, for diagnosing intermittent exit-time aborts after Acroname operations. Unset, nothing changes. ## Bug Fixes * `lager update --check` could still promise a cached build immediately before a long rebuild. The preview measured the build inputs in the Lager Box's current working tree, so on a Lager Box far behind its target it reported `Estimated: ~90s (cached build)` and then rebuilt for six minutes once the update landed a different image recipe. The preview now measures the build inputs at the target version itself, without touching the working tree, which also turns the old "unknown until pull" answer on rollbacks and branch switches into a measured one. Where the target genuinely cannot be measured, the preview says so instead of guessing. * `lager install` and `lager uninstall` now offer the SSH key Lager itself installs. `~/.ssh/lager_box` is not one of ssh's default identity filenames, so both commands were relying on the operator's `~/.ssh/config` naming an identity for the Lager Box — and where that entry was missing, they failed with `Permission denied (publickey)` while `lager ssh` kept working from the same machine. Every SSH connection those commands make now offers the key first and falls back to ssh's own default identities if the Lager Box rejects it, so a Lager Box authorized only by an operator's own key is unaffected, and an unreachable Lager Box still fails once rather than twice. * `lager ssh-setup` and `lager update` could not tell whether the key was actually installed, so they never reinstalled a deleted one. Both decided by logging in — but a login proves only that *some* identity worked, and on a machine whose ssh configuration supplies an identity for every host, the check could never come back negative. Both now ask the Lager Box directly, checking `authorized_keys` for the key itself, with three honest outcomes: installed, absent, or "could not ask" — so an unreachable Lager Box no longer reads as a missing key. The install script's own SSH pre-flight had the same blind spot with a worse consequence — an install could report success and leave the Lager Box with no key at all — and now asks the same way. * The `lager_box` key is now registered on the Lager Box, not just appended to `authorized_keys`. A key that is only appended can be silently dropped by another key manager that rebuilds `authorized_keys` from its own source — and on a Lager Box that also refuses password authentication, that was unrecoverable from the CLI. All install paths now also record the public key in the Lager Box's managed key directory, which survives any rebuild, and `lager uninstall --all` removes that registration so a revoked key cannot be republished. No new sudo permissions are installed for this: on hardened fleets where the key directory is deliberately root-owned, `lager ssh-setup` prints the exact narrowly-scoped grant for the fleet's own provisioning to add, and registration failure is a warning rather than an error. Existing Lager Boxes are repaired by one `lager ssh-setup` or `lager update`. * `lager install` no longer offers password authentication when key authentication fails. A Lager Box configured to refuse passwords never received the password install asked for, so the resulting "Password authentication failed" pointed at the wrong problem. When no key on the machine is authorized, install now offers to set up the `lager_box` key inline — one password prompt, after which the rest of the install runs unattended — instead of stopping and sending you to run `lager ssh-setup` first. Declining still exits with an error naming that command as the fix. * `lager uninstall` no longer claims the next SSH connection will require a password. It removes the `lager_box` key and nothing else, so it now says exactly that: which key was removed, and that other credentials are untouched. * `lager install` no longer writes a `Host` block into `~/.ssh/config`. That file is commonly managed by other tools, whose next rebuild deleted the block — taking with it the only thing telling ssh which identity to present, so a Lager Box that worked yesterday failed today for no visible reason. The block also permanently disabled host-key verification for that Lager Box and broke access through jump hosts. Passing the identity per command has neither problem, and blocks written by earlier installs are left alone. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.37.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.37.2/) # Version 0.38.0 Source: https://docs.lagerdata.com/source/release-notes/v0.38.0 August 18, 2026 ## Features * Every hardware-interacting command now takes the Lager Box lock, not just `lager python` and the admin commands. Toggling a GPIO, driving a supply, flashing over SWD or opening a UART could previously collide with a running test or another operator with nothing to stop it, which is the most common source of unexplained failures on a shared bench. Measurement (`gpi`, `gpo`, `adc`, `dac`, `thermocouple`, `watt`, `energy`, `scope`, `logic`), communication (`spi`, `i2c`, `uart`, `wifi`, `ble`, `blufi`, `usb`, `router`), power (`supply`, `battery`, `eload`, `solar`) and development (`debug`, `arm`, `webcam`) all acquire a short-lived lock as they resolve the Lager Box, and release it when the command exits — including when it exits with an error. Read-only paths are deliberately untouched: `lager supply --box X` with no subcommand, `lager boxes list`, and the bare net listings still resolve without locking, so inspecting a bench never blocks anyone. Each lock carries a 30-minute expiry refreshed by a heartbeat, so a process that is killed outright cannot strand the bench. Set `LAGER_AUTO_LOCK_DISABLE=1` to opt out. * `lager update --pull` fetches a pre-built Lager Box container image instead of building it on the Lager Box. Release tags are published to GitHub Container Registry, and the client resolves the tag to an immutable digest, pulls that digest pinned to the Lager Box's own architecture, and verifies the image reports the version that was asked for before using it. Any miss at any step — a branch target, an unpublished tag, an unreachable registry, a mismatched image — falls back to the local build that has always run. It is opt-in for this release: pass `--pull`, or set `LAGER_BOX_IMAGE_PULL=1`. `--no-pull` never pulls. ## Bug Fixes * `lager logic enable`, `disable`, `start`, `start-single` and `stop` did nothing, and reported success doing it. The command validated the net as a logic net and then asked the Lager Box for an analog one, which matched nothing — so the worker returned without touching the instrument, printed no output, and exited zero. Confirmed against a Rigol MSO logic net. All five now resolve the net they were given. * A Lager Box whose Docker lacks the buildx plugin can now be updated. The BuildKit pre-flight rejected such a Lager Box before doing anything else, which was correct when every update built the image locally and wrong once a pre-built image could be pulled instead — a Lager Box that cannot build is precisely the one a pre-built image exists for. The pre-flight now runs only on the path that actually builds. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.38.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.38.0/) # Version 0.39.0 Source: https://docs.lagerdata.com/source/release-notes/v0.39.0 August 19, 2026 ## Features * **`lager usb cycle` power-cycles a port** — off, wait, on — for every supported USB hub. `--off-time` sets how long the port stays unpowered (default 1s, range 0.5-10s), set above the slowest cold boot measured on real hardware. Too *short* an off time is the failure that matters: the DUT's rails do not fully discharge and it warm-starts while appearing to have been reset. Prefer it over a scripted disable/sleep/enable — it holds the hub for the whole sequence so nothing else can switch the port while it is dark, restores power on every failure path, and reports whether the device came back. * **`lager usb recover` re-powers a port left unpowered** by an interrupted command. On hubs where the whole physical device can be identified it re-asserts power on every port, since the reason to reach for it is usually that something is off and it is not obvious what. * **Plugable RTS5411 USB docks are supported as switchable-USB-power instruments**, a third option alongside Acroname and Yepkit hubs. Plugable ships no SDK, so control is the standard USB hub class per-port power switching rather than a vendor library. Only the four external Type-A sockets switch VBUS; the dock's internal tier (billboard, audio, ethernet) does not, even though both tiers advertise per-port switching identically. Requires the udev rule for vendor `2230`, shipped in `99-instrument.rules`. ## Bug Fixes * **Disabling a USB hub port no longer reports a false failure and undoes itself.** A powered-off port raises no change notification, so the kernel never processes the disconnect: the device keeps its `lsusb` entry and its `/dev` nodes for the whole off window. The driver had treated that as proof the hardware could not switch power, and then powered the port back on. It now checks the hub's own power bit, which is the only thing observable while a port is off. **Never test for a device's absence to decide whether a port is off** — use `state`, or compare a device's USB device number either side of a `cycle`. * **`lager diagnose` no longer reports a non-Acroname USB hub as wedged.** Hub diagnostics are vendor-specific but every USB net reached them, so a hub from another vendor was opened with the wrong driver and the failure reported as an electrical fault. Such a hub now reports `NOT SUPPORTED`, kept distinct from the transient `BUSY` state, and its bus facts are still shown. * **`PlugableUSBNet` is importable from the Python API, and `YKUSHUSBNet` is the YKUSH driver again.** A copy-pasted entry in the driver export table had given one driver the other's name. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.39.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.39.0/) # Version 0.39.1 Source: https://docs.lagerdata.com/source/release-notes/v0.39.1 August 19, 2026 ## Bug Fixes * **`lager supply tui` now works when `--box` is a saved box name.** The subcommand looked up the box's address and then ignored it, sending the net-listing request to a host literally named after the saved box name. That name resolves only by accident — if it also happens to be a real DNS name. Where it did not, the request never reached the box; the listing came back empty and the command reported `'BATT' is not a power supply net` for a net `lager nets` displayed correctly. The error pointed at the net when the real fault was that the box was never contacted. Only `supply ... tui` was affected — every other `supply` subcommand already used the resolved address. Passing an IP instead of a name always worked, and remains the workaround on older CLIs. The fault is entirely client-side: no box needs updating, and box version is irrelevant. Present since 0.32.0, when net listing moved to the `:9000` HTTP API and the argument that had been harmless became the URL host. * **A net check against an unreachable box no longer reports the net as missing.** An unreachable box and a box with no nets both produce an empty listing, and the two were indistinguishable — so an unreachable box produced "net not found" plus advice to create a net that already existed on it. The check now separates the two cases and reports the connectivity failure. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.39.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.39.1/) # Version 0.4.0 Source: https://docs.lagerdata.com/source/release-notes/v0.4.0 March 3, 2026 ## Improvements ### HTTPS Deployment * `lager install` now deploys box code via HTTPS git clone instead of SSH, removing the need for GitHub deploy keys * `lager update` automatically migrates existing boxes from SSH to HTTPS remote URLs * Open-source release: the repository is publicly accessible, enabling installation and updates without GitHub credentials ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.4.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.4.0/) # Version 0.4.1 Source: https://docs.lagerdata.com/source/release-notes/v0.4.1 March 03, 2026 ## Bug Fixes * `lager install` GitHub connectivity check now uses `git ls-remote` instead of `curl`, fixing deployment failures on Lager Boxes where `curl` is not installed (e.g. Ubuntu 24.04) ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.4.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.4.1/) # Version 0.4.2 Source: https://docs.lagerdata.com/source/release-notes/v0.4.2 March 04, 2026 ## Improvements * `lager install` and `lager uninstall` now provide detailed SSH error diagnostics (connection refused, no route to host, host key changes) * `lager uninstall` supports `--dry-run` flag to preview what would be removed without making changes * Deployment script uses SSH connection multiplexing for reliability over VPN connections * Shared `host_in_known_hosts` utility extracted to `ssh_utils` for consistent host key handling across commands ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.4.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.4.2/) # Version 0.40.0 Source: https://docs.lagerdata.com/source/release-notes/v0.40.0 August 21, 2026 ## Features * **`lager install` uses the pre-built box image for a release tag.** Installing a published release now pulls the box image instead of building it on the box, taking a fresh install from roughly fourteen minutes to about two. An install always paid the full cold-build cost, because the deployment clears the builder cache before it starts. On by default; pass `--no-pull` to build on the box instead. Branch targets such as `main` have no published image and still build. * **A Rigol MSO5204's logic channel can now have a net.** `lager instruments` accepts a logic net on the MSO5204, and the role tables are asserted to agree. * **`lager logic measure`, `trigger` and `cursor` work.** All sixteen subcommands dispatch to the script that implements them, rather than to a path that did not exist. ## Bug Fixes * **`lager python --timeout` now stops the script.** The deadline reached the box but sent only SIGTERM, so a script blocked in an uninterruptible call — a pyvisa, libusb or serial read, which is the normal case on a box — or one with its own handler ran on regardless. The deadline now escalates to SIGKILL after the cleanup grace window. * **A script killed by its timeout reports 137 instead of 247.** * **`--timeout` above the box's ceiling says so** instead of quietly running a shorter deadline, which read as the timeout firing early. * **A failed Docker install step names the command that failed.** The step ran eight commands behind one generic error, four of which print nothing on success, so a failure left a transcript that simply stopped. Each command now reports itself and its exit status, and the printed recovery instructions match the commands the step actually runs. * **`ssh_t` no longer prints ssh's own errors out of order**, so an error and the message explaining it stay together in the transcript. * **A deliberate `ctx.exit()` is no longer reported as a crash**, and no longer has its exit code rewritten to 1. The visible case was `lager update --check` against a box whose SSH key is not set up. * **A CI job is no longer refused by its own box lock.** * **The host CLI installs to `~/.lager_venv`.** * **A command that dispatches to a missing helper script now says so**, instead of failing later with something unrelated. ## Improvements * **A Getting Started guide covering box setup end to end** — nine pages spanning setting up a Lager Box, adding your first box, instruments, nets, a first test, a glossary and troubleshooting, including the sudo behaviour operators hit on Ubuntu 25.10 and newer. * **The box's MCP server is ported to MCP Python SDK v2**, and the `mcp` version ceiling is lifted. * **`mcp` is a direct test requirement**, rather than arriving transitively. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.40.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.40.0/) # Version 0.41.0 Source: https://docs.lagerdata.com/source/release-notes/v0.41.0 August 24, 2026 ## Features * **`--version` accepts a commit SHA.** `lager update` and `lager install` now take a full 40-character commit SHA as well as a release tag or a branch, and deploy that exact commit. A branch is re-resolved against the remote every time it is evaluated, so `--version main` can mean a different commit minutes apart -- fine for "bring me up to date", wrong for "is this box running the code I am testing". Only the full 40 characters are accepted, because a short hex prefix cannot be told apart from a branch name. A commit has no published image, so a SHA target builds on the box just as a branch does. ## Bug Fixes * **`lager install` completes on a box running `sudo-rs`.** Ubuntu made `sudo-rs` the default sudo in 25.10, and 26.04 LTS ships it as the default. It rejects wildcards in command arguments, and Lager wrote nineteen such rules to `/etc/sudoers.d/`, so the install stopped at step 2 of 9 with `wildcards are not allowed in command arguments` before deploying anything. Every rule now names its arguments exactly. The sudoers file is also validated before it is installed rather than after, so a rejected file no longer replaces a working one. * **`needrestart` no longer runs during an install.** `DEBIAN_FRONTEND` and `NEEDRESTART_SUSPEND` were passed as `sudo VAR=value`, which sudo discards unless the sudoers rule grants `SETENV` -- and during an install no such rule exists yet. Both variables now reach `apt`, so the service-restart scan they are set to suppress stays suppressed. * **`lager --box ""` no longer runs against your default box.** An explicitly empty `--box` fell into the "no box given" branch and resolved to the default, so the caller named one box and got another with nothing in the output saying so. It is now refused, matching `lager boxes add --name ""`. ## Improvements * **Hardware CI reports what it actually tested.** The bench ran on every push to `main` but had no deploy step, so those runs could only ever test whatever the previous night left on the box -- and the guard correctly refused them. The bench now runs nightly and on demand, both of its jobs pin to the commit under test rather than to a moving branch, and its recovery step says when it leaves the bench without box software instead of reporting success. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.41.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.41.0/) # Version 0.42.0 Source: https://docs.lagerdata.com/source/release-notes/v0.42.0 August 25, 2026 ## Features * **`lager install --timeout `** sets how long the deploy step may take, with `LAGER_INSTALL_TIMEOUT` as an environment override and `0` to remove the limit entirely. The budget covers the container build, which is the longest part of a first install. * **`lager python --timeout` now applies to `--detach`.** A detached job is wrapped only when a deadline was actually requested, so the default detached process tree is unchanged. The box ceiling does not apply, because it tracks a streaming read timeout that nothing reads on a detached run. ## Bug Fixes * **`lager debug erase` no longer reports "Erase complete!" when nothing was erased.** With the probe enumerated but the target unreachable over SWD -- unplugged, unpowered, or held in reset -- the command printed success in green and exited 0 over a part it had never touched. Both the box and the CLI now take their verdict from the programmer's own output, so a current CLI reports the failure correctly even against a Lager Box that has not been updated yet. `flash` erases by default, and that pre-erase step now takes the same verdict and stops before programming a part that was never reached. * **`lager python --detach` returns as soon as the box has accepted the job.** Everything before the process was spawned ran inside the HTTP request -- unpacking the module, `pip install -r requirements.txt` with no bound on it, the quiesce gate that can wait 69 seconds for a previous job's teardown, and the direct-USB handoff. A detached launch of a module carrying a `requirements.txt` therefore blocked the CLI on its 320-second read timeout before it could see the response saying the job had detached, which is the one thing `--detach` exists to avoid. The box now answers first and does the rest on a background thread; a job that fails to start reports through `--reattach` and exits 1. * **A detached run no longer holds the Lager Box lock forever.** The lock was taken with no expiry, and nothing reaped it, so a detached job that failed to start left the box locked until someone ran `lager boxes unlock` by hand. The box now heartbeats the lock while the job is alive and releases it when the job ends, however it ends. * **`lager install` no longer kills a healthy build at 30 minutes.** The deploy timeout was hardcoded in two places with no override. On hardware slower than a typical Lager Box -- an emulated guest, a low-power mini PC, a throttled VM -- a legitimate build exceeded it and was terminated mid-build, after the previous container had already been removed, leaving the box with nothing running. The lock TTL is now derived from the configured timeout rather than a second fixed number, so raising one cannot leave an install's own lock expiring underneath it. * **The box's JSON responses now carry a `Content-Length`.** They were delimited only by the connection closing, which worked by accident of an undeclared HTTP/1.0 default. ## Improvements * The timeout message from `lager install` now names the override, states that re-running is safe and reuses whatever layers the interrupted build cached, and says the budget is not a verdict on the box. * The documented expectation for a first install and the point at which the tool gives up are no longer the same number. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.42.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.42.0/) # Version 0.43.0 Source: https://docs.lagerdata.com/source/release-notes/v0.43.0 August 25, 2026 ## Upgrade Notes Two behaviour changes in this release affect code that already works. Neither is caught at install time. * **`DebugNet.connect(script=...)` now raises on an OpenOCD debug net when handed a J-Link script.** It used to accept the argument and silently ignore it, so a script written for one backend ran the target under whatever attach sequence the net already had. The argument now works on both backends -- it is classified by file format and routed -- but a `.JLinkScript` given to an OpenOCD net is a `ValueError` rather than a silent no-op. If an automated run passes a `.JLinkScript` in-process to a net whose probe uses OpenOCD, pass a `.cfg` instead, or name the format explicitly with `openocd_config=` / `jlink_script=`. The `lager debug` CLI is unaffected; it has always routed by file extension. * **Persisted FTDI GPIO output state resets once on upgrade.** The state a Lager Box remembers between separate CLI invocations is now keyed by interface as well as device, so that two channels of one multi-channel adapter stop sharing an entry. Existing entries do not carry an interface and are not read back. The first read of a pin after upgrading reports no remembered state; setting it once restores normal behaviour. ## Features * **`DebugNet.halt()` stops the target where it is, without a reset.** OpenOCD only. `reset(halt=True)` runs OpenOCD's `reset halt`, which pulses nRESET and re-enters through the reset vector; on a part that executes in place out of external QSPI that re-runs the bootloader rather than stopping on the image just programmed. `halt()` issues a bare halt, so execute-in-place memory still holds what was written. J-Link has no standalone halt-in-place primitive, so on that backend the call raises and names the halt-first `.JLinkScript` as the supported route. * **`connect()` accepts `halt`, `openocd_config` and `jlink_script`.** `halt` runs `reset halt` once the daemon is up, and is documented as the reset-then-halt operation it is. The two script arguments are the unambiguous per-backend forms of `script`, for a base64 blob that carries no filename to classify. * **FTDI GPIO, I2C and SPI nets can address a specific channel on a multi-channel adapter.** A net may now carry `params.interface`, taking `A`-`D` or `0`-`3` -- the same vocabulary debug nets already accept as the `@A` suffix on their device field. Which channels are legal is enforced per net: I2C and SPI are MPSSE protocols and on an FT4232H only channels A and B have an MPSSE engine, while GPIO runs as asynchronous bitbang and works on all four. `FTDI_FT4232H` accordingly gains the `spi`, `i2c` and `gpio` roles its siblings already advertised. ## Bug Fixes * **`DebugNet.connect(script=...)` was ignored under the OpenOCD backend.** No error, no warning, no log line: the script was written to disk and then never read, because only the J-Link path passed it downstream. A caller supplying a per-run attach script in-process -- the way an automated run avoids mutating shared Lager Box state -- got a run that silently used whatever attach sequence the net already had. The argument is now classified by extension, then by content, and routed to whichever backend it is for. Per-connect overrides are written to a per-net path rather than the box-wide config file, and `disconnect` clears them, so one session's override cannot reach another net. * **`gpio`, `i2c` and `spi` nets on an FT2232H could not be opened.** The instrument has advertised all three roles for as long as the role table has existed, so `lager nets add` accepted them; but the drivers addressed the device by a product selector that matches only the FT232H, so every such net failed to find its hardware. The part is now selected from the USB product ID already present in the net's own address. * **An FTDI net whose address was written as a full `ftdi://` URL had it silently discarded.** The address was recognised as "not a serial number" and then dropped, with a default URL rebuilt over the top, so a user who spelled out exactly which device and channel they wanted got the first channel of the first FT232H instead. Such an address is now used verbatim. * **A box deployed from a branch now says so.** After `lager update --version `, nothing on the box recorded which ref produced the code: the version file holds a version number, and a branch not yet bumped past the last release serialises to the same string as the release tag. `lager hello` reported the release version and a box running a branch was indistinguishable from one on the release tag. The deployed ref is now recorded and `lager hello` prints it, flagged when it is not a release build. * **`/etc/lager/ref` was never written when the box was already up to date.** The write sat on the path taken by an update that actually pulled; a run that found the box already at the target version exited before reaching it. That is the case the file matters most in, and because the way to confirm a branch deploy took is that `lager hello` names a ref, its absence reported failure for a deploy that had succeeded. * **"SSH key not configured for this box" on a box where the key was installed and working.** The check that asks a box whether it has the key did not offer that key to the connection carrying the question, so on a machine whose default credentials the box does not accept it could not authenticate to ask, and reported the key missing on a box it was perfectly able to answer for. It now offers the key explicitly, which widens the credentials tried rather than narrowing them. ## Improvements * `DebugNet.connect(script=...)` now documents that an OpenOCD override must be a complete config rather than a fragment: the launch line still carries lager's own channel-selection command, which is not recognised unless a config has selected the FTDI adapter driver. ## Known Limitations * **OpenOCD debug operations return an empty string where J-Link returns programmer output.** A test harness that decides pass or fail by scanning the output of `erase()` or `flash()` for failure markers finds none in an empty string and reads that as success. Against an OpenOCD net, check the target's state directly rather than parsing the return value. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.43.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.43.0/) # Version 0.44.0 Source: https://docs.lagerdata.com/source/release-notes/v0.44.0 August 28, 2026 ## Upgrade Notes * **Lager rewrote its user-facing messages to one written standard.** Command output, error text and help strings now follow ASD-STE100. The wording changed across the whole CLI. Scripts that match on message text can break. A test suite in this repository broke this way: it matched `could not connect to the box|may be offline`, and both strings became different sentences. If your automation greps Lager output, check those patterns against this release. Exit codes did not change. * **`lager debug status` reports two states, and the Lager Box must be updated.** The box reported one `connected` flag that meant "the gdbserver process is alive". Commands ran against a target that was not attached. The box now reports `gdbserver_running` and `target_attached` separately, and the CLI prints both. `connected` keeps its old meaning, so an older CLI behaves as before. `target_attached` has a third value, `Unknown`, for a box that cannot answer. Run `lager update --box ` to get this. * **The box python service no longer serves `/pip`.** Its only caller addressed a port and a path that the box never served, so both halves were dead code. Update your boxes to a release that contains this change. * **`lager install` deploys a firewall allowlist.** Provisioning writes one allowlist that matches the ports the Lager Box publishes. Every rule names `proto tcp`. An install on a host with its own firewall rules can need review. * **`LAGER_DISABLE_UART_SERVICE` frees port 9000.** The variable disabled the service but left the port bound. Anything that relied on port 9000 staying occupied will now find it free. ## Features * **The Net-Manager Add screen makes LabJack I2C/SPI pins changeable.** Readers saw `Ch: FIO4-FIO5` and read it as a fixed assignment. A row's pencil button now opens a combined editor for the name and the pins. The screen carries a dismissable notices block that states the pins are defaults. * **A second device of one model no longer disables that instrument family.** Lager decides net creation by address ambiguity, not by instrument model. Two Acroname hubs now yield sixteen usb nets. Two LabJack T7 units stay refused, because a T7 reports no serial number. * **The Rust API reference mirrors the Python one, page for page.** `Router`, `Arm`, `Webcam`, `Wifi`, `Analog` and `Logic` gained MCP API reference pages. A guide for running Lager from CI is new. ## Bug Fixes * **`lager debug` commands ran against a target that was not there.** The `connect()` check asked the gdbserver about itself, which answers with no part attached. `flash`, `reset`, `erase`, `memrd` and the RTT paths all ran believing they were connected. * **A box install failed at the firewall step.** `ufw` refuses a port range that names no protocol, so the first range stopped the script and `lager install` exited 1 with nothing configured. * **Per-probe runtime file paths are built from a checked serial number.** A probe's pid and log file names come from a field of a net's address that accepts almost anything. Each path is now checked where it is built. * **`lager logic measure`, `trigger` and `cursor` resolve a logic net again.** * **The supply suites wait for the output to reach regulation.** They slept a fixed interval and read the supply before it settled. * **A `lager python` connection error printed the literal `{box_ip}`.** The hint was a plain string, not an f-string. * **The scope's `SUPPORTED_USB` key spelled the Rigol MSO5204 with a zero.** The correct spelling works now, and the old spelling still resolves. * **`lager box config apply` reports what the container-side package steps did.** The box no longer advertises host URLs that it does not publish. ## Improvements * Sixteen `reference/cli/` pages, fourteen `lager` command pages, the Python API reference, the Rust API reference and the root prose files follow ASD-STE100. A CI gate holds them there. * `tools/check_docs.py` checks the documentation against the shipping CLI, and `mint broken-links` runs in the static-checks gate. * The Architecture page draws its diagrams. Four of its claims about the Lager Box were wrong and are corrected. * The Release Notes navigation groups 158 entries into five version ranges. ## Known Limitations * **Two LabJack T7 units on one Lager Box stay unsupported.** A T7 reports no serial number and the scanner does not address it by topology, so two of them enumerate as the same address. A net cannot say which one it means. The message now states this instead of telling the reader to unplug extras. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.44.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.44.0 ``` Then update your boxes: ```bash theme={null} lager update --box ``` # Version 0.45.0 Source: https://docs.lagerdata.com/source/release-notes/v0.45.0 September 1, 2026 ## Upgrade Notes Most of this release runs on the Lager Box, not in the CLI. Update every box you use, or none of the fixes below reach you. See Installation. * **`lager logic` now exits non-zero when a command fails.** Earlier versions printed nothing and exited 0 when the net name was wrong, or when the box rejected the call. A script that reads the exit code can now fail where it passed before. That failure is real, and it was always there. * **`LogicDisplaySize.Medium` and `LogicDisplaySize.Large` now set the sizes they name.** The two values were swapped. Code that passes `Large` to get a medium display must swap the value back. * **The box refuses a probe serial it cannot bind to.** It used to fall through to whichever probe the backend picked. A script that relied on that fallback will now see an error that names the serial. * **A failed `/invoke` returns an error message without the box's stack trace.** The trace goes to the box log. Code that parses a traceback out of the response must read the log instead. ## Bug Fixes * **`lager logic enable` and `disable` failed on the Rigol MSO5000.** The mapper called ten methods that no driver defined, so each one returned `Function not found` at runtime. The driver implements them now. * **`lager logic disable` waited on a query the scope never answers.** An MSO5204 accepts `:LA:DIGital:DISPlay?` and returns nothing, and reports no error when asked. The driver uses a form the instrument answers. * **`lager logic` reported success after the box failed.** The worker returned no status, so the command printed nothing and exited 0. It prints the box's error and exits 1. * **`POST /debug/connect` validates the probe serial and port overrides it accepts.** A malformed request returns 400 instead of a 500 from deeper in the stack. * **`GET /download-file` builds its `Content-Disposition` header safely.** A crafted filename can no longer inject header lines. ## Improvements * The box checks that every path it builds from a client-supplied name stays inside the directory that name belongs to. This covers binary names, probe serials, detached job ids, device-lock keys, debug scripts, OpenOCD config files and tty names. Each check sits beside the join it guards. * `SECURITY.md` gains a Threat Model. It records which behavior is deliberate, and it states that network reachability is the security boundary. The host firewall does not filter the ports the box's containers publish. Treat the network as the control, and put the box on a VPN or an isolated LAN. * The Architecture page corrects three claims about box internals, and the troubleshooting page documents `[Errno 16] Resource busy`. ## Known Limitations * **The MSO5000 trigger-configuration and bus-decode surfaces are incomplete.** 115 mapper methods call driver functions that do not exist, across pulse, UART, I2C, SPI and CAN triggers and every bus decode type. Each one fails at runtime with `Function not found`. The defaults paths work; anything past them can fail. Tracked in issue #418. * **`lager usb cycle` cannot confirm re-enumeration on every hub.** On Acroname and YKUSH hubs it reports `no device on this port to watch for` even when the port holds a device that does re-enumerate. The power cycle itself works. Only the Plugable driver observes the reconnect. Tracked in issue #423. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.45.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.45.0 ``` Then update your boxes: ```bash theme={null} lager update --box ``` # Version 0.45.1 Source: https://docs.lagerdata.com/source/release-notes/v0.45.1 September 2, 2026 ## Upgrade Notes * **The robot-arm scan now writes only to a port that identifies as a Dexarm.** Before, the scan opened every `/dev/ttyUSB*` and `/dev/ttyACM*` node. It wrote a G-code handshake to each one. Now it reads each node's USB vendor and product ID. It writes only to `0483:5740`. An arm that reports a different ID will not appear in `lager instruments`. To restore the wider scan, set `LAGER_ARM_PROBE=force` in the Lager Box container environment. That setting drops the identity check alone. Every other guard stays active. * **Seven range checks in the instrument mappers now reject bad values.** Each check carried inverted bounds, so no value satisfied it. The check rejected nothing. Seven settings are affected: * the UART trigger data width * the I2C trigger address width * the I2C data byte width * the SPI trigger data width * the UART and SPI bus data widths * the Keithley battery state-of-charge An out-of-range value now raises an error that names the valid range. Such a value used to reach the instrument. What happened there is not established. Check the values your scripts pass to these settings. * **`secure_box_firewall.sh` reports what it configured, not what it achieved.** A successful run ended with `[OK] External access blocked for Lager services`. It now ends with `[OK] Host firewall configured for Lager services`. It adds a note that states the limit and points to the Security Model section of `SECURITY.md`. The script writes the same rules as before. Update any automation that matches the old text. ## Bug Fixes * **`lager ssh` refused boxes that a plain `ssh` reached.** When `~/.ssh/lager_box` exists, `lager ssh` passes it with `-i`. That flag replaces ssh's default identity list instead of adding to it. ssh's own defaults -- `id_rsa`, `id_ecdsa`, `id_ed25519` and their `-sk` variants -- were no longer offered. A box that authorizes one of those keys, and not `lager_box`, answered `Permission denied (publickey)`. A plain `ssh user@box` still worked. A stale or never-installed `lager_box` key therefore locked `lager ssh` out of every box the user set up with `ssh-copy-id`. `lager ssh` now names `lager_box` first, then each default identity file present, in ssh's own order. Your `~/.ssh/config` identities and agent keys stay on offer. With no `lager_box` key, the command passes no `-i` at all. * **A `lager uart` session received the text `M105` from a scan it did not ask for.** The device under test echoed it and answered `Error: Unknown command: M105`. `M105` is the G-code handshake that finds a Dexarm robot arm. The scan wrote it to every serial port outside an exclusion set. That set had three gaps. It listed only hardware the scan recognizes, so it missed any unlisted USB-serial chip. It read one interface per adapter, so it left the other channels of a multi-channel FTDI adapter open. It never consulted saved nets. The scan also opened each port without an exclusive lock. It therefore opened through the lock a live session held. The scan now writes only to a port that identifies as a Dexarm. It excludes every channel of every known adapter. It excludes every port a saved UART net owns. It opens each port with an exclusive lock. `GET /instruments/list` is the only trigger, and it runs a full scan on every request. These writes arrived from a request that another terminal made. * **An attached Dexarm did not appear in `lager instruments`.** The arm answered the handshake and the scan then discarded it. Two faults caused this. The scan read the USB serial number with `udevadm info`, which needs the udev runtime database. A container without `/run/udev` mounted has no such database, so `udevadm` returned no serial. The scan now reads the serial number from sysfs. The scan also waited 10 milliseconds and then read whatever had arrived. The arm answers more slowly than that, so the read returned nothing. The scan now waits for the reply, up to the port's one-second timeout. * **`GET /instruments/list` now records the client that asked.** The scan runs on every request and caches nothing. This line identifies the caller behind any given write. ## Improvements * `tools/check_coverage_counts.py` names a missing pytest plugin instead of reporting the test suite as failed. * The bench watchdog reads its thresholds from one place. A workflow copy can no longer drift from the tool that consumes it. ## Known Limitations * `LAGER_ARM_PROBE=force` drops the identity check. The scan then writes a G-code handshake to every serial port it can open. Ports that saved nets own stay excluded. Ports another process holds stay excluded. Use this setting to diagnose a missing arm, and unset it afterward. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.45.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.45.1 ``` Then update your boxes: ```bash theme={null} lager update --box ``` # Version 0.46.0 Source: https://docs.lagerdata.com/source/release-notes/v0.46.0 September 4, 2026 ## Upgrade Notes * **The box now serves net and box metadata over HTTP, and the control plane starts syncing it.** The control plane has had its half of this since May. It gates each push on two capability flags, and nothing advertised them, so it skipped every push in silence. A description typed into the dashboard went to its own database and no further, and `lager nets describe` on the box stayed invisible to it. Update your boxes to turn the sync on. After the update, a description written on either side reaches the other. * **The endpoint rejects the pre-0.24.0 field names.** A client that sends `description`, `dut_connection` or `test_hints` gets an error rather than a stored key that nothing reads back. Send `purpose`, `notes` and `tags`, the fields `lager nets describe` writes and the MCP server reads. * **`lager usb cycle` now waits for the device to come back.** It used to return as soon as power returned. It now confirms re-enumeration from the USB topology, so a successful cycle takes up to five seconds longer. A script that assumed an instant return will run slower and will not break. * **A U3 DAC refuses an out-of-range value.** The range is 0.04 V to 4.95 V, not 0 V to 5 V. The hardware used to clamp such a value in silence. Code that relied on that clamp must send a value inside the range. ## Features * **`lager adc`, `dac`, `gpi` and `gpo` work against a LabJack U3.** The box drives the T-series through LJM, which does not support the U3 at all, so this is a second driver stack: the Exodriver plus LabJackPython's `u3` module. The T7 path is untouched and the two families share a box. Validated on a U3-HV with both DACs looped back to analog inputs: all 16 analog channels, all 16 usable digital lines, and both DACs linear to within 20 mV. SPI and I2C are not supported on the U3, and `lager instruments` advertises only the roles that have a driver behind them. * **`lager uart --sessions` and `lager uart --force`.** A held UART net had no recovery path. `--sessions` lists which nets are held and whether each holder is still connected. `--force` releases the holder before it connects. The "already in use" error now names the take-over command. ## Bug Fixes * **A UART net stayed held forever, and only a container restart freed it.** The box reclaimed a session by asking whether its read loop still made progress. The loop writes that heartbeat itself, so it proves the loop iterates, never that anyone still listens. The read loop now asks whether its client is still connected and exits on the first iteration where it is not. * **`lager usb cycle` reported "no device on this port" on ports that had one.** Only one of the three hub drivers observed re-enumeration. On an Acroname or YKUSH hub that message printed every time, whatever was plugged in. During a hardware fault a false "no device here" points the investigation at the device rather than at the tool. The verdict now comes from the kernel's USB topology, so every driver reports a real answer. A box that cannot read its own topology says that instead. * **`lager logic trigger spi` failed with `Function not found: get_trigger_spi_width`.** The mapper called a name that no driver defined. The whole SPI trigger surface behind it was missing the same way, and all of it is now implemented. Every query was confirmed against an MSO5074, because this instrument accepts some queries that it never answers. * **`setup_battery(soc=0)` set nothing and reported nothing.** A state of charge of 0 is falsy, and a truthiness test discarded it before the instrument saw it. There was no exception and no log line. 0 is the interesting end of the range for a discharge test. * **A LabJack that is not a T7 no longer lands on the T7's code paths.** * **`DebugNet.flash()` and `.erase()` take the DA1469x flash-loader path.** The CLI took it and the Python Net API did not, so the same operation worked one way and failed the other. * **The `authorized_keys` probe withdrew the operator's own SSH identities.** It failed to answer for the boxes it exists to repair, and both callers read that silence as success. * **A box without a pigpio container gets the default pigpio address.** * **Editing a net in the Net-Manager TUI no longer discards the rest of the record.** ## Improvements * A second role on a dual-role instrument is a notice rather than a block. One battery net and one supply net on the same chip is a supported setup. * The bench watchdog reports a late night instead of filing an issue about it. * The supply suite prints its bench-fixture note on a failure, not on a pass. ## Known Limitations * **`setup_battery(soc=0)` is not confirmed on hardware.** The Keithley 2281S used for this work has AC power and never appears on the USB bus, so `:BATTery:SIMulator:SOC 0` is unverified on a real instrument. The unit tests cover the code path, and the driver already handled 0 correctly. * **The MSO5000 SPI trigger surface is not confirmed end to end.** Every SCPI node was verified against an MSO5074, but the logic-analyzer suite was not run against the merged code. * **SPI and I2C are not supported on the LabJack U3.** The T7 drives both through firmware registers that the U3 does not have. * **LJM fails outright when a U3 is present.** Asked for device type `"ANY"` it returns `LJME_U3_NOT_SUPPORTED_BY_LJM` rather than skipping the device it does not support. Every T7 path in Lager names the device type explicitly and is unaffected. Your own code that calls `ljm.openS("ANY", ...)` or `ljm.listAllS("ANY", ...)` will stop working once you plug in a U3. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.46.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.46.0 ``` Then update your boxes: ```bash theme={null} lager update --box ``` # Version 0.46.1 Source: https://docs.lagerdata.com/source/release-notes/v0.46.1 September 8, 2026 ## Upgrade Notes * **The channel table lives on the Lager Box.** A 0.46.1 CLI against a 0.46.0 Lager Box still sees the old list, including the `FIO0`-`FIO3` gpio channels this release removes. Run `lager update` to get the fix. ## Bug Fixes * **A LabJack U3 no longer offers `FIO0`-`FIO3` as `gpio` channels.** Those four pins are the U3-HV's fixed high-voltage analog inputs. The scanner advertised them anyway. `lager nets add` then accepted a `gpio` net on one of them, and the net failed at first use. `lager nets add-all` generated four such nets on every Lager Box with a U3. Read those pins with an `adc` net on `AIN0`-`AIN3` instead. The usable digital lines are `FIO4`-`FIO7`, `EIO0`-`EIO7` and `CIO0`-`CIO3`. ## Improvements * **A rejected channel now names the ones that work.** `lager nets add` lists the valid channels for the role and instrument, and points a `FIO0`-`FIO3` attempt on a U3 at `AIN0`-`AIN3`. `lager nets add-batch` gained the same check and reports every bad record at once. ## Known Limitations * **A U3-LV loses four digital lines.** The scanner reads a USB descriptor, and a U3-LV reports the same product id as a U3-HV. Lager therefore treats every U3 as an HV. On a U3-LV, `FIO0`-`FIO3` are flexible, and Lager no longer offers them as `gpio`. They remain readable as an `adc` net on `AIN0`-`AIN3`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.46.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.46.1 ``` Then update your boxes: ```bash theme={null} lager update --box ``` # Version 0.46.2 Source: https://docs.lagerdata.com/source/release-notes/v0.46.2 September 8, 2026 ## Upgrade Notes * **`lager exec` runs your command in place when the CI job is already inside the devenv image.** Before this version, `lager exec` always started a container. A job that runs inside the image has no Docker, so the command failed with `Docker is not installed or not in PATH`. It now runs the command directly in that job. GitHub Actions, GitLab CI, Drone, and Bitbucket Pipelines get this behavior. A Jenkins agent and a runner with no `container:` still start a container. If your CI sets the container variables and also has a working Docker, your commands now run in the job instead of in a new container. Set `LAGER_CI_OVERRIDE=1` to keep the old behavior. ## Bug Fixes * **`lager exec` failed with `Docker is not installed or not in PATH` inside a CI job container.** The CLI has two ways to run a saved command: it starts a container, or it runs the command directly. The second way was lost when the command moved to a new module, and no test covered either way. Both ways work again. The command runs in the directory of the check-out. The `--env` option and the `environment` key reach the command. The options `--mount`, `--volume`, `--user`, and `--group` need a container, so the CLI now gives a warning for each one instead of ignoring it. * **OpenOCD `flash` and `erase` made the same decision in two places.** The HTTP debug service and the Net API each decided for themselves whether a target needs the RAM-resident flash loader. The Net API was missing the DA1469x path because of that split. Both now use one module. A loader failure names the step that failed instead of an OpenOCD tcl traceback. A flash that stops after its erase stage will tell you that the board can be blank. An address outside the DA1469x flash window is refused before any I/O. Callers keep passing absolute XIP addresses. Other OpenOCD targets and the J-Link backend do not change. ## Improvements * The CI guide covers the job that runs inside the devenv image. It shows the `container:` block, the options that do nothing in that job, and the error you will see on a CLI that is too old. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.46.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli==0.46.2 ``` Then update your boxes: ```bash theme={null} lager update --box ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.46.2/) # Version 0.5.0 Source: https://docs.lagerdata.com/source/release-notes/v0.5.0 March 05, 2026 > Historical note: this release captured an earlier migration window where control-plane bootstrap lived in Lager. Current open-source Lager keeps shared box primitives such as `/status`, while downstream control planes own enterprise install/bootstrap. ## Features * **Control plane heartbeat**: WebSocket-based heartbeat client reports Lager Box status (health, version, nets) to the control plane * **Box status endpoint**: `/status` endpoint on both Flask and Python HTTP servers returning box health, version, and connected nets * **`lager boxes connect`**: historical migration command to configure a Lager Box for control plane heartbeat reporting ## Improvements * Refactored version file reading in `service.py` into reusable `_read_box_version()` helper * `start-services.sh` starts control plane heartbeat when configured * Added `websocket-client` dependency to box Docker image ## Installation ```bash theme={null} pip install lager-cli==0.5.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.5.0/) # Version 0.6.0 Source: https://docs.lagerdata.com/source/release-notes/v0.6.0 March 06, 2026 ## Features * **Reattach to detached processes**: `lager python --reattach ` streams all output from a detached process, replayed from the beginning. Press Ctrl+D to detach without killing, or Ctrl+C to kill. * **Kill detached processes**: `lager python --kill ` kills a specific process; `lager python --kill-all` kills all running `lager python` processes on a Lager Box. * **Venv shadowing detection**: warns at startup if a system-installed `lager` CLI is running instead of the version in your active virtual environment, with instructions to fix. ## Bug Fixes * **Ctrl+C no longer breaks Acroname hub**: previously, interrupting `lager python` with Ctrl+C left the Acroname USB hub in a broken state requiring a Lager Box reboot. The hub connection is now properly released on exit. * **`--detach` no longer hangs**: detached mode returns immediately with the process ID and hints for reattach/kill. * **`--kill` actually works**: was silently doing nothing; now correctly kills the targeted process. * **Invalid process IDs handled gracefully**: `--kill` and `--reattach` with invalid IDs show friendly error messages instead of tracebacks. * **Multi-user Lager Box provisioning**: new users are always added to the docker group, even if Docker was installed by a previous user. `start_box.sh` uses `$HOME` instead of hardcoded paths. ## Improvements * Detached process output now shows Lager Box name instead of IP address * 10 MB log cap for detached process output prevents disk abuse on Lager Boxes ## Installation ```bash theme={null} pip install lager-cli==0.6.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.6.0/) # Version 0.7.0 Source: https://docs.lagerdata.com/source/release-notes/v0.7.0 March 10, 2026 > Historical note: the control-plane items in this release were part of a migration stage before downstream control-plane install/bootstrap was separated from open-source Lager. ## Features * `lager devenv terminal --attach ` to attach to a running Docker container via `docker exec` * `lager devenv terminal --shell ` to override the default shell when attaching * Jobs WebSocket client added to control plane heartbeat for receiving and executing job dispatch commands during the migration window ## Improvements * Default control plane URL updated to the new control-plane API domain during the migration window ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.7.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.7.0/) # Version 0.8.0 Source: https://docs.lagerdata.com/source/release-notes/v0.8.0 March 12, 2026 ## Features * RTT RAM search parameters for Python API: `dbg.rtt(search_addr=0x20000000, search_size=0x10000, chunk_size=0x1000)` allows specifying where to search for the SEGGER RTT control block in target RAM * RTT RAM search CLI flags: `--rtt-search-addr`, `--rtt-search-size`, `--rtt-chunk-size` for `lager debug gdbserver --rtt` * Instruments and nets HTTP handlers on Lager Box for remote configuration queries ## Bug Fixes * Fixed PID file path mismatch where `status()` and `rtt()` only checked `/tmp/jlink.pid` but `connect()` writes to `/tmp/jlink_gdbserver.pid` — both paths are now checked everywhere * Fixed `detect_and_configure_rtt()` always reporting "No debugger connection" even when the debugger was connected, preventing RTT control block auto-detection from running * Fixed `erase_flash()` and `read_memory()` failing to find a running debugger when connected via the GDB server PID path ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.8.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.8.0/) # Version 0.9.0 Source: https://docs.lagerdata.com/source/release-notes/v0.9.0 March 16, 2026 ## Features * `disconnect_wifi()` standalone function for the Python WiFi API * `lager boxes` now reads project-level `.lager` files in addition to the global `~/.lager` — boxes defined in a project `.lager` take precedence over global ones * WiFi Python API docs updated to use standalone functions instead of class-based patterns ## Bug Fixes * Fixed `lager boxes` showing empty results in fresh Docker containers when boxes were defined in a project-level `.lager` file but no global `~/.lager` existed * Fixed typo in `wifi/status.py` ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.9.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.9.0/) # Lager Supported Instruments Source: https://docs.lagerdata.com/source/supported-instruments/supported-instruments All instruments currently supported by the Lager platform. ## Overview This document outlines the instruments currently supported by the Lager platform. All supported devices can be controlled locally or remotely through the `lager` CLI and integrated directly into automated hardware testing workflows and CI/CD systems. Lager transforms traditional bench equipment into programmable infrastructure for embedded engineering teams. If you use equipment not listed here, custom integration support is available. *** # 1. Power Control Lager provides automated control of bench power equipment for deterministic hardware testing, fault injection, and CI-driven validation. ## Power Supplies | Manufacturer | Model | Channels | Control Command | | -------------------- | ------------ | ------------------ | ----------------------------- | | Rigol | DP711 \* | 1 | `lager supply` | | Rigol | DP811 | 1 | `lager supply` | | Rigol | DP821 | 2 | `lager supply` | | Rigol | DP831 | 3 | `lager supply` | | Rigol | DP832 | 3 | `lager supply` | | Keysight | E36233A | 2 | `lager supply` | | Keysight | E36312A | 3 | `lager supply` | | Keysight | E36313A | 3 | `lager supply` | | EA Elektro-Automatik | PSB 10060/60 | 1 supply + 1 solar | `lager supply`, `lager solar` | | EA Elektro-Automatik | PSB 10080/60 | 1 supply + 1 solar | `lager supply`, `lager solar` | \* RS-232 only — connects through a USB-serial adapter and requires a one-time manual assignment with [`lager nets assign`](/source/reference/cli/nets#assign). The DP711 also requires a **null-modem (crossover) cable** between the supply and the RS232-to-USB adapter. Both ends are wired as DTE, so a straight-through connection will not communicate. See [RS-232 Instruments](/source/getting-started/setting-up-instruments#rs-232-instruments-manual-assignment). **Enables:** * Automated voltage/current control * Power sequencing * Brownout and glitch testing * CI-integrated hardware validation ## Battery Simulators | Manufacturer | Model | Control Command | | ------------ | ----- | ------------------------------- | | Keithley | 2281S | `lager battery`, `lager supply` | **Enables:** * Programmable battery emulation * Charge/discharge profile simulation * Low-voltage testing ## Electronic Loads | Manufacturer | Model | Control Command | | ------------ | ------ | --------------- | | Rigol | DL3021 | `lager eload` | **Enables:** * Load step testing * Current sink validation * Automated power integrity testing *** # 2. Measurement & Analysis Lager integrates measurement equipment into programmable test workflows, enabling automated signal inspection and power analysis. ## Oscilloscopes & Logic Analyzers | Manufacturer | Model | Channels | Control Command | | --------------- | -------------- | ------------------ | ---------------------------- | | Rigol | MSO5204 | 4 analog + 1 logic | `lager scope`, `lager logic` | | Pico Technology | PicoScope 2000 | 2 analog | `lager scope` | **Enables:** * Automated waveform capture * Trigger-based signal validation * Digital + analog correlation ## Power Measurement | Manufacturer | Model | Control Command | | -------------------- | ---------- | ---------------------------- | | Yoctopuce | Yocto-Watt | `lager watt` | | Joulescope | JS220 | `lager watt`, `lager energy` | | Nordic Semiconductor | PPK2 | `lager watt`, `lager energy` | **Enables:** * Current consumption profiling * Energy and charge integration * Power statistics (mean/min/max/std) * Power regression testing ## Temperature Monitoring | Manufacturer | Model | Channels | Control Command | | ------------ | ------------ | -------- | -------------------- | | Phidgets | Thermocouple | 4 | `lager thermocouple` | **Enables:** * Thermal monitoring during hardware tests * Environmental validation * Long-duration soak testing *** # 3. Embedded Interfaces Lager supports programmable hardware interface control for automated communication testing and validation. ## Multi-Protocol Adapters ### LabJack T7 **Control Commands:** `lager i2c`, `lager spi`, `lager adc`, `lager dac`, `lager gpi`, `lager gpo` **Interfaces:** * I2C (1 bus) * SPI (1 bus) * 14 ADC channels * 2 DAC channels * 24 GPIO pins ### Total Phase Aardvark **Control Commands:** `lager i2c`, `lager spi`, `lager gpi`, `lager gpo` **Interfaces:** * I2C (1 bus) * SPI (1 bus) ### FTDI FT232H **Control Commands:** `lager i2c`, `lager spi`, `lager gpi`, `lager gpo` **Interfaces:** * I2C (1 bus) * SPI (1 bus) * 12 GPIO pins ### Measurement Computing USB-202 **Control Commands:** `lager adc`, `lager dac`, `lager gpi`, `lager gpo` **Interfaces:** * 8 ADC channels * 2 DAC channels * 8 GPIO pins **Enables:** * Automated peripheral communication testing * Sensor validation * Protocol-level fault injection * Hardware-in-the-loop simulation *** # 4. Debug & Flashing Lager supports industry-standard ARM debug probes, all driven through the same `lager debug` command so probe choice is transparent to your scripts and CI pipelines. | Manufacturer | Model | Control Command | | ------------------ | ---------------------------------------------------- | --------------- | | SEGGER | J-Link | `lager debug` | | SEGGER | J-Link Plus | `lager debug` | | SEGGER | J-Link Base Compact | `lager debug` | | SEGGER | J-Link Flasher Pro | `lager debug` | | SEGGER | Flasher ARM | `lager debug` | | STMicroelectronics | ST-Link/V2, V2-1, V3 (including V3 Mini and V3 2VCP) | `lager debug` | | Raspberry Pi | Debug Probe (RP2040 / Picoprobe, CMSIS-DAP) | `lager debug` | | FTDI | FT232H (e.g. C232HM cable, Adafruit FT232H breakout) | `lager debug` | | FTDI | FT2232H (e.g. Olimex ARM-USB-OCD-H) | `lager debug` | | FTDI | FT4232H (requires custom `openocd_config`) | `lager debug` | | Any vendor | CMSIS-DAP compatible (Atmel EDBG, NXP DAPLink, etc.) | `lager debug` | **Capabilities:** * Flash firmware * Erase device * Reset target * Launch GDB server * Memory read/write **Notes:** * OpenOCD-backed probes auto-select an interface configuration from the USB VID/PID for the chips listed above. The FT4232H is supported but has no safe default, because quad-MPSSE boards have too many wiring variants. You must pass an `openocd_config` that points at the right interface cfg for your board. The same escape hatch covers non-standard FT232H/FT2232H wiring and any probe not in the table (e.g. Black Magic Probe, Glasgow Interface Explorer). * Multi-channel FTDIs (FT2232H, FT4232H) expose each MPSSE channel as a separate debug net. The channel is an `@A`/`@B`/`@C`/`@D` suffix on the device field, so a single chip can drive multiple targets independently. Channels C and D on the FT4232H are UART-only. See the [nets reference](../reference/cli/nets) for the channel-suffix syntax. * Lager selects the target chip configuration from the device name for the STM32, nRF5x, RP2040/RP2350, ATSAM, LPC, i.MX RT, and ESP32 families. A custom OpenOCD target config drives anything else. *** # 5. Connectivity & Control ## USB Power Switching | Manufacturer | Model | Ports | Control Command | | ------------ | ----------------------------------------- | ----- | --------------- | | Acroname | 8-Port USB Hub | 8 | `lager usb` | | Acroname | 4-Port USB Hub | 4 | `lager usb` | | Yepkit | YKUSH | 3 | `lager usb` | | Plugable | UD-CAM and other RTS5411 docks[^plugable] | 4 | `lager usb` | [^plugable]: Unlike the Acroname and Yepkit hubs, Plugable ships no SDK. Control uses the standard USB hub class per-port power switching (PPPS), so support depends on the individual hub genuinely implementing it. Matched by VID:PID `2230:5411`, which Plugable reuses across its RTS5411 dock line; validated on a UD-CAM. **Only the four external Type-A sockets switch VBUS** — see below. Requires the udev rule for vendor `2230` (shipped in `99-instrument.rules`); without it libusb cannot open the hub and every operation fails with a permission error. ### Which Plugable dock ports actually switch power A Plugable dock enumerates as **two cascaded 4-port hubs with identical descriptors**. Both advertise per-port power switching (`wHubCharacteristics=0x00a9`, `PortPwrCtrlMask=0xff`), so the descriptor cannot tell them apart. Only the topology can: | Tier | What is on it | Switches VBUS | | ------------------- | ------------------------------------------------------------------------------------- | -------------- | | Downstream (user) | the dock's four external Type-A sockets | Yes — verified | | Upstream (internal) | DP-AltMode billboard, audio codec, the dock's own ethernet, the link to the user tier | No | Lager exposes only the user tier. The four lager ports are therefore the four external Type-A sockets. On the validated model, three are on the rear and one is on the front. The dock's own network adapter can never be switched off by accident. **Which lager port number is which physical socket is not defined.** Nothing ties USB hub port order to the order of connectors on a case, and the order can differ between models. Determine it once, empirically. Move a device that enumerates through each socket in turn. Any USB stick or debug probe will do. Read back which port it lands on: ```bash theme={null} lager ssh --box -- ls -l /sys/bus/usb/devices/ ``` A **charge-only cable cannot do this**. It presents no device to the hub, so it is indistinguishable from an empty socket (see the warning above). **Moving a cable between sockets silently repoints the net.** A `usb` net names a hub *port*, not the device plugged into it. After someone reseats a cable, the same command switches something else. There is no error, because from the hub's point of view nothing is wrong. This cuts the opposite way to every other instrument on the box. A debug probe, a USB-UART bridge, and a power supply carry a **serial number** address. Their nets follow the device wherever it is plugged in. A hub port net is addressed by **topology**, so it stays with the socket. Both are correct, and they behave oppositely under exactly the same physical change. So: after any bench rework, re-derive the map before running anything scripted. For automation that must survive reseating, locate the device by VID:PID under the hub. Then drive whichever port it is actually on, rather than a hard-coded net name. The verification wrote a volatile setting to a device on a user port, cycled power, and read the power-on default back. That is a real cold boot, not a logical disconnect. The upstream tier's behavior comes from re-power timing rather than from a register. A device there reasserts CONNECT within a millisecond, so it never lost power. Treat that as strong evidence rather than proof. **A powered-off port still shows up everywhere.** While a port is unpowered the hub raises no change bit, so Linux never polls it and never processes the disconnect. The device keeps its entry in `lsusb`, its `/sys` node, and any `/dev/ttyUSB*` it owns, for as long as the port stays off. The kernel logs `USB disconnect` only when power comes *back*. An off port stays off indefinitely; nothing re-powers it on its own. So **never test for a device's absence to decide whether a port is off.** It is wrong in both directions. Use `lager usb state`, which reads the hub's own power bit, or `lager usb cycle`, which waits for the port to re-enumerate after power returns. **SuperSpeed docks are untested.** A dock linked over USB 3 enumerates as two virtual hubs, and VBUS drops only when power is cleared on both halves. Lager implements that pairing, but no SuperSpeed-linked dock was available to validate it. A topology that it cannot pair unambiguously is refused rather than half-switched. The validated configuration is a USB 2.0 link. **Recovering a port left unpowered.** `lager usb recover` re-asserts power on every port of that dock. If that cannot reach the hub, unbinding and rebinding the kernel hub driver re-powers every port and re-enumerates the children. That must be run **on the box host**, not in the container, which mounts `/sys` read-only — replace the path with your hub's: ```bash theme={null} echo 1-1.4.4:1.0 | sudo tee /sys/bus/usb/drivers/hub/unbind echo 1-1.4.4:1.0 | sudo tee /sys/bus/usb/drivers/hub/bind ``` **Enables:** * Automated USB device cycling * Remote power reset of DUTs * CI-controlled peripheral management ## Serial / UART Adapters | Manufacturer | Model | Control Command | | ------------ | ----------------- | --------------- | | Prolific | PL2303 | `lager uart` | | Silicon Labs | CP210x | `lager uart` | | FTDI | FT232R | `lager uart` | | Espressif | ESP32 JTAG Serial | `lager uart` | **Enables:** * Automated log capture * Bootloader interaction * Serial-based test automation ## Wireless | Protocol | Control Command | Capabilities | | -------------------------- | --------------- | -------------------------------------------- | | Bluetooth Low Energy (BLE) | `lager ble` | Scan, connect, disconnect, service discovery | **Enables:** * Wireless device testing * Remote connectivity validation * Automated provisioning tests *** # 6. Vision & Automation ## Cameras | Manufacturer | Model | Control Command | | ------------ | ---------------------- | --------------- | | Logitech | BRIO HD | `lager webcam` | | Logitech | BRIO | `lager webcam` | | Logitech | BRIO 4K Stream Edition | `lager webcam` | | Logitech | 4K Pro (Logi 4K Pro) | `lager webcam` | | Logitech | C930e | `lager webcam` | | Logitech | C925e | `lager webcam` | | Logitech | C922 Pro Stream | `lager webcam` | | Logitech | C920 | `lager webcam` | | Logitech | C615 | `lager webcam` | | Logitech | C270 | `lager webcam` | | Logitech | StreamCam | `lager webcam` | **Enables:** * Visual DUT inspection * Automated visual verification ## Robotic Automation | Manufacturer | Model | Control Command | | ------------ | ------ | --------------- | | Rotrics | Dexarm | `lager arm` | **Capabilities:** * Position read/write * Homing * Motor enable/disable * Acceleration configuration **Enables:** * Automated physical interaction with hardware * Button press automation * Mechanical test workflows *** # Custom Instrument Support Lager's modular architecture allows new instruments to be integrated quickly through backend extensions. If you use equipment not listed in this document, please contact us to discuss integration support. *** # Contact Lager Data [GitHub](https://github.com/lagerdata/lager) [GitHub Issues](https://github.com/lagerdata/lager/issues)