# 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 * 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 ``` You can add all of your configured Lager Boxes using the command below and view your list of them using the following command. ```bash theme={null} lager boxes ``` Note that this list is unique to your personal computer which means you can give your Lager Boxes any name you want (though it is normally in your best interest to agree on a naming convention with your team). *** ## 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 | The box may need a restart or a software update. Contact your administrator. | | `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 -- from CLI commands on your laptop to instruments connected to your DUT. ## High-Level Overview ``` USER COMPUTER LAGERBOX (x86-64, Ubuntu 22.04+) +---------------------------+ +----------------------------------------------+ | | | | | $ lager supply psu1 | | Docker Container ("lager") | | voltage 3.3 | Tailscale VPN | +----------------------------------------+ | | --yes | (WireGuard) | | Flask/WebSocket Server | | | | ───────────────> | | Python execution service | | | $ lager python | SSH + HTTP | | Debug service (GDB) | | | my_test.py | over encrypted | | | | | | tunnel | | Hardware Service | | +---------------------------+ | | | | | | | v | | | | Dispatchers --> Drivers | | GITHUB ACTIONS RUNNER | | | | | | +---------------------------+ | +-----|--------------|-------------------+ | | | | | | | | $ lager python | Tailscale VPN | USB / Serial / VISA / LAN | | tests/ci/test.py | ───────────────> +--------|--------------|----------------------+ | --box $BOX_IP | (ephemeral key) | | | | +--------|--------------|----------+ +---------------------------+ | INSTRUMENTS | | | | Power Supply Oscilloscope | | LabJack T7 Debug Probe | | Battery Sim USB Hub | | E-Load Thermocouple | +--------|-------------------------+ | | Wires / probes / pins v +------------------+ | | | DUT (Device | | Under Test) | | | +------------------+ ``` All three entry points -- developer CLI commands, developer custom scripts, and CI runners -- share the same execution infrastructure. They all reach the Lagerbox through Tailscale VPN and hit Flask on port 5000, which executes the uploaded script or impl module inside the Docker container with full access to `lager.*` hardware libraries. *** ## Terminology | Term | Definition | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CLI** | The `lager-cli` Python package (installed via `pip install lager-cli`). A Click-based command-line tool that runs on the developer's laptop. | | **Tailscale VPN** | A WireGuard-based mesh VPN that creates an encrypted tunnel between the developer's machine and the Lagerbox. | | **Lagerbox** | Any x86-64 machine running Ubuntu 22.04 or newer, physically co-located with the test instruments. Runs a Docker container hosting the Flask/WebSocket server and hardware drivers. | | **Net** | A logical name (e.g., `psu1`, `uart0`) that maps to a specific instrument + channel + address. Stored in `/etc/lager/saved_nets.json` on the box. | | **DUT** | Device Under Test -- the embedded board or product being tested. | | **Instrument** | A piece of test equipment (power supply, oscilloscope, LabJack, debug probe, etc.) connected to the box via USB, serial, or LAN. | | **`lager python`** | CLI command that uploads a user-written Python script to the box for execution with full access to `lager.*` hardware libraries. | *** ## Lagerbox Internals ``` LAGERBOX HOST (x86-64, Ubuntu 22.04+) +-----------------------------------------------------------------------+ | | | /etc/lager/ ~/third_party/ | | saved_nets.json JLink_Linux_*/ (optional) | | available_instruments.json customer-binaries/ (optional) | | box_id | | | | Docker Container "lager" (--restart always) | | +-------------------------------------------------------------------+ | | | | | | | Port 9000 -- Flask + SocketIO -- Main Box API | | | | | (UART, supply, battery, instruments, | | | | | nets, lock) | | | | v | | | | Port 5000 -- HTTP Server -- Python Execution Service | | | | | | | | | | receives impl script + LAGER_COMMAND_DATA | | | | | env var (JSON), executes it, streams results | | | | v | | | | Port 8765 -- WebSocket Server -- Debug Service (GDB, OpenOCD) | | | | | | | | Port 8100 -- HTTP Server -- MCP Service (AI tool integration) | | | | | | | | Port 8080 -- Hardware Service (internal only, not exposed) | | | | | | | | | v | | | | +------------------+ | | | | | NetsCache | Thread-safe singleton | | | | | (cache.py) | mtime-based invalidation | | | | +--------+---------+ O(1) lookup by net name | | | | | | | | | v | | | | +------------------+ | | | | | Dispatchers | BaseDispatcher subclasses | | | | | (per domain) | driver caching, net resolution | | | | +--------+---------+ | | | | | | | | | v | | | | +------------------+ | | | | | Drivers | VISA/SCPI, pySerial, LJM, | | | | | (per instrument)| pyOCD, aardvark_py, etc. | | | | +--------+---------+ | | | | | | | | +--------------------+----------------------------------------------+ | | | | | USB / Serial / VISA / LAN | +----------------------+------------------------------------------------+ | v INSTRUMENTS ``` ### Port Summary | Port | Service | Exposed | Purpose | | --------- | ---------------- | -------------------------- | --------------------------------------------------------------------------------------------------------- | | 9000 | Flask + SocketIO | Yes (VPN only) | Main box API: UART streaming, live supply/battery WebSockets, instrument discovery, net listing, box lock | | 5000 | HTTP | Yes (VPN only) | Python execution service: receives CLI commands, runs impl scripts | | 8765 | WebSocket | Yes (VPN only) | Debug sessions (GDB, flash, reset) | | 8100 | HTTP | Yes (VPN only) | MCP service for AI tool integration | | 8080 | Hardware Service | No (container-internal) | Instrument control via Device proxy | | 8081 | HTTP | Yes (if PicoScope present) | Oscilloscope streaming UI | | 8082-8085 | TCP / WebSocket | Yes (if PicoScope present) | Oscilloscope daemon (commands, browser streaming, database streaming, CLI WebSocket) | | 8086+ | HTTP | Yes (if webcams present) | Webcam MJPEG streaming (one port per camera) | | 22 | SSH | Yes | Direct SSH access for deployment and debugging | *** ## Optional Control Plane Integration Lager boxes publish SSH keys from a key directory, `/etc/lager/authorized_keys.d/`, so an external control plane can provision access without a human typing SSH commands. Dropping a `.pub` file there gets the key into the box account's `~/.ssh/authorized_keys` within about five seconds. Because `/etc/lager` is bind-mounted into the runtime container, a control plane can write that file from inside the container — which is how it bootstraps before it has any SSH access to the box at all. `start_box.sh` owns only the region of `authorized_keys` between its `# BEGIN LAGER MANAGED KEYS` and `# END LAGER MANAGED KEYS` markers, and rebuilds that region from the key directory on every pass. Two consequences worth knowing: * **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 are untouched.** `lager ssh-setup`, `ssh-copy-id`, and cloud-init all append outside the marked region, and are preserved verbatim. Any other 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. 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. Commercial control planes that build on this hook — for fleets that need org/RBAC/SSO, audit logging, and scheduling on top of Lager — are listed on the [Professional Services directory](https://lagerdata.com/professional-services). *** ## Net Abstraction A **Net** is the central abstraction that decouples CLI commands from physical hardware details. ``` CLI command saved_nets.json entry Physical hardware +---------------------+ +----------------------------+ +---------------------+ | | | { | | | | lager supply psu1 | ----> | "name": "psu1", | ---> | Rigol DP832 | | voltage 3.3 | | "type": "power-supply", | | Channel 1 | | | | "channel": 1, | | VISA: USB0::... | +---------------------+ | "instrument": { | +---------------------+ | "name": "rigol-dp832",| | "address": "USB0::.."| | }, | | "params": { | | "voltage_limit": 5.0 | | } | | } | +----------------------------+ ``` ### Supported Net Types | Net Type | Instruments | | -------------- | ------------------------------------------------ | | `power-supply` | Rigol DP800, Keithley 2200/2280, Keysight E36x00 | | `battery` | Keithley 2281S | | `eload` | Rigol DL3021 | | `solar` | EA PSI / EL series | | `analog` | Rigol MSO5000 (oscilloscope analog channel) | | `logic` | Rigol MSO5000 (logic analyzer channel) | | `adc` | LabJack T7, USB-202 | | `dac` | LabJack T7, USB-202 | | `gpio` | LabJack T7, USB-202 | | `thermocouple` | Phidget thermocouple | | `watt` | Yocto-Watt, Joulescope JS220 | | `debug` | J-Link, CMSIS-DAP, ST-Link (via pyOCD) | | `uart` | USB-to-serial adapters | | `i2c` | Aardvark, LabJack T7, FT232H | | `spi` | LabJack T7, FT232H | | `arm` | Rotrics Dexarm | | `usb-hub` | Acroname, YKUSH | *** ## Execution Flows ### CLI Command Execution Step-by-step data path for `lager supply psu1 voltage 3.3 --yes`: ``` DEVELOPER LAPTOP NETWORK LAGERBOX ================ ======= ======== 1. User runs: $ lager supply psu1 voltage 3.3 --yes | v 2. CLI resolves box - reads .lager/config - finds box IP for "psu1" | v 3. CLI builds command JSON | v 4. CLI uploads impl script 5. SSH/HTTP over cli/impl/power/supply.py ---------> Tailscale VPN --------> 6. Flask receives + sets env var (encrypted) request on :5000 LAGER_COMMAND_DATA= | v 7. Box executes supply.py in subprocess | v 8. Dispatcher looks up "psu1" in NetsCache, selects driver | v 9. Driver sends SCPI command to instrument | v 14. CLI displays: <--------------- result JSON <----------- 10. Result streamed back "Voltage set to 3.300V" streamed back ``` ### Custom Script Execution (`lager python`) The `lager python` command uploads a user-written Python script to the box for execution. It uses the same Flask :5000 execution path as CLI commands. ```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 `lager.*` hardware libraries, and output is streamed back in real time. *** ## Physical Wiring How instruments physically connect between the Lagerbox and the DUT: ``` LAGERBOX (x86-64, Ubuntu 22.04+) +----------+ | | USB-A | USB-A | USB-A LAN port | port | port port | | | | | | | +----+-----+ | | | | | | +---------+ +----+----+ +--------+ +------------+ | | | | | | | v v v v v v | +--------+ +---------+ +------+ +--------+ +--------+ | | LabJack| | Debug | | USB | |Aardvark| | Phidget| | | T7 | | Probe | | Hub | | I2C/SPI| | Thermo | | +---+----+ +----+----+ +--+---+ +---+----+ +---+----+ | | | | | | | | v v | | | | SWD/JTAG USB to DUT | | | | | | | v v v v +----------------------------------------------------------------+ | DUT (Device Under Test) | | VCC, GND, SDA, SCL, SWD, TX, RX, GPIO, TEMP, USB | +----------------------------------------------------------------+ | +--------------------------------------------------------+ | VISA-over-LAN Instruments (on same LAN) | | Rigol DP832, Rigol MSO5074, Keithley 2281S | | Connected to DUT via banana jacks / BNC / probes | +--------------------------------------------------------+ ``` ### Connection Types | Connection | Used For | Protocol | | ------------- | ----------------------------------- | --------------------------------- | | USB | LabJack, debug probes, serial, hubs | Vendor-specific, CDC-ACM | | USB-VISA | Rigol/Keithley/Keysight instruments | USBTMC (SCPI) | | LAN-VISA | Bench instruments on local network | VXI-11 / raw TCP (SCPI) | | Serial (UART) | DUT communication | RS-232 / TTL via USB adapter | | SWD / JTAG | Firmware flash, debug, reset | ARM debug (via probe) | | I2C / SPI | Peripheral communication with DUT | I2C / SPI via Aardvark or LabJack | *** ## GitHub Actions CI Integration A CI runner is an ephemeral GitHub Actions VM that joins Tailscale and runs `lager` commands exactly like a developer would -- no special CI-specific infrastructure is needed. ``` GITHUB CLOUD TAILSCALE NETWORK HARDWARE BENCH ============ ================= ============== +---------------------------+ | GitHub Actions Runner | | (ubuntu-latest) | | | | 1. checkout code | | 2. pip install lager-cli | | 3. tailscale up -----------> Tailscale +------------------+ | (TAILSCALE_AUTHKEY) | Coordination --> | LAGERBOX | | | Server | 100.x.y.z | | 4. lager hello --.--------> | | | --box $BOX_IP | encrypted tunnel | Docker container | | | (WireGuard) | :5000 Flask | | 5. lager python --.--------> | | | tests/ci/test.py | +--------+---------+ | --box $BOX_IP | | | | USB / VISA / LAN +---------------------------+ | +------+------+ | Instruments | +------+------+ | +------+------+ | DUT | +-------------+ ``` ### Required GitHub Secrets | Secret | Purpose | | ------------------- | ------------------------------------------------- | | `TAILSCALE_AUTHKEY` | Ephemeral auth key so the runner can join the VPN | | `LAGER_BOX_IP` | Tailscale IP of the Lagerbox (e.g., `100.x.y.z`) | ### Workflow Example ```yaml theme={null} name: Hardware-in-the-Loop Test on: workflow_dispatch: concurrency: group: hardware-test cancel-in-progress: false jobs: hardware-test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install Lager CLI run: cd cli && pip install -q -e . - name: Connect to hardware bench via Tailscale uses: tailscale/github-action@v2 with: authkey: ${{ secrets.TAILSCALE_AUTHKEY }} - name: Verify box connectivity run: lager hello --box ${{ secrets.LAGER_BOX_IP }} - name: Run hardware integration test run: | lager python tests/ci/demo_test.py \ --box ${{ secrets.LAGER_BOX_IP }} \ --add-file test/assets/firmware/nrf_blinky.hex \ --env FIRMWARE_PATH=nrf_blinky.hex \ --timeout 300 - name: Emergency cleanup if: failure() run: | lager python tests/ci/cleanup.py \ --box ${{ secrets.LAGER_BOX_IP }} || true ``` # 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 -- from verifying connectivity to running an automated test, in Python or Rust. By the end, you'll have used the CLI interactively and written your first Lager test script. This tutorial assumes you have completed the [Getting Started](/source/getting-started/overview) guide: the CLI is installed, your box is added, and your instruments are configured 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 let's convert the manual CLI steps into a repeatable test. The key advantage of a script is that it always cleans up after itself (disabling power) even if an error occurs. You can write it in Python (runs on the box via `lager python`) or in Rust (an ordinary `cargo test` in your firmware repo, using the [`lager-net` crate](/source/reference/rust/overview)). Both versions below do the same thing: flash, power on, measure, assert, 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 matters: the Python version uses `try/finally` to guarantee the power supply is disabled even if an error occurs, and the Rust version disables power before its assertion (any earlier `?` error also ends the test with the supply's state visible in the failure). This is important for protecting your hardware. *** ## 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.2" }` 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 For a more comprehensive automation example combining robot arm control, USB hub power cycling, debug probe flashing, and ADC measurement, see the [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py). # 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 you are testing 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 running Ubuntu 22.04 or newer (for example, a compact mini PC) that sits on your bench, physically connected to your instruments and DUT. It runs the Lager container (Flask + SocketIO) which exposes HTTP and WebSocket APIs for hardware control. ## Net A named logical connection to a physical instrument on a Lager Box — e.g. `supply1` mapping to channel 1 of a Rigol DP832 power supply, or `uart0` mapping to `/dev/ttyUSB0` at 115200 baud. Nets decouple test code from specific hardware addresses, so the same script works as instruments move or change. ## 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. Once your Nets are configured, you can begin controlling those instruments using the Lager Client — via the CLI or Python SDK. The examples below demonstrate how to issue commands to common instrument types such as power supplies, battery simulators, debuggers, and communication buses. *** ## Setting Defaults Before diving into examples, you can set default values for `--box` and common nets to avoid 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) represents the battery's charge level as a percentage (0-100%). Setting SOC to 50 simulates a half-charged battery, which is useful for testing how your device behaves at different battery levels. 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 For more complex automation or integration into test frameworks, you can use the Lager Python SDK to perform the same operations programmatically. 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 to run the script in the Lager Box environment, ensuring it has access to 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 provides a unified interface for interacting with embedded hardware, allowing firmware engineers to build repeatable development and validation workflows that run consistently on a developer's desk, in CI, or under the control of AI agents. By replacing one-off scripts and manual bench procedures with reusable automation, Lager helps teams build 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 running Ubuntu 22.04 or newer (for example, a compact mini PC) that sits alongside your hardware, physically connected to your test equipment and Device Under Test (DUT). Once connected to your instruments and your DUT, the Lager Box exposes them through a consistent programmable interface that you can drive 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 and run it with `cargo test`, right 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 In order to interact with your test environment using a Lager Box, the Lager Box must be configured with an assortment of **Nets**. Each Net corresponds to a specific instrument, channel of an instrument, serial port, or other interface that you may want to interact with using your Lager Box. For example, you might have your DUT powered by channel 1 of a power supply. You'd then be able to create a Power Supply Net called `DUT_POWER` which maps to that power supply's first channel and allows you to toggle on/off your DUT or perform any other function that power supply supports. *** ## Next Steps Ready to get started? Install the CLI and connect to your first 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 Your Instruments Source: https://docs.lagerdata.com/source/getting-started/setting-up-instruments Configure and manage instruments connected to a Lager Box Before you can use instruments with the Lager Client (CLI or Python Library), you may need to perform a minimal setup to identify and organize connected devices. *** ## 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 an instrument is plugged into a Lager Box, Lager automatically creates a default Net for each function the instrument supports. * A simple, single-function device like a power supply will create one `Supply` Net. * A multi-function device like a LabJack T7 DAQ will create several Nets: one for `GPIO`, one for `ADC`, one for `DAC`, and (if configured) one each for `I2C` and `SPI`. * An I2C/SPI adapter like a Total Phase Aardvark will create both an `I2C` Net and an `SPI` Net, since the adapter supports both protocols. For instruments that support multiple channels of the same type (e.g., a 4-channel oscilloscope), you can assign and configure Nets for each channel individually. *** ## Modify or Assign Nets Using the TUI For instruments that support multiple channels (e.g. LabJack, PicoScope), you can assign new Nets using the interactive TUI (text user interface). You can also re-name existing Nets. 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 and connect through a USB-serial adapter — for example, a Rigol DP711 power supply on its RS-232 port. The Lager Box sees only the adapter cable, so it can't tell what instrument is behind it, and 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 same flow is available in the Net TUI (`lager nets tui`) via the **Assign Device** button: pick the cable, pick the instrument, and name the net in the same flow — done. After assignment, the instrument shows up in `lager instruments` and in the TUI like any auto-detected device, and nets can be added for it normally. The assignment is stored on the box and survives reboots and replugs. 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 issues you may encounter when using Lager, organized by category. Each section includes 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 is not running. **Fix:** 1. The Docker container on the box may have stopped. Contact 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`. **Cause:** The network request is being sent but not reaching the box, often due to a firewall or incorrect IP. **Fix:** 1. Double-check the IP address with `lager boxes`. 2. If the box was recently moved or reprovisioned, its IP may have changed. Check your Tailscale admin panel or router DHCP table. 3. Try pinging the box: `ping `. **Cause:** The box may have lost power, network connectivity, or had its IP address change. **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. Ensure the Docker container is running with USB passthrough. Run `lager hello --box `. 4. Run `lager update --box ` to install the latest udev rules and drivers. **Cause:** The instrument may not be recognized, may be using a different USB port, or may need updated drivers. **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) is actively using the instrument's USB connection. **Fix:** 1. Close any running TUI sessions (press `q` to exit). 2. Wait a moment and retry -- the previous command may still be completing. 3. If the issue persists, the instrument handle may be stuck. Run `lager update --box ` to restart the service. *** ## Power Supply Issues **Cause:** The output voltage or current exceeded the protection threshold you set, so 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 may not actually be enabled, or the load may be pulling 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:** The J-Link or pyOCD process may be stuck from a previous session. **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 are running the script directly with `python` instead of using `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:** The script may be failing silently, or output may not be flushed. **Fix:** 1. Add `print()` statements to confirm the script is executing. 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. # 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` | Lagerbox name or IP address | | `--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` | Lagerbox 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` - Lagerbox 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 X Y Z [OPTIONS] ``` **Arguments:** * `X` - Target X position (mm) * `Y` - Target Y position (mm) * `Z` - Target Z position (mm) **Options:** * `--box TEXT` - Lagerbox name or IP * `--timeout FLOAT` - Move timeout in seconds (default: 5.0) * `--yes` - Confirm the action without prompting **Examples:** ```bash theme={null} # Move to specific coordinates lager arm ARM1 move 100 200 50 --box my-lager-box --yes # Move with custom timeout lager arm ARM1 move 150 250 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 [DX] [DY] [DZ] [OPTIONS] ``` **Arguments:** * `DX` - Delta X offset (default: 0.0) * `DY` - Delta Y offset (default: 0.0) * `DZ` - Delta Z offset (default: 0.0) **Options:** * `--box TEXT` - Lagerbox name or IP * `--timeout FLOAT` - Move timeout in seconds (default: 5.0) * `--yes` - Confirm the action without prompting **Examples:** ```bash theme={null} # Jog the arm by +5 mm in every axis lager arm ARM1 move-by 5 5 5 --box my-lager-box --yes # Move only in Z axis lager arm ARM1 move-by 0 0 10 --yes # Move with custom timeout lager arm ARM1 move-by 10 0 0 --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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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 ACCELERATION TRAVEL [RETRACT] [OPTIONS] ``` **Arguments:** * `ACCELERATION` - Acceleration value (integer, >= 0) * `TRAVEL` - Travel acceleration value (integer, >= 0) * `RETRACT` - Retract acceleration value (integer, >= 0, default: 60) **Options:** * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} # Set acceleration parameters lager arm ARM1 set-acceleration 100 80 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 100 250 30 --box my-lager-box --yes # Jog the arm by +5 mm in X direction lager arm ARM1 move-by 5 0 0 --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 100 80 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` | Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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. Saving overwrites the slot's previous model, and the instrument has no way to delete a model from a slot — a slot, once written, can only be overwritten with a different model. 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` - Lagerbox name or IP Writes the slot's 101 `voc,resistance` points in the format `model-create` accepts, so a saved model can be exported, edited, and written 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` - Lagerbox 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` - Lagerbox name or IP * `--yes` - Skip confirmation prompt ### `disable` Disable battery simulator output. ```bash theme={null} lager battery NETNAME disable [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox 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` - Lagerbox name or IP ### `clear-ovp` Clear over-voltage protection trip condition. ```bash theme={null} lager battery NETNAME clear-ovp [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox name or IP ### `clear-ocp` Clear over-current protection trip condition. ```bash theme={null} lager battery NETNAME clear-ocp [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox 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` - Lagerbox 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 Lagerboxes 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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 may not advertise their name until connected # 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 — handy for bringing 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` (Lagerbox 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 * [WiFi](/source/reference/cli/wifi) — manage the Lager Box's own WiFi settings # 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 should have — USB device permissions (udev rules), apt packages, bind mounts, environment variables, pip/cargo/npm packages, sysctl values — and then `apply` puts it 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` | Lagerbox 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 can't open it (for example `dfu-util` failing 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. ### `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` | Show what would change, 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 Lagerbox configurations Manage Lagerbox 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 Lagerbox 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 Lagerboxes 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 (typical Lagerbox naming convention) and automatically adds them as boxes with uppercase names. **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` | Lagerbox name or IP address | | `--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` | Lagerbox 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` - Lagerbox 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 based on the probe's slot (2331 for the first probe, 2334 for the second, etc.); pass this only if you need a specific port, and 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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` - Lagerbox 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 Connected: True ``` ### `health` Check debug service health and resource usage. ```bash theme={null} lager debug [NET_NAME] health [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox 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** — they must be decoded by `defmt-print` using 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 the `lager` process is killed 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 are using. The `defmt-rtt` crate sets up only the up buffer — with no down buffer the target discards what it receives and gives no indication it did so, which reads as a host-side failure but 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, so every engineer (and your CI) 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; config-defined `ports`, `volumes`, and `environment` are applied first, then anything passed on the command line is appended. `terminal` also wires up SSH for you automatically: it forwards your `SSH_AUTH_SOCK` agent socket and mounts `~/.ssh/id_ed25519` and `~/.ssh/known_hosts` (read-only) when they exist, and it mounts your global `.lager` config into the container so nested `lager` calls are authenticated. Use `--info` to debug what would run 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 may contain only letters, numbers, dashes, and underscores. If you omit the command string, `add` prompts for it. By default `add` warns when overwriting an existing command; pass `--no-warn` to suppress that. *** ## 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 may 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, so a command saved with `lager devenv add` is runnable with `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 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` | Lagerbox 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. Skips the open (with a clear note) if the hardware service already holds a shared session for this address — collisions would 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 and walks the debug stack outside-in — software → USB → probe-visible → gdbserver → target connect — so 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 running, 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 and the CLI notes that the section is unavailable (the box may 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` | Lagerbox 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. So agents see your changes, either have a connected agent call the `box_manage` tool with `action="reload"` (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` | Lagerbox name or IP address | | `--help` | Show help message and exit | ## 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` | Lagerbox name or IP address | | `--duration FLOAT` | Integration duration in seconds (default: 10.0) | | `--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` | Lagerbox name or IP address | | `--duration FLOAT` | Measurement duration in seconds (default: 1.0) | | `--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 | | `--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` | Lagerbox 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) | | `--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 and may be re-enabled in a future release. 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` | Lagerbox name or IP address | | `--hold` | Hold output state (keeps process alive until Ctrl+C) | *** ## 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 setting the output level. The pin state is maintained until you press Ctrl+C. This is useful when you need to assert a signal for manual testing or when the pin state would otherwise be reset 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 and may be re-enabled in a future release. 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` | Lagerbox 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 success message when connection is working # 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 Lagerbox. 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` | Lagerbox 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` | Lagerbox 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` | Lagerbox 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` | Lagerbox 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` | Lagerbox 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` | Lagerbox 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 may return a bus error; this is normal and handled 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 | | `--user TEXT` | string | `lagerdata` | SSH username | | `--version TEXT` | string | `main` | Box version to deploy: a release tag (e.g. `v0.15.0`) or a git branch | | `--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 | | `--help` | | | Show help message and exit | Either `--box` or `--ip` is required. If both are provided, the command exits with an error. ## 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, falls back to password if needed 3. **Show summary** - Displays what will be installed and asks for confirmation 4. **Deploy** - Runs the deployment script (up to 30 minutes for a fresh install) 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 # Non-interactive installation lager install --ip 192.168.1.100 --yes ``` ## SSH Authentication The command attempts key-based SSH authentication first. If that fails, it offers to continue with password authentication. For new hosts, the SSH host key is accepted automatically. If the host key has changed since a previous connection, the command asks you to verify the change manually before proceeding. ## Notes * 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 Lagerbox. Use it to push a locally built package onto a box without publishing it to an index first — handy for iterating 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` | Lagerbox 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 Lagerbox. ## Syntax ```bash theme={null} lager instruments [OPTIONS] ``` ## Options | Option | Description | | ----------- | --------------------------- | | `--box BOX` | Lagerbox 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 that serve different purposes: a **global** file shared across all projects, and a **project-local** file 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, the global `~/.lager` file is automatically mounted inside the container at `/lager/.lager` (with `LAGER_CONFIG_FILE_DIR=/lager`), so that box and net definitions are 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 serves as a local cache managed by `lager nets`. **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 and walks up the directory tree until it finds a `.lager` file (that is not the global `~/.lager`). The first one found is used. ``` /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 to determine 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` may 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 should be uploaded 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 and 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 may use lowercase section names. The CLI automatically upgrades these when writing: | 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 | Command | Lock window | Why | | --------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `lager python` | Full test run (acquire → heartbeat → release) | Canonical test runner. | | `lager install` | The `setup_and_deploy_box.sh` step (the part that restarts the container) | Container restart mid-test would kill the test outright. | | `lager uninstall` | Container teardown, image wipe, `~/box` and `/etc/lager` removal | Same — destructive on-box mutation. | | `lager update` | Container stop → image rebuild → restart → health check | Container restart is the test-clobbering action. Read-only probe / fetch are deliberately outside the lock. | | `lager install-wheel` | The `pip install` invocation inside the container | `pip install` mutates the container's Python environment; a concurrent test could race on imports. | Read-only commands (`lager hello`, `lager boxes list`, `lager boxes lock` / `unlock` itself, status / dry-run paths, etc.) do **not** acquire the auto-lock. Note: this is intentionally narrower than the v0.12–0.13.3 behavior, which slapped a `--force-command`-overridable lock on every single CLI command. See *Backward compatibility* below for the v0.13.4 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 have already `lager boxes lock`ed the box as yourself before running `lager python`, the CLI sees the lock as already-ours and **does not release it on exit** — 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, so another caller waits at most one TTL. ### `--detach` keeps the lock `lager python script.py --detach` acquires the lock with `ttl_seconds: null` (no auto-expiry) because the heartbeat thread dies with the CLI. The detached script keeps running on the box, but the lock must be released manually: ```bash theme={null} lager python long_test.py --box my-lager-box --detach # Box 'my-lager-box' locked for detached run; release with: lager boxes unlock --box my-lager-box # ... later, after the script finishes on the box: lager boxes unlock --box my-lager-box ``` ### 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 would otherwise be `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 (`...GITHUB_JOB=hardware-tests/:` differs per item), POST `/lock`, and whichever loses the race waits up to 30 minutes for the winner to finish before retrying. 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, but legacy clients (e.g. older CLIs against the new box server) get the same eternal-lock behavior automatically because 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 this PR avoids it | | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *"Supply commands never released the lock"* | The auto-lock is only attached to **5 commands** (`python`, `install`, `uninstall`, `update`, `install-wheel`), not every CLI surface. Supply commands etc. don't touch the lock — no decorator-on-everything to leak from. | | *"Long-running commands blocked all other commands on the same box"* | Only those 5 commands check the lock; status / list / read-only paths are unaffected. For genuine concurrent test runs, dev gets fail-fast in \<5s and CI gets a queue (default 60s, configurable). That's the *desired* policy. | | *"Detached processes left stale locks"* | `--detach` is **opt-in** for a long-lived hold (`ttl_seconds: null` is intentional). Non-detached runs have heartbeat + TTL reap, so an abnormal CLI exit self-recovers in ≤ TTL + grace. | `--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` | Lagerbox 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 ever see it when a box has been deliberately placed behind a gateway. ## Signing in The first time you run a command against a gated box, it tells you exactly what to do — the URL is filled in 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. ```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 which servers you're signed in to, as whom, when each session expires, and which gated boxes it has seen — enough to tell at a glance whether a problem is "not signed in", "signed in as the wrong account", or "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 Whether a box requires sign-in, and who may use it, is managed from your control-plane dashboard, not the CLI. 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 Lagerboxes. ## 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` | Lagerbox 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` - Lagerbox 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 (`--cs` is optional; omit it for 3-pin SPI with manual chip select) 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. **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](#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 | | `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 are MPSSE-capable (JTAG/SWD via OpenOCD); channels C and D on the FT4232H are UART-only. The user picks an interface per net via an `@` suffix on the device type. **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). **UART nets** distinguish channels by their tty path. The USB scanner enumerates every `/dev/ttyUSB` bound to the chip's USB serial; each shows up as a separate add-list entry, so on an FT4232H you'll 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` (which all bind to channel A in the OpenOCD interface config — incompatible with `debug@A`) The user is responsible for not double-booking channel A (e.g. picking `debug@A` *and* `spi`); 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` - Lagerbox 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` - Lagerbox 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 and are reached over RS-232 through a generic USB-serial adapter — for example, a **Rigol DP711** power supply behind a Prolific cable. 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`, 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` | Lagerbox 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 moving the cable to a different port breaks the assignment. **Removing an assignment:** ```bash theme={null} lager nets assign --remove --serial 00000006 --box my-lager-box ``` The cable is offered as a generic UART device again. **Nets live and die with their assignment**: any saved nets bound to the assignment's `serial://` address are deleted automatically and reported in the output. The same cascade applies when re-assigning a cable to a different instrument (or switching its identity from `--serial` to `--port`) — only a baud-only re-assign keeps the existing nets. **Currently assignable devices:** Rigol DP711 (single-channel RS-232 power supply). Run `lager nets assign --list` to see the catalog your box supports. ### `delete` Delete a specific net by its name and type. ```bash theme={null} lager nets delete NAME NET_TYPE [OPTIONS] ``` **Arguments:** * `NAME` - Name of the net to delete * `NET_TYPE` - Type of the net (supply, debug, adc, i2c, spi, etc.) **Options:** * `--box TEXT` - Lagerbox name or IP * `--yes` - Skip confirmation prompt **Example:** ```bash theme={null} # Delete a supply net (with confirmation) lager nets delete supply1 supply --box my-lager-box # Delete without confirmation lager nets delete temp_sensor adc --box my-lager-box --yes # Delete an I2C net lager nets delete i2c_bus i2c --box my-lager-box --yes ``` ### `delete-all` Delete all saved nets on a Lager Box. **This is a dangerous operation.** ```bash theme={null} lager nets delete-all [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox name or IP * `--yes` - Skip confirmation prompt **Example:** ```bash theme={null} # Delete all nets (requires confirmation) lager nets delete-all --box my-lager-box # Delete all nets without prompting lager nets delete-all --box my-lager-box --yes ``` ### `rename` Rename an existing net. ```bash theme={null} lager nets rename NAME NEW_NAME [OPTIONS] ``` **Arguments:** * `NAME` - Current name of the net * `NEW_NAME` - New name for the net (must be unique) **Options:** * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} lager nets rename supply1 main_power --box my-lager-box ``` ### `tui` Launch an interactive terminal-based UI for managing nets. The TUI provides a visual interface for viewing, creating, and deleting nets. ```bash theme={null} lager nets tui [OPTIONS] ``` **Options:** * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} lager nets tui --box my-lager-box ``` **TUI Features:** * Browse all connected instruments and their channels * Create new nets with guided prompts * Pick custom LabJack pins when adding i2c/spi nets — a pin dialog opens with the defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI: CS=FIO0/SCK=FIO1/ MOSI=FIO2/MISO=FIO3); any DIO pin can be chosen per signal, CS can be set to none for 3-pin SPI, and pins already used by saved nets show a warning * Assign custom serial devices (RS-232 instruments) to their USB cables — the interactive twin of [`assign`](#assign), including the optional create-the-net step (`--as-net`) * Delete existing nets * View net details and instrument information * Keyboard navigation ### `set-script` Attach a debug script — either a JLinkScript or an OpenOCD `.cfg`/`.tcl` — to an existing debug net. The file is stored on the box and used automatically during connect, flash, erase, and reset operations. The backend (J-Link vs. OpenOCD) is auto-detected from two signals: 1. **The probe's USB VID** on the net's `address` field (J-Link → `jlink`; ST-Link, FTDI, CMSIS-DAP, etc. → `openocd`). 2. **The file** — extension first (`.JLinkScript` → jlink; `.cfg`/`.tcl`/`.ocd` → openocd), with a content sniff as a tie-breaker for extensionless files or stdin. If the two signals disagree, `set-script` refuses with a clear error and asks you to pick one via `--backend`. A debug net only carries **one** script at a time. If the other field is already set, `set-script` clears it and prints a yellow notice on stderr so nothing disappears silently. ```bash theme={null} lager nets set-script NAME SCRIPT_PATH [OPTIONS] ``` **Arguments:** * `NAME` - Name of the debug net * `SCRIPT_PATH` - Path to the script file, or `-` to read from stdin **Options:** * `--backend [jlink|openocd]` - Force a specific backend instead of auto-detecting (required if the probe and file disagree) * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} # Attach a J-Link script to a J-Link net (auto-detected) lager nets set-script debug1 ./custom_connect.JLinkScript --box my-lager-box # Attach an OpenOCD config to an FTDI/ST-Link/CMSIS-DAP net (auto-detected) lager nets set-script debug1 ./probe.cfg --box my-lager-box # Read the script from stdin cat probe.cfg | lager nets set-script debug1 - --box my-lager-box # Force a specific backend when the file or probe is ambiguous lager nets set-script debug1 ./generic_script --backend openocd --box my-lager-box ``` ### `remove-script` Remove the debug script (J-Link or OpenOCD) attached to a debug net. ```bash theme={null} lager nets remove-script NAME [OPTIONS] ``` **Arguments:** * `NAME` - Name of the debug net **Options:** * `--backend [jlink|openocd]` - Only remove the named backend's script (default: remove whichever is set) * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} # Remove whichever script is attached lager nets remove-script debug1 --box my-lager-box # Only remove the J-Link script (leave any OpenOCD config in place) lager nets remove-script debug1 --backend jlink --box my-lager-box ``` ### `show-script` Display the contents of the debug script attached to a debug net. The script content is written to stdout (so `> out.cfg` works); a one-line summary like `# OpenOCD config, 1247 bytes` is written to stderr so interactive use tells you which backend's script you're looking at without polluting redirects. ```bash theme={null} lager nets show-script NAME [OPTIONS] ``` **Arguments:** * `NAME` - Name of the debug net **Options:** * `--backend [jlink|openocd]` - Only show the named backend's script (default: show whichever is set) * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} # Display the attached script (J-Link or OpenOCD) lager nets show-script debug1 --box my-lager-box # Save to a local file (the stderr banner doesn't end up in the file) lager nets show-script debug1 --box my-lager-box > script.txt ``` ### `show` Display all fields of a saved net, including user-provided metadata (`purpose`, `notes`, `tags`) set with `describe`. ```bash theme={null} lager nets show NAME [OPTIONS] ``` **Arguments:** * `NAME` - Name of the net **Options:** * `--json` - Output as raw JSON * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} lager nets show battery1 --box my-lager-box lager nets show battery1 --json --box my-lager-box ``` ### `describe` Set metadata on a saved net so AI agents (and humans) understand what the net does on the DUT. Introduced in **lager 0.24.0**; the fields feed agent-assisted testing via the [MCP server](/source/reference/mcp/overview). At least one of `--purpose`, `--notes`, or `--tag` (or `--clear-tags`) must be provided. ```bash theme={null} lager nets describe NAME [OPTIONS] ``` **Arguments:** * `NAME` - Name of the net **Options:** * `-p`, `--purpose TEXT` - One sentence: what this net does on the DUT * `-n`, `--notes TEXT` - Optional notes (gotchas, jumper positions, scope probe points) * `-t`, `--tag TEXT` - Tag for categorisation/matching (repeatable) * `--clear-tags` - Remove all existing tags before adding new ones * `--box TEXT` - Lagerbox name or IP **Example:** ```bash theme={null} # Describe a net's purpose and add tags lager nets describe battery1 \ --purpose "Main battery rail powering the MCU" \ --tag power --tag critical \ --box my-lager-box # Replace the existing tags lager nets describe battery1 --clear-tags --tag power --box my-lager-box # View what was set lager nets show battery1 --box my-lager-box ``` ## Net Types Reference | Net Type | Description | Typical Instruments | | ------------------------------- | ------------------- | --------------------------------------------------------------------------------------- | | `power-supply` (alias `supply`) | Power supply output | Rigol DP800, Rigol DP711 (via [`assign`](#assign)), Keithley 2200/2280, Keysight E36200 | | `battery` (alias `batt`) | Battery simulator | Keithley 2281S | | `solar` | Solar simulator | EA PSI/EL series | | `eload` | Electronic load | Rigol DL3021 | | `debug` | Debug probe | J-Link, CMSIS-DAP, ST-Link | | `adc` | Analog input | LabJack T7 | | `dac` | Analog output | LabJack T7 | | `gpio` | Digital I/O | LabJack T7, MCC USB-202 | | `i2c` | I2C bus | LabJack T7, Aardvark | | `spi` | SPI bus | LabJack T7, Aardvark | | `scope` | Oscilloscope | Rigol MSO5000, PicoScope | | `uart` | Serial port | Prolific USB, SiLabs CP210x | | `usb` | USB port control | Acroname hub, YKUSH | | `camera` | Video capture | Logitech BRIO | | `arm` | Robot arm | Rotrics Dexarm | | `watt-meter` | Power measurement | Yocto Watt | | `thermocouple` | Temperature sensor | Phidget thermocouples | ## Debug Script Workflow Both J-Link and OpenOCD debug probes can carry a custom script for handling reset sequences, clock initialization, board-specific signal pinning, or other device-specific behavior. Lager stores one script per debug net (either a JLinkScript or an OpenOCD `.cfg`/`.tcl`, never both) and applies it automatically during connect, flash, erase, and reset operations. ```bash theme={null} # Attach a J-Link script to a J-Link probe (auto-detected from .JLinkScript extension) lager nets set-script debug1 ./my_device.JLinkScript --box my-lager-box # Attach an OpenOCD config to an FTDI / ST-Link / CMSIS-DAP probe (auto-detected) lager nets set-script debug1 ./probe.cfg --box my-lager-box # Verify what's attached (stderr says which backend, stdout has the content) lager nets show-script debug1 --box my-lager-box # Scripts are used automatically for all debug operations: lager debug debug1 flash --hex firmware.hex --box my-lager-box lager debug debug1 gdbserver --box my-lager-box # Remove the script when no longer needed lager nets remove-script debug1 --box my-lager-box ``` You can also attach a script at net creation time: ```bash theme={null} lager nets add debug1 debug STM32F407VG USB::001::002 \ --jlink-script ./my_device.JLinkScript --box my-lager-box lager nets add debug2 debug d2763 USB::003::004 \ --openocd-config ./probe.cfg --box my-lager-box ``` You can also configure J-Link scripts per-project in the local `.lager` config file: ```json theme={null} { "DEBUG": { "debug1": "./scripts/my_device.JLinkScript" } } ``` When both a net-level script (via `set-script`) and a project-level script (via `.lager` config) exist, the project-level script takes priority. ## Examples ```bash theme={null} # List all nets lager nets --box my-lager-box # Create a new power supply net lager nets add vdd_main power-supply 1 TCPIP::192.168.1.100::INSTR --box my-lager-box # Create I2C and SPI bus nets lager nets add i2c_sensors i2c 0 USB::470026574 --box my-lager-box lager nets add spi_flash spi 0 USB::2238595116 --box my-lager-box # Auto-create all available nets lager nets add-all --box my-lager-box --yes # Delete a specific net lager nets delete old_supply supply --box my-lager-box --yes # Rename a net lager nets rename supply1 main_power --box my-lager-box # Launch interactive manager lager nets tui --box my-lager-box # Bulk create from JSON file lager nets add-batch testbed-nets.json --box my-lager-box # Manage debug-probe scripts (J-Link or OpenOCD, auto-detected) lager nets set-script debug1 ./custom_init.JLinkScript --box my-lager-box lager nets set-script debug2 ./probe.cfg --box my-lager-box lager nets show-script debug1 --box my-lager-box lager nets remove-script debug1 --box my-lager-box ``` ## Notes * Net names are globally unique regardless of type * Use `lager instruments --box ` to see available instruments and channels * The TUI provides the easiest way to set up nets for the first time * Use `add-all` to quickly configure a new Lager Box with sensible defaults * I2C and SPI nets are supported on LabJack T7 and Aardvark adapters * Debug scripts (both J-Link and OpenOCD) are base64-encoded for storage and decoded automatically during debug operations * A debug net carries at most one script (`jlink_script` or `openocd_config`); `set-script` enforces this by clearing the other field when present # CLI Overview Source: https://docs.lagerdata.com/source/reference/cli/overview Introduction to the Lager Command-Line Interface (CLI) for hardware control and automation. The Lager Command-Line Interface (CLI) provides a powerful and scriptable way to interact with your Lager Box and connected hardware directly from your terminal. It is the ideal tool for manual control, shell scripting, and integration into CI/CD pipelines. ## Core Concepts The CLI follows a standard `GROUP COMMAND` structure. Most commands operate on a specific Lager Box (Lagerbox), which is specified using the `--box` option. ```bash theme={null} # General command structure lager [GLOBAL_OPTIONS] [ARGS]... # Example: Read an ADC value from a specific Lager Box lager adc SENSOR_1 --box my-lager-box # Example: Set power supply voltage lager supply VDD_MAIN voltage 3.3 --box my-lager-box ``` ### Nets Most hardware commands operate on **nets** - named abstractions representing physical test points or signals. Nets map friendly names to instrument channels. ```bash theme={null} # List available nets lager nets --box my-lager-box # Use a net in a command lager supply supply1 voltage 3.3 --box my-lager-box ``` ## Key Command Groups Below is a summary of the main command groups available in the Lager CLI. ### Lager Box Management * **[Boxes](/source/reference/cli/boxes)**: Manage Lager Box configurations (add, delete, sync, import/export) * **[Hello](/source/reference/cli/hello)**: Verify connectivity to your Lager Box * **[Update](/source/reference/cli/update)**: Update Lager Box software * **[SSH](/source/reference/cli/ssh)**: Direct SSH access to Lager Box * **[Logs](/source/reference/cli/logs)**: View Lager Box service logs ### Configuration * **[Instruments](/source/reference/cli/instruments)**: List connected test equipment * **[Nets](/source/reference/cli/nets)**: Create and manage nets (test point abstractions) * **[Defaults](/source/reference/cli/defaults)**: Set default Lager Box and net configurations ### Power & Simulation * **[Supply](/source/reference/cli/supply)**: Control programmable power supplies * **[Battery](/source/reference/cli/battery)**: Simulate battery characteristics (SOC, voltage, capacity) * **[Solar](/source/reference/cli/solar)**: Control solar panel simulators * **[E-Load](/source/reference/cli/eload)**: Control electronic loads (CC/CV/CR/CP modes) * **[Watt](/source/reference/cli/watt)**: Read power consumption from watt meters * **[Energy](/source/reference/cli/energy)**: Integrate energy/charge and compute power statistics (Joulescope JS220) ### Measurement * **[Scope](/source/reference/cli/scope)**: Control oscilloscopes for waveform capture * **[Logic](/source/reference/cli/logic)**: Control logic analyzers with protocol decoding * **[ADC](/source/reference/cli/adc)**: Read analog voltage values * **[Thermocouple](/source/reference/cli/tc)**: Read temperature sensors ### I/O & Communication * **[GPI](/source/reference/cli/gpi)**: Read digital inputs * **[GPO](/source/reference/cli/gpo)**: Write digital outputs * **[DAC](/source/reference/cli/dac)**: Analog output voltage control * **[UART](/source/reference/cli/uart)**: Serial communication * **[USB](/source/reference/cli/usb)**: USB port power control * **[BLE](/source/reference/cli/ble)**: Bluetooth Low Energy scanning ### Development * **[Debug](/source/reference/cli/debug)**: Flash firmware, GDB server, memory access, RTT logging * **[Python](/source/reference/cli/python)**: Execute Python scripts on Lager Box * **[Exec](/source/reference/cli/exec)**: Run build/test commands in a local Docker dev container * **[Devenv](/source/reference/cli/devenv)**: Configure the local Docker development environment * **[Binaries](/source/reference/cli/binaries)**: Run custom binaries on Lager Box ### Utilities * **[Webcam](/source/reference/cli/webcam)**: Video capture and streaming * **[Arm](/source/reference/cli/arm)**: Control robotic arm positioning ## Example: Test Script Workflow This example shell script demonstrates a typical hardware test workflow. ```bash theme={null} #!/bin/bash # Define variables LAGER_BOX="my-test-rig" FIRMWARE_PATH="build/my_app.hex" VOLTAGE_NET="supply1" SENSOR_NET="adc1" # 1. Flash the latest firmware echo "--> Flashing firmware..." lager debug flash --hex "$FIRMWARE_PATH" --box "$LAGER_BOX" # 2. Power on the device echo "--> Enabling power..." lager supply "$VOLTAGE_NET" voltage 3.3 --box "$LAGER_BOX" --yes lager supply "$VOLTAGE_NET" enable --box "$LAGER_BOX" sleep 2 # 3. Take a sensor reading echo "--> Reading sensor..." READING=$(lager adc "$SENSOR_NET" --box "$LAGER_BOX") echo "Sensor reading: $READING V" # 4. Check if value is within expected range if (( $(echo "$READING > 1.0" | bc -l) )) && (( $(echo "$READING < 2.0" | bc -l) )); then echo "PASS: Value within expected range" else echo "FAIL: Value out of range!" exit 1 fi # 5. Power down echo "--> Disabling power..." lager supply "$VOLTAGE_NET" disable --box "$LAGER_BOX" --yes echo "--> Test complete." ``` ## Setting Default Lager Box To avoid specifying `--box` on every command, set a default Lager Box: ```bash theme={null} # Set default Lager Box lager defaults add --box my-lager-box # Now commands use the default lager supply supply1 voltage 3.3 lager adc adc1 ``` ## Global Options All commands support these global options: | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lager Box name or IP address | | `--help` | Show help for any command | | `--version` | Show CLI version | ## Tips * Use `lager --help` to see all options for any command * Most commands support `--yes` to skip confirmation prompts * Set defaults with `lager defaults add` to reduce typing * Use the `tui` subcommand (where available) for interactive control * Commands that read values (like `adc`, `soc`) can be used in scripts # Python Source: https://docs.lagerdata.com/source/reference/cli/python Run Python scripts on box Run Python scripts inside the container on the specified box for test automation, hardware control, and data processing. ## Syntax ```bash theme={null} lager python [OPTIONS] [RUNNABLE] [ARGS]... ``` ## Global Options | Option | Short | Type | Default | Description | | ------------------- | ----- | -------- | --------- | ------------------------------------------------------------------------ | | `--box TEXT` | | String | | Lagerbox name or IP address | | `--env FOO=BAR` | | Multiple | | Set environment variables for the script | | `--passenv VAR` | | Multiple | | Pass environment variables from current shell | | `--kill TEXT` | | String | | Kill a specific running process by its process ID | | `--kill-all` | | Flag | | Kill all running scripts | | `--signal SIGNAL` | | Choice | `SIGTERM` | Signal to use with `--kill`/`--kill-all` | | `--download FILE` | | Multiple | | Download files from box after script completes | | `--allow-overwrite` | | Flag | | Allow overwriting existing local files with `--download` | | `--timeout SECONDS` | | Integer | 0 (none) | Maximum runtime in seconds | | `--detach` | `-d` | Flag | | Run in detached mode (background) | | `--port PORT` | `-p` | Multiple | | Forward ports to the Python process | | `--add-file FILE` | | Multiple | | Add extra files to upload with script | | `--reattach TEXT` | | String | | Reattach to a detached process by its process ID | | `--continue TEXT` | | String | | Resume a script paused at a breakpoint, by its process ID | | `--console TEXT` | | String | | Connect to the interactive console of a paused script, by its process ID | | `--help` | | Flag | | Show help message and exit | **Arguments:** * `RUNNABLE` - Python script file or directory to execute (required unless a process-management flag such as `--kill`, `--kill-all`, `--reattach`, `--continue`, or `--console` is used) * `ARGS` - Additional arguments passed to the script **Signal choices for `--kill`/`--kill-all`:** SIGINT, SIGQUIT, SIGABRT, SIGKILL, SIGUSR1, SIGUSR2, SIGTERM, SIGSTOP ## Basic Usage ```bash theme={null} # Run a Python script on the box lager python script.py --box my-lager-box # Run with arguments lager python test.py --box my-lager-box -- --verbose --target DUT1 # Run a directory as a module lager python my_test_suite/ --box my-lager-box ``` ## Environment Variables ### Setting Variables ```bash theme={null} # Set explicit environment variables lager python test.py --box my-lager-box --env API_KEY=abc123 --env DEBUG=true # Pass variables from your current shell export SECRET_TOKEN=xyz lager python test.py --box my-lager-box --passenv SECRET_TOKEN ``` ### Auto-Injected Variables The following environment variables are automatically available inside your script: | Variable | Description | | ---------------------- | ------------------------------------------------------------- | | `LAGER_OUTPUT_CHANNEL` | File path for structured output (see Structured Output below) | | `LAGER_PROCESS_ID` | Unique UUID for this execution | | `LAGER_RUNNABLE` | Path to the script being executed | | `LAGER_BOX` | Box name (if `--box` was provided) | ## File Downloads Download files generated by your script after it completes: ```bash theme={null} # Download a single file lager python data_processor.py --box my-lager-box --download results.csv # Download multiple files lager python test.py --box my-lager-box --download report.json --download log.txt # Allow overwriting existing local files lager python test.py --box my-lager-box --download results.csv --allow-overwrite ``` Files are downloaded to the current working directory using the basename of the remote path. If a local file with the same name already exists, the command fails unless `--allow-overwrite` is set. Gzip-compressed files are automatically decompressed during download. ## Detached Mode Run scripts in the background without waiting for output: ```bash theme={null} # Start a long-running script in the background lager python long_running.py --box my-lager-box --detach ``` In detached mode: * The command returns immediately after the script starts * No stdout/stderr is streamed back * The script continues running on the box * Use `--kill` to stop it later ## Killing Running Scripts ```bash theme={null} # Kill with default SIGTERM lager python --box my-lager-box --kill # Kill with specific signal lager python --box my-lager-box --kill --signal SIGKILL ``` ## Port Forwarding Forward network ports from the box to your local machine: ```bash theme={null} # Forward port 8080 lager python web_server.py --box my-lager-box -p 8080 # Forward with different local/remote ports lager python server.py --box my-lager-box -p 8080:80 # Forward with protocol lager python server.py --box my-lager-box -p 8080:80/tcp ``` Port format: `SRC_PORT[:DST_PORT][/PROTOCOL]` ## Timeout Limit script execution time: ```bash theme={null} # Kill after 5 minutes lager python analysis.py --box my-lager-box --timeout 300 ``` When a script exceeds the timeout: * First, SIGTERM is sent (exit code 124) * If the script doesn't exit, SIGKILL is sent (exit code 137) ## Structured Output Scripts running on the box can send structured data back to the CLI using the `lager.core.output()` function. This uses a dedicated output channel (file descriptor 3) separate from stdout/stderr. ### Box-Side API ```python theme={null} from lager.core import output, OutputEncoders # Output a Python dict (default: pickle encoding) output({'status': 'pass', 'measurement': 3.14}) # Output as JSON output({'voltage': 3.3, 'current': 0.5}, encoder=OutputEncoders.JSON) # Output as YAML output({'device': 'PSU-1', 'readings': [1.0, 2.0]}, encoder=OutputEncoders.YAML) # Output raw binary data output(image_bytes, encoder=OutputEncoders.Raw) ``` **Available encoders:** | Encoder | ID | Use Case | | ----------------------- | -- | --------------------------- | | `OutputEncoders.Raw` | 1 | Binary data (images, files) | | `OutputEncoders.Pickle` | 2 | Python objects (default) | | `OutputEncoders.JSON` | 3 | JSON-serializable data | | `OutputEncoders.YAML` | 4 | YAML-serializable data | Structured output is printed to the CLI console as it arrives. Standard stdout and stderr are streamed separately in real time. ## Module Includes If your script depends on local modules, configure includes in a `.lager` file in your project: ```yaml theme={null} # .lager (in project root) includes: my_lib: /path/to/my_lib fixtures: /path/to/fixtures ``` The CLI searches up the directory tree for a `.lager` file, then zips the script along with all include directories before uploading to the box. ```bash theme={null} # Script can import from included directories lager python test.py --box my-lager-box ``` In `test.py`: ```python theme={null} from my_lib import helpers from fixtures import test_data ``` ## Additional Files Upload extra files alongside your script: ```bash theme={null} lager python flash_and_test.py --box my-lager-box --add-file firmware.hex --add-file config.json ``` Files are available in the same directory as your script on the box. ## Exit Codes | Exit Code | Meaning | | --------- | ----------------------------------------- | | 0 | Success | | -1 | Failed to retrieve exit code from box | | 124 | Script terminated by SIGTERM (timeout) | | 137 | Script killed by SIGKILL (timeout, force) | | Other | Script's own exit code | ## Examples ```bash theme={null} # Basic script execution lager python script.py --box my-lager-box # Run with environment variables lager python test.py --box my-lager-box --env API_KEY=abc123 --env DEBUG=true # Run in detached mode lager python long_running.py --box my-lager-box --detach # Download files after completion lager python data_processor.py --box my-lager-box --download results.csv # Kill running script lager python --box my-lager-box --kill # Run with port forwarding lager python web_server.py --box my-lager-box -p 8080 # Run with timeout lager python analysis.py --box my-lager-box --timeout 300 # Upload extra files with script lager python flash_test.py --box my-lager-box --add-file firmware.hex # Pass arguments to the script lager python test.py --box my-lager-box -- --device DUT1 --verbose ``` ### Hardware Test Script Example ```python theme={null} # test_power.py - run with: lager python test_power.py --box my-lager-box from lager import Net, NetType from lager.core import output, OutputEncoders import time # Get the power supply net psu = Net.get("PSU_CH1", type=NetType.PowerSupply) # Set voltage and enable psu.voltage(value=3.3, ovp=3.6) psu.enable() time.sleep(1) # Read state state = psu.get_full_state() print(f"Voltage: {state['voltage']}V, Current: {state['current']}A") # Send structured results output({ 'test': 'power_on', 'voltage': state['voltage'], 'current': state['current'], 'status': 'pass' if state['voltage'] > 3.0 else 'fail' }, encoder=OutputEncoders.JSON) # Cleanup psu.disable() ``` ## Notes * Use `--env` for script-specific configuration values * Use `--passenv` for secrets/tokens from your current shell * `--download` retrieves files only after script completion (not during) * Port forwarding syntax: `SRC_PORT[:DST_PORT][/PROTOCOL]` * After script execution, the hardware cache on the box is automatically cleared to release VISA connections * Output is streamed in real time via a multiplexed HTTP protocol with keepalive (20-second interval) *** ## Installing Python packages Scripts run inside the Lager Python container on the box. To install packages your scripts depend on, use the declarative `lager box-config pip` commands, which record the packages in the box config and rebuild the container so they persist across `lager python` runs and box updates. ```bash theme={null} # Add one or more packages lager box-config pip add pandas requests --box my-box # List configured packages lager box-config pip list --box my-box # Remove a package lager box-config pip remove numpy --box my-box # Apply the changes (rebuild the container) lager box-config apply --box my-box ``` See the [Box Config reference](/source/reference/cli/box-config) for the full declarative provisioning workflow (pip/cargo/npm packages, apt, udev, mounts, env, and more). The standalone `lager pip` command was removed and folded into `lager box-config pip`. # Router Source: https://docs.lagerdata.com/source/reference/cli/router Manage routers as Lager nets Register and control network routers as Lager nets. A router net wraps a router's REST API (e.g. a MikroTik hAP) so you can inspect interfaces, list wireless clients and DHCP leases, toggle interfaces, block internet access, and reset the router to a clean baseline — useful for connectivity and network-resilience tests. Introduced in **lager 0.10.0**. The default instrument type is `MikroTik_hAP`. ## Syntax ```bash theme={null} lager router COMMAND [ARGS] [OPTIONS] ``` Every subcommand accepts `--box BOX` (Lagerbox name or IP; uses the default box if omitted). Most operate on a `NETNAME` — the name of a router net previously registered with `add-net`. ## Commands | Command | Description | | --------------------- | ----------------------------------------------------- | | `add-net` | Register a router as a net on the box | | `connect` | Verify connectivity to a router net | | `interfaces` | List network interfaces | | `wireless-interfaces` | List wireless interfaces and their configuration | | `wireless-clients` | List connected wireless clients | | `dhcp-leases` | List DHCP leases (devices that received IP addresses) | | `system-info` | Get system resource information | | `reboot` | Reboot the router | | `enable-interface` | Enable a wireless interface | | `disable-interface` | Disable a wireless interface | | `block-internet` | Block all internet access (drops forwarded traffic) | | `reset` | Reset the router net to a clean baseline state | | `run` | Run an arbitrary router REST API GET call | *** ## Command Reference ### `add-net` Register a router as a net on the box. ```bash theme={null} lager router add-net NAME --address IP [OPTIONS] ``` | Option | Default | Description | | -------------- | -------------- | ------------------------- | | `--address` | (required) | IP address of the router | | `--username` | `admin` | Router username | | `--password` | (empty) | Router password | | `--instrument` | `MikroTik_hAP` | Router instrument type | | `--use-ssl` | off | Use HTTPS instead of HTTP | | `--box` | | Lagerbox name or IP | ```bash theme={null} lager router add-net router1 --address 192.168.88.1 --username admin --password secret --box my-lager-box ``` ### `connect` Verify connectivity to a router net. ```bash theme={null} lager router connect router1 --box my-lager-box ``` ### `interfaces` List network interfaces on a router net. ```bash theme={null} lager router interfaces router1 --box my-lager-box ``` ### `wireless-interfaces` List wireless interfaces and their configuration. ```bash theme={null} lager router wireless-interfaces router1 --box my-lager-box ``` ### `wireless-clients` List currently connected wireless clients. ```bash theme={null} lager router wireless-clients router1 --box my-lager-box ``` ### `dhcp-leases` List DHCP leases — devices that have received IP addresses. ```bash theme={null} lager router dhcp-leases router1 --box my-lager-box ``` ### `system-info` Get system resource information from a router net. ```bash theme={null} lager router system-info router1 --box my-lager-box ``` ### `reboot` Reboot a router net. Prompts for confirmation unless `--yes` is passed. ```bash theme={null} lager router reboot router1 --box my-lager-box lager router reboot router1 --yes --box my-lager-box ``` ### `enable-interface` / `disable-interface` Enable or disable a wireless interface by name. ```bash theme={null} lager router enable-interface router1 wlan1 --box my-lager-box lager router disable-interface router1 wlan1 --box my-lager-box ``` ### `block-internet` Block all internet access on a router net (drops forwarded traffic). Use `reset` to restore access. ```bash theme={null} lager router block-internet router1 --box my-lager-box ``` ### `reset` Reset a router net to a clean baseline state: removes all test-tagged firewall rules, bandwidth limits, and access-list entries; re-enables DHCP and all wireless interfaces. If `--ssid` and `--password` are provided, a fresh baseline WPA2 network is applied. Prompts for confirmation unless `--yes` is passed. | Option | Description | | ------------ | ----------------------------------------------- | | `--ssid` | Baseline SSID to restore on wireless interfaces | | `--password` | Baseline WPA2 password | | `--yes` | Skip the confirmation prompt | ```bash theme={null} lager router reset router1 --box my-lager-box lager router reset router1 --ssid HomeNet --password secret123 --box my-lager-box ``` ### `run` Run an arbitrary router REST API GET call. `PATH` is the API path relative to `/rest`, e.g. `/ip/address`. ```bash theme={null} lager router run router1 /ip/address --box my-lager-box ``` *** ## See Also * [Nets](/source/reference/cli/nets) — manage instrument and device nets * [WiFi](/source/reference/cli/wifi) — manage the Lager Box's own WiFi settings # Oscilloscope Source: https://docs.lagerdata.com/source/reference/cli/scope Control oscilloscope settings and capture waveforms Control oscilloscope nets through the Lager CLI for waveform capture, triggering, measurements, and streaming. ## Syntax ```bash theme={null} lager scope [NETNAME] [OPTIONS] COMMAND [ARGS]... ``` If `NETNAME` is omitted, lists all available scope nets on the box. ## Global Options | Option | Description | | ----------- | ---------------------------------------------------------------------------- | | `--box BOX` | Lagerbox name or IP address | | `--mcu MCU` | MCU identifier (passed to the backend; use when multiple MCUs share a scope) | | `--help` | Show help message and exit | ## Commands | Command | Description | Hardware | | ----------- | ------------------------------------------------- | --------------- | | `enable` | Enable oscilloscope channel | Both | | `disable` | Disable oscilloscope channel | Both | | `start` | Start waveform capture (continuous or single) | Both | | `stop` | Stop waveform capture | Both | | `force` | Force trigger manually (bypass trigger condition) | Both | | `autoscale` | Automatically adjust vertical scale and timebase | Rigol only | | `coupling` | Set channel coupling mode (dc, ac, or gnd) | Rigol only | | `probe` | Set probe attenuation ratio | Rigol only | | `scale` | Set vertical scale (volts per division) | Rigol only | | `timebase` | Set horizontal timebase (seconds per division) | Rigol only | | `measure` | Measure waveform characteristics | Rigol only | | `trigger` | Configure trigger settings | Both (see note) | | `cursor` | Control scope cursor positions | Rigol only | | `stream` | Stream oscilloscope data with web visualization | PicoScope only | Edge trigger works with both PicoScope and Rigol. Protocol triggers (I2C, SPI, UART) and pulse width trigger are Rigol only. *** ## Validation Ranges The CLI validates input values before sending commands to the oscilloscope: | Parameter | Minimum | Maximum | Unit | | ------------------- | ------------ | ----------- | ------- | | Vertical scale | 0.001 | 100.0 | V/div | | Horizontal timebase | 1e-9 (1 ns) | 50.0 | s/div | | Capture duration | 0.001 (1 ms) | 3600 (1 hr) | seconds | | Sample count | 1 | 100,000,000 | samples | *** ## Supported Hardware | Manufacturer | Model | Features | | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------- | | Rigol | MSO5000 series | 4 analog + 16 digital channels, SCPI/VISA control, advanced triggers (edge, pulse, I2C, SPI, UART), measurements, cursors | | PicoScope | 2000 series | Real-time streaming, web-based visualization, CSV export, edge trigger | ### Feature Comparison | Feature | Rigol MSO5000 | PicoScope 2000 | | ------------------- | --------------------------- | ----------------- | | Channels | 4 analog + 16 digital | Up to 4 analog | | Real-time streaming | No (single capture) | Yes (continuous) | | Measurements | 11 built-in types | Via streaming/CSV | | Trigger types | Edge, pulse, I2C, SPI, UART | Edge only | | Cursor control | Manual XY cursors | Web UI cursors | | Autoscale | Yes | No | | Web visualization | No | Yes (HTML5) | | Data export | Via measurements | CSV streaming | | Control method | SCPI over USB/LAN | WebSocket daemon | *** ## Basic Control Commands ### `enable` Enable oscilloscope channel for the specified net. ```bash theme={null} lager scope NET_NAME enable [--box BOX] [--mcu MCU] ``` ### `disable` Disable oscilloscope channel. ```bash theme={null} lager scope NET_NAME disable [--box BOX] [--mcu MCU] ``` ### `start` Start waveform capture (continuous or single). ```bash theme={null} lager scope NET_NAME start [--box BOX] [--mcu MCU] [--single] ``` **Options:** * `--single` - Capture single waveform then stop (one-shot mode) ### `stop` Stop waveform capture. ```bash theme={null} lager scope NET_NAME stop [--box BOX] [--mcu MCU] ``` ### `force` Force trigger manually, bypassing the trigger condition. Useful when the signal doesn't meet trigger criteria. ```bash theme={null} lager scope NET_NAME force [--box BOX] [--mcu MCU] ``` ### `autoscale` Automatically adjust vertical scale and horizontal timebase for optimal display (Rigol only). ```bash theme={null} lager scope NET_NAME autoscale [--box BOX] [--mcu MCU] ``` *** ## Channel Configuration (Rigol) ### `coupling` Set the input coupling mode for the oscilloscope channel. ```bash theme={null} lager scope NET_NAME coupling MODE [--box BOX] [--mcu MCU] ``` **Arguments:** * `MODE` - Coupling mode: `dc`, `ac`, or `gnd` | Mode | Description | | ----- | -------------------------------- | | `dc` | Pass both DC and AC components | | `ac` | Block DC component, pass AC only | | `gnd` | Ground reference (0V baseline) | ### `probe` Set the probe attenuation ratio for accurate voltage measurements. ```bash theme={null} lager scope NET_NAME probe RATIO [--box BOX] [--mcu MCU] ``` **Arguments:** * `RATIO` - Probe attenuation: `1`, `10`, `100`, or `1000` ```bash theme={null} lager scope ANALOG1 probe 10 --box my-lager-box # 10:1 probe lager scope ANALOG1 probe 1 --box my-lager-box # Direct connection (1:1) lager scope ANALOG1 probe 100 --box my-lager-box # 100:1 high-voltage probe ``` ### `scale` Set the vertical scale (volts per division) for the oscilloscope channel. ```bash theme={null} lager scope NET_NAME scale VOLTS_PER_DIV [--box BOX] [--mcu MCU] ``` **Arguments:** * `VOLTS_PER_DIV` - Vertical scale in volts per division (0.001 to 100.0) ```bash theme={null} lager scope ANALOG1 scale 1.0 --box my-lager-box # 1V/div lager scope ANALOG1 scale 0.5 --box my-lager-box # 500mV/div lager scope ANALOG1 scale 0.1 --box my-lager-box # 100mV/div lager scope ANALOG1 scale 0.001 --box my-lager-box # 1mV/div (minimum) ``` ### `timebase` Set the horizontal timebase (seconds per division) for the oscilloscope. ```bash theme={null} lager scope NET_NAME timebase SEC_PER_DIV [--box BOX] [--mcu MCU] ``` **Arguments:** * `SEC_PER_DIV` - Horizontal timebase in seconds per division (1e-9 to 50.0) ```bash theme={null} lager scope ANALOG1 timebase 0.001 --box my-lager-box # 1ms/div lager scope ANALOG1 timebase 0.0001 --box my-lager-box # 100us/div lager scope ANALOG1 timebase 0.000001 --box my-lager-box # 1us/div lager scope ANALOG1 timebase 0.000000001 --box my-lager-box # 1ns/div (minimum) ``` *** ## Measure Subcommands (Rigol) Measure waveform characteristics on Rigol oscilloscopes. PicoScope users should use the streaming capture commands to export waveform data for analysis. **Common Options for all measure commands:** | Option | Description | | ----------- | ------------------------------------------ | | `--box BOX` | Lagerbox name or IP | | `--mcu MCU` | MCU identifier | | `--display` | Display measurement on oscilloscope screen | | `--cursor` | Enable measurement cursor on screen | ### Time Measurements #### `measure period` Measure waveform period. ```bash theme={null} lager scope NET_NAME measure period [--box BOX] [--display] [--cursor] ``` #### `measure freq` Measure waveform frequency. ```bash theme={null} lager scope NET_NAME measure freq [--box BOX] [--display] [--cursor] ``` #### `measure pulse-width-pos` Measure positive pulse width. ```bash theme={null} lager scope NET_NAME measure pulse-width-pos [--box BOX] [--display] [--cursor] ``` #### `measure pulse-width-neg` Measure negative pulse width. ```bash theme={null} lager scope NET_NAME measure pulse-width-neg [--box BOX] [--display] [--cursor] ``` #### `measure duty-cycle-pos` Measure positive duty cycle (percentage of time signal is high). ```bash theme={null} lager scope NET_NAME measure duty-cycle-pos [--box BOX] [--display] [--cursor] ``` #### `measure duty-cycle-neg` Measure negative duty cycle (percentage of time signal is low). ```bash theme={null} lager scope NET_NAME measure duty-cycle-neg [--box BOX] [--display] [--cursor] ``` ### Voltage Measurements #### `measure vpp` Measure peak-to-peak voltage. ```bash theme={null} lager scope NET_NAME measure vpp [--box BOX] [--display] [--cursor] ``` #### `measure vmax` Measure maximum voltage. ```bash theme={null} lager scope NET_NAME measure vmax [--box BOX] [--display] [--cursor] ``` #### `measure vmin` Measure minimum voltage. ```bash theme={null} lager scope NET_NAME measure vmin [--box BOX] [--display] [--cursor] ``` #### `measure vavg` Measure average voltage. ```bash theme={null} lager scope NET_NAME measure vavg [--box BOX] [--display] [--cursor] ``` #### `measure vrms` Measure RMS (root mean square) voltage. ```bash theme={null} lager scope NET_NAME measure vrms [--box BOX] [--display] [--cursor] ``` *** ## Trigger Subcommands Configure trigger settings. Edge trigger works with both PicoScope and Rigol. Protocol triggers (I2C, SPI, UART) and pulse trigger are Rigol only. **Common Trigger Options:** | Option | Description | Default | | --------------- | ---------------------------------------------------------- | ----------- | | `--mode` | Trigger mode: `normal`, `auto`, `single` | `normal` | | `--coupling` | Coupling mode: `dc`, `ac`, `low_freq_rej`, `high_freq_rej` | `dc` | | `--source NET` | Trigger source net | Current net | | `--level VOLTS` | Trigger level in volts | - | ### `trigger edge` Set edge trigger configuration. Works with both PicoScope and Rigol. ```bash theme={null} lager scope NET_NAME trigger edge [OPTIONS] ``` **Options (in addition to common):** | Option | Description | | --------- | ------------------------------------------ | | `--slope` | Trigger slope: `rising`, `falling`, `both` | **Examples:** ```bash theme={null} # Rising edge at 1.5V lager scope ANALOG1 trigger edge --slope rising --level 1.5 --box my-lager-box # Falling edge in single capture mode lager scope ANALOG1 trigger edge --slope falling --level 0.5 --mode single --box my-lager-box # Either edge with AC coupling lager scope ANALOG1 trigger edge --slope both --coupling ac --level 0 --box my-lager-box ``` ### `trigger pulse` Set pulse width trigger (Rigol only). Triggers when pulse width meets specified conditions. ```bash theme={null} lager scope NET_NAME trigger pulse [OPTIONS] ``` **Options (in addition to common):** | Option | Description | Default | | ----------------- | --------------------------- | ---------- | | `--trigger-on` | Condition (see table below) | `positive` | | `--upper SECONDS` | Upper pulse width limit | - | | `--lower SECONDS` | Lower pulse width limit | - | **Trigger-on Conditions:** | Condition | Description | | ------------------ | ---------------------------------------- | | `positive` | Positive pulse | | `negative` | Negative pulse | | `positive_greater` | Positive pulse wider than upper limit | | `negative_greater` | Negative pulse wider than upper limit | | `positive_less` | Positive pulse narrower than upper limit | | `negative_less` | Negative pulse narrower than upper limit | **Examples:** ```bash theme={null} # Detect short positive glitches (< 100us) lager scope ANALOG1 trigger pulse --trigger-on positive_less --upper 0.0001 --box my-lager-box # Detect long negative pulses (> 1ms) lager scope ANALOG1 trigger pulse --trigger-on negative_greater --upper 0.001 --level 1.0 --box my-lager-box ``` ### `trigger i2c` Set I2C protocol trigger (Rigol only). Triggers on I2C bus events. ```bash theme={null} lager scope NET_NAME trigger i2c [OPTIONS] ``` **Options (in addition to common mode/coupling):** | Option | Description | Default | | ------------------- | ---------------------------------------- | ------------ | | `--source-scl NET` | SCL source net | - | | `--source-sda NET` | SDA source net | - | | `--level-scl VOLTS` | SCL trigger level | - | | `--level-sda VOLTS` | SDA trigger level | - | | `--trigger-on` | Condition (see table below) | `start` | | `--address HEX` | I2C address (hex) | - | | `--addr-width` | Address width: `7`, `8`, `10` bits | `7` | | `--data HEX` | Data pattern to match (hex) | - | | `--data-width INT` | Data width in bits | `8` | | `--direction` | Direction: `read`, `write`, `read_write` | `read_write` | **Trigger-on Conditions:** | Condition | Description | | ----------- | ------------------------ | | `start` | Start condition | | `restart` | Repeated start condition | | `stop` | Stop condition | | `ack_miss` | Missing ACK (NACK) | | `address` | Specific address match | | `data` | Specific data match | | `addr_data` | Address + data match | **Examples:** ```bash theme={null} # Trigger on I2C start condition lager scope ANALOG1 trigger i2c \ --source-scl SCL_NET --source-sda SDA_NET \ --trigger-on start --box my-lager-box # Trigger on specific I2C address (7-bit) lager scope ANALOG1 trigger i2c \ --source-scl SCL_NET --source-sda SDA_NET \ --trigger-on address --address 0x48 --box my-lager-box # Trigger on I2C write to address 0x76 with data 0xF4 lager scope ANALOG1 trigger i2c \ --source-scl SCL_NET --source-sda SDA_NET \ --trigger-on addr_data --address 0x76 --data 0xF4 \ --direction write --box my-lager-box # Trigger on NACK (missing acknowledgment) lager scope ANALOG1 trigger i2c \ --source-scl SCL_NET --source-sda SDA_NET \ --trigger-on ack_miss --box my-lager-box ``` ### `trigger spi` Set SPI protocol trigger (Rigol only). Triggers on SPI bus events. ```bash theme={null} lager scope NET_NAME trigger spi [OPTIONS] ``` **Options (in addition to common mode/coupling):** | Option | Description | Default | | ------------------------- | ------------------------------- | -------- | | `--source-mosi-miso NET` | MOSI/MISO source net | - | | `--source-sck NET` | SCK (clock) source net | - | | `--source-cs NET` | CS (chip select) source net | - | | `--level-mosi-miso VOLTS` | MOSI/MISO trigger level | - | | `--level-sck VOLTS` | SCK trigger level | - | | `--level-cs VOLTS` | CS trigger level | - | | `--trigger-on` | Condition: `timeout`, `cs` | `cs` | | `--data HEX` | Data pattern to match (hex) | - | | `--data-width INT` | Data width in bits | `8` | | `--clk-slope` | Clock edge: `rising`, `falling` | `rising` | | `--cs-idle` | CS idle state: `high`, `low` | `high` | | `--timeout SECONDS` | Timeout value in seconds | - | **Examples:** ```bash theme={null} # Trigger on CS assertion lager scope ANALOG1 trigger spi \ --source-mosi-miso MOSI_NET --source-sck SCK_NET --source-cs CS_NET \ --trigger-on cs --box my-lager-box # Trigger on SPI data pattern with falling clock edge lager scope ANALOG1 trigger spi \ --source-mosi-miso MOSI_NET --source-sck SCK_NET --source-cs CS_NET \ --data 0xFF --clk-slope falling --box my-lager-box ``` ### `trigger uart` Set UART protocol trigger (Rigol only). Triggers on UART serial events. ```bash theme={null} lager scope NET_NAME trigger uart [OPTIONS] ``` **Options (in addition to common):** | Option | Description | Default | | ------------------ | ------------------------------------------- | ------- | | `--baud INT` | Baud rate | `9600` | | `--parity` | Parity: `none`, `even`, `odd` | `none` | | `--stop-bits` | Stop bits: `1`, `1.5`, `2` | `1` | | `--data-width INT` | Data width in bits | `8` | | `--trigger-on` | Condition: `start`, `stop`, `data`, `error` | `start` | | `--data HEX` | Data pattern to match (hex) | - | **Examples:** ```bash theme={null} # Trigger on UART start bit at 115200 baud lager scope ANALOG1 trigger uart \ --source UART_TX --baud 115200 --trigger-on start --box my-lager-box # Trigger on specific UART data byte lager scope ANALOG1 trigger uart \ --source UART_TX --baud 9600 --trigger-on data --data 0x55 --box my-lager-box # Trigger on UART framing error lager scope ANALOG1 trigger uart \ --source UART_RX --baud 115200 --trigger-on error --box my-lager-box ``` *** ## Cursor Subcommands (Rigol) Control manual XY cursors on Rigol oscilloscopes for precise time and voltage measurements. Cursors A and B can be positioned independently, and the oscilloscope calculates the delta between them. ### `cursor set-a` / `cursor set-b` Set the absolute position of cursor A or B. ```bash theme={null} lager scope NET_NAME cursor set-a [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU] lager scope NET_NAME cursor set-b [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU] ``` **Options:** | Option | Description | | ------ | ------------------------------- | | `--x` | X coordinate (time position) | | `--y` | Y coordinate (voltage position) | ### `cursor move-a` / `cursor move-b` Move a cursor by a relative offset from its current position. ```bash theme={null} lager scope NET_NAME cursor move-a [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU] lager scope NET_NAME cursor move-b [--x FLOAT] [--y FLOAT] [--box BOX] [--mcu MCU] ``` **Options:** | Option | Description | | ------ | --------------------------- | | `--x` | Relative X movement (delta) | | `--y` | Relative Y movement (delta) | ### `cursor hide` Hide the cursor display. ```bash theme={null} lager scope NET_NAME cursor hide [--box BOX] [--mcu MCU] ``` **Examples:** ```bash theme={null} # Position cursors for time measurement lager scope ANALOG1 cursor set-a --x -0.001 --box my-lager-box lager scope ANALOG1 cursor set-b --x 0.001 --box my-lager-box # Fine-tune cursor B position lager scope ANALOG1 cursor move-b --x 0.0001 --box my-lager-box # Set voltage measurement cursors lager scope ANALOG1 cursor set-a --y 0.5 --box my-lager-box lager scope ANALOG1 cursor set-b --y 3.3 --box my-lager-box # Hide cursors when done lager scope ANALOG1 cursor hide --box my-lager-box ``` *** ## Stream Subcommands (PicoScope) Stream oscilloscope data in real time from PicoScope devices. The streaming system uses a dedicated daemon on the Lager Box with a web-based visualization interface. ### Daemon Architecture The PicoScope streaming daemon communicates over multiple ports: | Port | Purpose | | ---- | ------------------------------------- | | 8080 | HTTP server for web visualization UI | | 8082 | WebSocket command channel (browser) | | 8083 | WebTransport streaming (browser data) | | 8085 | CLI command port (WebSocket) | ### `stream start` Start oscilloscope streaming acquisition with web visualization. ```bash theme={null} lager scope NET_NAME stream start [OPTIONS] ``` **Options:** | Option | Short | Description | Default | | ----------------- | ----- | ------------------------------------ | -------- | | `--channel` | `-c` | Channel: `A`, `B`, `1`, `2` | `A` | | `--volts-per-div` | `-v` | Vertical scale (V/div) | `1.0` | | `--time-per-div` | `-t` | Horizontal scale (s/div) | `0.001` | | `--trigger-level` | | Trigger threshold voltage | `0.0` | | `--trigger-slope` | | Slope: `rising`, `falling`, `either` | `rising` | | `--capture-mode` | | Mode: `auto`, `normal`, `single` | `auto` | | `--coupling` | | Coupling: `dc`, `ac` | `dc` | | `--quiet` | `-q` | Minimal output | | | `--json` | | JSON output format | | | `--verbose` | | Verbose debugging output | | **Examples:** ```bash theme={null} # Start streaming on channel A with default settings lager scope PICO1 stream start --box my-lager-box # Start with specific configuration lager scope PICO1 stream start -c A -v 2.0 -t 0.0001 --box my-lager-box # Start with trigger configuration lager scope PICO1 stream start \ --trigger-level 1.5 --trigger-slope rising \ --capture-mode normal --box my-lager-box # Start in single capture mode with AC coupling lager scope PICO1 stream start \ --capture-mode single --coupling ac --box my-lager-box ``` ### `stream stop` Stop oscilloscope streaming acquisition. ```bash theme={null} lager scope NET_NAME stream stop [--box BOX] ``` ### `stream status` Check oscilloscope streaming daemon status. ```bash theme={null} lager scope NET_NAME stream status [--box BOX] ``` ### `stream web` Open web browser for real-time oscilloscope visualization. ```bash theme={null} lager scope NET_NAME stream web [--box BOX] [--port PORT] ``` **Options:** * `--port` - HTTP server port (default: 8080) The web interface provides an HTML5 oscilloscope display with real-time waveform rendering via WebTransport. ### `stream capture` Capture oscilloscope waveform data to a CSV file. ```bash theme={null} lager scope NET_NAME stream capture [OPTIONS] ``` **Options:** | Option | Short | Description | Default | | ------------ | ----- | ---------------------------------------- | ---------------- | | `--output` | `-o` | CSV output file path | `scope_data.csv` | | `--duration` | `-d` | Capture duration in seconds (0.001-3600) | `1.0` | | `--samples` | `-n` | Maximum samples to capture (1-100M) | unlimited | | `--quiet` | `-q` | Minimal output | | | `--json` | | JSON output format | | | `--verbose` | | Verbose debugging output | | **Examples:** ```bash theme={null} # Capture 1 second of data (default) lager scope PICO1 stream capture --box my-lager-box # Capture 5 seconds to specific file lager scope PICO1 stream capture -o waveform.csv -d 5.0 --box my-lager-box # Capture up to 1M samples lager scope PICO1 stream capture -n 1000000 -o data.csv --box my-lager-box # Capture with JSON output format lager scope PICO1 stream capture --json --box my-lager-box ``` ### `stream config` Configure oscilloscope streaming settings without starting/stopping acquisition. ```bash theme={null} lager scope NET_NAME stream config [OPTIONS] ``` **Options:** | Option | Short | Description | | ------------------------ | ----- | ------------------------------------------ | | `--channel` | `-c` | Channel: `A`, `B`, `1`, `2` | | `--volts-per-div` | `-v` | Volts per division | | `--time-per-div` | `-t` | Time per division (seconds) | | `--trigger-level` | | Trigger level (volts) | | `--trigger-source` | | Trigger source channel: `A`, `B`, `1`, `2` | | `--trigger-slope` | | Slope: `rising`, `falling`, `either` | | `--capture-mode` | | Mode: `auto`, `normal`, `single` | | `--coupling` | | Coupling: `dc`, `ac` | | `--enable` / `--disable` | | Enable or disable channel | **Examples:** ```bash theme={null} # Change vertical scale while streaming lager scope PICO1 stream config -v 0.5 --box my-lager-box # Switch to channel B lager scope PICO1 stream config -c B --enable --box my-lager-box # Change trigger settings lager scope PICO1 stream config --trigger-level 2.0 --trigger-slope falling --box my-lager-box # Switch to single capture mode lager scope PICO1 stream config --capture-mode single --box my-lager-box ``` *** ## Examples ### Basic Rigol Workflow ```bash theme={null} # Enable scope channel lager scope ANALOG1 enable --box my-lager-box # Auto-scale to find signal lager scope ANALOG1 autoscale --box my-lager-box # Fine-tune settings lager scope ANALOG1 scale 0.5 --box my-lager-box lager scope ANALOG1 timebase 0.001 --box my-lager-box # Start continuous capture lager scope ANALOG1 start --box my-lager-box # Take measurements lager scope ANALOG1 measure freq --display --box my-lager-box lager scope ANALOG1 measure vpp --display --box my-lager-box # Single capture mode lager scope ANALOG1 start --single --box my-lager-box # Stop capture lager scope ANALOG1 stop --box my-lager-box ``` ### Channel Configuration ```bash theme={null} # Configure for 10x probe at 500mV/div, 1ms/div lager scope ANALOG1 probe 10 --box my-lager-box lager scope ANALOG1 scale 0.5 --box my-lager-box lager scope ANALOG1 timebase 0.001 --box my-lager-box # Set AC coupling for audio signals lager scope ANALOG1 coupling ac --box my-lager-box # Force trigger when signal doesn't meet criteria lager scope ANALOG1 force --box my-lager-box ``` ### Measurements ```bash theme={null} # Time measurements lager scope ANALOG1 measure freq --display --box my-lager-box lager scope ANALOG1 measure period --box my-lager-box lager scope ANALOG1 measure duty-cycle-pos --box my-lager-box # Voltage measurements lager scope ANALOG1 measure vpp --box my-lager-box lager scope ANALOG1 measure vrms --display --box my-lager-box lager scope ANALOG1 measure vmax --box my-lager-box ``` ### Triggering ```bash theme={null} # Edge trigger lager scope ANALOG1 trigger edge --slope rising --level 1.5 --box my-lager-box # Pulse width trigger (glitch detection) lager scope ANALOG1 trigger pulse --trigger-on positive_less --upper 0.0001 --box my-lager-box # UART trigger at 115200 baud lager scope ANALOG1 trigger uart \ --source UART_NET --baud 115200 --trigger-on start --box my-lager-box # I2C trigger on address 0x48 lager scope ANALOG1 trigger i2c \ --source-scl SCL --source-sda SDA \ --trigger-on address --address 0x48 --box my-lager-box # SPI trigger on chip select lager scope ANALOG1 trigger spi \ --source-mosi-miso MOSI --source-sck SCK --source-cs CS \ --trigger-on cs --box my-lager-box ``` ### Cursor Measurements ```bash theme={null} # Measure time between two events lager scope ANALOG1 cursor set-a --x -0.001 --box my-lager-box lager scope ANALOG1 cursor set-b --x 0.002 --box my-lager-box # Measure voltage difference lager scope ANALOG1 cursor set-a --y 0.0 --box my-lager-box lager scope ANALOG1 cursor set-b --y 3.3 --box my-lager-box # Clean up lager scope ANALOG1 cursor hide --box my-lager-box ``` ### PicoScope Streaming ```bash theme={null} # Start streaming with default settings lager scope PICO1 stream start --box my-lager-box # Open web visualization lager scope PICO1 stream web --box my-lager-box # Capture data to file lager scope PICO1 stream capture -o waveform.csv -d 5.0 --box my-lager-box # Adjust settings while streaming lager scope PICO1 stream config -v 2.0 -t 0.0001 --box my-lager-box # Check streaming status lager scope PICO1 stream status --box my-lager-box # Stop streaming lager scope PICO1 stream stop --box my-lager-box ``` *** ## Command Structure ``` scope [NETNAME] [--box BOX] Basic Control ├── enable [--mcu] ├── disable [--mcu] ├── start [--mcu] [--single] ├── stop [--mcu] └── force [--mcu] Channel Configuration (Rigol) ├── scale VOLTS_PER_DIV [--mcu] ├── coupling {dc|ac|gnd} [--mcu] ├── probe {1|10|100|1000} [--mcu] ├── timebase SECONDS_PER_DIV [--mcu] └── autoscale [--mcu] Measurements (Rigol) └── measure ├── period [--display] [--cursor] ├── freq [--display] [--cursor] ├── vpp [--display] [--cursor] ├── vmax [--display] [--cursor] ├── vmin [--display] [--cursor] ├── vrms [--display] [--cursor] ├── vavg [--display] [--cursor] ├── pulse-width-pos [--display] [--cursor] ├── pulse-width-neg [--display] [--cursor] ├── duty-cycle-pos [--display] [--cursor] └── duty-cycle-neg [--display] [--cursor] Triggers └── trigger ├── edge [--slope] [--level] [--mode] [--coupling] [--source] ├── pulse [--trigger-on] [--upper] [--lower] [--level] ... ├── i2c [--source-scl] [--source-sda] [--trigger-on] [--address] ... ├── spi [--source-mosi-miso] [--source-sck] [--source-cs] ... └── uart [--baud] [--parity] [--trigger-on] [--data] ... Cursors (Rigol) └── cursor ├── set-a [--x] [--y] ├── set-b [--x] [--y] ├── move-a [--x] [--y] ├── move-b [--x] [--y] └── hide Streaming (PicoScope) └── stream ├── start [-c] [-v] [-t] [--trigger-level] [--trigger-slope] ... ├── stop ├── status ├── web [--port] ├── capture [-o] [-d] [-n] [--quiet] [--json] [--verbose] └── config [-c] [-v] [-t] [--enable/--disable] ... ``` *** ## Notes * Net names refer to names assigned when setting up your testbed with `lager nets` * The `--mcu` option is available on all commands and is passed to the backend; use it when multiple MCUs share a scope channel * Streaming commands are only available for PicoScope devices * Measurement and cursor commands are only available for Rigol devices * Edge trigger is the only trigger type supported on both PicoScope and Rigol * Web visualization requires port 8080 to be accessible on the Lager Box * Validation rejects values outside the allowed ranges before sending to hardware * Use `lager nets` to see available scope nets ## See Also * [Logic Analyzer](/source/reference/cli/logic) -- Digital signal capture and protocol decode * [Python Scope API](/source/reference/python/scope) -- Automate oscilloscope operations in Python scripts # Solar Simulation Source: https://docs.lagerdata.com/source/reference/cli/solar Control solar panel simulator settings and output Control solar panel simulator Nets through the Lager CLI. Solar simulation enables testing of solar-powered devices by simulating various irradiance conditions, temperatures, and panel characteristics. ## Syntax ```bash theme={null} lager solar [OPTIONS] NET_NAME COMMAND [ARGS]... ``` ## Global Options | Option | Description | | ----------- | --------------------------- | | `--box BOX` | Lagerbox name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | ------------- | ------------------------------------------ | | `set` | Initialize and start solar simulation mode | | `stop` | Stop solar simulation mode | | `irradiance` | Set or read irradiance (W/m²) | | `mpp-current` | Read maximum power point current (A) | | `mpp-voltage` | Read maximum power point voltage (V) | | `resistance` | Set or read dynamic panel resistance (Ω) | | `temperature` | Read cell temperature (°C) | | `voc` | Read open-circuit voltage (V) | ## Command Reference ### `set` Initialize and start the solar simulation mode. ```bash theme={null} lager solar NET_NAME set [--box BOX] ``` This command configures the power supply to operate in solar panel simulation mode, enabling I-V curve emulation. ### `stop` Stop the solar simulation mode and return to normal operation. ```bash theme={null} lager solar NET_NAME stop [--box BOX] ``` ### `irradiance` Set or read the irradiance level in watts per square meter (W/m²). ```bash theme={null} lager solar NET_NAME irradiance [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Irradiance value in W/m² (0.0 - 1500.0). If omitted, reads current value. **Examples:** ```bash theme={null} # Set irradiance to 1000 W/m² (standard test condition) lager solar SOLAR1 irradiance 1000 # Read current irradiance lager solar SOLAR1 irradiance ``` ### `mpp-current` Read the maximum power point (MPP) current in amps. ```bash theme={null} lager solar NET_NAME mpp-current [--box BOX] ``` Returns the current at which the simulated solar panel produces maximum power. ### `mpp-voltage` Read the maximum power point (MPP) voltage in volts. ```bash theme={null} lager solar NET_NAME mpp-voltage [--box BOX] ``` Returns the voltage at which the simulated solar panel produces maximum power. ### `resistance` Set or read the dynamic panel resistance in ohms. ```bash theme={null} lager solar NET_NAME resistance [VALUE] [--box BOX] ``` **Arguments:** * `VALUE` - Resistance value in Ω (0.1 - 100.0). If omitted, reads current value. ### `temperature` Read the simulated cell temperature in degrees Celsius. ```bash theme={null} lager solar NET_NAME temperature [--box BOX] ``` ### `voc` Read the open-circuit voltage (Voc) in volts. ```bash theme={null} lager solar NET_NAME voc [--box BOX] ``` Returns the voltage when no load is connected to the simulated solar panel. *** ## Examples ```bash theme={null} # Start solar simulation mode lager solar SOLAR_PANEL set --box my-lager-box # Set irradiance to standard test condition (1000 W/m²) lager solar SOLAR_PANEL irradiance 1000 # Read MPP voltage and current lager solar SOLAR_PANEL mpp-voltage lager solar SOLAR_PANEL mpp-current # Read open-circuit voltage lager solar SOLAR_PANEL voc # Set panel resistance lager solar SOLAR_PANEL resistance 5.0 # Stop solar simulation lager solar SOLAR_PANEL stop ``` *** ## Supported Hardware | Manufacturer | Model Series | Features | | ------------ | ------------- | -------------------------------------------- | | EA | PSI/EL series | Two-quadrant operation, I-V curve simulation | | EA | PSB 10060-60 | Bidirectional power supply | | EA | PSB 10080-60 | Bidirectional power supply | *** ## Solar Panel Simulation Concepts ### I-V Curve Solar panel simulators create an I-V (current-voltage) characteristic curve that mimics real solar panel behavior: * **Short-circuit current (Isc)**: Maximum current when terminals are shorted * **Open-circuit voltage (Voc)**: Voltage with no load * **Maximum Power Point (MPP)**: Optimal operating point for maximum power ### Standard Test Conditions (STC) Industry standard for solar panel testing: * Irradiance: 1000 W/m² * Cell temperature: 25°C * Air mass: AM1.5 *** ## Notes * Solar simulation requires compatible bidirectional power supplies * The EA PSI/EL series supports two-quadrant operation for realistic simulation * Use `lager nets` to see available solar nets on your box * Irradiance values above 1500 W/m² are outside the valid range # SPI Source: https://docs.lagerdata.com/source/reference/cli/spi Perform SPI data transfers Perform SPI (Serial Peripheral Interface) data transfers with devices connected to a Lagerbox. SPI is a synchronous serial protocol using four lines: SCLK (clock), MOSI (master out), MISO (master in), and CS (chip select). ## Syntax ```bash theme={null} lager spi [NETNAME] [OPTIONS] [SUBCOMMAND] ``` ## Arguments | Argument | Description | | --------- | ---------------------------------------------------------------------------- | | `NETNAME` | SPI net name (optional if default is set via `lager defaults add --spi-net`) | ## Options | Option | Description | | ----------- | --------------------------- | | `--box BOX` | Lagerbox name or IP address | When invoked without a subcommand, lists SPI nets on the box (or shows configuration for the specified net). *** ## Subcommands ### `config` Configure SPI bus parameters. Settings persist across subsequent commands. ```bash theme={null} lager spi NETNAME config [OPTIONS] ``` | Option | Description | | ------------------------ | -------------------------------------------------------------------- | | `--box BOX` | Lagerbox name or IP address | | `--mode 0\|1\|2\|3` | SPI mode (clock polarity and phase) | | `--frequency FREQ` | Clock frequency (e.g., `1M`, `500k`, `5M`) | | `--bit-order msb\|lsb` | Bit order: MSB first or LSB first | | `--word-size 8\|16\|32` | Word size in bits | | `--cs-active low\|high` | Chip select active polarity | | `--cs-mode auto\|manual` | CS assertion mode: `auto` (hardware) or `manual` (user-managed GPIO) | **SPI Modes:** | Mode | CPOL | CPHA | Description | | ---- | ---- | ---- | --------------------------------------------- | | 0 | 0 | 0 | Clock idle low, data sampled on rising edge | | 1 | 0 | 1 | Clock idle low, data sampled on falling edge | | 2 | 1 | 0 | Clock idle high, data sampled on falling edge | | 3 | 1 | 1 | Clock idle high, data sampled on rising edge | **Examples:** ```bash theme={null} # Configure SPI mode 0 at 5MHz lager spi MY_SPI config --mode 0 --frequency 5M # Set 16-bit word size with LSB-first lager spi MY_SPI config --word-size 16 --bit-order lsb # Use manual CS mode (for Aardvark with separate GPIO for CS) lager spi MY_SPI config --cs-mode manual ``` *** ### `transfer` Perform a full-duplex SPI transfer. Sends data while simultaneously receiving response. If provided data is shorter than `NUM_WORDS`, the remaining words are padded with the fill value. If longer, data is truncated. ```bash theme={null} lager spi NETNAME transfer NUM_WORDS [OPTIONS] ``` | Argument | Description | | ----------- | --------------------------- | | `NUM_WORDS` | Number of words to transfer | | Option | Description | Default | | ----------------------- | ------------------------------------- | ------- | | `--box BOX` | Lagerbox name or IP address | | | `--data DATA` | Hex data to transmit (e.g., `0x9f01`) | | | `--data-file PATH` | File containing data to transmit | | | `--fill VALUE` | Fill value for padding | `0xFF` | | `--mode 0\|1\|2\|3` | SPI mode override | | | `--frequency FREQ` | Clock frequency override | | | `--bit-order msb\|lsb` | Bit order override | | | `--word-size 8\|16\|32` | Word size override | | | `--cs-active low\|high` | CS polarity override | | | `--keep-cs` | Keep CS asserted after transfer | `false` | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | **Examples:** ```bash theme={null} # Read device ID: send 0x9F command, read 3 response bytes (4 words total) lager spi MY_SPI transfer --data 0x9f 4 # Send data at 5MHz lager spi MY_SPI transfer --data "01 02 03 04" --frequency 5M 4 # Output as JSON lager spi MY_SPI transfer --data 0x9f 4 --format json # Keep CS asserted for multi-part transfer lager spi MY_SPI transfer --data 0x03 --keep-cs 1 ``` *** ### `read` Read data from an SPI slave device. Sends fill bytes while clocking in the response. ```bash theme={null} lager spi NETNAME read NUM_WORDS [OPTIONS] ``` | Argument | Description | | ----------- | ----------------------- | | `NUM_WORDS` | Number of words to read | | Option | Description | Default | | ----------------------- | ------------------------------------- | ------- | | `--box BOX` | Lagerbox name or IP address | | | `--fill VALUE` | Fill byte sent while reading | `0xFF` | | `--mode 0\|1\|2\|3` | SPI mode override | | | `--frequency FREQ` | Clock frequency override | | | `--bit-order msb\|lsb` | Bit order override | | | `--word-size 8\|16\|32` | Word size override | | | `--cs-active low\|high` | CS polarity override | | | `--keep-cs` | Keep CS asserted after transfer | `false` | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | **Examples:** ```bash theme={null} # Read 5 bytes from SPI slave lager spi MY_SPI read 5 # Read with 0x00 fill instead of default 0xFF lager spi MY_SPI read 5 --fill 0x00 # Read 4 words at 16-bit word size lager spi MY_SPI read 4 --word-size 16 ``` *** ### `write` Write data to an SPI slave device. Performs a full-duplex transfer and displays the received response. ```bash theme={null} lager spi NETNAME write DATA [OPTIONS] ``` | Argument | Description | | -------- | ---------------------------------------- | | `DATA` | Hex data to write (e.g., `0x9f01020304`) | | Option | Description | Default | | ----------------------- | ------------------------------------- | ------- | | `--box BOX` | Lagerbox name or IP address | | | `--mode 0\|1\|2\|3` | SPI mode override | | | `--frequency FREQ` | Clock frequency override | | | `--bit-order msb\|lsb` | Bit order override | | | `--word-size 8\|16\|32` | Word size override | | | `--cs-active low\|high` | CS polarity override | | | `--keep-cs` | Keep CS asserted after transfer | `false` | | `--format FORMAT` | Output format: `hex`, `bytes`, `json` | `hex` | **Examples:** ```bash theme={null} # Send JEDEC ID command and read response lager spi MY_SPI write 0x9f01020304 # Write with mode override lager spi MY_SPI write 0x0102 --mode 3 # Write and keep CS low for continued transfer lager spi MY_SPI write 0x03000000 --keep-cs ``` *** ## Hex Data Formats Data arguments accept multiple hex formats. Parsing behavior depends on word size: **8-bit word size (default):** | Format | Example | Parsed As | | --------------------- | ---------- | -------------------- | | Prefixed continuous | `0x9f01` | `[0x9f, 0x01]` | | Unprefixed continuous | `9f01` | `[0x9f, 0x01]` | | Space-separated | `9f 01 02` | `[0x9f, 0x01, 0x02]` | | Comma-separated | `9f,01,02` | `[0x9f, 0x01, 0x02]` | **16-bit or 32-bit word size:** | Format | Example | Parsed As | | --------------- | --------------- | ------------------ | | Continuous | `0x1234` | `[0x1234]` | | Space-separated | `0x1234 0x5678` | `[0x1234, 0x5678]` | Values are validated against the configured word size range. *** ## Fill Value Format The `--fill` option accepts hex or decimal values: | Format | Example | Value | | -------------- | ------- | ----- | | Hex prefixed | `0xff` | 255 | | Hex unprefixed | `ff` | 255 | | Decimal | `255` | 255 | *** ## Frequency Format Clock frequencies accept numeric values with optional suffixes: | Format | Example | Value | | ---------- | ----------- | ------- | | Plain Hz | `1000000` | 1 MHz | | kHz suffix | `500k` | 500 kHz | | MHz suffix | `5M` | 5 MHz | | Hz suffix | `1000000hz` | 1 MHz | *** ## Supported Hardware | Adapter | Pins | CS Control | Notes | | ------------ | --------------------------------------- | ------------------ | ------------------------------------------------------ | | LabJack T7 | Configurable FIO pins (e.g., FIO0-FIO3) | Manual GPIO | \~450 kHz max (throttle=0); CS managed via GPIO writes | | Aardvark USB | Fixed MOSI/MISO/SCK/SS | Hardware or manual | Up to 8 MHz | | FT232H | Configurable | MPSSE-based | FTDI MPSSE SPI | *** ## Net Configuration SPI nets are configured in `saved_nets.json` on the box. Example net record: ```json theme={null} { "name": "my_spi", "role": "spi", "instrument": "labjack_t7", "pin": "FIO0-FIO3", "params": { "cs_pin": 0, "clk_pin": 1, "mosi_pin": 2, "miso_pin": 3, "mode": 0, "frequency_hz": 1000000, "word_size": 8, "bit_order": "msb" } } ``` For Aardvark adapters: ```json theme={null} { "name": "my_spi", "role": "spi", "instrument": "aardvark", "pin": "SPI0", "params": { "mode": 0, "frequency_hz": 1000000, "word_size": 8, "bit_order": "msb" } } ``` *** ## Output Formats | Format | Description | | ------- | --------------------------------------------- | | `hex` | Space-separated hex values (e.g., `9f 01 02`) | | `bytes` | Raw byte values | | `json` | JSON object with data array and metadata | *** ## Examples ```bash theme={null} # List all SPI nets on a box lager spi --box my-lager-box # Show configuration for a specific net lager spi MY_SPI --box my-lager-box # Configure SPI mode and speed lager spi MY_SPI config --mode 0 --frequency 5M # Read flash JEDEC ID (send 0x9F command, read 3 bytes) lager spi MY_SPI transfer --data 0x9f 4 # Read 256 bytes from flash lager spi MY_SPI read 256 # Write a command byte lager spi MY_SPI write 0x06 # Multi-step transfer with CS held low lager spi MY_SPI write 0x03 --keep-cs lager spi MY_SPI read 4 ``` *** ## Troubleshooting ### No Response from Device * Verify MOSI, MISO, SCLK, and CS wiring * Check SPI mode matches the device datasheet * Confirm CS polarity (`--cs-active low` for most devices) * Try reducing frequency with `--frequency 100k` ### Garbled Data * Verify SPI mode (CPOL/CPHA) matches the device * Check bit order (MSB vs LSB first) * Ensure word size matches the device protocol ### CS Pin Not Working (LabJack T7) * LabJack T7 uses manual GPIO-based CS control * The driver automatically asserts/deasserts CS via GPIO writes * Verify the `cs_pin` in the net configuration matches your wiring *** ## Notes * Default SPI net can be set with `lager defaults add --spi-net NETNAME` * LabJack T7 operates at \~450 kHz regardless of requested frequency due to hardware limitations * SPI is full-duplex: data is always sent and received simultaneously * Use `--keep-cs` for multi-part transactions that require CS to stay asserted * Configuration set via `config` persists across subsequent `transfer`/`read`/`write` commands ## See Also * [I2C](/source/reference/cli/i2c) -- I2C bus communication (the other common serial protocol) * [Python SPI API](/source/reference/python/spi) -- Automate SPI operations in Python scripts * [Glossary](/source/getting-started/glossary) -- Definitions of SPI, I2C, and other terms # SSH Source: https://docs.lagerdata.com/source/reference/cli/ssh SSH into a Lager Box Open an interactive SSH session to a Lagerbox, or run a single command on it and return. ## Syntax ```bash theme={null} lager ssh [OPTIONS] [COMMAND]... ``` ## Options | Option | Description | | ----------- | --------------------------- | | `--box BOX` | Lagerbox name or IP address | ## Arguments | Argument | Description | | --------- | ------------------------------------------------------------------------------- | | `COMMAND` | Optional command to run on the box. If omitted, an interactive shell is opened. | *** ## Usage ```bash theme={null} # SSH to specific Lager Box (interactive shell) lager ssh --box my-lager-box # SSH to default Lager Box lager ssh ``` ### Run a command on the box With a `COMMAND`, `lager ssh` behaves like `ssh user@host `: it runs the command on the box, streams its output back, and exits with the command's exit code — no interactive shell. This is handy for scripting and one-off checks. ```bash theme={null} # Run a single command and return lager ssh --box lab-lager-box -- cat /etc/lager/version # Flags pass through; use `--` to separate them from lager's own options lager ssh --box lab-lager-box -- ls -la /etc/lager # Inspect the running containers lager ssh --box lab-lager-box -- sudo docker ps ``` Use `--` to separate lager's options from the remote command whenever the command contains its own dashed flags, so they aren't parsed as `lager` options. The remote command's exit code is propagated as `lager ssh`'s exit code, so it composes cleanly in scripts. *** ## How It Works The command: 1. Resolves the Lager Box name to IP address 2. Looks up the SSH username (default: `lagerdata`) 3. Opens an interactive SSH session *** ## Username Resolution SSH usernames are resolved in order: 1. Username stored with box configuration (`lager boxes add --user`) 2. Default username: `lagerdata` To use a different username for a Lager Box: ```bash theme={null} # Configure username when adding box lager boxes add --name pi-lager-box --ip --user pi # Or edit existing box lager boxes edit --name pi-lager-box --user pi ``` *** ## SSH Key Setup For passwordless access, set up SSH keys: ```bash theme={null} # Generate key if needed ssh-keygen -t ed25519 # Copy to Lager Box ssh-copy-id lagerdata@ ``` *** ## Examples ```bash theme={null} # Quick interactive SSH access lager ssh --box my-lager-box # One-off commands (no interactive shell) lager ssh --box my-lager-box -- cat /etc/lager/version lager ssh --box my-lager-box -- sudo docker ps lager ssh --box my-lager-box -- sudo ufw status ``` *** ## Common Tasks via SSH Each of these can be run as a one-liner with `lager ssh --box -- `, or interactively after `lager ssh --box `. ### Check Container Status ```bash theme={null} lager ssh --box my-lager-box -- sudo docker ps -a lager ssh --box my-lager-box -- sudo docker logs controller --tail 100 ``` ### View Lager Box Version ```bash theme={null} lager ssh --box my-lager-box -- cat /etc/lager/version ``` ### Check Disk Space ```bash theme={null} lager ssh --box my-lager-box -- df -h ``` ### View Firewall Status ```bash theme={null} lager ssh --box my-lager-box -- sudo ufw status verbose ``` *** ## Notes * With no `COMMAND`, opens a fully interactive shell session (exit with `exit` or Ctrl+D) * With a `COMMAND`, runs it on the box and exits with the command's exit code * The session runs as a child process so Lager's cleanup hooks still fire * Default Lager Box is used if `--box` not specified # SSH Setup Source: https://docs.lagerdata.com/source/reference/cli/ssh-setup Set up passwordless SSH from this machine to a Lager Box Install this machine's Lager SSH key on a Lagerbox so that `lager` commands and `lager ssh` work without a password. Run it once per box, enter the box password when prompted, and subsequent commands authenticate with the key. This wraps the `ssh-keygen` / `ssh-copy-id` dance into a single command, so a `Permission denied (publickey,password)` error can be fixed without knowing the key path or the `ssh-copy-id` incantation by hand. Introduced in **lager 0.27.1** as `lager authorize`; renamed to `lager ssh-setup` because the old name read like authentication once `lager login` (gateway sign-in) arrived. The old spelling still works with a deprecation warning. ## Syntax ```bash theme={null} lager ssh-setup [OPTIONS] ``` ## Options | Option | Description | | ----------- | ------------------------------------------------------------- | | `--box BOX` | Lagerbox name or IP address (uses the default box if omitted) | *** ## Usage ```bash theme={null} # Authorize a specific Lager Box lager ssh-setup --box my-lager-box # Authorize the default Lager Box lager ssh-setup ``` You are prompted for the box password **once** (by `ssh-copy-id`). After the key is installed, no further password prompts appear for that box. *** ## How It Works 1. **Resolves** the box name to an IP and looks up its SSH user (see [SSH username resolution](/source/reference/cli/ssh#username-resolution)). 2. **Generates** the key pair `~/.ssh/lager_box` (and `lager_box.pub`) if it does not already exist. 3. **Skips early if already authorized** — if key authentication already works for the box, it reports success and changes nothing (the command is idempotent). 4. **Copies** the public key to the box with `ssh-copy-id` (one password prompt). 5. **Verifies** that passwordless key authentication now works, reporting a clear error if it does not. The key lives at `~/.ssh/lager_box`. Because that filename is not one of SSH's default identities, `lager ssh` passes `-i ~/.ssh/lager_box` explicitly when the key exists (since lager 0.28.1), so authorized boxes connect without a password. *** ## Examples ```bash theme={null} # First-time setup for a new box lager boxes add --name my-lager-box --ip 100.x.y.z lager ssh-setup --box my-lager-box # Re-running against an already-authorized box is safe (no-op) lager ssh-setup --box my-lager-box # my-lager-box is already authorized — no password needed. ``` *** ## Troubleshooting | Issue | Cause | Fix | | ------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ | | `ssh-copy-id was not found` | OpenSSH client tools are not installed | Install the OpenSSH client, or append the key manually (see below) | | `ssh-copy-id ... failed` | Wrong password, or the box rejected the connection | Confirm the box user and password with your admin, then retry | | Key copied but auth still fails | The box's `sshd_config` may disallow `publickey` auth | Test with `ssh -i ~/.ssh/lager_box @` and check `sshd_config` | If `ssh-copy-id` is unavailable, append the public key to the box manually: ```bash theme={null} cat ~/.ssh/lager_box.pub | ssh @ 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys' ``` *** ## See Also * [SSH](/source/reference/cli/ssh) — open an interactive shell on a Lager Box * [Boxes](/source/reference/cli/boxes) — register box names, IPs, and SSH users # Power Supply Source: https://docs.lagerdata.com/source/reference/cli/supply Control power supply on box Control and monitor power supply Nets through the Lager CLI. Supports multi-vendor bench power supplies with voltage/current setting, protection thresholds, real-time monitoring via TUI, and concurrent access. ## Syntax ```bash theme={null} lager supply [OPTIONS] NET_NAME COMMAND [ARGS]... ``` When invoked without a subcommand, lists all available power supply nets on the box: ```bash theme={null} lager supply --box my-lager-box ``` ``` Name Net Type Instrument Channel Address ================================================================ PSU_CH1 power-supply Rigol_DP832 CH1 USB0::0x1AB1::0x0E11::DP8... PSU_CH2 power-supply Rigol_DP832 CH2 USB0::0x1AB1::0x0E11::DP8... DUT_POWER power-supply Keysight_E36312A 1 USB0::0x2A8D::0x1602::... ``` ## Global Options | Option | Description | | ------------ | --------------------------- | | `--box TEXT` | Lagerbox name or IP address | | `--help` | Show help message and exit | ## Commands | Command | Description | | ----------- | --------------------------------------------------------------------- | | `voltage` | Set or read output voltage with optional protection thresholds | | `current` | Set or read output current with optional protection thresholds | | `enable` | Enable power output (requires confirmation) | | `disable` | Disable power output (requires confirmation) | | `state` | Show current power state including measurements and protection status | | `clear-ovp` | Clear over-voltage protection fault | | `clear-ocp` | Clear over-current protection fault | | `set` | Set power supply mode | | `tui` | Launch interactive terminal UI for real-time monitoring and control | ## CLI Validation Ranges The CLI enforces conservative upper bounds before sending commands to hardware: | Parameter | Maximum | Notes | | --------- | ------- | -------------------------------- | | Voltage | 100.0 V | Most bench supplies are 30-60 V | | Current | 30.0 A | Typical bench supply limit | | OVP | 110.0 V | Can be slightly above max output | | OCP | 33.0 A | Can be slightly above max output | All values must be positive. OVP must be greater than or equal to the voltage setpoint. ## Command Reference ### `voltage` Set or read output voltage with optional protection thresholds. ```bash theme={null} lager supply NET_NAME voltage [VALUE] [OPTIONS] ``` **Arguments:** * `VALUE` - Voltage in volts. Omit to read the current voltage setting. **Options:** | Option | Type | Description | | ------------- | ------ | ------------------------------------------------------ | | `--ovp FLOAT` | Volts | Over-voltage protection threshold (must be >= voltage) | | `--ocp FLOAT` | Amps | Over-current protection threshold | | `--yes` | Flag | Apply without confirmation prompt | | `--box TEXT` | String | Lagerbox name or IP | When VALUE is provided, the CLI prompts for confirmation unless `--yes` is passed: ``` Set voltage to 3.3 V? [y/N]: ``` **Examples:** ```bash theme={null} # Read current voltage lager supply PSU voltage # Set voltage to 3.3V (will prompt for confirmation) lager supply PSU voltage 3.3 # Set voltage with automatic confirmation lager supply PSU voltage 3.3 --yes # Set voltage with OVP and OCP thresholds lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes ``` ### `current` Set or read output current with optional protection thresholds. ```bash theme={null} lager supply NET_NAME current [VALUE] [OPTIONS] ``` **Arguments:** * `VALUE` - Current in amps. Omit to read the current limit setting. **Options:** | Option | Type | Description | | ------------- | ------ | ------------------------------------------------------ | | `--ovp FLOAT` | Volts | Over-voltage protection threshold | | `--ocp FLOAT` | Amps | Over-current protection threshold (must be >= current) | | `--yes` | Flag | Apply without confirmation prompt | | `--box TEXT` | String | Lagerbox name or IP | When VALUE is provided, the CLI prompts for confirmation unless `--yes` is passed: ``` Set current to 1.0 A? [y/N]: ``` **Examples:** ```bash theme={null} # Read current limit lager supply PSU current # Set current limit to 1A lager supply PSU current 1.0 --yes # Set current with protection thresholds lager supply PSU current 1.0 --ocp 1.2 --ovp 5.0 --yes ``` ### `enable` Enable power output to device. Requires confirmation to prevent accidental power-on. ```bash theme={null} lager supply NET_NAME enable [OPTIONS] ``` **Options:** | Option | Description | | ------------ | ---------------------------------- | | `--yes` | Enable without confirmation prompt | | `--box TEXT` | Lagerbox name or IP | ``` Enable Net? [y/N]: ``` ### `disable` Disable power output. Requires confirmation to prevent accidental power-off. ```bash theme={null} lager supply NET_NAME disable [OPTIONS] ``` **Options:** | Option | Description | | ------------ | ----------------------------------- | | `--yes` | Disable without confirmation prompt | | `--box TEXT` | Lagerbox name or IP | ``` Disable Net? [y/N]: ``` ### `state` Show comprehensive power supply state including measurements, setpoints, and protection status. ```bash theme={null} lager supply NET_NAME state [--box TEXT] ``` **Example output (Rigol DP800):** ``` Channel: CH1 Enabled: ON Mode: CV Voltage: 3.3000 Current: 0.1520 Power: 0.5016 OCP Limit: 1.0000 OCP Tripped: NO OVP Limit: 3.6000 OVP Tripped: NO ``` Fields: * **Channel** - Active channel on multi-channel supplies * **Enabled** - Output ON or OFF * **Mode** - CV (constant voltage) or CC (constant current) * **Voltage/Current/Power** - Live measurements (4 decimal places) * **OCP/OVP Limit** - Protection thresholds * **OCP/OVP Tripped** - Whether protection has triggered (color-coded: green=NO, red=YES) ### `clear-ovp` Clear over-voltage protection fault. Use after an OVP trip to reset the protection and allow the output to be re-enabled. ```bash theme={null} lager supply NET_NAME clear-ovp [--box TEXT] ``` ### `clear-ocp` Clear over-current protection fault. Use after an OCP trip to reset the protection and allow the output to be re-enabled. ```bash theme={null} lager supply NET_NAME clear-ocp [--box TEXT] ``` ### `set` Set power supply mode. The available modes depend on the hardware. ```bash theme={null} lager supply NET_NAME set [--box TEXT] ``` ### `tui` Launch an interactive terminal UI for real-time power supply monitoring and control. The TUI provides live-updating measurements, inline command entry, and keyboard shortcuts. ```bash theme={null} lager supply NET_NAME tui [--box TEXT] ``` Requires the `textual` Python package (`pip install textual`). **TUI display:** * Live voltage, current, and power measurements (updated every second) * Output status (ON/OFF) with color coding * Mode indicator (CV/CC) with color coding * Protection thresholds and trip status * Hardware maximum ratings * Scrollable command log **TUI commands** (enter at the prompt): | Command | Description | | --------------------- | ------------------------- | | `voltage [VALUE]` | Set or read voltage | | `current [VALUE]` | Set or read current limit | | `ocp [VALUE]` | Set or read OCP threshold | | `ovp [VALUE]` | Set or read OVP threshold | | `enable` | Enable output | | `disable` | Disable output | | `state` | Display current state | | `clear-ocp` | Clear OCP trip | | `clear-ovp` | Clear OVP trip | | `help` | Show available commands | | `clear` | Clear the command log | | `q` / `quit` / `exit` | Exit the TUI | **Keyboard shortcuts:** | Key | Action | | -------------- | ------------------------ | | `q` | Quit | | `Ctrl+C` | Quit | | `r` | Refresh display | | Up/Down arrows | Navigate command history | **Concurrent access:** While the TUI is running, other `lager supply` CLI commands (e.g., `lager supply PSU voltage 3.3 --yes`) will automatically route through the TUI's WebSocket connection on port 9000, sharing the USB instrument connection. If the TUI is not running, commands use direct USB access. ## OVP / OCP Protection Over-voltage protection (OVP) and over-current protection (OCP) thresholds protect your device under test from damage. **Setting thresholds:** ```bash theme={null} # Set OVP and OCP when setting voltage lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes # Set OCP when setting current lager supply PSU current 1.0 --ocp 1.2 --yes ``` **Validation rules:** * All values must be positive * OVP must be >= the voltage setpoint * OCP must be >= the current setpoint * Values are validated against CLI maximum limits before being sent to hardware **When protection trips:** 1. The supply output is disabled automatically 2. `state` shows the tripped status in red 3. Clear the fault with `clear-ovp` or `clear-ocp` 4. Re-enable the output with `enable` **Automatic OVP management (Rigol):** When setting a new voltage that would exceed the current OVP limit, the driver temporarily raises OVP by 10% headroom, sets the new voltage, then restores the desired OVP. This prevents false trips during voltage changes. ## Supported Hardware | Manufacturer | Model Series | Channels | Specs | Notes | | ------------ | -------------- | -------- | -------------------------- | ---------------------- | | Rigol | DP832 / DP832A | 3 | Ch1-2: 30V/3A, Ch3: 5V/3A | Most common | | Rigol | DP821 | 2 | Ch1: 60V/1A, Ch2: 8V/10A | | | Rigol | DP811 / DP811A | 1 | 20V/10A or 40V/5A (range) | | | Keithley | 2281S | 1 | 20V/6A/120W | Battery simulator mode | | Keysight | E36200 series | 2 | E36233A: 30V/20A per ch | Dual output | | Keysight | E36300 series | 3 | E36311A/12A/13A | Triple output | | EA | PSI/EL series | 1 | PSB 10080-60, PSB 10060-60 | Two-quadrant | Multi-channel supplies use the channel number configured in the net record. Each channel is typically configured as a separate net. ## Default Net Set a default power supply net to avoid specifying the name each time: ```bash theme={null} lager defaults add --supply-net PSU ``` Then commands can omit the net name: ```bash theme={null} lager supply voltage 3.3 --yes lager supply state ``` ## Examples ```bash theme={null} # List all power supply nets lager supply --box my-lager-box # Set voltage with protection lager supply PSU voltage 3.3 --ovp 3.6 --ocp 0.5 --yes # Set current limit lager supply PSU current 1.0 --ocp 1.2 --yes # Enable output (skip confirmation) lager supply PSU enable --yes # Check state lager supply PSU state # Disable output lager supply PSU disable --yes # Clear protection faults lager supply PSU clear-ovp lager supply PSU clear-ocp # Launch interactive TUI lager supply PSU tui ``` ### Scripting Example ```bash theme={null} #!/bin/bash # Power cycle a device under test BOX="my-lager-box" NET="DUT_POWER" lager supply $NET voltage 3.3 --ovp 3.6 --ocp 0.5 --yes --box $BOX lager supply $NET enable --yes --box $BOX sleep 2 # Run tests while powered lager python test_script.py --box $BOX # Power down lager supply $NET disable --yes --box $BOX ``` ## Troubleshooting | Error | Cause | Fix | | ------------------------ | ---------------------------------------- | ----------------------------------------------- | | "Resource busy" | TUI is using the supply's USB connection | Close the TUI (press `q`), then retry | | "No route to host" | Box unreachable | Check VPN/Tailscale: `lager hello --box ` | | Connection refused | Box service not running | Verify box is online: `lager hello --box ` | | OVP/OCP validation error | Protection threshold below setpoint | Set OVP >= voltage, OCP >= current | | "exceeds maximum limit" | Value above CLI safety limits | Check equipment specs; CLI max is 100V / 30A | ## Notes * All `voltage` and `current` set operations require confirmation (use `--yes` to skip) * `enable` and `disable` also require confirmation * Net names refer to names assigned when setting up your testbed with `lager nets` * The TUI connects via WebSocket (port 9000) for real-time updates * Protection thresholds help prevent damage to your device under test * Multi-channel supplies: each channel is configured as a separate net ## See Also * [Battery Simulation](/source/reference/cli/battery) -- Control battery simulators (Keithley 2281S) * [Electronic Load](/source/reference/cli/eload) -- Programmable electronic loads * [Watt Meter](/source/reference/cli/watt) -- Power measurement with Yocto-Watt and Joulescope * [Python Supply API](/source/reference/python/supply) -- Automate power supply control in Python scripts # Thermocouple Source: https://docs.lagerdata.com/source/reference/cli/tc Read thermocouple temperature measurements Read thermocouple temperature values from thermocouple nets through the Lager CLI for temperature measurement and monitoring. ## Syntax ```bash theme={null} lager thermocouple [NETNAME] [OPTIONS] ``` ## Arguments | Argument | Description | | --------- | -------------------------------------------------- | | `NETNAME` | Thermocouple net name (optional if default is set) | ## Options | Option | Description | | ------------ | --------------------------- | | `--box TEXT` | Lagerbox name or IP address | | `--help` | Show help message and exit | *** ## Usage ```bash theme={null} # Read temperature from thermocouple net lager thermocouple TEMP_SENSOR --box my-lager-box # List available thermocouple nets (when no net specified) lager thermocouple --box my-lager-box # Using default net lager thermocouple ``` *** ## Output Returns temperature in degrees Celsius: ```bash theme={null} $ lager thermocouple TEMP_SENSOR --box my-lager-box 23.5 ``` *** ## Supported Hardware | Device | Type | Range | | --------------- | ---------------------- | ----------------- | | Phidget TMP1101 | 4-channel thermocouple | -200°C to +1300°C | *** ## Examples ```bash theme={null} # Read temperature from sensor lager thermocouple TEMP_SENSOR --box my-lager-box # Monitor multiple thermocouples lager thermocouple TC1 --box my-lager-box lager thermocouple TC2 --box my-lager-box lager thermocouple TC3 --box my-lager-box # Use in shell script TEMP=$(lager thermocouple OVEN_TC --box my-lager-box) if (( $(echo "$TEMP > 100" | bc -l) )); then echo "Temperature exceeded 100°C!" fi ``` *** ## Creating Thermocouple Nets ```bash theme={null} # Create a thermocouple net lager nets add TEMP_SENSOR thermocouple 0 PHIDGET_SERIAL --box my-lager-box ``` Where: * `TEMP_SENSOR` - Net name * `thermocouple` - Net type * `0` - Channel number (0-3 for 4-channel Phidget) * `PHIDGET_SERIAL` - Phidget device serial number *** ## Notes * Net names refer to names assigned when setting up your testbed * Results are returned in degrees Celsius (°C) * Only works with nets of type `thermocouple` * If no net is specified and no default is set, lists available thermocouple nets * Default net can be set with `lager defaults add --thermocouple-net` # Terminal Source: https://docs.lagerdata.com/source/reference/cli/terminal Interactive REPL for running Lager commands Launch an interactive terminal with tab completion, command history, and auto-suggestions for running Lager CLI commands. ## Syntax ```bash theme={null} lager terminal ``` The terminal also launches automatically when you run `lager` with no subcommand. ## Features | Feature | Description | | -------------------- | -------------------------------------------------------- | | Tab completion | Auto-complete commands, subcommands, and flags | | Command history | Persistent history stored in `~/.lager_terminal_history` | | Auto-suggest | History-based suggestions as you type | | Arrow key navigation | Browse previous commands with up/down arrows | | Color-coded output | Green for success, red for errors | | Execution timing | Shows duration for each command | ## Built-in Commands These commands are available inside the REPL in addition to all `lager` commands: | Command | Description | | -------------- | -------------------------------- | | `help`, `?` | Show available commands and help | | `clear` | Clear the screen | | `exit`, `quit` | Exit the terminal | ## Keyboard Shortcuts | Shortcut | Action | | ----------- | --------------------------- | | `Tab` | Auto-complete current input | | `Up / Down` | Navigate command history | | `Ctrl+R` | Search command history | | `Ctrl+C` | Cancel current input | | `Ctrl+D` | Exit terminal | ## Usage All Lager commands are available inside the terminal without the `lager` prefix: ```bash theme={null} # Launch the terminal lager terminal # Inside the REPL: > hello --box my-lager-box > supply voltage 3.3 --yes > adc read VCC > status --box my-lager-box > debug flash --hexfile firmware.hex > ? > exit ``` The terminal automatically prepends `lager` to each command and executes it through the CLI, so authentication, box resolution, and all standard behavior works as expected. ## Blocked Commands Interactive commands that require their own terminal session are blocked inside the REPL: * Commands with `tui` subcommands (e.g., `supply tui`, `battery tui`) * Interactive UART sessions The terminal warns you and suggests running these from a regular shell instead. ## Dependencies The terminal requires `prompt_toolkit` and `rich`. If these are not installed, the CLI falls back to showing normal help output: ```bash theme={null} pip install prompt_toolkit rich ``` ## Notes * Running `lager` with no arguments launches the terminal automatically * Command history persists across sessions in `~/.lager_terminal_history` * Exit codes from each command are shown with a check mark or X indicator * The welcome screen adapts to your terminal width # UART Source: https://docs.lagerdata.com/source/reference/cli/uart Connect to UART serial ports Connect to UART serial ports on Lagerboxes for serial communication with devices. ## Syntax ```bash theme={null} lager uart [NETNAME] [OPTIONS] ``` ## Arguments | Argument | Description | | --------- | ------------------------------------------ | | `NETNAME` | UART net name (optional if default is set) | ## Options | Option | Description | | ---------------------------- | --------------------------------------- | | `--box BOX` | Lagerbox name or IP address | | `--baudrate RATE` | Baudrate (e.g., 9600, 115200) | | `--bytesize SIZE` | Data bits (5, 6, 7, 8) | | `--parity MODE` | Parity: none, even, odd, mark, space | | `--stopbits BITS` | Stop bits (1, 1.5, 2) | | `--xonxoff` / `--no-xonxoff` | Software flow control | | `--rtscts` / `--no-rtscts` | Hardware flow control (RTS/CTS) | | `--dsrdtr` / `--no-dsrdtr` | Hardware flow control (DSR/DTR) | | `-i` / `--interactive` | Enable input mode for typing | | `--opost` / `--no-opost` | Convert \n to \r\n on output | | `--line-ending MODE` | Line ending: lf, crlf, cr (default: lf) | *** ## Usage ### Read-Only Mode (Default) ```bash theme={null} # Monitor serial output lager uart SERIAL1 --box my-lager-box # With specific baudrate lager uart SERIAL1 --baudrate 115200 ``` ### Interactive Mode ```bash theme={null} # Type commands and see responses lager uart SERIAL1 --interactive # With specific settings lager uart SERIAL1 -i --baudrate 9600 --parity none ``` ### Get Serial Port Info ```bash theme={null} # Show device path for a UART net lager uart SERIAL1 serial-port ``` *** ## Serial Configuration ### Baudrate Common baudrates: * 9600 (default for many devices) * 19200 * 38400 * 57600 * 115200 (common for embedded development) * 230400 * 460800 * 921600 ### Data Format | Setting | Options | Default | | -------- | ---------------------------- | ------- | | Bytesize | 5, 6, 7, 8 | 8 | | Parity | none, even, odd, mark, space | none | | Stopbits | 1, 1.5, 2 | 1 | ### Flow Control | Type | Options | Description | | -------- | ----------- | ------------------- | | Software | `--xonxoff` | XON/XOFF characters | | Hardware | `--rtscts` | RTS/CTS pins | | Hardware | `--dsrdtr` | DSR/DTR pins | Flow control types cannot be combined. *** ## Line Endings | Mode | Sequence | Use Case | | ------ | -------- | -------------- | | `lf` | \n | Unix/Linux | | `crlf` | \r\n | Windows | | `cr` | \r | Legacy systems | Use `--opost` to automatically convert line endings on output. *** ## Supported USB Serial Adapters The following USB-to-serial adapters are automatically detected and supported: | Adapter | VID:PID | Description | | --------------------- | --------- | --------------------------- | | Prolific USB-Serial | 067b:23a3 | Common USB-Serial adapter | | Silicon Labs CP210x | 10c4:ea60 | Popular embedded dev boards | | FTDI FT232R | 0403:6001 | Single-channel USB-Serial | | FTDI FT4232H | 0403:6011 | Quad-channel USB-Serial | | ESP32 USB JTAG/Serial | 303a:1001 | Built-in ESP32-S3/C3 USB | These adapters are automatically recognized by `lager instruments` and can be configured as UART nets. *** ## Device Path Support UART nets can reference devices two ways: **USB Serial Number** (preferred): ``` Net configured with USB serial number Automatically resolves to correct /dev/ttyUSBx ``` **Direct Device Path** (fallback): ```bash theme={null} # When adapter has no serial number lager uart /dev/ttyUSB0 --baudrate 115200 ``` *** ## Examples ```bash theme={null} # Basic serial monitor lager uart DEBUG_UART --box my-lager-box # Interactive shell with common embedded settings lager uart CONSOLE -i --baudrate 115200 # Legacy device with specific format lager uart LEGACY --baudrate 9600 --bytesize 7 --parity even --stopbits 2 # With software flow control lager uart MODEM --xonxoff # Windows-style line endings lager uart TERMINAL --line-ending crlf --opost ``` *** ## WebSocket Connection The UART command uses WebSocket for communication: * Provides real-time bidirectional data * Supports both read-only and interactive modes * Automatically reconnects on connection loss *** ## Troubleshooting ### Device Not Found ```bash theme={null} # List available instruments to find UART devices lager instruments --box my-lager-box # Check if USB serial adapter is connected ssh lagerdata@my-lager-box 'ls -la /dev/ttyUSB*' ``` ### Permission Denied ```bash theme={null} # Ensure udev rules are installed lager update --box my-lager-box --yes ``` ### No Output * Check baudrate matches device * Verify TX/RX connections * Try interactive mode to test input * Check flow control settings *** ## Notes * Interactive mode requires a TTY terminal * USB serial numbers are truncated for display * Default net can be set with `lager defaults add --uart-net` * Connection retry logic handles temporary disconnections ## See Also * [Python UART API](/source/reference/python/uart) -- Access UART nets from Python scripts * [Python Serial API](/source/reference/python/serial) -- Native pyserial support for advanced serial use cases # Uninstall Source: https://docs.lagerdata.com/source/reference/cli/uninstall Remove Lager box code from a box Remove the Lager box software, Docker containers, and supporting files from a box. ## Syntax ```bash theme={null} lager uninstall [OPTIONS] ``` ## Options | Option | Type | Default | Description | | ---------------------- | ------ | ----------- | ----------------------------------------------------------------------------------------- | | `--box TEXT` | string | | Box name (uses stored IP and username from `.lager` config) | | `--ip TEXT` | string | | Target box IP address | | `--user TEXT` | string | `lagerdata` | SSH username | | `--keep-config` | flag | | Preserve `/etc/lager` directory (saved nets, box ID, etc.) | | `--keep-docker-images` | flag | | Remove containers only, keep Docker images | | `--all` | flag | | Remove everything including udev rules, sudoers, third-party tools, and legacy deploy key | | `--yes` | flag | | Skip confirmation prompts | | `--dry-run` | flag | | Show what would be removed without making changes | | `--help` | | | Show help message and exit | Either `--box` or `--ip` is required. ## What Gets Removed ### Default Removal | Component | Description | | ---------------------- | -------------------------------------------------------------- | | Docker containers | Stops and removes the `lager` container | | Docker images | Removes images and build cache (unless `--keep-docker-images`) | | `~/box` directory | Box code and services | | `/etc/lager` directory | Saved nets, box ID, version (unless `--keep-config`) | ### With `--all` In addition to the above: | Component | Description | | ----------------- | ---------------------------------------------------------------------------------- | | Udev rules | `/etc/udev/rules.d/lager-*.rules` | | Sudoers config | `/etc/sudoers.d/lagerdata-udev` | | `~/third_party` | J-Link, custom binaries | | Legacy deploy key | `~/.ssh/lager_deploy_key*` and SSH config entry (from pre-open-source deployments) | ## Examples ```bash theme={null} # Basic uninstall lager uninstall --ip 192.168.1.100 # Uninstall but keep saved nets lager uninstall --ip 192.168.1.100 --keep-config # Uninstall but keep Docker images for faster reinstall lager uninstall --ip 192.168.1.100 --keep-docker-images # Complete cleanup lager uninstall --ip 192.168.1.100 --all # Non-interactive uninstall of a stored box lager uninstall --box my-lager-box --yes ``` ## Uninstall Flow 1. **Resolve target** - Looks up box IP from `--box` name or uses `--ip` directly 2. **Verify SSH** - Tests key-based authentication, falls back to password if needed 3. **Show summary** - Lists what will be removed based on flags 4. **Confirm** - Requires explicit confirmation (unless `--yes`) 5. **Remove step by step:** * Stop and remove Docker containers * Clean Docker images and build cache * Remove `~/box` directory * Remove `/etc/lager` directory * Remove additional components (if `--all`) 6. **Report** - Confirms completion and suggests `--all` if not used ## Notes * Each removal step continues even if a previous step fails, so partial uninstalls are possible * Use `--keep-config` if you plan to reinstall and want to preserve your net configuration * Use `--keep-docker-images` for a faster reinstall since images won't need to be rebuilt * After uninstalling, use `lager install` to redeploy # Update Source: https://docs.lagerdata.com/source/reference/cli/update Update Lager Box code from GitHub repository Update Lager Box software on Lagerboxes with comprehensive progress tracking and automatic configuration. ## Syntax ```bash theme={null} lager update [OPTIONS] ``` ## Options | Option | Description | | ------------------- | --------------------------------------------------------------------------------------------------------- | | `--box BOX` | Lagerbox name or IP address | | `--all` | Update all saved boxes that need updating | | `--version VERSION` | Release tag, semver pin, or branch to update to (default: main). See [Version pinning](#version-pinning). | | `--yes` | Skip confirmation prompt | | `--check` | Dry run: report what would change without modifying the box | | `--skip-restart` | Skip container restart after update | | `--check-jlink` | Check for J-Link and offer to install if missing | | `--force` | Force fresh Docker build by removing cached image | | `--verbose` / `-v` | Show detailed output (default shows progress bar) | Either `--box` or `--all` is required. They cannot be used together. ## Version pinning Since **lager 0.22.0**, a semver value passed to `--version` — with or without a leading `v` (e.g. `0.26.0` or `v0.26.0`), including common pre-release suffixes (`-rc1`, `-beta2`, `-alpha`, `-preview`) — resolves to the release **tag** `vX.Y.Z`. Release tags are the single source of truth for a pinned version. ```bash theme={null} # All three pin the same release tag (v0.26.0) lager update --box my-lager-box --version v0.26.0 --yes lager update --box my-lager-box --version 0.26.0 --yes lager update --box my-lager-box --version v0.27.0-rc1 --yes ``` Any other value (`main`, `staging`, or a feature branch name) resolves to `origin/` as before. Per-release **version branches** (bare `X.Y.Z` branches) are deprecated in favour of tags. A value like `0.26.0` now resolves to the tag `v0.26.0`, not a branch of the same name. See `RELEASE_PROCESS.md` in the repository. ## Usage ### Basic Update ```bash theme={null} # Update to latest main branch lager update --box my-lager-box --yes # Update to staging branch lager update --box my-lager-box --version staging --yes # Update with verbose output lager update --box my-lager-box --yes --verbose # Force fresh Docker build (use for major code changes) lager update --box my-lager-box --force --yes ``` ### Update All Boxes The `--all` flag queries every saved box's version and updates only those that are behind the current CLI version. ```bash theme={null} # Update all boxes that need updating lager update --all --yes # Preview which boxes need updating (without --yes, shows confirmation) lager update --all ``` **How `--all` works:** 1. Queries each saved box's `/cli-version` endpoint 2. Compares box version against the current CLI version 3. Builds a list of boxes that need updating (version older than CLI or unknown) 4. Skips boxes that are already current, newer, or unreachable 5. Updates each box sequentially 6. Displays a summary of successes and failures **Example output:** ``` CLI version: 0.3.22 Checking box versions... my-lager-box 0.3.20 will update staging-box 0.3.22 current pi-box 0.3.19 will update offline-box --- unreachable (skipped) 2 box(es) need updating. Proceed? [Y/n]: y Updating my-lager-box... [OK] Updated to 0.3.22 Updating pi-box... [OK] Updated to 0.3.22 Update complete (45.2s total) Successful: 2 Failed: 0 ``` ### Skip Container Restart ```bash theme={null} # Update code but don't restart containers lager update --box my-lager-box --skip-restart --yes # Manually restart later ssh lagerdata@my-lager-box 'cd ~/box && ./start_box.sh' ``` ### J-Link Installation Check ```bash theme={null} # Check if J-Link is installed, offer to install if missing lager update --box my-lager-box --check-jlink --yes ``` *** ## Update Process The update command performs the following steps (shown in progress bar): 1. **SSH Connection** - Establish secure connection to Lager Box 2. **Git Repository Check** - Validate lager repository exists 3. **Git Pull** - Pull latest code from specified branch 4. **Udev Rules** - Install USB instrument access rules 5. **Docker Build** - Build updated containers (no-cache) 6. **Firewall Setup** - Configure UFW if needed 7. **Customer Binaries** - Set up custom binaries directory 8. **J-Link Check** - Verify debug probe software (optional) 9. **Version Update** - Record version in `/etc/lager/version` 10. **Container Restart** - Start updated containers 11. **Status Verification** - Confirm containers are running *** ## Version Tracking The Lager Box tracks its current version in `/etc/lager/version`. The `lager boxes` listing queries each configured box and shows its current version in the `version` column: ```bash theme={null} # List all boxes with their current versions lager boxes ``` *** ## SSH Key Authentication The update command automatically detects SSH key configuration: * **With SSH keys**: Updates proceed without password prompts * **Without SSH keys**: Prompts for password (interactive mode) To set up SSH keys: ```bash theme={null} ssh-copy-id lagerdata@ ``` *** ## Firewall Auto-Configuration If UFW is not configured, the update command will: 1. Detect missing firewall rules 2. Offer to install and configure UFW 3. Apply secure defaults (VPN-only access to Lager ports) ```bash theme={null} # Manual firewall configuration cli/deployment/security/secure_box_firewall.sh ``` *** ## Examples ```bash theme={null} # Standard update workflow lager update --box my-lager-box --version main --yes # Update all boxes at once lager update --all --yes # Update all boxes to staging branch lager update --all --version staging --yes # Force fresh build on a specific box lager update --box my-lager-box --force --yes # Verbose update for troubleshooting lager update --box my-lager-box --verbose ``` *** ## Troubleshooting ### Update Fails at Git Pull ```bash theme={null} # Check the remote URL (should be HTTPS, not SSH) ssh lagerdata@ 'cd ~/box && git remote get-url origin' # If it shows git@github.com:..., switch to HTTPS: ssh lagerdata@ 'cd ~/box && git remote set-url origin https://github.com/lagerdata/lager.git' ``` ### Container Build Fails ```bash theme={null} # SSH in and check Docker status ssh lagerdata@ docker ps -a docker logs controller ``` ### Firewall Issues ```bash theme={null} # Check firewall status ssh lagerdata@ 'sudo ufw status verbose' # Re-run firewall setup cli/deployment/security/secure_box_firewall.sh ``` *** ## Notes * Progress bar is disabled when `--verbose` is used * Container builds use `--no-cache` to ensure fresh builds * Version defaults to `main` if not specified * Update automatically verifies container health after restart * `--all` checks versions automatically and only updates outdated boxes * `--force` removes cached Docker images before building (useful after major code changes) * `--box` and `--all` are mutually exclusive # USB Source: https://docs.lagerdata.com/source/reference/cli/usb Control USB hub port power Control programmable USB hub ports through the Lager CLI for power management and device connectivity. ## Syntax ```bash theme={null} lager usb [OPTIONS] [NET_NAME] [COMMAND] ``` ## Global Options | Option | Description | | ------------ | --------------------------- | | `--box TEXT` | Lagerbox name or IP address | | `--help` | Show help message and exit | ## Arguments | Argument | Description | | ---------- | ----------------------------------------------- | | `NET_NAME` | USB net name (optional - lists nets if omitted) | | `COMMAND` | Power command: `enable`, `disable`, or `toggle` | ## Commands | Command | Description | | --------- | -------------------------------- | | `enable` | Enable (power on) the USB port | | `disable` | Disable (power off) the USB port | | `toggle` | Toggle the current power state | *** ## Usage ### List USB Nets When invoked without a net name, lists all USB nets on the box: ```bash theme={null} lager usb --box my-lager-box ``` **Output:** ``` Name Net Type Instrument Channel Address USB1 usb Acroname_Hub 0 USB::123456 USB2 usb Acroname_Hub 1 USB::123456 CAM_USB usb YKUSH 0 USB::789012 ``` ### Control USB Port Power ```bash theme={null} lager usb NET_NAME COMMAND [--box BOX] ``` **Examples:** ```bash theme={null} # Enable USB port lager usb USB1 enable --box my-lager-box # Disable USB port lager usb USB1 disable --box my-lager-box # Toggle USB port state lager usb USB1 toggle --box my-lager-box ``` *** ## Examples ```bash theme={null} # List all USB nets lager usb --box my-lager-box # Power on a USB port for a camera lager usb CAM_USB enable --box my-lager-box # Power off USB port to reset a device lager usb USB1 disable --box my-lager-box # Toggle power state (useful for power cycling) lager usb USB1 toggle --box my-lager-box # Power cycle a device (disable then enable) lager usb USB1 disable --box my-lager-box sleep 2 lager usb USB1 enable --box my-lager-box ``` *** ## Supported Hardware | Manufacturer | Model | Description | | ------------ | --------- | ------------------------ | | Acroname | USBHub3+ | Programmable USB 3.0 hub | | Acroname | USBHub2x4 | 4-port programmable hub | | YKUSH | YKUSH3 | USB switchable hub | *** ## Notes * Net names (e.g., `USB1`, `CAM_USB`) refer to USB ports configured on your testbed * Commands are case-insensitive (`enable`, `ENABLE`, and `Enable` all work) * Useful for power cycling USB devices during testing * USB hubs must be connected to the box and configured as instruments * Default net can be set with `lager defaults add --usb-net` * Create USB nets with `lager nets add usb

` # Watt Meter Source: https://docs.lagerdata.com/source/reference/cli/watt Read power, current, and voltage from a watt meter Read power, current, and voltage measurements from watt meter Nets through the Lager CLI. Supports Yocto-Watt, Joulescope JS220, and Nordic PPK2 hardware. ## Syntax ```bash theme={null} lager watt [NET_NAME] [COMMAND] [OPTIONS] ``` With no `COMMAND`, `lager watt NET_NAME` reads power (watts). The `current`, `voltage`, and `all` subcommands read the other quantities (Joulescope JS220 and Nordic PPK2 only). ## Commands | Command | Description | | ------------------ | ----------------------------------------- | | *(none)* / `power` | Read power in watts | | `current` | Read current in amps | | `voltage` | Read voltage in volts | | `all` | Read current, voltage, and power together | ## Options These options are available on each read subcommand (`power`/`current`/`voltage`/`all`). `--box` is also accepted directly after `lager watt NET_NAME` for the default power read. | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `--box BOX` | Lagerbox name or IP address | | `-d, --duration FLOAT` | Averaging window in seconds (default `0.1`). Longer windows average more samples for a lower-noise, higher-resolution reading. | | `--json` | Emit a machine-readable JSON object instead of formatted text | | `--help` | Show help message and exit | ## Arguments | Argument | Description | | ---------- | --------------------------------------------------------------------------------------------------------- | | `NET_NAME` | Name of the watt meter net to read (optional if a default is set). Must appear **before** the subcommand. | ## Usage ```bash theme={null} lager watt NET_NAME [--box BOX] # power lager watt NET_NAME current [--box BOX] # current lager watt NET_NAME voltage [--box BOX] # voltage lager watt NET_NAME all [--box BOX] # current + voltage + power ``` If `NET_NAME` is omitted and no default is set, `lager watt` lists all available watt meter nets on the box. ## Output Readings are formatted with an SI prefix so small magnitudes stay readable (for example a 52.34 µW load is shown as `52.340 µW` rather than rounding to `0.000 W`): ``` Power 'POWER_METER': 52.340 µW Current 'POWER_METER': 12.000 mA Voltage 'POWER_METER': 3.300 V ``` `all` prints all three quantities: ``` Measurements 'POWER_METER' (0.1s): Current: 12.000 mA Voltage: 3.300 V Power: 39.600 mW ``` With `--json`, output is a single JSON object (units are base SI — amps, volts, watts): ```bash theme={null} lager watt POWER_METER all --json --box my-lager-box {"netname": "POWER_METER", "current": 0.012, "voltage": 3.3, "power": 0.0396, "duration_s": 0.1} ``` The default reading timeout is 30 seconds (scaled up for long `--duration` windows). If no reading is received within that time, the command exits with an error suggesting the device may be disconnected or experiencing USB issues. ### Increasing resolution Two levers improve a power/current reading: * **Display** — output is SI-scaled automatically, so sub-milliwatt and sub-milliamp readings are shown in µ/n units instead of rounding to zero. For values too small for even the nano prefix, the reading is shown in scientific notation (e.g. `3.000e-13 W`) rather than rounding to `0.000` — a nonzero reading is never lost. * **Averaging window** — pass `--duration` to average over a longer capture. A longer window reduces noise on the mean, giving a steadier, higher-effective-resolution value: ```bash theme={null} lager watt POWER_METER current --duration 1.0 --box my-lager-box ``` ### Long averaging windows `--duration` also works for long windows — e.g. the average current over a minute: ```bash theme={null} lager watt POWER_METER current --duration 60 --box my-lager-box ``` On a Joulescope JS220, windows longer than \~10 s are measured with the instrument's **on-device charge accumulator** (average current = Δcharge ÷ Δt) rather than by buffering raw samples. This is **gapless** (it captures every transient, not just the sampled fraction), uses constant memory, and scales to arbitrarily long windows (a minute, ten minutes, longer). Short windows continue to use direct sampling. ## Supported Hardware | Manufacturer | Model | Identification | Features | | -------------------- | ---------- | ----------------------- | ------------------------------------------------------- | | Yoctopuce | Yocto-Watt | USB VID:PID `24e0:002a` | Real-time power measurement | | Joulescope | JS220 | USB VID:PID `16d0:10ba` | High-precision power, voltage, and current measurement | | Nordic Semiconductor | PPK2 | USB VID:PID `1915:c00a` | Current, voltage, and power measurement via source mode | ### Hardware Feature Comparison | Feature | Yocto-Watt | Joulescope JS220 | Nordic PPK2 | | ------------------------------------- | ------------------ | ------------------- | ------------------------------- | | Power reading (`power`) | Yes | Yes | Yes | | Voltage reading (`voltage`) | No | Yes | Yes (configured source voltage) | | Current reading (`current`) | No | Yes | Yes | | Combined reading (`all`) | No | Yes | Yes | | Configurable averaging (`--duration`) | No (instantaneous) | Yes | Yes | | Device selection | Channel-based | Serial number-based | Serial number-based | The `current`, `voltage`, and `all` subcommands require a Joulescope JS220 or Nordic PPK2. On a Yocto-Watt (power only) they exit with a clear "not supported" message — use `lager watt NET_NAME` for power instead. The same readings are also available through the [Python API](/reference/python/watt). ### Instrument Name Matching The backend driver is selected based on the instrument name in the net configuration: | Pattern | Driver | | ------------------------------------------------------ | ---------------- | | Contains `joulescope` or `js220` (case-insensitive) | Joulescope JS220 | | Contains `ppk2`, `ppk`, or `nordic` (case-insensitive) | Nordic PPK2 | | All other watt meter instruments | Yocto-Watt | ## Default Net To avoid specifying the net name each time: ```bash theme={null} lager defaults add --watt-meter-net POWER_METER ``` Then: ```bash theme={null} lager watt ``` ## Examples ```bash theme={null} # Read power from watt meter lager watt POWER_METER --box my-lager-box # Read current / voltage (Joulescope JS220 or Nordic PPK2) lager watt POWER_METER current --box my-lager-box lager watt POWER_METER voltage --box my-lager-box # Read current, voltage, and power together lager watt POWER_METER all --box my-lager-box # Average over 1 second for a lower-noise reading lager watt POWER_METER current --duration 1.0 --box my-lager-box # Machine-readable output for scripts lager watt POWER_METER all --json --box my-lager-box # Read using default net lager watt # List available watt meter nets lager watt --box my-lager-box ``` ## Scripting Examples ### Power Threshold Check (JSON) ```bash theme={null} #!/bin/bash # Verify power consumption is within limits using JSON output POWER=$(lager watt POWER all --json --box my-lager-box | python3 -c 'import sys,json; print(json.load(sys.stdin)["power"])') if (( $(echo "$POWER > 10" | bc -l) )); then echo "FAIL: Power consumption too high: ${POWER}W" exit 1 fi echo "PASS: Power within limits (${POWER}W)" ``` ### Current Profiling (JSON) ```bash theme={null} #!/bin/bash # Sample current over time BOX="my-lager-box" NET="POWER" echo "timestamp,current_a" for i in $(seq 1 10); do CURRENT=$(lager watt $NET current --json --box $BOX | python3 -c 'import sys,json; print(json.load(sys.stdin)["current"])') echo "$(date +%s),$CURRENT" sleep 1 done ``` ## Troubleshooting | Error | Cause | Fix | | ------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------- | | `does not support reading current/voltage` | Net is backed by a Yocto-Watt (power only) | Use `lager watt NET_NAME` for power, or move the net to a Joulescope/PPK2 | | Timeout | Device disconnected or USB issue | Check USB connection; replug device | | Connection refused | Box service not running | Check box: `lager hello --box ` | | Device not found | Watt meter not detected | Verify device is connected: `lager instruments --box ` | ## Notes * Readings are SI-scaled (W/mW/µW/nW, A/mA/µA/nA, V/mV) with 3 significant decimal places; values too small for the nano prefix fall back to scientific notation instead of rounding to `0.000`. `--json` emits base SI units (W, A, V). * `--duration` sets the averaging window; the Joulescope JS220 and Nordic PPK2 honor it, while the Yocto-Watt returns an instantaneous value and ignores it. On the JS220, windows longer than \~10 s are measured gaplessly with the on-device charge accumulator (constant memory, any length). * The Nordic PPK2 operates in source mode (supplies a configurable voltage 0.8–5V and measures current); its `voltage` reading is the configured source voltage. * The `current`, `voltage`, and `all` subcommands must follow the net name: `lager watt NET_NAME current`. * Net names refer to names assigned when setting up your testbed. * Use `lager nets` to see available watt meter nets, and `lager instruments --box ` to verify the device is detected. ``` ``` # Webcam Source: https://docs.lagerdata.com/source/reference/cli/webcam Manage webcam streams on boxes Control webcam streaming for visual monitoring of devices. ## Syntax ```bash theme={null} lager webcam [NETNAME] COMMAND [OPTIONS] ``` ## Commands | Command | Description | | ----------- | -------------------------------- | | `start` | Start webcam stream | | `stop` | Stop webcam stream | | `url` | Print URLs of all active streams | | `start-all` | Start all webcam streams | | `stop-all` | Stop all webcam streams | ## Options | Option | Description | | ----------- | --------------------------- | | `--box BOX` | Lagerbox name or IP address | *** ## Command Reference ### `start` Start a webcam stream. ```bash theme={null} lager webcam CAM1 start --box my-lager-box ``` ### `stop` Stop a webcam stream. ```bash theme={null} lager webcam CAM1 stop --box my-lager-box ``` ### `url` Print URLs of all active webcam streams. ```bash theme={null} lager webcam url --box my-lager-box ``` Output: ``` Active webcam streams: CAM1: http://:8081/stream CAM2: http://:8082/stream ``` ### `start-all` Start all configured webcam streams. ```bash theme={null} lager webcam start-all --box my-lager-box ``` ### `stop-all` Stop all webcam streams. ```bash theme={null} lager webcam stop-all --box my-lager-box ``` *** ## Usage ### Basic Workflow ```bash theme={null} # Start a webcam lager webcam CAM1 start --box my-lager-box # Get the stream URL lager webcam url --box my-lager-box # Open in browser # http://:8081/stream # Stop when done lager webcam CAM1 stop --box my-lager-box ``` ### Multiple Cameras ```bash theme={null} # Start all cameras lager webcam start-all --box my-lager-box # View all URLs lager webcam url --box my-lager-box # Stop all cameras lager webcam stop-all --box my-lager-box ``` *** ## Stream Format Webcam streams are provided as: * **HTTP MJPEG streams** for browser viewing * **Per-webcam ports** (8081, 8082, etc.) * **Device path reporting** for debugging *** ## Use Cases ### Visual Inspection Monitor physical state of device during testing: ```bash theme={null} lager webcam BENCH_CAM start # Run tests while monitoring lager webcam BENCH_CAM stop ``` ### Remote Debugging View LED states, display outputs, or physical connections: ```bash theme={null} lager webcam url --box my-lager-box # Open stream in browser ``` ### Documentation Capture visual evidence of test results: ```bash theme={null} lager webcam BOARD_VIEW start # Capture screenshots from stream lager webcam BOARD_VIEW stop ``` *** ## Examples ```bash theme={null} # Set default webcam lager defaults add --webcam-net MAIN_CAM # Quick start/stop lager webcam start lager webcam stop # Check status lager webcam url ``` *** ## Notes * Webcams must be connected via USB to the box * Each webcam uses a unique port (8081+) * Streams are accessible via box IP address * Default net can be set with `lager defaults add --webcam-net` * USB webcams should be compatible with V4L2 # WiFi Source: https://docs.lagerdata.com/source/reference/cli/wifi Manage WiFi network settings on a Lager Box View, connect to, and manage WiFi networks on a Lager Box. ## Syntax ```bash theme={null} lager wifi COMMAND [OPTIONS] ``` ## Commands | Command | Description | | ------------------- | ------------------------------------------ | | `status` | Get the current WiFi status of the box | | `access-points` | List WiFi access points visible to the box | | `connect` | Connect the box to a WiFi network | | `delete-connection` | Delete a saved network from the box | *** ## `lager wifi status` Get the current WiFi connection status of the box. ```bash theme={null} lager wifi status [--box BOX] ``` ### Options | Option | Description | | ------------ | --------------------------- | | `--box TEXT` | Lagerbox name or IP address | ### Examples ```bash theme={null} lager wifi status --box my-lager-box ``` *** ## `lager wifi access-points` Scan for and list WiFi access points visible to the box. ```bash theme={null} lager wifi access-points [--box BOX] [--interface IFACE] ``` ### Options | Option | Default | Description | | ------------------ | ------- | -------------------------------------- | | `--box TEXT` | | Lagerbox name or IP address | | `--interface TEXT` | `wlan0` | Wireless interface to use for scanning | ### Examples ```bash theme={null} # Scan using default interface lager wifi access-points --box my-lager-box # Scan using a specific interface lager wifi access-points --box my-lager-box --interface wlan1 ``` *** ## `lager wifi connect` Connect the box to a WiFi network. ```bash theme={null} lager wifi connect [--box BOX] --ssid SSID [--password PASS] [--interface IFACE] ``` ### Options | Option | Default | Description | | ------------------ | ------------ | ------------------------------------------------- | | `--box TEXT` | | Lagerbox name or IP address | | `--ssid TEXT` | *(required)* | SSID of the network to connect to | | `--password TEXT` | *(empty)* | Password for the network (omit for open networks) | | `--interface TEXT` | `wlan0` | Wireless interface to use | ### Validation * SSID must be 1-32 printable characters (IEEE 802.11 limit) * WPA/WPA2 passwords must be 8-63 characters ### Examples ```bash theme={null} # Connect to a WPA2 network lager wifi connect --box my-lager-box --ssid MyNetwork --password mypassword # Connect to an open network lager wifi connect --box my-lager-box --ssid OpenNetwork # Connect using a specific interface lager wifi connect --box my-lager-box --ssid MyNetwork --password mypassword --interface wlan1 ``` *** ## `lager wifi delete-connection` Delete a saved WiFi network from the box. This removes the network configuration so the box will no longer auto-connect to it. ```bash theme={null} lager wifi delete-connection SSID [--box BOX] [--yes] ``` ### Arguments | Argument | Description | | -------- | ----------------------------- | | `SSID` | Name of the network to delete | ### Options | Option | Description | | ------------ | ---------------------------- | | `--box TEXT` | Lagerbox name or IP address | | `--yes` | Skip the confirmation prompt | ### Examples ```bash theme={null} # Delete with confirmation prompt lager wifi delete-connection MyNetwork --box my-lager-box # Delete without prompting lager wifi delete-connection MyNetwork --box my-lager-box --yes ``` Deleting a WiFi connection may disconnect the box from the network. An ethernet connection will be required to bring the box back online if it was connected via WiFi only. *** ## Notes * WiFi commands execute on the Lager Box itself, not on your local machine * The box must be reachable (via ethernet or an existing WiFi connection) to run these commands * Common wireless interface names: `wlan0`, `wlan1`, `wlp2s0`, `wlp3s0` * Use `lager hello --box ` to verify connectivity before managing WiFi settings # Authoring DUT Context Source: https://docs.lagerdata.com/source/reference/mcp/dut-context Give AI agents system-level understanding of your device under test and its schematics An AI agent can read a netlist, but a netlist alone doesn't tell it *what the box is for* or *what each wire means*. Knowing `uart1` is a UART is not the same as knowing it's the DUT's debug CLI. **DUT context** is the narrative you author once so agents reason about your bench at the level of *systems*, not loose wires. DUT context lives in `/etc/lager/bench.json` and is surfaced to agents through the MCP resources `lager://dut/overview.md` and `lager://dut/context`, and the `discover_dut()` and `cite_schematic()` tools. ## The two things to author ### 1. Per-net purpose Each net carries a single-sentence **purpose** plus optional **notes**. Set them in the Net Manager TUI: ```bash theme={null} lager nets tui --box my-lager-box ``` Select a net, open its details, and fill in: * **Purpose** — *"DUT debug CLI over UART; primary command/response channel."* * **Notes** (optional) — gotchas, jumper positions, scope probe points. * **Tags** (optional) — short keywords the planning tools match on, e.g. `flash`, `boot-critical`. `purpose` and `notes` are prose for the agent to read; `tags` are keywords the planning tools score against (a tag matching a test goal is the strongest relevance signal). You can also set these without the TUI: ```bash theme={null} lager nets describe uart1 \ --purpose "DUT debug CLI over UART" \ --notes "PA9/PA10; 115200 8N1" \ --tag cli --tag boot-critical \ --box my-lager-box ``` ### 2. DUT-wide context The DUT context describes the board as a whole: its purpose, MCU, key peripherals, subsystems, and references to documents. Author it with the [`lager dut`](/source/reference/cli/dut) command group. ```bash theme={null} # View current context lager dut show --box my-lager-box # Edit the whole DUT block in $EDITOR lager dut edit --box my-lager-box ``` A fully authored DUT context looks like this in `bench.json`: ```json theme={null} { "dut_context": { "name": "main", "purpose": "Power-regression rig for FeatureA boards", "mcu": "STM32H7", "key_peripherals": ["QSPI flash", "PMIC"], "summary": "STM32H7-based DUT used to validate the power tree under fault injection.", "schematic_refs": [ {"title": "Main schematic", "kind": "schematic", "repo_path": "docs/sch.pdf"} ], "datasheet_refs": [ {"title": "STM32H7 RM", "kind": "datasheet", "url": "https://...", "pages": "150-200"} ], "subsystems": [ { "name": "Flash subsystem", "summary": "QSPI flash", "nets": ["flash_cs", "flash_clk"], "doc_refs": [ {"title": "Flash sheet", "kind": "schematic", "repo_path": "docs/sch.pdf", "pages": "3"} ] }, {"name": "Power tree", "summary": "PMIC + LDOs", "nets": ["psu1"]} ] } } ``` **Subsystems** group related nets (Power tree, Flash subsystem, Debug, ...) so the agent reasons about functional blocks. The agent can ask for one net and learn which subsystem it belongs to and which schematic sheet covers it. ## Attaching schematics and datasheets The Lager Box is **not** a document store. It records *pointers* to your documents; the agent fetches and analyses them with its own (vision-capable) tools. This keeps the box lean and lets the agent use the best tool for reading a PDF or board image. Attach a pointer without hand-editing JSON: ```bash theme={null} lager dut add-doc --kind schematic \ --title "Main board" --repo-path docs/sch.pdf --pages 3-5 --box my-lager-box lager dut add-doc --kind datasheet \ --title "STM32H7 reference manual" --url "https://..." --pages 150-200 --box my-lager-box ``` A document reference (`DocRef`) has: | Field | Meaning | | ----------- | ------------------------------------------------------------------------------- | | `title` | Human label. | | `kind` | `schematic`, `layout`, `datasheet`, `firmware`, `manual`, `errata`, or `other`. | | `url` | External URL (any URL the agent can fetch). | | `repo_path` | Path relative to your test project (synced to the box on `lager python`). | | `pages` | Optional page/sheet hint, e.g. `"3-5"` or `"POWER sheet"`. | | `notes` | Optional free-form note. | You must supply at least one of `--url` or `--repo-path`. ### URL vs. repo-path: which to use | Situation | Recommended | Why | | ----------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------- | | Automated / CI / headless agent | `--repo-path` | The file is synced with your project; no network, no auth, fully deterministic. | | Publicly fetchable doc | `--url` | Any agent with a web-fetch tool can pull it. | | Private doc (Google Doc, Confluence, SSO) | `--repo-path` **or** a Drive connector | The box never authenticates; a login-walled URL returns an auth page, not content. | For Google Docs, prefer an **export** URL over the editor URL — the `/edit` URL returns the JS app, not the content: ``` https://docs.google.com/document/d//export?format=pdf ``` Either share it "anyone with the link," give your agent a Google Drive connector/MCP server that holds the credentials, or export it into your repo and use `--repo-path`. ## How the agent uses it Once authored, the context drives the whole agent loop: 1. The agent reads `lager://dut/overview.md` and learns: *"power-regression rig, STM32H7, flash + power-tree subsystems, schematic at `docs/sch.pdf`."* 2. `plan_firmware_test("flash driver", "exercise QSPI")` returns a plan already scoped to the flash subsystem, with a pointer to schematic page 3. 3. `cite_schematic("flash_cs")` returns just the refs for that net: ```json theme={null} { "net": "flash_cs", "net_purpose": "SPI flash chip-select", "subsystem": "Flash subsystem", "subsystem_doc_refs": [ {"title": "Flash sheet", "repo_path": "docs/sch.pdf", "pages": "3"} ] } ``` The agent opens `docs/sch.pdf` at page 3 with its own file tools — no scanning the whole PDF. ## Applying changes The MCP server watches `/etc/lager/bench.json`, `/etc/lager/saved_nets.json`, and `/etc/lager/box_id` and **auto-reloads when any of them changes on disk**. So after `lager dut edit`, `lager dut add-doc`, or `lager nets describe`, agents see the new context on their next `discover_dut()`, `discover_bench()`, or `lager://dut/overview.md` request — no manual step required. If you want to force a reload immediately (e.g. to confirm a change took), a connected agent can still call the `box_manage` tool with `action="reload"`, or you can restart the box service. # MCP Server Overview Source: https://docs.lagerdata.com/source/reference/mcp/overview How AI agents discover hardware and plan tests on a Lager Box via the Model Context Protocol Every Lager Box runs an **MCP (Model Context Protocol) server** that lets an AI agent understand the bench, understand the device under test (DUT), and plan hardware-in-the-loop tests. It runs on-box and is reachable over the box's local IP. The MCP server is **read-only**: it describes the bench and DUT but never drives hardware or runs code. The agent executes tests over a separate channel — the `lager` CLI. ``` MCP-compatible AI agent | MCP (streamable-http via box IP) ← discovery + planning (read-only) v Lager MCP Server (on-box, port 8100) | reads /etc/lager bench config (nets, DUT context, instruments) v Bench / DUT metadata AI agent ── lager python path/to/test.py --box ──▶ Hardware (execution happens over the CLI, not MCP) ``` ## Connecting an agent Point any MCP-compatible client at the box: ```json theme={null} { "mcpServers": { "lager": { "url": "http://:8100/mcp" } } } ``` The MCP server is for **discovery and planning** only — it does not run code or drive hardware. To **execute** a test, the agent writes a Python file locally and runs it with `lager python path/to/test.py --box `, which syncs the project to the box and runs it with full project context. Pass the box's **IP address** to `--box` — the same IP you connected to the MCP server on. Local box names are just client-side aliases, so the IP is the only identifier both sides can rely on. To make this concrete, `discover_bench()` echoes the address you actually connected on as `box_address` and hands back a ready-to-run `lager python … --box ` command. ## What the agent sees The server exposes two kinds of things: **resources** (read-only context the agent reads) and **tools** (callable functions). ### Resources | Resource | What it gives the agent | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lager://dut/overview.md` | **Read this first.** A narrative briefing: what the box tests, the DUT MCU/peripherals, subsystems, and which documents to fetch. | | `lager://dut/context` | The full DUT context as structured JSON. | | `lager://bench/identity` | Box ID, hostname, version, and a per-DUT summary (purpose, MCU). | | `lager://bench/netlist` | Every net with type, roles, instrument, and metadata. | | `lager://bench/interfaces` | Protocol interfaces (SPI, I2C, UART) and their nets. | | `lager://guide/overview` | What Lager is and what a "net" is. | | `lager://guide/workflow` | The recommended orient → discover → plan → write → run loop. | | `lager://guide/rtt-defmt` | The core firmware-log workflow: streaming RTT and decoding `defmt`. Covers the `dbg.session()` scope, the reconnect-aware RTT reader and self-healing `reset()`/`read_memory()` (so you don't write flash/reset workarounds), and the DA1469x post-flash reconnect exception. | | `lager://reference/{net_type}` | The full API reference for one net type as JSON (e.g. `lager://reference/Debug`) — methods, gotchas, and a runnable example snippet. | | `lager://guide/api-quick-reference` | Compact `lager.Net` API cheat sheet by net type. | | `lager://guide/docs` | Links to the full hosted docs (`docs.lagerdata.com`) and the `llms.txt` page index for anything not covered on-box. | ### Tools | Tool | Purpose | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `discover_dut()` | One call for orientation: DUT purpose, MCU, peripherals, subsystems, and document references. | | `discover_bench(net_name?)` | Enumerate hardware — nets, plus instruments with their channels, capabilities, and authored specs/ranges. With a net name, return that net's full metadata, capabilities, parent subsystem, and relevant document references (and, if the net is unknown, the list of available net names). | | `cite_schematic(net_name)` | Return just the schematic/datasheet references and page hints relevant to one net. | | `plan_firmware_test(firmware_description, test_goals)` | Generate a phased test plan, scoped to relevant nets, with DUT context and document references attached. | | `assess_suitability(test_type)` | Check whether the bench can run a given test type. | | `get_test_example(query)` | Find runnable example scripts by net type, pattern, or keyword. | | `box_manage(action)` | `health` check or `reload` the bench config from disk. | The tool surface is intentionally read-only. There are no tools that drive hardware (set a voltage, toggle a GPIO, flash firmware) or mutate the box. All of that lives in the test script the agent writes and runs with `lager python`, or in dedicated [CLI commands](/source/reference/cli). ### Prompts The server also registers a few **prompts** — slash-command-style entry points that steer a client (e.g. Cursor) through the discover → plan → write → run workflow. They don't do work themselves; each returns an instruction the agent follows using the tools above. | Prompt | What it does | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `write_lager_test(what_to_test)` | Walks the agent through discovering the bench/DUT, planning, writing a test, and running it over the CLI. | | `explore_bench()` | Orients on what the box is, what it tests, and what it can run. | | `assess_test_feasibility(test_description)` | Checks whether the bench has the capabilities for a described test. | ## The recommended agent workflow Read `lager://dut/overview.md` (or call `discover_dut()`) to learn what the box tests, the MCU and peripherals, the subsystems, and which documents to fetch. Call `discover_bench()` to enumerate nets, instruments, and capabilities. Call `discover_bench(net_name)` for detail on a specific net, including its subsystem and the schematic sheet it lives on. Call `plan_firmware_test(...)` to get a phased plan with API references and document pointers per step. Author a Python test file using `from lager import Net, NetType`. Identify the box by the IP address you connected to the MCP server on — local box names are arbitrary client-side aliases. `--box` accepts a raw IP, so no registration is needed: run `lager python path/to/test.py --box `. The runnable can also be a **folder** (entrypoint `main.py`), which syncs and imports everything in it — handy for shipping reusable helper modules: `lager python path/to/test_dir --box `. (Optionally, `lager boxes add --name --ip ` registers a friendly alias.) Review the CLI output, adjust the script, and re-run it with `lager python`. ## Where context comes from The quality of everything above depends on the metadata you author once, at bench setup: * **Per-net `purpose`** — set in the Net Manager TUI (`lager nets tui`). One sentence describing what each wire does on the DUT. * **DUT context** — set with [`lager dut`](/source/reference/cli/dut): the box's purpose, MCU, subsystems, and references to schematics and datasheets. See [Authoring DUT Context](/source/reference/mcp/dut-context) for the full guide. # ADC Source: https://docs.lagerdata.com/source/reference/python/adc Read analog-to-digital converter values Read analog voltage values from ADC pins. Supports LabJack T7 and MCC USB-202 hardware. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | --------- | ------------------- | | `input()` | Read analog voltage | ## Method Reference ### `Net.get(name, type=NetType.ADC)` Get an ADC net by name. ```python theme={null} from lager import Net, NetType adc = Net.get('SENSOR', type=NetType.ADC) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------- | | `name` | `str` | Name of the ADC net | | `type` | `NetType` | Must be `NetType.ADC` | **Returns:** ADC Net instance ### `input()` Read the analog voltage. ```python theme={null} voltage = adc.input() print(f"Voltage: {voltage}V") ``` **Returns:** `float` - Voltage in volts ## Examples ### Single Reading ```python theme={null} from lager import Net, NetType sensor = Net.get('TEMP_SENSOR', type=NetType.ADC) voltage = sensor.input() print(f"Sensor voltage: {voltage:.3f}V") ``` ### Continuous Monitoring ```python theme={null} from lager import Net, NetType import time battery = Net.get('BATTERY_SENSE', type=NetType.ADC) for i in range(10): voltage = battery.input() print(f"Battery: {voltage:.2f}V") time.sleep(1) ``` ### Data Logging ```python theme={null} from lager import Net, NetType import time import csv sensor = Net.get('CURRENT_SENSE', type=NetType.ADC) with open('readings.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['time', 'voltage']) start = time.time() for i in range(100): elapsed = time.time() - start voltage = sensor.input() writer.writerow([elapsed, voltage]) time.sleep(0.1) ``` ### Multiple Sensors ```python theme={null} from lager import Net, NetType sensors = { 'TEMP': Net.get('TEMP_SENSOR', type=NetType.ADC), 'CURRENT': Net.get('CURRENT_SENSE', type=NetType.ADC), 'BATTERY': Net.get('BATTERY_SENSE', type=NetType.ADC), } for name, sensor in sensors.items(): voltage = sensor.input() print(f"{name}: {voltage:.3f}V") ``` ## Supported Hardware | Hardware | Channels | Range | Resolution | | ----------- | --------------- | ------- | --------------------- | | LabJack T7 | 14 (AIN0-AIN13) | +/-10 V | 16-bit (\~0.3 mV/LSB) | | MCC USB-202 | 8 (CH0-CH7) | +/-10 V | 12-bit (\~4.9 mV/LSB) | Both devices support bipolar measurement (positive and negative voltages). ### Pin Naming **LabJack T7:** | Pin Input | Channel | | ------------------ | ---------- | | `0`-`13` | AIN0-AIN13 | | `"AIN0"`-`"AIN13"` | AIN0-AIN13 | **MCC USB-202:** | Pin Input | Channel | | --------------- | -------------------------- | | `0`-`7` | CH0-CH7 | | `"CH0"`-`"CH7"` | CH0-CH7 (case-insensitive) | ## Notes * ADC nets work directly without `enable()`/`disable()` calls * Returns voltage as a float in volts * Both devices have a +/-10 V input range (bipolar) * LabJack T7 shares a connection handle with DAC, GPIO, and SPI operations * USB-202 opens and closes the connection on each read * Net names must match those configured on the Lager Box # Robot Arm Source: https://docs.lagerdata.com/source/reference/python/arm Control robotic arms for automated positioning and manipulation Control robotic arms for automated pick-and-place operations, positioning, and test fixture manipulation. ## Import ```python theme={null} from lager import Net, NetType # For exception handling from lager import InvalidNetError ``` ## Methods | Method | Description | | --------------------- | ---------------------------------------- | | `position()` | Get current arm position (x, y, z) | | `move_to()` | Move to absolute coordinates | | `move_relative()` | Move relative to current position | | `go_home()` | Return arm to home position | | `enable_motor()` | Enable arm motors | | `disable_motor()` | Disable arm motors | | `save_position()` | Save current position to memory | | `get_full_position()` | Get full position including joint angles | | `sliding_rail_init()` | Initialize the sliding rail | ## Method Reference ### Getting an Arm Net Get a robotic arm net instance by name and type. ```python theme={null} from lager import Net, NetType # Get the arm net arm = Net.get('arm1', type=NetType.Arm) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------------------- | | `name` | `str` | Net name (configured in Lager system) | | `type` | `NetType` | Must be `NetType.Arm` | **Returns:** Arm net instance with all methods listed below ### `position()` Get the current arm position. ```python theme={null} x, y, z = arm.position() print(f"Position: X={x}, Y={y}, Z={z}") ``` **Returns:** `tuple[float, float, float]` - (x, y, z) coordinates in mm ### `move_to(x, y, z, timeout=15.0)` Move the arm to absolute coordinates with blocking wait. ```python theme={null} # Move to specific position arm.move_to(100, 200, 50) # Move with longer timeout arm.move_to(100, 200, 50, timeout=10.0) ``` **Parameters:** | Parameter | Type | Description | | --------- | ------- | -------------------------------------------- | | `x` | `float` | Target X coordinate (mm) | | `y` | `float` | Target Y coordinate (mm) | | `z` | `float` | Target Z coordinate (mm) | | `timeout` | `float` | Maximum wait time in seconds (default: 15.0) | **Raises:** `MovementTimeoutError` if position not reached within timeout ### `move_relative(dx=0, dy=0, dz=0, timeout=15.0)` Move relative to current position. ```python theme={null} # Move 10mm in X direction new_x, new_y, new_z = arm.move_relative(dx=10) # Move diagonally new_x, new_y, new_z = arm.move_relative(dx=10, dy=10, dz=-5) ``` **Parameters:** | Parameter | Type | Description | | --------- | ------- | --------------------------------- | | `dx` | `float` | Relative X movement (mm) | | `dy` | `float` | Relative Y movement (mm) | | `dz` | `float` | Relative Z movement (mm) | | `timeout` | `float` | Maximum wait time (default: 15.0) | **Returns:** `tuple[float, float, float]` - New (x, y, z) position after move ### `go_home()` Return the arm to its home position (X=0, Y=300, Z=0). ```python theme={null} arm.go_home() ``` ### `enable_motor()` Enable the arm motors. ```python theme={null} arm.enable_motor() ``` ### `disable_motor()` Disable the arm motors (arm will be loose). ```python theme={null} arm.disable_motor() ``` ### `save_position()` Save the current position to the arm's internal memory. ```python theme={null} arm.save_position() ``` ### `get_full_position()` Get the full position including joint angles. ```python theme={null} x, y, z, e, a, b, c = arm.get_full_position() print(f"Cartesian: ({x}, {y}, {z})") print(f"Joint angles: A={a}, B={b}, C={c}") ``` **Returns:** `tuple[float, ...]` - (x, y, z, e, a, b, c) where a, b, c are joint angles ## End Effector Methods ### Soft Gripper ```python theme={null} arm.soft_gripper_pick() # Activate gripper to pick arm.soft_gripper_place() # Release gripper to place arm.soft_gripper_neutral() # Return to neutral state arm.soft_gripper_stop() # Stop gripper action ``` ### Air Picker ```python theme={null} arm.air_picker_pick() # Activate vacuum to pick arm.air_picker_place() # Release vacuum to place arm.air_picker_neutral() # Return to neutral state arm.air_picker_stop() # Stop vacuum action ``` ### Laser Module ```python theme={null} arm.laser_on(value=0) # Turn laser on with power level arm.laser_off() # Turn laser off ``` ## Conveyor Belt Methods ```python theme={null} arm.conveyor_belt_forward(speed=0) # Move belt forward (speed 0-100) arm.conveyor_belt_backward(speed=0) # Move belt backward (speed 0-100) arm.conveyor_belt_stop() # Stop belt ``` ## Sliding Rail Methods ```python theme={null} arm.sliding_rail_init() # Initialize the sliding rail ``` ## Utility Methods ### `delay_ms(value)` / `delay_s(value)` Add delay to the arm's command queue. ```python theme={null} arm.delay_ms(500) # 500ms delay arm.delay_s(2) # 2 second delay ``` ### `set_acceleration(acceleration, travel_acceleration, retract_acceleration=60)` Configure acceleration settings. ```python theme={null} arm.set_acceleration(100, 100, 60) ``` ### `set_module_type(module_type)` Set the attached module type. ```python theme={null} # 0=PEN, 1=LASER, 2=PNEUMATIC, 3=3D arm.set_module_type(0) # Set to PEN module ``` ### `get_module_type()` Get the currently detected module type. ```python theme={null} module = arm.get_module_type() print(f"Module: {module}") # 'PEN', 'LASER', 'PUMP', or '3D' ``` ## Examples ### Basic Movement ```python theme={null} from lager import Net, NetType # Get the arm net arm = Net.get('arm1', type=NetType.Arm) # Go to home position arm.go_home() # Get current position x, y, z = arm.position() print(f"Home position: ({x}, {y}, {z})") # Move to test position arm.move_to(100, 250, 0) # Move up arm.move_relative(dz=50) # Return home arm.go_home() ``` ### Pick and Place ```python theme={null} from lager import Net, NetType # Get the arm net arm = Net.get('arm1', type=NetType.Arm) arm.go_home() # Move to pick position arm.move_to(100, 200, 50) # Above target arm.move_relative(dz=-30) # Lower to pick height # Pick up object arm.soft_gripper_pick() arm.delay_ms(500) # Lift arm.move_relative(dz=30) # Move to place position arm.move_to(150, 200, 50) arm.move_relative(dz=-30) # Place object arm.soft_gripper_place() arm.delay_ms(500) # Return home arm.move_relative(dz=30) arm.go_home() ``` ### Automated Test Positioning ```python theme={null} from lager import Net, NetType # Test positions for probe points PROBE_POSITIONS = [ (100, 200, -10), # Test point 1 (120, 200, -10), # Test point 2 (140, 200, -10), # Test point 3 ] # Get arm and probe nets arm = Net.get('arm1', type=NetType.Arm) adc = Net.get('PROBE', type=NetType.ADC) arm.go_home() results = [] for i, (x, y, z) in enumerate(PROBE_POSITIONS): # Move above position arm.move_to(x, y, z + 20) # Lower probe arm.move_to(x, y, z) arm.delay_ms(200) # Settle time # Take measurement voltage = adc.input() results.append((i, voltage)) print(f"Point {i}: {voltage}V") # Lift arm.move_relative(dz=20) arm.go_home() ``` ### Error Handling ```python theme={null} from lager import Net, NetType from lager import InvalidNetError try: arm = Net.get('arm1', type=NetType.Arm) arm.move_to(100, 200, 50, timeout=3.0) except InvalidNetError as e: print(f"Arm net not found: {e}") except RuntimeError as e: print(f"Movement failed: {e}") except Exception as e: print(f"Error: {e}") ``` ## Supported Hardware | Manufacturer | Model | Features | | ------------ | ------ | ------------------------------------------------ | | Rotrics | Dexarm | 4-axis, multiple end effectors, conveyor support | ## Exceptions | Exception | Module | Description | | ---------------------- | ------------------- | ---------------------------------------- | | `MovementTimeoutError` | `lager.arm.arm_net` | Arm didn't reach target position in time | | `RuntimeError` | builtin | Arm device not found or cannot be opened | ## Notes * Always use the context manager (`with` statement) to ensure proper cleanup * The arm must be homed before accurate movements * Movement timeout errors may indicate obstructions or out-of-bounds coordinates * Joint angles (a, b, c) are in the arm's internal coordinate system * Default tolerance for position verification is 0.5mm * The arm auto-detects via USB VID/PID (0x0483:0x5740) # Battery Simulation Source: https://docs.lagerdata.com/source/reference/python/battery Control battery simulation nets Simulate battery behavior to test your DUT's response to various charge levels, voltages, and protection events. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods The Net-based API provides methods that can either get or set values. When called without a value parameter, methods read and print the current value. When called with a value, they set it. | Method | Description | | ---------------------- | --------------------------------------------------- | | `mode(mode_type)` | Set or read simulation mode ('static' or 'dynamic') | | `set_mode_battery()` | Initialize battery simulation mode | | `soc(value)` | Set or read state of charge (0-100%) | | `voc(value)` | Set or read open-circuit voltage | | `voltage_full(value)` | Set or read full charge voltage | | `voltage_empty(value)` | Set or read empty battery voltage | | `capacity(value)` | Set or read battery capacity (Ah) | | `current_limit(value)` | Set or read current limit (A) | | `ovp(value)` | Set or read over-voltage protection threshold | | `ocp(value)` | Set or read over-current protection threshold | | `model(partnumber)` | Set or read battery model | | `enable()` | Enable battery simulation output | | `disable()` | Disable battery simulation output | | `clear_ovp()` | Clear over-voltage protection fault | | `clear_ocp()` | Clear over-current protection fault | | `print_state()` | Print comprehensive battery state | | `terminal_voltage()` | Read terminal voltage (returns float) | | `current()` | Read current (returns float) | | `esr()` | Read ESR (returns float) | ## Method Reference ### `Net.get(name, type=NetType.Battery)` Get a battery simulation net by name. ```python theme={null} from lager import Net, NetType batt = Net.get('BATT', type=NetType.Battery) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------- | | `name` | `str` | Name of the battery net | | `type` | `NetType` | Must be `NetType.Battery` | **Returns:** Battery simulation Net instance ### `set_mode_battery()` Initialize the instrument for battery simulation mode. ```python theme={null} batt.set_mode_battery() ``` ### `mode(mode_type=None)` Set or read the battery simulation mode. ```python theme={null} # Set mode batt.mode('static') # Fixed parameters batt.mode('dynamic') # Parameters evolve based on battery model # Read mode (prints current mode) batt.mode() ``` **Parameters:** | Parameter | Type | Description | | ----------- | --------------- | --------------------------------------------------- | | `mode_type` | `str` or `None` | 'static' or 'dynamic'. If None, reads current mode. | ### `soc(value=None)` Set or read the state of charge. ```python theme={null} # Set SOC batt.soc(80) # Set to 80% # Read SOC (prints current value) batt.soc() ``` **Parameters:** | Parameter | Type | Description | | --------- | ----------------- | ---------------------------------------------------------------------------------- | | `value` | `float` or `None` | State of charge (0-100). Rounded to nearest integer. If None, reads current value. | ### `voc(value=None)` Set or read the open-circuit voltage. ```python theme={null} # Set VOC batt.voc(3.7) # Set to 3.7V # Read VOC (prints current value) batt.voc() ``` **Parameters:** | Parameter | Type | Description | | --------- | ----------------- | ----------------------------------------------- | | `value` | `float` or `None` | Voltage in volts. If None, reads current value. | ### `voltage_full(value=None)` / `voltage_empty(value=None)` Set or read the full/empty battery voltages. ```python theme={null} # Set voltages batt.voltage_full(4.2) # Full charge at 4.2V batt.voltage_empty(3.0) # Empty at 3.0V # Read voltages batt.voltage_full() batt.voltage_empty() ``` ### `capacity(value=None)` Set or read the battery capacity. Must be greater than 0. The instrument may clamp the value to its supported range and will print a warning if the applied value differs. ```python theme={null} # Set capacity batt.capacity(2.5) # 2.5 Ah # Read capacity batt.capacity() ``` ### `current_limit(value=None)` Set or read the maximum charge/discharge current. Range: 0.001 A to 6.0 A (Keithley 2281S). ```python theme={null} # Set current limit batt.current_limit(1.5) # 1.5A max # Read current limit batt.current_limit() ``` ### `ovp(value=None)` / `ocp(value=None)` Set or read protection thresholds. ```python theme={null} # Set protection thresholds batt.ovp(4.4) # Over-voltage protection at 4.4V batt.ocp(2.0) # Over-current protection at 2.0A # Read thresholds batt.ovp() batt.ocp() ``` ### `model(partnumber=None)` Set or read the battery model. ```python theme={null} # Set model to discharge (always available) batt.model('discharge') # Or use pre-configured battery models (if available) batt.model('18650') # Requires model saved in slot 1 batt.model('liion') # Requires model saved in slot 1 batt.model('nimh') # Requires model saved in slot 2 # Read current model batt.model() ``` **Keithley 2281S Battery Models:** The Keithley 2281S stores battery models in memory slots (0-9): | Model Alias | Slot | Availability | | -------------------- | ---- | ---------------------------------------------------- | | `'discharge'` | 0 | Always available (basic constant-voltage simulation) | | `'18650'`, `'liion'` | 1 | Requires pre-saved model | | `'nimh'` | 2 | Requires pre-saved model | | `'nicd'` | 3 | Requires pre-saved model | | `'lead-acid'` | 4 | Requires pre-saved model | **Note:** If a slot is empty, you'll get an error suggesting to use 'discharge' or save a model via the instrument front panel. Use `'discharge'` for basic battery simulation that works on all instruments. ### `enable()` / `disable()` Enable or disable battery simulation output. ```python theme={null} batt.enable() # Enable output batt.disable() # Disable output ``` ### `clear_ovp()` / `clear_ocp()` Clear protection faults. ```python theme={null} batt.clear_ovp() # Clear over-voltage fault batt.clear_ocp() # Clear over-current fault ``` ### `print_state()` Print comprehensive battery simulator state. ```python theme={null} batt.print_state() # Prints: terminal voltage, current, ESR, SOC, VOC, capacity, protection status ``` ### `terminal_voltage()` / `current()` / `esr()` Read measurements (return values, don't print). ```python theme={null} v = batt.terminal_voltage() # Returns terminal voltage in volts i = batt.current() # Returns current in amps r = batt.esr() # Returns ESR in ohms print(f"Voltage: {v}V, Current: {i}A, ESR: {r} ohms") ``` **Returns:** `float` - Measurement value ## Examples ### Basic Battery Simulation ```python theme={null} from lager import Net, NetType # Get battery net batt = Net.get('BATT', type=NetType.Battery) # Initialize and configure batt.set_mode_battery() batt.mode('static') batt.model('discharge') # Use discharge mode (always available) batt.voc(3.7) batt.capacity(2.5) # Enable output batt.enable() # Read state batt.print_state() print(f"Terminal voltage: {batt.terminal_voltage()}V") # Disable when done batt.disable() ``` ### Simulate Battery Discharge ```python theme={null} from lager import Net, NetType import time batt = Net.get('BATT', type=NetType.Battery) batt.set_mode_battery() # Configure battery parameters batt.mode('static') batt.model('discharge') batt.voc(4.2) batt.voltage_full(4.2) batt.voltage_empty(3.0) batt.capacity(3.0) batt.soc(100) # Start fully charged batt.enable() # Simulate discharge by stepping SOC for soc_level in [100, 75, 50, 25, 10]: batt.soc(soc_level) time.sleep(0.5) v = batt.terminal_voltage() print(f"SOC: {soc_level}%, Terminal: {v:.2f}V") batt.disable() ``` ### With Protection Monitoring ```python theme={null} from lager import Net, NetType batt = Net.get('BATT', type=NetType.Battery) batt.set_mode_battery() # Configure with protection batt.model('discharge') batt.voc(3.7) batt.capacity(2.0) batt.ovp(4.3) # Over-voltage at 4.3V batt.ocp(2.0) # Over-current at 2.0A batt.enable() # Monitor print(f"Voltage: {batt.terminal_voltage():.2f}V") print(f"Current: {batt.current():.3f}A") # Clear any faults if needed batt.clear_ovp() batt.clear_ocp() batt.disable() ``` ## Supported Hardware | Manufacturer | Model | Features | | ------------ | ----- | ------------------------------------ | | Keithley | 2281S | Battery simulation, dynamic modeling | ## Notes * Call `set_mode_battery()` before using other battery methods * Methods like `soc()`, `voc()`, etc. can get or set values depending on whether a value is passed * `soc()` values are rounded to the nearest integer before being sent to the instrument * Use `mode('static')` for fixed parameters, `mode('dynamic')` for evolving behavior * Always call `disable()` when finished * `terminal_voltage()`, `current()`, and `esr()` return values (for use in code) * `print_state()` prints values (for debugging) * Protection thresholds help prevent damage to your DUT * Keithley 2281S limits: 0-20 V output, 0.001-6.0 A current, capacity must be > 0 * OVP range: 0-60 V; OCP range: 0.001-6.0 A # Custom Binaries Source: https://docs.lagerdata.com/source/reference/python/binaries Execute custom binaries on the Lager Box Execute custom binaries that have been uploaded to the Lager Box via the CLI. This is useful for running customer-specific tools, device interaction utilities, or third-party command-line applications. ## Import ```python theme={null} from lager.binaries import run_custom_binary, get_binary_path, list_binaries, BinaryNotFoundError ``` ## Functions | Function | Description | | --------------------- | ---------------------------------------- | | `run_custom_binary()` | Execute a custom binary with arguments | | `get_binary_path()` | Get the full filesystem path to a binary | | `list_binaries()` | List all available custom binaries | ## Exception Classes | Exception | Description | | --------------------- | ------------------------------------------ | | `BinaryNotFoundError` | Binary does not exist or is not executable | ## Function Reference ### `run_custom_binary(binary_name, *args, **kwargs)` Execute a custom binary that was uploaded via `lager binaries add`. ```python theme={null} from lager.binaries import run_custom_binary # Simple usage result = run_custom_binary('rt_newtmgr', 'image', 'list') print(result.stdout) # With timeout result = run_custom_binary('slow_tool', '--verbose', timeout=60) # Check return code if result.returncode != 0: print(f"Error: {result.stderr}") ``` **Parameters:** | Parameter | Type | Default | Description | | ---------------- | ---------------- | ------- | ----------------------------------------------------- | | `binary_name` | `str` | - | Name of the binary to run (e.g., 'rt\_newtmgr') | | `*args` | `str` | - | Arguments to pass to the binary | | `timeout` | `int` | `30` | Maximum time in seconds to wait (None for no timeout) | | `capture_output` | `bool` | `True` | Capture stdout/stderr | | `text` | `bool` | `True` | Return stdout/stderr as strings instead of bytes | | `check` | `bool` | `False` | Raise `CalledProcessError` if return code is non-zero | | `cwd` | `str` | `None` | Working directory for the process | | `env` | `dict` | `None` | Environment variables (inherits from parent if None) | | `input` | `str` or `bytes` | `None` | Input to send to stdin | **Returns:** `subprocess.CompletedProcess` with attributes: * `returncode` - Exit code of the process * `stdout` - Captured standard output (if `capture_output=True`) * `stderr` - Captured standard error (if `capture_output=True`) **Raises:** * `BinaryNotFoundError` - If the binary doesn't exist or isn't executable * `subprocess.TimeoutExpired` - If the process times out * `subprocess.CalledProcessError` - If `check=True` and return code is non-zero ### `get_binary_path(binary_name)` Get the full filesystem path to a custom binary. ```python theme={null} from lager.binaries import get_binary_path path = get_binary_path('rt_newtmgr') print(f"Binary located at: {path}") # Output: /home/www-data/customer-binaries/rt_newtmgr ``` **Parameters:** | Parameter | Type | Description | | ------------- | ----- | --------------------------------- | | `binary_name` | `str` | Name of the binary (without path) | **Returns:** `str` - Full path to the binary **Raises:** `BinaryNotFoundError` - If the binary doesn't exist or isn't executable ### `list_binaries()` List all available custom binaries on the Lager Box. ```python theme={null} from lager.binaries import list_binaries binaries = list_binaries() print("Available binaries:") for name in binaries: print(f" - {name}") ``` **Returns:** `list[str]` - Sorted list of binary names ## Examples ### Run Device Interaction Tool ```python theme={null} from lager.binaries import run_custom_binary, BinaryNotFoundError try: # Run rt_newtmgr to list images on device result = run_custom_binary('rt_newtmgr', 'image', 'list', '-c', '/dev/ttyUSB0') if result.returncode == 0: print("Images on device:") print(result.stdout) else: print(f"Error: {result.stderr}") except BinaryNotFoundError as e: print(f"Binary not found: {e}") ``` ### Run with Custom Environment ```python theme={null} from lager.binaries import run_custom_binary # Pass environment variables to the binary result = run_custom_binary( 'my_tool', '--config', 'test.json', env={'DEBUG': '1', 'LOG_LEVEL': 'verbose'}, timeout=60 ) print(result.stdout) ``` ### Check Available Binaries ```python theme={null} from lager.binaries import list_binaries, run_custom_binary # List what's available binaries = list_binaries() if not binaries: print("No custom binaries installed.") print("Use 'lager binaries add --box ' to upload binaries.") else: print(f"Found {len(binaries)} custom binaries:") for name in binaries: print(f" - {name}") ``` ### Error Handling ```python theme={null} from lager.binaries import run_custom_binary, BinaryNotFoundError import subprocess try: result = run_custom_binary('my_tool', '--version', check=True, timeout=10) print(f"Version: {result.stdout.strip()}") except BinaryNotFoundError as e: print(f"Binary not available: {e}") except subprocess.TimeoutExpired: print("Command timed out") except subprocess.CalledProcessError as e: print(f"Command failed with exit code {e.returncode}") print(f"stderr: {e.stderr}") ``` ### Integration with Test Script ```python theme={null} from lager.binaries import run_custom_binary from lager import Net, NetType import time def flash_device_firmware(): """Flash firmware using custom programming tool.""" # Power on the device psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.enable() time.sleep(1) # Run custom flashing tool result = run_custom_binary( 'device_flasher', '--port', '/dev/ttyUSB0', '--firmware', '/tmp/firmware.bin', '--verify', timeout=120 ) if result.returncode == 0: print("Firmware flashed successfully!") return True else: print(f"Flash failed: {result.stderr}") return False def run_device_tests(): """Run device-specific test tool.""" result = run_custom_binary( 'device_test_runner', '--all', '--json-output', timeout=300 ) if result.returncode == 0: import json results = json.loads(result.stdout) return results else: raise RuntimeError(f"Tests failed: {result.stderr}") ``` ## Uploading Binaries Custom binaries are uploaded to the Lager Box using the CLI: ```bash theme={null} # Upload a binary to the Lager Box lager binaries add ./my_tool --box my-lager-box # List binaries on the Lager Box lager binaries list --box my-lager-box # Remove a binary lager binaries remove my_tool --box my-lager-box ``` ## Binary Location On the Lager Box, custom binaries are stored at: * **Host path:** `/home/lagerdata/third_party/customer-binaries/` * **Container path:** `/home/www-data/customer-binaries/` The directory is mounted into the container, so binaries are immediately available after upload without rebuilding. ## Notes * Binaries must be Linux x86\_64 compatible (matching Lager Box architecture) * Binary names cannot contain path separators (`/`, `\`) or `..` * Binaries must be executable (set automatically during upload) * Default timeout is 30 seconds; use `timeout=None` for no limit * Use `capture_output=False` for interactive or streaming output * Environment variables are inherited from parent process unless `env` is specified # Bluetooth Low Energy Source: https://docs.lagerdata.com/source/reference/python/ble BLE device scanning, connection, and GATT operations Communicate with Bluetooth Low Energy (BLE) devices for scanning, connecting, reading/writing characteristics, and subscribing to notifications. ## Import ```python theme={null} from lager.ble import Client, Central, noop_handler, notify_handler, waiter ``` ## Classes | Class | Description | | --------- | -------------------------------------------------------- | | `Central` | BLE central role for scanning and initiating connections | | `Client` | BLE client for GATT operations on a connected device | ## Functions | Function | Description | | ------------------ | -------------------------------- | | `noop_handler()` | No-op notification handler | | `notify_handler()` | Event-based notification handler | | `waiter()` | Async wait helper | ## Central Class The `Central` class provides BLE scanning and connection initiation. ### `Central(loop=None)` Create a BLE central instance. ```python theme={null} from lager.ble import Central central = Central() ``` **Parameters:** | Parameter | Type | Description | | --------- | ------------------- | ----------------------------------- | | `loop` | `asyncio.EventLoop` | Event loop (optional, uses default) | ### `scan(scan_time=5.0, name=None, address=None)` Scan for nearby BLE devices. ```python theme={null} central = Central() # Scan for all devices devices = central.scan(scan_time=5.0) for device in devices: print(f"{device.name}: {device.address}") # Scan for specific device name devices = central.scan(name="MyDevice") # Scan for specific MAC address devices = central.scan(address="AA:BB:CC:DD:EE:FF") ``` **Parameters:** | Parameter | Type | Description | | ----------- | ------- | --------------------------------------- | | `scan_time` | `float` | Scan duration in seconds (default: 5.0) | | `name` | `str` | Filter by device name (optional) | | `address` | `str` | Filter by MAC address (optional) | **Returns:** `list` - List of discovered BLE devices ### `connect(address)` Connect to a BLE device by address. ```python theme={null} central = Central() client = central.connect("AA:BB:CC:DD:EE:FF") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | ------------------------- | | `address` | `str` | MAC address of the device | **Returns:** `Client` - Connected BLE client ### `pair(address)` Pair with a BLE device. ```python theme={null} central = Central() client = central.pair("AA:BB:CC:DD:EE:FF") ``` ## Client Class The `Client` class provides GATT operations on a connected BLE device. ### Creating a Client ```python theme={null} from lager.ble import Client, Central from bleak import BleakClient import asyncio # Method 1: Using Central central = Central() client = central.connect("AA:BB:CC:DD:EE:FF") # Method 2: Direct creation with context manager loop = asyncio.get_event_loop() with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client: # Use client pass ``` ### `connect()` Establish connection to the BLE device. ```python theme={null} client.connect() ``` ### `disconnect()` Disconnect from the BLE device. ```python theme={null} client.disconnect() ``` ### `pair()` Pair with the connected device. ```python theme={null} client.pair() ``` ### `get_services()` Discover and retrieve all GATT services. ```python theme={null} services = client.get_services() for service in services: print(f"Service: {service.uuid}") for char in service.characteristics: print(f" Characteristic: {char.uuid}") ``` **Returns:** `BleakGATTServiceCollection` - Collection of discovered services ### `has_characteristic(uuid)` Check if a characteristic exists on the device. ```python theme={null} if client.has_characteristic("00002a19-0000-1000-8000-00805f9b34fb"): print("Battery level characteristic found") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | ------------------- | | `uuid` | `str` | Characteristic UUID | **Returns:** `bool` - True if characteristic exists ### `read_gatt_char(char_specifier)` Read a characteristic value. ```python theme={null} # Read by UUID data = client.read_gatt_char("00002a19-0000-1000-8000-00805f9b34fb") print(f"Battery level: {data[0]}%") # Read by handle data = client.read_gatt_char(0x0012) ``` **Parameters:** | Parameter | Type | Description | | ---------------- | -------------- | ---------------------------- | | `char_specifier` | `str` or `int` | UUID string or handle number | **Returns:** `bytearray` - Characteristic value ### `write_gatt_char(char_specifier, data)` Write a value to a characteristic. ```python theme={null} # Write bytes client.write_gatt_char("characteristic-uuid", b'\x01\x02\x03') # Write string client.write_gatt_char("characteristic-uuid", "hello".encode('utf-8')) ``` **Parameters:** | Parameter | Type | Description | | ---------------- | -------------- | ---------------------------- | | `char_specifier` | `str` or `int` | UUID string or handle number | | `data` | `bytes` | Data to write | ### `start_notify(char_specifier, callback=noop_handler, max_messages=None, timeout=None)` Subscribe to characteristic notifications. ```python theme={null} from lager.ble import noop_handler # Simple notification subscription def my_callback(handle, data): print(f"Received: {data.hex()}") timed_out, messages = client.start_notify( "characteristic-uuid", callback=my_callback, max_messages=10, timeout=30.0 ) if timed_out: print("Timed out waiting for notifications") else: print(f"Received {len(messages)} messages") ``` **Parameters:** | Parameter | Type | Description | | ---------------- | -------------- | --------------------------------------- | | `char_specifier` | `str` or `int` | Characteristic UUID or handle | | `callback` | `callable` | Function called for each notification | | `max_messages` | `int` | Stop after receiving this many messages | | `timeout` | `float` | Timeout in seconds | **Returns:** `tuple[bool, list]` - (timed\_out, messages) where timed\_out is True if timeout occurred ### `stop_notify(char_specifier)` Unsubscribe from characteristic notifications. ```python theme={null} client.stop_notify("characteristic-uuid") ``` ### `sleep(timeout)` Sleep for a duration (async-safe). ```python theme={null} client.sleep(1.0) # Sleep for 1 second ``` ## Helper Functions ### `noop_handler(handle, data)` A no-op notification handler. ```python theme={null} from lager.ble import noop_handler # Use when you only care about collecting messages timed_out, messages = client.start_notify( "uuid", callback=noop_handler, max_messages=5 ) ``` ### `notify_handler(evt, messages, callback, max_messages, handle, data)` Internal notification handler that collects messages and signals completion. ### `waiter(event, timeout)` Async wait helper for notification events. ## Examples ### Scan for Devices ```python theme={null} from lager.ble import Central central = Central() # Discover all nearby BLE devices print("Scanning for BLE devices...") devices = central.scan(scan_time=10.0) for device in devices: name = device.name or "Unknown" print(f" {name}: {device.address}") ``` ### Connect and Read Characteristic ```python theme={null} from lager.ble import Client, Central from bleak import BleakClient import asyncio # Standard BLE UUIDs BATTERY_SERVICE = "0000180f-0000-1000-8000-00805f9b34fb" BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb" loop = asyncio.get_event_loop() with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client: # Check if battery service exists if client.has_characteristic(BATTERY_LEVEL): data = client.read_gatt_char(BATTERY_LEVEL) print(f"Battery level: {data[0]}%") ``` ### Subscribe to Notifications ```python theme={null} from lager.ble import Client, Central from bleak import BleakClient import asyncio NOTIFY_UUID = "your-characteristic-uuid" def handle_notification(handle, data): print(f"Notification from {handle}: {data.hex()}") loop = asyncio.get_event_loop() with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client: # Subscribe and wait for 10 messages or 30 seconds timed_out, messages = client.start_notify( NOTIFY_UUID, callback=handle_notification, max_messages=10, timeout=30.0 ) if timed_out: print(f"Timeout - received {len(messages)} messages") else: print(f"Received all {len(messages)} messages") # Process collected messages for msg in messages: print(f" {msg.hex()}") client.stop_notify(NOTIFY_UUID) ``` ### Write Command and Read Response ```python theme={null} from lager.ble import Client from bleak import BleakClient import asyncio WRITE_UUID = "write-characteristic-uuid" READ_UUID = "read-characteristic-uuid" loop = asyncio.get_event_loop() with Client(BleakClient("AA:BB:CC:DD:EE:FF"), loop=loop) as client: # Send command command = b'\x01\x02\x03' client.write_gatt_char(WRITE_UUID, command) # Wait for processing client.sleep(0.1) # Read response response = client.read_gatt_char(READ_UUID) print(f"Response: {response.hex()}") ``` ### Device Firmware Version Check ```python theme={null} from lager.ble import Client, Central from bleak import BleakClient import asyncio # Standard Device Information Service UUIDs DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb" FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb" MANUFACTURER_NAME = "00002a29-0000-1000-8000-00805f9b34fb" def check_device_info(address): loop = asyncio.get_event_loop() with Client(BleakClient(address), loop=loop) as client: # Read manufacturer if client.has_characteristic(MANUFACTURER_NAME): data = client.read_gatt_char(MANUFACTURER_NAME) print(f"Manufacturer: {data.decode('utf-8')}") # Read firmware version if client.has_characteristic(FIRMWARE_REVISION): data = client.read_gatt_char(FIRMWARE_REVISION) print(f"Firmware: {data.decode('utf-8')}") # First scan to find device central = Central() devices = central.scan(name="MyDevice") if devices: check_device_info(devices[0].address) ``` ### BLE Production Test ```python theme={null} from lager.ble import Client, Central from bleak import BleakClient import asyncio DEVICE_NAME = "DUT_BLE" TEST_CHAR = "test-characteristic-uuid" def ble_production_test(): central = Central() loop = asyncio.get_event_loop() # Step 1: Scan for DUT print("Scanning for DUT...") devices = central.scan(name=DEVICE_NAME, scan_time=10.0) if not devices: print("FAIL: DUT not found") return False address = devices[0].address print(f"Found DUT at {address}") # Step 2: Connect and test try: with Client(BleakClient(address), loop=loop) as client: # Test read data = client.read_gatt_char(TEST_CHAR) if len(data) == 0: print("FAIL: Empty response") return False # Test write client.write_gatt_char(TEST_CHAR, b'\x55') client.sleep(0.1) # Verify write data = client.read_gatt_char(TEST_CHAR) if data[0] != 0x55: print("FAIL: Write verification failed") return False print("PASS: BLE test complete") return True except Exception as e: print(f"FAIL: {e}") return False # Run test ble_production_test() ``` ## Hardware Requirements | Requirement | Description | | ------------ | ---------------------------------------- | | BLE Hardware | Bluetooth 4.0+ adapter on Lager Box | | Permissions | May require root/sudo for BLE operations | ## Dependencies The BLE module uses [Bleak](https://bleak.readthedocs.io/) as the underlying BLE library, which provides cross-platform BLE support. ## Notes * BLE operations are synchronous wrappers around async Bleak operations * The Client class supports context manager (`with` statement) for automatic cleanup * Notification callbacks receive `(handle, data)` parameters * Use `max_messages` and `timeout` together to control notification collection * MAC addresses are typically in format `AA:BB:CC:DD:EE:FF` * Some BLE operations may require pairing before they work * Signal strength (RSSI) is available on scanned device objects # Breakpoints Source: https://docs.lagerdata.com/source/reference/python/breakpoints Pause a running lager python script to inspect the bench, then continue Pause a `lager python` script mid-run so you can inspect the bench with ad-hoc `lager` commands (or a live Python prompt), then resume where it left off. Useful for a long test that reaches a known trouble spot, or for checking on a device in an unknown state without killing and restarting the run. Introduced in **lager 0.21.0**. ## Import ```python theme={null} from lager import pause ``` ## `pause(label=None, *, timeout=None, interactive=False)` Blocks the script at the call site until it is resumed (or the timeout elapses). | Argument | Default | Description | | ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `label` | `None` | A short note shown in the pause banner and console — e.g. the reason for the breakpoint. | | `timeout` | `None` | Seconds to wait before auto-resuming. `None` uses the `LAGER_BREAKPOINT_TIMEOUT` env var, or **300 s** if unset. `0` waits indefinitely (until you resume). | | `interactive` | `False` | When `True`, also exposes a Python console attached to the paused script (see [Interactive console](#interactive-console)). | ```python theme={null} from lager import pause pause("check the DUT before the final step") ``` When the line runs, the script prints a banner and stops: ``` === lager breakpoint "check the DUT before the final step" at test.py:14 (id 7f3a…e9) resume: press Enter here, or `lager python --continue 7f3a…e9 --box mybox` inspect: `lager python --console 7f3a…e9 --box mybox` auto-resume in 300s ``` `pause()` is a safe no-op when it can't pause — when the script isn't running under `lager python` (no breakpoint context), or when [breakpoints are disabled](#disabling-breakpoints). It never raises. ## Resuming A paused script can be resumed three ways: 1. **Press Enter** in the terminal running the script (the foreground `lager python` session). 2. **`lager python --continue --box `** — from any terminal, anywhere. Use the `id` from the banner. Handy when the script is detached or you're already in another terminal. 3. **Auto-resume** — after the timeout (default 300 s) the script continues on its own and logs that it did. This keeps an unattended or forgotten breakpoint from hanging a run. ### Controlling the auto-resume timeout The default is **300 seconds**. Override it per breakpoint, per run, or disable it entirely: ```python theme={null} pause("inspect", timeout=1800) # wait up to 30 minutes pause("inspect", timeout=0) # wait forever — never auto-resume ``` ```bash theme={null} # whole run, via the existing --env flag (applies to pause() calls with no explicit timeout=) lager python test.py --box mybox --env LAGER_BREAKPOINT_TIMEOUT=1800 ``` Resolution order is **`timeout=` argument → `LAGER_BREAKPOINT_TIMEOUT` env → 300 s default**, so an explicit `timeout=` in the script wins over the env var. `lager python --timeout` is a different setting — the script's total runtime limit, capped at 300 s on the box — and it will terminate the whole run when it elapses, paused or not. Leave it at its default (`0`, unlimited) when using long breakpoint pauses. ## Interactive console With `pause(interactive=True)`, the breakpoint also opens a Python console **running inside the paused script's process**, seeded with the variables in scope at the pause: ```python theme={null} readings = read_adcs() pause("inspect bench", interactive=True) ``` Connect to it from another terminal: ```bash theme={null} lager python --console --box mybox ``` ``` Connected to interactive console (Ctrl+D to disconnect) >>> readings {'adc1': -10.6032, 'adc2': -10.6031, 'adc3': -10.6032} >>> readings['adc1'] * 1000 -10603.2 >>> read_adcs() {'adc1': -10.6032, 'adc2': -10.6032, 'adc3': -10.6032} ``` You can read any variable, evaluate expressions, and call functions the script defines. `Ctrl+D` disconnects (the script stays paused). The console is for **inspection**: it operates on a snapshot of the script's namespace, so changes you make in the console do **not** carry back into the running script when it resumes. ## Inspecting hardware while paused Because a paused script holds no box-wide lock, you can run normal `lager` commands against the bench from another terminal while it waits: ```bash theme={null} lager supply supply2 state --box mybox # power supply lager battery battery1 state --box mybox # battery lager adc adc4 --box mybox # an ADC the script isn't using ``` Two hardware rules to keep in mind, both a consequence of USB instruments allowing only one owner at a time: * **A device the script itself has open is claimed by the paused process.** Reading it from a second terminal returns a "device busy / claimed by another process" error. Read it through the **`--console`** instead — that runs in the same process and shares the open handle. * **One net per physical instrument per process.** Two nets on the same instrument can't both be open at once in a single script (e.g. the two channels of one Rigol DP821, `supply2`/`supply3`, or the dual-role Keithley 2281S, `supply1`/`battery1`). Read them from separate terminals, or one at a time. ## Built-in `breakpoint()` Calling Python's built-in `breakpoint()` in a `lager python` script triggers the same interactive pause as `lager.pause()`: ```python theme={null} breakpoint() # same as pause() breakpoint("check DUT") # same as pause("check DUT") ``` ## Disabling breakpoints Set `LAGER_BREAKPOINTS` to an off value to turn every `pause()` (and `breakpoint()`) into a no-op — useful for a clean, non-interactive run of a script that has breakpoints left in it: ```bash theme={null} lager python test.py --box mybox --env LAGER_BREAKPOINTS=off ``` Accepts `off`, `0`, `false`, or `no` (case-insensitive). ## Full example `test.py`: ```python theme={null} import time from lager import Net, NetType, pause adc_nets = ["adc1", "adc2", "adc3"] def read_adcs(): return {n: round(float(Net.get(n, type=NetType.ADC).input()), 4) for n in adc_nets} print("Running test...") for step in range(1, 4): print(f" step {step}/3 ...") time.sleep(1) readings = read_adcs() print(f"sensor readings: {readings}") pause("inspect bench before final step", interactive=True) print("Resuming - running final step.") print("Done.") ``` Run it (terminal 1): ```bash theme={null} lager python test.py --box mybox ``` ``` Running test... step 1/3 ... step 2/3 ... step 3/3 ... sensor readings: {'adc1': -10.6032, 'adc2': -10.6031, 'adc3': -10.6032} === lager breakpoint "inspect bench before final step" at test.py:18 (id 7f3a…e9) resume: press Enter here, or `lager python --continue 7f3a…e9 --box mybox` inspect: `lager python --console 7f3a…e9 --box mybox` auto-resume in 300s ``` While it's paused, check the bench (terminal 2): ```bash theme={null} lager supply supply2 state --box mybox # a shared instrument — reads fine lager python --console 7f3a…e9 --box mybox # then: readings / read_adcs() ``` Press **Enter** in terminal 1 (or run `lager python --continue 7f3a…e9 --box mybox`) and the script finishes: ``` === resumed Resuming - running final step. Done. ``` # DAC Source: https://docs.lagerdata.com/source/reference/python/dac Control digital-to-analog converter outputs Set analog voltage outputs using DAC pins. Supports LabJack T7 and MCC USB-202 hardware. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ----------------- | ------------------------------ | | `output(voltage)` | Set analog output voltage | | `get_voltage()` | Read configured output voltage | | `input()` | Alias for `get_voltage()` | ## Method Reference ### `Net.get(name, type=NetType.DAC)` Get a DAC net by name. ```python theme={null} from lager import Net, NetType dac = Net.get('VREF', type=NetType.DAC) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------- | | `name` | `str` | Name of the DAC net | | `type` | `NetType` | Must be `NetType.DAC` | **Returns:** DAC Net instance ### `output(voltage)` Set the analog output voltage. ```python theme={null} dac.output(2.5) # Set to 2.5V ``` | Parameter | Type | Description | | --------- | ------- | ----------------------- | | `voltage` | `float` | Output voltage in volts | ### `get_voltage()` Read the currently configured output voltage. ```python theme={null} v = dac.get_voltage() print(f"DAC set to: {v}V") ``` **Returns:** `float` - Configured voltage in volts ### `input()` Alias for `get_voltage()`. Returns the currently configured output voltage. ```python theme={null} v = dac.input() print(f"DAC output: {v}V") ``` **Returns:** `float` - Configured voltage in volts ## Examples ### Set Reference Voltage ```python theme={null} from lager import Net, NetType vref = Net.get('VREF', type=NetType.DAC) vref.output(1.8) # Set to 1.8V ``` ### Generate Ramp Signal ```python theme={null} from lager import Net, NetType import time signal = Net.get('SIGNAL_OUT', type=NetType.DAC) # Ramp from 0 to 5V in 0.5V steps for v in range(0, 51, 5): voltage = v / 10.0 signal.output(voltage) print(f"Output: {voltage}V") time.sleep(0.1) ``` ### Voltage Sweep Test ```python theme={null} from lager import Net, NetType import time control = Net.get('CONTROL', type=NetType.DAC) sensor = Net.get('RESPONSE', type=NetType.ADC) # Sweep control voltage and measure response for mv in range(0, 3301, 100): voltage = mv / 1000.0 control.output(voltage) time.sleep(0.1) # Settling time response = sensor.input() print(f"Control: {voltage:.2f}V, Response: {response:.3f}V") ``` ### Set and Verify ```python theme={null} from lager import Net, NetType dac = Net.get('ANALOG_OUT', type=NetType.DAC) # Set output dac.output(3.3) # Read back to verify readback = dac.get_voltage() print(f"Set: 3.3V, Readback: {readback}V") ``` ## Supported Hardware | Hardware | Channels | Range | | ----------- | ----------- | ------ | | LabJack T7 | DAC0-DAC1 | 0-10 V | | MCC USB-202 | AOUT0-AOUT1 | 0-5 V | ### Pin Naming **LabJack T7:** | Pin Input | Channel | | ----------------- | --------- | | `0`-`1` | DAC0-DAC1 | | `"DAC0"`-`"DAC1"` | DAC0-DAC1 | **MCC USB-202:** | Pin Input | Channel | | ------------------- | ----------- | | `0`-`1` | AOUT0-AOUT1 | | `"DAC0"`-`"DAC1"` | AOUT0-AOUT1 | | `"AOUT0"`-`"AOUT1"` | AOUT0-AOUT1 | ## Notes * DAC nets work directly without `enable()`/`disable()` calls * LabJack T7 output range: 0-10 V * USB-202 output range: 0-5 V * `input()` is an alias for `get_voltage()` and returns the configured output value * Output values are maintained until changed or power cycle * Net names must match those configured on the Lager Box # Debug Source: https://docs.lagerdata.com/source/reference/python/debug Embedded debug operations for J-Link probes Control embedded debug operations including device connection, firmware flashing, reset, and memory access using J-Link debug probes. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods The Net-based API provides methods for embedded debugging operations. | Method | Description | | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | `connect(speed, transport, *, script, force, ignore_if_connected)` | Connect to target device (optional per-connect J-Link script override) | | `disconnect()` | Disconnect from target | | `reset(halt)` | Reset the device | | `flash(firmware_path)` | Flash firmware to device | | `erase()` | Perform full chip erase | | `read_memory(address, length)` | Read memory from device | | `status()` | Get connection status | | `rtt(channel, search_addr, search_size, chunk_size)` | Create RTT session for bidirectional communication (raw bytes) | | `rtt_defmt(elf, channel)` | RTT session decoded through `defmt-print` (yields log lines) | | `session(...)` | Scoped session: connect on entry, guaranteed teardown on exit | ## Method Reference ### `Net.get(name, type=NetType.Debug)` Get a debug net by name. ```python theme={null} from lager import Net, NetType dbg = Net.get('DUT', type=NetType.Debug) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ----------------------- | | `name` | `str` | Name of the debug net | | `type` | `NetType` | Must be `NetType.Debug` | **Returns:** Debug Net instance **Note:** The debug net must be configured with the target device name stored in the `channel` field (e.g., 'NRF52840\_XXAA', 'R7FA0E107'). ### `connect(speed=None, transport=None, *, script=None, force=False, ignore_if_connected=False)` Connect to the target device (start the gdbserver for this probe). The backend (J-Link or OpenOCD) is chosen automatically from the probe. ```python theme={null} # Connect with default settings (4000 kHz, SWD) dbg.connect() # Connect with custom speed dbg.connect(speed='adaptive') # Connect with JTAG dbg.connect(transport='JTAG') # Connect with a per-connect J-Link script override (box path or base64 blob) dbg.connect(script='/home/lagerdata/probes/my_target.JLinkScript') ``` **Parameters:** | Parameter | Type | Default | Description | | --------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `speed` | `str` | `'4000'` | Interface speed in kHz (e.g., '4000') or 'adaptive' | | `transport` | `str` | `'SWD'` | Transport protocol ('SWD' or 'JTAG') | | `script` | `str` | `None` | **J-Link only** — a per-connect `.JLinkScript` override: a path on the box **or** a base64-encoded script blob. It is copied to the shared script temp path so subsequent `flash()` / `reset()` / `read_memory()` calls pick it up immediately. The OpenOCD backend ignores this. | | `force` | `bool` | `False` | Stop any gdbserver already running for this probe and start fresh. | | `ignore_if_connected` | `bool` | `False` | If a gdbserver is already running for this probe, return its status without touching it. | **Returns:** `dict` - Status dictionary with connection information A `script` override is only adopted by a *relaunch* of the gdbserver. If a server is already running, pass `force=True` to restart it with the new script; `ignore_if_connected=True` returns early without relaunching (though the script file is still repointed for subsequent Commander operations). Invalid input — a missing path that isn't valid base64, or an empty string — is silently ignored and the net's previously materialised script stays in effect. **Concurrency caveat:** two debug nets connecting with different scripts share one temp path and can clobber each other. ### `disconnect()` Disconnect from the target device. ```python theme={null} dbg.disconnect() ``` **Returns:** `dict` - Status dictionary ### `reset(halt=False)` Reset the device. ```python theme={null} # Reset and continue execution output = dbg.reset(halt=False) print(output) # Reset and halt for debugging output = dbg.reset(halt=True) print(output) ``` **Parameters:** | Parameter | Type | Default | Description | | --------- | ------ | ------- | -------------------- | | `halt` | `bool` | `False` | Halt CPU after reset | **Returns:** `str` - Combined output from reset operation **Self-heal (both backends):** Right after a `flash()` there is a short window where the debug server isn't reachable yet — for J-Link the restarted GDB server's PID isn't observable, for OpenOCD a transient daemon/RPC fault — and a bare call would raise. `reset()` now retries with bounded backoff on both the J-Link and OpenOCD backends, and only (re)starts a server when one is genuinely down. It never tears down a server that is already running, so an attached RTT session is left intact, and callers no longer need their own retry wrappers. **DA1469x exception:** On DA1469x, `flash()` deliberately leaves the server down (it ends in a software reset rather than a server restart) and the documented flow is an explicit, halt-aware reconnect. So on DA1469x the self-heal retries but never auto-starts a server — a genuinely-down server still surfaces the original error, exactly as before, instead of silently coming up unhalted (which can yield garbage QSPI-XIP reads). ### `flash(firmware_path)` Flash firmware to the device. ```python theme={null} # Flash a hex file output = dbg.flash('/path/to/firmware.hex') print(output) # Flash a binary file (address 0x00000000 assumed) output = dbg.flash('/path/to/firmware.bin') print(output) # Flash an ELF file output = dbg.flash('/path/to/firmware.elf') print(output) ``` **Parameters:** | Parameter | Type | Description | | --------------- | ----- | ------------------------------------------- | | `firmware_path` | `str` | Path to firmware file (.hex, .bin, or .elf) | **Returns:** `str` - Combined output from flash operation **Note:** For .bin files, flash address defaults to 0x00000000. ### `erase()` Perform full chip erase. This erases ALL flash memory including protection settings. ```python theme={null} # Full chip erase output = dbg.erase() print(output) ``` **Returns:** `str` - Combined output from erase operation ### `read_memory(address, length)` Read memory from the target device. ```python theme={null} # Read 256 bytes starting at address 0x20000000 data = dbg.read_memory(0x20000000, 256) print(f"Read {len(data)} bytes") print(data.hex()) ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | ----------------------- | | `address` | `int` | Starting memory address | | `length` | `int` | Number of bytes to read | **Returns:** `bytes` - Memory data **Self-heal:** like `reset()`, `read_memory()` retries with bounded backoff across the brief post-`flash()` settling window on both backends and only reconnects when no server is running, never disturbing a live session. `erase()` behaves the same way. The same DA1469x exception applies — no server is auto-started, so a post-flash DA1469x read raises clearly rather than returning unhalted-XIP garbage. ### `status()` Get the current connection status. ```python theme={null} status = dbg.status() print(f"Connected: {status.get('connected', False)}") ``` **Returns:** `dict` - Status dictionary with connection information ### `session(speed=None, transport=None, connect=True, ignore_if_connected=True, disconnect_on_exit=True)` Scoped debug session. Connects on entry and guarantees teardown on exit, so the safe flash → attach-RTT → reset ordering is encoded once instead of being rediscovered in every script. The `with` target is the net itself, so the full surface (`flash`, `rtt_defmt`, `reset`, `read_memory`, …) is available inside the block. ```python theme={null} with dbg.session() as s: s.flash('build/app.hex') # built-in stop->flash->restart handoff with s.rtt_defmt(elf='build/app.elf') as logs: s.reset(halt=False) # reader re-attaches across the reset blip for line in logs: if 'boot ok' in line: break # GDB server is torn down here (disconnect_on_exit=True) ``` **Parameters:** | Parameter | Type | Default | Description | | --------------------- | --------------- | ------- | ----------------------------------------------------------------------------------------- | | `speed` | `str` or `None` | `None` | Forwarded to `connect()` | | `transport` | `str` or `None` | `None` | Forwarded to `connect()` | | `connect` | `bool` | `True` | Connect on entry. Set `False` to attach to a server you manage yourself | | `ignore_if_connected` | `bool` | `True` | Reuse a running server instead of raising (regression-safe: never restarts a live server) | | `disconnect_on_exit` | `bool` | `True` | Stop the GDB server on exit. Set `False` to leave it running for later commands | **Returns:** a context manager yielding the debug net. **Why it pairs with RTT:** the in-process RTT reader is reconnect-aware (see below), so a `flash()` or `reset()` inside the session that bounces the GDB server doesn't kill a log stream opened in the same block. ### `rtt(channel=0, search_addr=None, search_size=None, chunk_size=None)` Create an RTT (Real-Time Transfer) session for bidirectional communication with the target device. ```python theme={null} # Open RTT session on default channel (0) with dbg.rtt() as rtt: # Read debug output data = rtt.read_some(timeout=1.0) if data: print(data.decode('utf-8')) # Send commands to device rtt.write(b'test_command\n') # Use different RTT channel with dbg.rtt(channel=1) as rtt: data = rtt.read_some(timeout=2.0) # Specify RAM search region for RTT control block with dbg.rtt(search_addr=0x20000000, search_size=0x10000) as rtt: data = rtt.read_some(timeout=1.0) ``` **Parameters:** | Parameter | Type | Default | Description | | ------------- | --------------- | ------- | ---------------------------------------------- | | `channel` | `int` | `0` | RTT channel number (typically 0-15) | | `search_addr` | `int` or `None` | `None` | RAM start address for RTT control block search | | `search_size` | `int` or `None` | `None` | Size of RAM region to search in bytes | | `chunk_size` | `int` or `None` | `None` | Size of each read chunk in bytes | **Returns:** RTT context manager with methods: * `read_some(timeout)` - Read available data with timeout (returns bytes or None) * `write(data)` - Write data to target (accepts bytes or str) **Note:** Debug connection must be active before using RTT. Call `connect()` first. **Reconnect-aware (both backends):** a J-Link `flash()` (and `reset()` via its Commander grab) briefly frees the probe's USB and restarts the GDB server on the *same* ports, dropping the RTT socket. The reader transparently re-attaches to the same RTT telnet port instead of going silent, so a long-lived `read_some()` / `rtt_defmt()` loop keeps producing across a flash. The OpenOCD reader is reconnect-aware too — OpenOCD keeps its daemon up across an ordinary flash so the socket rarely drops, but if it does (daemon force-restart or rtt-server bounce) the reader re-runs the `rtt setup` / `rtt server start` and re-attaches. In both cases reconnection is bounded (default 30 s) and only re-attaches once the server/daemon is actually back up — it never *starts* one — so a flash that deliberately leaves the server down (e.g. DA1469x) won't spin forever, and the reader can't disturb a DA1469x left intentionally down. Pass `reconnect=False` for the legacy one-shot behavior. ## Examples ### Flash Firmware and Reset ```python theme={null} from lager import Net, NetType # Get debug net dbg = Net.get('DUT', type=NetType.Debug) # Connect to target status = dbg.connect() print(f"Connected: {status}") # Flash firmware output = dbg.flash('/etc/lager/firmware/app.hex') print(output) # Reset and run output = dbg.reset(halt=False) print(output) # Disconnect dbg.disconnect() ``` ### Chip Erase Before Programming ```python theme={null} from lager import Net, NetType # Get debug net dbg = Net.get('DUT', type=NetType.Debug) # Connect to target status = dbg.connect() print(f"Connected: {status}") # Erase entire chip first (ensures clean state) print("Erasing chip...") output = dbg.erase() print(output) # Flash new firmware output = dbg.flash('/etc/lager/firmware/app.hex') print(output) # Disconnect dbg.disconnect() ``` ### Read Memory ```python theme={null} from lager import Net, NetType dbg = Net.get('DUT', type=NetType.Debug) # Connect to target dbg.connect() # Read 256 bytes from RAM data = dbg.read_memory(0x20000000, 256) print(f"Read {len(data)} bytes") print(data.hex()) # Disconnect dbg.disconnect() ``` ## CLI Commands (Recommended) For most use cases, the CLI provides a simpler interface: ```bash theme={null} # Start GDB server (connect to target) lager debug gdbserver --box # Flash firmware lager debug flash --hex firmware.hex --box # Reset device lager debug reset --box # Erase flash lager debug erase --box # Read memory lager debug memrd 0x20000000 256 --box # Disconnect lager debug disconnect --box # Check status lager debug status --box ``` See the [CLI Debug Reference](/source/reference/cli/debug) for full CLI documentation. ## RTT Streaming SEGGER Real-Time Transfer (RTT) enables high-speed bidirectional communication with embedded devices during debugging (faster than UART, no timing impact). ```python theme={null} from lager import Net, NetType # Connect debug probe first debug = Net.get('debug1', type=NetType.Debug) debug.connect() # Open RTT session for reading debug output with debug.rtt() as rtt: # Read debug output from MCU data = rtt.read_some(timeout=1.0) if data: print(data.decode('utf-8')) # Can also write commands to MCU rtt.write(b'start_test\n') ``` **RTT Methods:** | Method | Description | | -------------------- | -------------------------------------------------------- | | `read_some(timeout)` | Read available data with timeout (returns bytes or None) | | `write(data)` | Write data to RTT (accepts bytes or str) | `rtt().read_some()` returns **raw, still-encoded** bytes. Firmware that logs with [defmt](https://defmt.ferrous-systems.com/) (the de-facto standard for embedded Rust) emits a compressed binary format — calling `.decode('utf-8')` on it yields garbage. For defmt firmware, use `rtt_defmt()` below or the CLI pipe, both of which decode through `defmt-print`. ### Decoding defmt logs with `rtt_defmt()` `rtt_defmt(elf, channel=0)` opens an RTT session and pipes it through `defmt-print` (preinstalled on the Lager Box), yielding **decoded log lines** instead of raw bytes. The `elf` must be the exact firmware flashed on the target — defmt needs its symbol metadata to decode. ```python theme={null} from lager import Net, NetType import time dbg = Net.get('debug1', type=NetType.Debug) dbg.connect(ignore_if_connected=True) # reuse a running gdbserver if one is up dbg.flash('build/app.elf') # skip if already flashed; same ELF you decode against dbg.reset() # restart to capture boot logs # Capture a bounded ~10s window of decoded logs with dbg.rtt_defmt(elf='build/app.elf', channel=0) as logs: deadline = time.time() + 10 while time.time() < deadline: line = logs.read_line(timeout=1.0) # decoded str, or None if line: print(line) assert 'panic' not in line.lower(), f"firmware panicked: {line}" ``` `rtt_defmt()` returns a context manager exposing: | Method | Description | | ------------------------------- | ----------------------------------------------------------------- | | `read_line(timeout=None)` | Next decoded log line as `str`, or `None` on timeout / stream end | | iteration (`for line in logs:`) | Yield decoded lines until the stream ends | | `write(data)` | Send bytes or `str` to the target's RTT down-channel | Like the CLI pipe, the RTT stream never ends on its own — bound your read loop with a time budget or line count, then exit the `with` block. #### Driving the firmware while decoding its logs `write()` makes a decoding session bi-directional, so a script can send a command and assert on the decoded response. Decoding is one-way — `defmt-print` only sees the up-channel — so writes bypass it and go straight to the target. This is the only way to do both at once: the RTT telnet port accepts a single client, so you cannot open a raw `rtt()` alongside a `rtt_defmt()`. ```python theme={null} with dbg.rtt_defmt(elf='build/app.elf') as logs: logs.write(b'self_test\n') # command the firmware deadline = time.time() + 5 while time.time() < deadline: line = logs.read_line(timeout=1.0) if line and 'self_test: pass' in line: break else: raise AssertionError('firmware never reported a passing self-test') ``` This requires the firmware to declare an RTT **down** buffer on the channel you opened. `defmt-rtt` alone only sets up the up buffer — with no down buffer the target silently discards whatever you write, which looks like a host-side failure and is not one. **Parameters:** | Parameter | Type | Default | Description | | ----------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `elf` | `str` | required | Path to the firmware ELF flashed on the DUT (relative paths resolve against the script's working dir on the box) | | `channel` | `int` | `0` | RTT channel number | | `defmt_print_bin` | `str` or `None` | `None` | Override the `defmt-print` binary (path or name on PATH) | | `read_timeout` | `float` | `0.5` | Poll interval (seconds) for the internal RTT read loop | **CLI Alternative:** For interactive tailing, pipe the CLI directly: `lager debug gdbserver --box --rtt 2>/dev/null | defmt-print -e build/app.elf`. See the [CLI Debug Reference](/source/reference/cli/debug#decoding-defmt-logs). Use `rtt_defmt()` when you need to assert on log content inside a test script; use the pipe when you just want to watch logs. ## Supported Devices J-Link supports a wide range of ARM Cortex-M and other microcontrollers. Common device names: | Manufacturer | Device Name | Description | | ------------ | ------------------ | ------------------------ | | Nordic | `NRF52840_XXAA` | nRF52840 | | Nordic | `NRF52833_XXAA` | nRF52833 | | Nordic | `NRF5340_XXAA_APP` | nRF5340 Application Core | | Renesas | `R7FA0E107` | RA0E1 Series | | Renesas | `R7FA2L1` | RA2L1 Series | | STMicro | `STM32F103C8` | STM32F1 Series | | STMicro | `STM32F407VG` | STM32F4 Series | | STMicro | `STM32L476RG` | STM32L4 Series | For a complete list, see [SEGGER's supported devices](https://www.segger.com/supported-devices/jlink/). ## Supported Hardware | Debug Probe | Features | | ----------- | ------------------------------------- | | J-Link | JTAG/SWD debugging, flash programming | | CMSIS-DAP | SWD debugging (via pyOCD backend) | | ST-Link | SWD debugging (via pyOCD backend) | ## Notes * Debug nets must be configured with the target device name in the `channel` field * The CLI (`lager debug`) is recommended for most use cases * Python Net API is intended for advanced automation scripts running on the Lager Box * Always call `disconnect()` when finished to release the debug probe * Use `erase()` to perform a full chip erase and clear protection settings * RTT requires an active debug connection (see RTT Streaming section above) # Electronic Load Source: https://docs.lagerdata.com/source/reference/python/eload Control electronic load nets Control electronic loads to sink current in various modes for testing power systems. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods The Net-based API provides methods that can either get or set values. When called without a value parameter, methods read and return the current value. When called with a value, they set it. | Method | Description | | -------------------- | ---------------------------------------- | | `mode(mode_type)` | Set or read operation mode (CC/CV/CR/CW) | | `current(value)` | Set or read constant current (A) | | `voltage(value)` | Set or read constant voltage (V) | | `resistance(value)` | Set or read constant resistance (ohms) | | `power(value)` | Set or read constant power (W) | | `enable()` | Enable electronic load input | | `disable()` | Disable electronic load input | | `print_state()` | Print comprehensive state | | `measured_voltage()` | Read measured voltage (returns float) | | `measured_current()` | Read measured current (returns float) | | `measured_power()` | Read measured power (returns float) | ## Method Reference ### `Net.get(name, type=NetType.ELoad)` Get an electronic load net by name. ```python theme={null} from lager import Net, NetType eload = Net.get('LOAD', type=NetType.ELoad) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------------- | | `name` | `str` | Name of the electronic load net | | `type` | `NetType` | Must be `NetType.ELoad` | **Returns:** Electronic load Net instance ### `mode(mode_type=None)` Set or read the operation mode. ```python theme={null} # Set mode eload.mode('CC') # Constant Current eload.mode('CV') # Constant Voltage eload.mode('CR') # Constant Resistance eload.mode('CW') # Constant Power (also 'CP') # Read current mode current_mode = eload.mode() print(f"Mode: {current_mode}") ``` **Parameters:** | Parameter | Type | Description | | ----------- | --------------- | ------------------------------------------------------------ | | `mode_type` | `str` or `None` | 'CC', 'CV', 'CR', or 'CW'/'CP'. If None, reads current mode. | **Returns:** Current mode string if `mode_type` is None ### `current(value=None)` Set or read the constant current setting. ```python theme={null} # Set CC mode current eload.mode('CC') eload.current(0.5) # Set to 500mA # Read current setting i = eload.current() print(f"Current setting: {i}A") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----------------- | ------------------------------------------------ | | `value` | `float` or `None` | Current in amps. If None, reads current setting. | **Returns:** Current setting in amps if `value` is None ### `voltage(value=None)` Set or read the constant voltage setting. ```python theme={null} # Set CV mode voltage eload.mode('CV') eload.voltage(5.0) # Set to 5V # Read voltage setting v = eload.voltage() print(f"Voltage setting: {v}V") ``` ### `resistance(value=None)` Set or read the constant resistance setting. Values below 0.02 ohms are at the minimum range limit of the instrument and may be clamped. A warning is printed if the applied value differs from the requested value. ```python theme={null} # Set CR mode resistance eload.mode('CR') eload.resistance(100.0) # Set to 100 ohms # Read resistance setting r = eload.resistance() print(f"Resistance setting: {r} ohms") ``` ### `power(value=None)` Set or read the constant power setting. ```python theme={null} # Set CW mode power eload.mode('CW') eload.power(5.0) # Set to 5W # Read power setting p = eload.power() print(f"Power setting: {p}W") ``` ### `enable()` / `disable()` Enable or disable the electronic load input. ```python theme={null} eload.enable() # Start sinking current eload.disable() # Stop sinking current ``` ### `print_state()` Print comprehensive electronic load state. ```python theme={null} eload.print_state() # Prints: mode, current/voltage/resistance/power settings, measured values, input state ``` ### `measured_voltage()` / `measured_current()` / `measured_power()` Read actual measured values (return floats, for use in code). ```python theme={null} v = eload.measured_voltage() # Returns measured voltage i = eload.measured_current() # Returns measured current p = eload.measured_power() # Returns measured power print(f"V={v:.2f}V, I={i:.3f}A, P={p:.3f}W") ``` **Returns:** `float` - Measured value ## Load Modes | Mode | Code | Description | | ------------------- | ------------ | ------------------------------------------- | | Constant Current | `CC` | Sinks a fixed current regardless of voltage | | Constant Voltage | `CV` | Maintains a fixed voltage across the load | | Constant Resistance | `CR` | Behaves as a fixed resistance | | Constant Power | `CW` or `CP` | Dissipates a fixed power | ## Examples ### Constant Current Test ```python theme={null} from lager import Net, NetType eload = Net.get('LOAD', type=NetType.ELoad) # Configure constant current mode at 500mA eload.mode('CC') eload.current(0.5) eload.enable() # Read measurements print(f"Voltage: {eload.measured_voltage():.2f}V") print(f"Current: {eload.measured_current():.3f}A") print(f"Power: {eload.measured_power():.3f}W") eload.disable() ``` ### Battery Discharge Test ```python theme={null} from lager import Net, NetType import time eload = Net.get('LOAD', type=NetType.ELoad) # Configure constant resistance load eload.mode('CR') eload.resistance(10.0) eload.enable() # Monitor discharge for i in range(30): v = eload.measured_voltage() i_curr = eload.measured_current() print(f"V={v:.2f}V, I={i_curr:.3f}A") if v < 3.0: print("Discharge cutoff reached") break time.sleep(1) eload.disable() ``` ### Power Efficiency Test ```python theme={null} from lager import Net, NetType # Input power supply psu = Net.get('INPUT', type=NetType.PowerSupply) psu.voltage(12.0) psu.current(2.0) psu.enable() # Output load eload = Net.get('OUTPUT', type=NetType.ELoad) eload.mode('CC') eload.current(0.5) eload.enable() # Measure efficiency p_in = 12.0 * psu.measured_current() # Input power p_out = eload.measured_power() # Output power efficiency = (p_out / p_in) * 100 if p_in > 0 else 0 print(f"Input: {p_in:.2f}W") print(f"Output: {p_out:.2f}W") print(f"Efficiency: {efficiency:.1f}%") eload.disable() psu.disable() ``` ## Supported Hardware | Manufacturer | Model | Features | | ------------ | ---------------------- | ----------------- | | Rigol | DL3021 (DL3000 series) | CC/CV/CR/CP modes | ## Notes * Use `mode()` to set the operation mode before setting the corresponding value * Methods like `current()`, `voltage()`, `resistance()`, `power()` can get or set values * `measured_voltage()`, `measured_current()`, `measured_power()` return actual measurements * `print_state()` prints state (for debugging), measurement methods return values (for code) * Always call `enable()` to start sinking current * Always call `disable()` when finished * Resistance values below 0.02 ohms may be clamped by the instrument # Energy Analyzer Source: https://docs.lagerdata.com/source/reference/python/energy Integrate energy and charge, and compute power statistics using an energy-analyzer net Integrate energy and charge over time, or compute current/voltage/power statistics, from an energy-analyzer net. Requires the `energy-analyzer` net type. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ----------------------- | -------------------------------------------------------- | | `read_energy(duration)` | Integrate energy and charge over `duration` seconds | | `read_stats(duration)` | Compute mean/min/max/std for current, voltage, and power | ## Method Reference ### `Net.get(name, type=NetType.EnergyAnalyzer)` Get an energy analyzer net by name. ```python theme={null} from lager import Net, NetType power = Net.get('POWER_METER', type=NetType.EnergyAnalyzer) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | -------------------------------- | | `name` | `str` | Name of the energy-analyzer net | | `type` | `NetType` | Must be `NetType.EnergyAnalyzer` | **Returns:** Energy analyzer Net instance ### `read_energy(duration)` Integrate current and power over `duration` seconds. ```python theme={null} result = power.read_energy(10.0) print(f"Energy: {result['energy_wh'] * 1000:.3f} mWh") print(f"Charge: {result['charge_ah'] * 1000:.3f} mAh") ``` **Parameters:** | Parameter | Type | Description | | ---------- | ------- | ----------------------------- | | `duration` | `float` | Integration window in seconds | **Returns:** `dict` with keys: | Key | Type | Description | | -------------- | ------- | --------------------------- | | `"energy_j"` | `float` | Energy in joules | | `"energy_wh"` | `float` | Energy in watt-hours | | `"charge_c"` | `float` | Charge in coulombs | | `"charge_ah"` | `float` | Charge in amp-hours | | `"duration_s"` | `float` | Duration that was requested | ### `read_stats(duration)` Compute mean, minimum, maximum, and standard deviation for current, voltage, and power over `duration` seconds. ```python theme={null} stats = power.read_stats(1.0) print(f"Current: {stats['current']['mean'] * 1e6:.1f} uA (mean)") print(f"Voltage: {stats['voltage']['mean']:.3f} V (mean)") print(f"Power: {stats['power']['mean'] * 1000:.3f} mW (mean)") ``` **Parameters:** | Parameter | Type | Description | | ---------- | ------- | ----------------------------- | | `duration` | `float` | Measurement window in seconds | **Returns:** `dict` with keys: | Key | Type | Description | | -------------- | ------- | --------------------------- | | `"current"` | `dict` | Current statistics in amps | | `"voltage"` | `dict` | Voltage statistics in volts | | `"power"` | `dict` | Power statistics in watts | | `"duration_s"` | `float` | Duration that was requested | Each statistics sub-dict contains: | Key | Type | Description | | -------- | ------- | ------------------ | | `"mean"` | `float` | Mean value | | `"min"` | `float` | Minimum value | | `"max"` | `float` | Maximum value | | `"std"` | `float` | Standard deviation | ## Examples ### Energy Budget Check ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer) result = power.read_energy(10.0) energy_mwh = result['energy_wh'] * 1000 charge_mah = result['charge_ah'] * 1000 print(f"Energy: {energy_mwh:.3f} mWh") print(f"Charge: {charge_mah:.3f} mAh") if energy_mwh > 50: print("FAIL: Energy consumption exceeds budget") else: print("PASS: Energy within budget") ``` ### Sleep Current Verification ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer) # Measure over 5 seconds for accuracy stats = power.read_stats(5.0) sleep_ua = stats['current']['mean'] * 1e6 supply_v = stats['voltage']['mean'] print(f"Sleep current: {sleep_ua:.1f} uA") print(f"Supply voltage: {supply_v:.3f} V") if sleep_ua < 100: print("PASS: Sleep current below 100 uA") else: print(f"FAIL: Sleep current {sleep_ua:.1f} uA exceeds 100 uA limit") ``` ### Battery Life Estimation ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer) # Measure average current draw over a representative workload stats = power.read_stats(30.0) avg_current_ma = stats['current']['mean'] * 1000 avg_voltage_v = stats['voltage']['mean'] # Estimate from a 1000 mAh battery battery_mah = 1000 hours = battery_mah / avg_current_ma if avg_current_ma > 0 else float('inf') print(f"Average current: {avg_current_ma:.3f} mA") print(f"Average voltage: {avg_voltage_v:.3f} V") print(f"Estimated battery life: {hours:.1f} hours") ``` ### Startup Energy Capture ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer) # Capture energy consumed during device startup (2 seconds) result = power.read_energy(2.0) print(f"Startup energy: {result['energy_j'] * 1000:.2f} mJ") print(f"Startup charge: {result['charge_c'] * 1000:.2f} mC") ``` ### Current Spike Analysis ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.EnergyAnalyzer) # Short window to capture peak current stats = power.read_stats(1.0) peak_ma = stats['current']['max'] * 1000 avg_ma = stats['current']['mean'] * 1000 std_ma = stats['current']['std'] * 1000 print(f"Peak current: {peak_ma:.3f} mA") print(f"Average current: {avg_ma:.3f} mA") print(f"Std deviation: {std_ma:.3f} mA") # Flag if peak is more than 10x average (unexpected spike) if peak_ma > avg_ma * 10: print("WARNING: Unexpected current spike detected") ``` ## Supported Hardware | Manufacturer | Model | Net Type | USB VID:PID | | -------------------- | ----- | ----------------- | ----------- | | Joulescope | JS220 | `energy-analyzer` | `16d0:10ba` | | Nordic Semiconductor | PPK2 | `energy-analyzer` | `1915:c00a` | For instantaneous power readings (also available on Yocto-Watt), see [Watt Meter](/source/reference/python/watt). ## Notes * The Joulescope JS220 samples at high frequency; longer durations yield more accurate statistics * The Nordic PPK2 operates in source mode (supplies a configurable voltage 0.8–5V and measures current); voltage readings reflect the configured value * `read_energy()` and `read_stats()` block for the full `duration` before returning * The same physical JS220 device is shared between `WattMeter` and `EnergyAnalyzer` net roles — opening both net types for the same device is safe * Use `lager instruments --box ` to verify the JS220 is detected before running scripts # GPIO Source: https://docs.lagerdata.com/source/reference/python/gpio Control digital input/output pins Control digital input/output pins for digital signaling and control. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ---------------------------- | ------------------------------------ | | `input()` | Read digital pin state | | `output(level)` | Set digital pin state | | `wait_for_level(level, ...)` | Wait for pin to reach a target level | ## Method Reference ### `Net.get(name, type=NetType.GPIO)` Get a GPIO net by name. ```python theme={null} from lager import Net, NetType pin = Net.get('LED', type=NetType.GPIO) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ---------------------- | | `name` | `str` | Name of the GPIO net | | `type` | `NetType` | Must be `NetType.GPIO` | **Returns:** GPIO Net instance ### `input()` Read the digital state of the pin. ```python theme={null} from lager import Net, NetType pin = Net.get('BUTTON', type=NetType.GPIO) state = pin.input() if state: print("Pin is HIGH") else: print("Pin is LOW") ``` **Returns:** `int` - 0 for LOW, 1 for HIGH ### `output(level)` Set the digital state of the pin. ```python theme={null} from lager import Net, NetType pin = Net.get('LED', type=NetType.GPIO) # Set HIGH pin.output(1) # Set LOW pin.output(0) ``` | Parameter | Type | Description | | --------- | -------------- | --------------------------------------------- | | `level` | `int` or `str` | 0/1, "low"/"high", "off"/"on", "true"/"false" | ### `wait_for_level(level, timeout=None, ...)` Wait for the pin to reach a target level. Blocks until the pin reads the specified level or the timeout expires. The LabJack T7 uses hardware streaming at up to 20 kHz for fast edge detection. Other hardware uses software polling. ```python theme={null} from lager import Net, NetType pin = Net.get('INTERRUPT', type=NetType.GPIO) # Wait for pin to go HIGH (block forever) elapsed = pin.wait_for_level(1) print(f"Pin went HIGH after {elapsed:.3f}s") # Wait with timeout try: elapsed = pin.wait_for_level(0, timeout=5.0) print(f"Pin went LOW after {elapsed:.3f}s") except TimeoutError: print("Timed out waiting for pin") ``` **Parameters:** | Parameter | Type | Default | Description | | ---------------- | ----------------- | -------- | -------------------------------------------------------------- | | `level` | `int` or `str` | required | Target level: 0/1, "low"/"high", "off"/"on" | | `timeout` | `float` or `None` | `None` | Maximum seconds to wait. `None` = wait forever. | | `scan_rate` | `int` | `20000` | Sample rate in Hz (LabJack T7 only) | | `scans_per_read` | `int` | `2` | Batch size per read (LabJack T7 only; lower = faster reaction) | | `poll_interval` | `float` | `0.01` | Seconds between polls (non-LabJack hardware only) | **Returns:** `float` - Elapsed time in seconds until the level was detected **Raises:** `TimeoutError` if the timeout expires before the target level is reached **Hardware-specific behavior:** | Hardware | Method | Default Rate | | ---------- | --------------------------------------- | ----------------------- | | LabJack T7 | Hardware streaming (`ljm.eStreamStart`) | 20,000 Hz | | USB-202 | Software polling | 100 Hz (10 ms interval) | ## Examples ### Read Button State ```python theme={null} from lager import Net, NetType button = Net.get('BUTTON', type=NetType.GPIO) state = button.input() if state: print("Button pressed") else: print("Button released") ``` ### Control LED ```python theme={null} from lager import Net, NetType led = Net.get('LED', type=NetType.GPIO) # Turn on led.output(1) # Turn off led.output(0) ``` ### Wait for Interrupt ```python theme={null} from lager import Net, NetType irq_pin = Net.get('INTERRUPT', type=NetType.GPIO) # Wait for interrupt (rising edge) try: elapsed = irq_pin.wait_for_level(1, timeout=10.0) print(f"Interrupt detected after {elapsed:.3f}s") except TimeoutError: print("No interrupt within 10 seconds") ``` ### Button-Controlled LED ```python theme={null} from lager import Net, NetType import time button = Net.get('BUTTON', type=NetType.GPIO) led = Net.get('LED', type=NetType.GPIO) print("Press Ctrl+C to exit") while True: try: if button.input(): led.output(1) else: led.output(0) time.sleep(0.1) except KeyboardInterrupt: led.output(0) break ``` ### Toggle Output ```python theme={null} from lager import Net, NetType import time output_pin = Net.get('SIGNAL', type=NetType.GPIO) # Generate 10 pulses for i in range(10): output_pin.output(1) time.sleep(0.5) output_pin.output(0) time.sleep(0.5) ``` ## Supported Hardware | Hardware | Pins | Logic Level | | ----------- | ------------------------------- | ----------- | | LabJack T7 | FIO0-FIO7, EIO0-EIO7, CIO0-CIO3 | 3.3V | | MCC USB-202 | DIO0-DIO7 | 5V TTL | ### Pin Naming **LabJack T7:** | Pin Input | Channel | | ----------------- | --------- | | `0`-`7` | FIO0-FIO7 | | `8`-`15` | EIO0-EIO7 | | `16`-`19` | CIO0-CIO3 | | `"FIO0"`-`"FIO7"` | FIO0-FIO7 | **MCC USB-202:** | Pin Input | Channel | | ----------------- | --------- | | `0`-`7` | DIO0-DIO7 | | `"DIO0"`-`"DIO7"` | DIO0-DIO7 | ## Notes * GPIO nets work directly without `enable()`/`disable()` calls * `input()` returns 0 or 1 * `output()` accepts integers (0/1) or strings ("high"/"low", "on"/"off", "true"/"false") * `wait_for_level()` blocks the calling thread until the target level is detected * LabJack T7 `wait_for_level()` uses hardware streaming for sub-millisecond detection * Net names must match those configured on the Lager Box # I2C Source: https://docs.lagerdata.com/source/reference/python/i2c Communicate with I2C devices on the bus Read, write, and scan I2C (Inter-Integrated Circuit) devices connected to a Lager Box. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | -------------- | -------------------------------------------------------- | | `config()` | Configure I2C bus parameters | | `scan()` | Scan bus for connected devices | | `read()` | Read bytes from a device | | `write()` | Write bytes to a device | | `write_read()` | Write then read in a single transaction (repeated start) | | `get_config()` | Get raw net configuration | ## Method Reference ### `Net.get(name, type=NetType.I2C)` Get an I2C net by name. ```python theme={null} from lager import Net, NetType i2c = Net.get('MY_I2C_NET', type=NetType.I2C) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------- | | `name` | `str` | Name of the I2C net | | `type` | `NetType` | Must be `NetType.I2C` | **Returns:** I2C Net instance ### `config(frequency_hz, pull_ups)` Configure I2C bus parameters. Only explicitly-provided parameters are changed; omitted parameters retain their stored values. ```python theme={null} i2c.config(frequency_hz=400_000) i2c.config(frequency_hz=100_000, pull_ups=True) ``` | Parameter | Type | Description | | -------------- | ---------------- | --------------------------------------------------------------------------- | | `frequency_hz` | `int` or `None` | Clock frequency in Hz (e.g., 100000, 400000). `None` keeps stored value | | `pull_ups` | `bool` or `None` | Enable/disable internal pull-ups (Aardvark only). `None` keeps stored value | ### `scan(start_addr, end_addr)` Scan the I2C bus for connected devices. ```python theme={null} devices = i2c.scan() print(f"Found: {[hex(a) for a in devices]}") ``` | Parameter | Type | Description | | ------------ | ----- | --------------------------------------------- | | `start_addr` | `int` | First 7-bit address to probe (default `0x08`) | | `end_addr` | `int` | Last 7-bit address to probe (default `0x77`) | **Returns:** `list[int]` - List of 7-bit addresses that responded with ACK ### `read(address, num_bytes, output_format, overrides)` Read bytes from an I2C device. ```python theme={null} data = i2c.read(address=0x48, num_bytes=2) temp = (data[0] << 8) | data[1] ``` | Parameter | Type | Description | | --------------- | ---------------- | ------------------------------------------------------------ | | `address` | `int` | 7-bit device address (`0x00`-`0x7F`) | | `num_bytes` | `int` | Number of bytes to read | | `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` | | `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) | **Returns:** `list[int]` - Received bytes as integers (when `output_format="list"`) ### `write(address, data, overrides)` Write bytes to an I2C device. ```python theme={null} i2c.write(address=0x48, data=[0x0A, 0x03]) ``` | Parameter | Type | Description | | ----------- | ---------------- | ------------------------------------------------------------ | | `address` | `int` | 7-bit device address (`0x00`-`0x7F`) | | `data` | `list[int]` | Bytes to write | | `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) | ### `write_read(address, data, num_bytes, output_format, overrides)` Write then read in a single I2C transaction using a repeated start condition. This is the standard pattern for reading device registers. ```python theme={null} # Read 2-byte temperature register at address 0x00 temp_bytes = i2c.write_read(address=0x48, data=[0x00], num_bytes=2) temperature = (temp_bytes[0] << 8) | temp_bytes[1] ``` | Parameter | Type | Description | | --------------- | ---------------- | ------------------------------------------------------------ | | `address` | `int` | 7-bit device address (`0x00`-`0x7F`) | | `data` | `list[int]` | Bytes to write before reading (typically a register address) | | `num_bytes` | `int` | Number of bytes to read after writing | | `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` | | `overrides` | `dict` or `None` | Per-call config overrides (e.g., `{"frequency_hz": 400000}`) | **Returns:** `list[int]` - Received bytes as integers (when `output_format="list"`) ### `get_config()` Get the raw net configuration dictionary. ```python theme={null} cfg = i2c.get_config() print(cfg['name']) print(cfg['params']) ``` **Returns:** `dict` - Full net configuration including name, role, instrument, and params ## Output Formats The `output_format` parameter on `read()` and `write_read()` controls how data is returned: | Format | Return Type | Example | | --------- | ----------- | -------------------------- | | `"list"` | `list[int]` | `[72, 118, 153]` | | `"hex"` | `str` | `"48 76 99"` | | `"bytes"` | `str` | `"72 118 153"` | | `"json"` | `dict` | `{"data": [72, 118, 153]}` | ## Examples ### Basic Device Read ```python theme={null} from lager import Net, NetType i2c = Net.get('my_i2c', type=NetType.I2C) # Scan for devices devices = i2c.scan() print(f"Found devices at: {[hex(a) for a in devices]}") # Read 2 bytes from device at 0x48 data = i2c.read(address=0x48, num_bytes=2) print(f"Data: {data}") ``` ### Register Read/Write ```python theme={null} from lager import Net, NetType i2c = Net.get('my_i2c', type=NetType.I2C) # Write configuration register i2c.write(address=0x48, data=[0x01, 0x60, 0xA0]) # Read temperature register (write register addr, then read 2 bytes) temp_bytes = i2c.write_read(address=0x48, data=[0x00], num_bytes=2) raw = (temp_bytes[0] << 8) | temp_bytes[1] celsius = raw / 256.0 print(f"Temperature: {celsius:.1f} C") ``` ### Bus Configuration ```python theme={null} from lager import Net, NetType i2c = Net.get('my_i2c', type=NetType.I2C) # Configure for 400 kHz Fast Mode with pull-ups i2c.config(frequency_hz=400_000, pull_ups=True) # Scan a specific address range devices = i2c.scan(start_addr=0x20, end_addr=0x7F) for addr in devices: print(f" 0x{addr:02x}") ``` ### Multi-Device Setup ```python theme={null} from lager import Net, NetType i2c = Net.get('sensor_bus', type=NetType.I2C) i2c.config(frequency_hz=100_000) # Read from multiple sensors on the same bus TEMP_SENSOR = 0x48 PRESSURE_SENSOR = 0x76 # Temperature (TMP102) temp_raw = i2c.write_read(address=TEMP_SENSOR, data=[0x00], num_bytes=2) temp_c = ((temp_raw[0] << 4) | (temp_raw[1] >> 4)) * 0.0625 print(f"Temperature: {temp_c:.1f} C") # Pressure (BMP280) - read chip ID register chip_id = i2c.write_read(address=PRESSURE_SENSOR, data=[0xD0], num_bytes=1) print(f"BMP280 chip ID: 0x{chip_id[0]:02x}") ``` ### Per-Call Configuration Override ```python theme={null} from lager import Net, NetType i2c = Net.get('my_i2c', type=NetType.I2C) i2c.config(frequency_hz=100_000) # Most devices use standard mode data = i2c.read(address=0x48, num_bytes=2) # One device needs fast mode for this transaction data = i2c.read(address=0x50, num_bytes=256, overrides={"frequency_hz": 400_000}) ``` ## Supported Hardware | Adapter | Description | | ---------------- | ---------------------------------------------- | | LabJack T7 | Uses GPIO pins (FIO/EIO) for SDA and SCL | | Aardvark I2C/SPI | Dedicated USB I2C adapter with pull-up support | ## Notes * Net must be configured as `NetType.I2C` * Addresses are 7-bit format (`0x00`-`0x7F`), not left-shifted * `pull_ups` only works on the Aardvark adapter; ignored on LabJack T7 * `write_read()` uses a repeated start condition for atomic register reads * Default scan range (`0x08`-`0x77`) skips reserved addresses * Configuration changes persist to `saved_nets.json` for subsequent commands * LabJack T7 runs at approximately 450 kHz regardless of requested frequency due to hardware limitations # Logic Analyzer (Preview) Source: https://docs.lagerdata.com/source/reference/python/logic Digital signal capture and protocol decoding Capture and analyze digital signals using the logic analyzer functionality of mixed-signal oscilloscopes. **Not Yet Available:** The Logic Analyzer Python API for Rigol MSO5000 series is currently under development. The Net-based API and associated methods are documented for preview purposes only. The underlying device implementation is not yet complete. Attempting to use logic analyzer nets will result in a "method not found" error. Check back in a future release for full functionality. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ------------------------------------------------- | --------------------------------- | | `enable()` | Enable the logic channel display | | `disable()` | Disable the logic channel display | | `start_capture()` | Start continuous acquisition | | `stop_capture()` | Stop acquisition | | `start_single_capture()` | Start single-shot capture | | `force_trigger()` | Force a trigger event | | `set_signal_threshold()` | Set logic level threshold voltage | | `display_position()` | Set channel display position | | `size_large()` / `size_medium()` / `size_small()` | Set display size | ## Method Reference ### `Net.get(name, type=NetType.Logic)` Get a logic analyzer net by name. ```python theme={null} from lager import Net, NetType logic = Net.get('SPI_CLK', type=NetType.Logic) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ----------------------- | | `name` | `str` | Name of the logic net | | `type` | `NetType` | Must be `NetType.Logic` | **Returns:** Logic analyzer Net instance ### `enable()` Enable the logic channel display. ```python theme={null} logic.enable() ``` ### `disable()` Disable the logic channel display. ```python theme={null} logic.disable() ``` ### `start_capture()` Start continuous waveform acquisition. ```python theme={null} logic.start_capture() ``` ### `stop_capture()` Stop waveform acquisition. ```python theme={null} logic.stop_capture() ``` ### `start_single_capture()` Start single-shot capture (captures one triggered event). ```python theme={null} logic.start_single_capture() ``` ### `force_trigger()` Force a trigger event immediately. ```python theme={null} logic.force_trigger() ``` ### `set_signal_threshold(voltage)` Set the logic level threshold voltage. ```python theme={null} logic.set_signal_threshold(1.65) # 1.65V for 3.3V CMOS logic.set_signal_threshold(2.5) # 2.5V for 5V TTL ``` | Parameter | Type | Description | | --------- | ------- | -------------------------- | | `voltage` | `float` | Threshold voltage in volts | **Note:** Channels 0-7 share one threshold, channels 8-15 share another. ### `display_position(position)` Set the channel display position. ```python theme={null} logic.display_position(100) # Set vertical position ``` | Parameter | Type | Description | | ---------- | ----- | ----------------------- | | `position` | `int` | Vertical position value | ### `size_large()` / `size_medium()` / `size_small()` Set the display size for enabled channels. ```python theme={null} logic.size_large() # Maximum visibility logic.size_medium() # Balanced logic.size_small() # Compact view ``` ## Trigger Settings Logic analyzer nets support advanced triggering through `trigger_settings`: ### Edge Trigger ```python theme={null} logic = Net.get('SPI_CLK', type=NetType.Logic) # Set edge trigger on this channel logic.trigger_settings.edge.set_source(logic) logic.trigger_settings.edge.set_slope_rising() logic.trigger_settings.set_mode_normal() ``` **Edge trigger methods:** | Method | Description | | -------------------------- | ------------------------- | | `edge.set_source(net)` | Set trigger source net | | `edge.set_slope_rising()` | Trigger on rising edge | | `edge.set_slope_falling()` | Trigger on falling edge | | `edge.set_slope_both()` | Trigger on either edge | | `edge.get_slope()` | Get current slope setting | ### Pulse Trigger ```python theme={null} logic = Net.get('PULSE_SIG', type=NetType.Logic) # Trigger on pulse width > 1ms logic.trigger_settings.pulse.set_source(logic) logic.trigger_settings.pulse.set_trigger_on_pulse_greater_than_width(0.001) # Trigger on pulse width < 100us logic.trigger_settings.pulse.set_trigger_on_pulse_less_than_width(0.0001) ``` ### Protocol Triggers #### UART Trigger ```python theme={null} logic = Net.get('UART_TX', type=NetType.Logic) logic.trigger_settings.uart.set_source(logic) logic.trigger_settings.uart.set_uart_params(baud=115200, bits=8, parity=None, stopbits=1) # Trigger on start bit logic.trigger_settings.uart.set_trigger_on_start() # Trigger on specific data logic.trigger_settings.uart.set_trigger_on_data(data=0x55) # Trigger on frame error logic.trigger_settings.uart.set_trigger_on_frame_error() ``` #### I2C Trigger ```python theme={null} scl = Net.get('I2C_SCL', type=NetType.Logic) sda = Net.get('I2C_SDA', type=NetType.Logic) scl.trigger_settings.i2c.set_source(net_scl=scl, net_sda=sda) # Trigger on start condition scl.trigger_settings.i2c.set_trigger_on_start() # Trigger on specific address scl.trigger_settings.i2c.set_trigger_on_address(bits=7, address=0x48) # Trigger on NACK scl.trigger_settings.i2c.set_trigger_on_nack() ``` #### SPI Trigger ```python theme={null} clk = Net.get('SPI_CLK', type=NetType.Logic) mosi = Net.get('SPI_MOSI', type=NetType.Logic) cs = Net.get('SPI_CS', type=NetType.Logic) clk.trigger_settings.spi.set_source(net_sck=clk, net_mosi_miso=mosi, net_cs=cs) clk.trigger_settings.spi.set_clk_edge_positive() # Trigger on specific data clk.trigger_settings.spi.set_trigger_data(bits=8, data=0xAA) # Trigger on CS clk.trigger_settings.spi.set_trigger_on_cs_low() ``` #### CAN Trigger ```python theme={null} can = Net.get('CAN_RX', type=NetType.Logic) can.trigger_settings.can.set_source(can) can.trigger_settings.can.set_baud(500000) # Trigger on start of frame can.trigger_settings.can.set_trigger_on_sof() # Trigger on error frame can.trigger_settings.can.set_trigger_on_error_frame() ``` ## Measurements Logic analyzer nets support digital timing measurements: ```python theme={null} logic = Net.get('CLK', type=NetType.Logic) # Frequency and period freq = logic.measurement.frequency() period = logic.measurement.period() # Pulse measurements pos_width = logic.measurement.pulse_width_positive() neg_width = logic.measurement.pulse_width_negative() pos_duty = logic.measurement.duty_cycle_positive() neg_duty = logic.measurement.duty_cycle_negative() # Rise/fall times rise = logic.measurement.rise_time() fall = logic.measurement.fall_time() # Edge counts pos_edges = logic.measurement.positive_edge_count() neg_edges = logic.measurement.negative_edge_count() ``` ## Bus Decoding For protocol analysis, create bus decoders using multiple logic channels: ### UART Bus ```python theme={null} from lager.nets.mappers.rigol_mso5000 import BusUART_RigolMSO5000FunctionMapper tx = Net.get('UART_TX', type=NetType.Logic) rx = Net.get('UART_RX', type=NetType.Logic) bus = BusUART_RigolMSO5000FunctionMapper(tx=tx, rx=rx) bus.set_baud(115200) bus.set_data_bits(8) bus.set_parity_none() bus.set_stop_bits(1) bus.enable() bus.show_table() ``` ### I2C Bus ```python theme={null} from lager.nets.mappers.rigol_mso5000 import BusI2C_RigolMSO5000FunctionMapper scl = Net.get('I2C_SCL', type=NetType.Logic) sda = Net.get('I2C_SDA', type=NetType.Logic) bus = BusI2C_RigolMSO5000FunctionMapper(scl=scl, sda=sda) bus.set_signal_threshold(sda=1.5, scl=1.5) bus.enable() bus.show_table() ``` ### SPI Bus ```python theme={null} from lager.nets.mappers.rigol_mso5000 import BusSPI_RigolMSO5000FunctionMapper clk = Net.get('SPI_CLK', type=NetType.Logic) mosi = Net.get('SPI_MOSI', type=NetType.Logic) miso = Net.get('SPI_MISO', type=NetType.Logic) cs = Net.get('SPI_CS', type=NetType.Logic) bus = BusSPI_RigolMSO5000FunctionMapper(clk=clk, mosi=mosi, miso=miso, cs=cs) bus.set_sck_phase_rising_edge() bus.set_data_width(8) bus.set_endianness_msb() bus.enable() bus.show_table() ``` ### CAN Bus ```python theme={null} from lager.nets.mappers.rigol_mso5000 import BusCAN_RigolMSO5000FunctionMapper can_net = Net.get('CAN_RX', type=NetType.Logic) bus = BusCAN_RigolMSO5000FunctionMapper(can=can_net) bus.set_baud(500000) bus.set_signal_type_rx() bus.set_signal_threshold(2.0) bus.enable() bus.show_table() ``` ## Examples ### Basic Digital Signal Capture ```python theme={null} from lager import Net, NetType import time # Get logic channel clk = Net.get('SYS_CLK', type=NetType.Logic) # Configure clk.enable() clk.set_signal_threshold(1.65) # 3.3V logic clk.size_medium() # Set trigger clk.trigger_settings.edge.set_source(clk) clk.trigger_settings.edge.set_slope_rising() clk.trigger_settings.set_mode_normal() # Capture clk.start_capture() time.sleep(1) # Measure freq = clk.measurement.frequency() print(f"Clock frequency: {freq / 1e6:.3f} MHz") clk.stop_capture() clk.disable() ``` ### SPI Communication Test ```python theme={null} from lager import Net, NetType from lager.nets.mappers.rigol_mso5000 import BusSPI_RigolMSO5000FunctionMapper import time # Get SPI signals clk = Net.get('SPI_CLK', type=NetType.Logic) mosi = Net.get('SPI_MOSI', type=NetType.Logic) miso = Net.get('SPI_MISO', type=NetType.Logic) cs = Net.get('SPI_CS', type=NetType.Logic) # Enable channels for net in [clk, mosi, miso, cs]: net.enable() net.set_signal_threshold(1.65) # Create bus decoder bus = BusSPI_RigolMSO5000FunctionMapper(clk=clk, mosi=mosi, miso=miso, cs=cs) bus.set_sck_phase_rising_edge() bus.set_data_width(8) bus.enable() bus.show_table() # Trigger on CS going low clk.trigger_settings.spi.set_trigger_on_cs_low() clk.trigger_settings.set_mode_single() # Start capture clk.start_single_capture() # Wait for trigger or timeout time.sleep(5) # View decoded data in table print("SPI transaction captured - check scope display") # Clean up bus.disable() for net in [clk, mosi, miso, cs]: net.disable() ``` ### I2C Address Scanner ```python theme={null} from lager import Net, NetType import time scl = Net.get('I2C_SCL', type=NetType.Logic) sda = Net.get('I2C_SDA', type=NetType.Logic) scl.enable() sda.enable() scl.set_signal_threshold(1.65) sda.set_signal_threshold(1.65) # Configure I2C trigger scl.trigger_settings.i2c.set_source(net_scl=scl, net_sda=sda) scl.trigger_settings.i2c.set_scl_trigger_level(1.65) scl.trigger_settings.i2c.set_sda_trigger_level(1.65) # Trigger on start condition to capture all traffic scl.trigger_settings.i2c.set_trigger_on_start() scl.trigger_settings.set_mode_normal() scl.start_capture() print("Monitoring I2C bus - trigger on start condition") # Let it run and capture activity time.sleep(10) scl.stop_capture() scl.disable() sda.disable() ``` ### Protocol Timing Verification ```python theme={null} from lager import Net, NetType # Test UART timing uart_tx = Net.get('UART_TX', type=NetType.Logic) uart_tx.enable() uart_tx.set_signal_threshold(1.65) uart_tx.start_capture() # Measure bit timing period = uart_tx.measurement.period() if period: measured_baud = 1.0 / period expected_baud = 115200 error_pct = abs(measured_baud - expected_baud) / expected_baud * 100 print(f"Measured baud: {measured_baud:.0f}") print(f"Expected baud: {expected_baud}") print(f"Error: {error_pct:.2f}%") if error_pct < 3: print("PASS: Baud rate within tolerance") else: print("FAIL: Baud rate out of tolerance") uart_tx.stop_capture() uart_tx.disable() ``` ## Digital Channels | Channel | Range | | ------- | ------------------------ | | D0-D7 | Pod 1 (shared threshold) | | D8-D15 | Pod 2 (shared threshold) | ## Supported Hardware | Manufacturer | Model | Features | | ------------ | -------------- | ------------------------------------ | | Rigol | MSO5000 series | 16 digital channels, protocol decode | ## Notes * Logic channels are numbered D0-D15 * Channels D0-D7 share one threshold voltage, D8-D15 share another * Protocol decoding requires enabling bus analysis mode * Use `NetType.Logic` for digital channels, `NetType.Analog` for analog * Bus decoders work with both Logic and Analog nets as sources * The trigger can use any combination of analog and digital channels # Net Source: https://docs.lagerdata.com/source/reference/python/net Core Net class for managing hardware connections The `Net` class is the primary abstraction for interacting with hardware instruments. It provides a unified interface for controlling different types of hardware including power supplies, oscilloscopes, GPIO, ADC, DAC, and more. ## Import ```python theme={null} from lager import Net, NetType # For exception handling from lager import InvalidNetError, SetupFunctionRequiredError ``` ## Class Methods | Method | Description | | ----------------------------- | --------------------------------------------------------- | | `Net.get()` | Create a Net instance for the specified net name and type | | `Net.list_saved()` | List all nets configured on the Lager Box | | `Net.list_all_from_env()` | List nets from environment (legacy) | | `Net.get_local_nets()` | Get all local net configurations | | `Net.save_local_nets()` | Save multiple net configurations | | `Net.save_local_net()` | Save a single net configuration | | `Net.delete_local_net()` | Delete a net configuration | | `Net.delete_all_local_nets()` | Delete all net configurations | | `Net.rename_local_net()` | Rename a net configuration | | `Net.filter_nets()` | Filter nets by name and/or role | ## Instance Methods | Method | Description | | ----------- | -------------------------------------------- | | `enable()` | Enable the net and connect to hardware | | `disable()` | Disable the net and disconnect from hardware | ## Method Reference ### `Net.get(name, type, *, setup_function=None, teardown_function=None)` Create a Net instance for the specified net name and type. ```python theme={null} # Get a power supply net psu = Net.get('VDD', type=NetType.PowerSupply) # Get a GPIO net led = Net.get('LED', type=NetType.GPIO) # Get an analog oscilloscope net scope = Net.get('PROBE', type=NetType.Analog) ``` **Parameters:** | Parameter | Type | Description | | ------------------- | ---------- | --------------------------------------------------------- | | `name` | `str` | Name of the net to create | | `type` | `NetType` | Type of net (e.g., `NetType.PowerSupply`, `NetType.GPIO`) | | `setup_function` | `callable` | Optional function called when net is enabled | | `teardown_function` | `callable` | Optional function called when net is disabled | **Returns:** Net instance appropriate for the specified type ### `Net.list_saved()` List all nets configured on the Lager Box. ```python theme={null} nets = Net.list_saved() for net in nets: print(f"{net['name']}: {net['role']}") ``` **Returns:** `list[dict]` - List of net configurations with keys: * `name` (str) - Net name * `role` (str) - Net type/role * `channel` (int) - Hardware channel number * `instrument` (str) - Associated instrument type ### `Net.list_all_from_env()` List nets from the LAGER\_MUXES environment variable (legacy behavior). ```python theme={null} nets = Net.list_all_from_env() for net in nets: print(f"{net['name']}: {net['role']} on channel {net['channel']}") ``` **Returns:** `list[dict]` - List of net information ### `Net.save_local_net(data)` Save a net configuration to the Lager Box. ```python theme={null} Net.save_local_net({ 'name': 'VDD', 'role': 'power-supply', 'channel': 1, 'instrument': 'rigol_dp800', 'address': '192.168.1.100' }) ``` **Parameters:** | Parameter | Type | Description | | --------- | ------ | ---------------------------- | | `data` | `dict` | Net configuration dictionary | ### `Net.delete_local_net(name, role=None)` Delete a net configuration from the Lager Box. ```python theme={null} # Delete by name only Net.delete_local_net('VDD') # Delete by name and role Net.delete_local_net('VDD', role='power-supply') ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | ---------------------- | | `name` | `str` | Name of net to delete | | `role` | `str` | Optional role to match | **Returns:** `bool` - True if net was deleted ### `Net.rename_local_net(old_name, new_name)` Rename a net configuration. ```python theme={null} Net.rename_local_net('OLD_NAME', 'NEW_NAME') ``` **Parameters:** | Parameter | Type | Description | | ---------- | ----- | ---------------- | | `old_name` | `str` | Current net name | | `new_name` | `str` | New net name | **Returns:** `bool` - True if net was renamed ### `Net.get_local_nets()` Get all local net configurations. ```python theme={null} nets = Net.get_local_nets() for net in nets: print(f"{net['name']}: {net['role']}") ``` **Returns:** `list[dict]` - List of net configuration dictionaries ### `Net.save_local_nets(nets)` Save multiple net configurations at once. ```python theme={null} Net.save_local_nets([ {'name': 'VDD', 'role': 'power-supply', 'channel': 1, 'instrument': 'rigol_dp800', 'address': '192.168.1.100'}, {'name': 'GND', 'role': 'power-supply', 'channel': 2, 'instrument': 'rigol_dp800', 'address': '192.168.1.100'} ]) ``` **Parameters:** | Parameter | Type | Description | | --------- | ------------ | -------------------------------------- | | `nets` | `list[dict]` | List of net configuration dictionaries | ### `Net.delete_all_local_nets()` Delete all net configurations from the Lager Box. ```python theme={null} Net.delete_all_local_nets() ``` **Returns:** `bool` - True if nets were deleted ### `Net.filter_nets(all_nets, name, role=None)` Filter a list of nets by name and optionally by role. ```python theme={null} all_nets = Net.get_local_nets() # Find all nets named 'VDD' vdd_nets = Net.filter_nets(all_nets, 'VDD') # Find 'VDD' with specific role psu_nets = Net.filter_nets(all_nets, 'VDD', role='power-supply') ``` **Parameters:** | Parameter | Type | Description | | ---------- | ------------ | ---------------------- | | `all_nets` | `list[dict]` | List of nets to search | | `name` | `str` | Net name to match | | `role` | `str` | Optional role to match | **Returns:** `list[dict]` - Matching nets ### `enable()` Enable the net and connect to hardware. ```python theme={null} scope = Net.get('PROBE', type=NetType.Analog) scope.enable() # Connect to oscilloscope ``` **Behavior by net type:** * **Analog**: Connects to multiplexer and enables oscilloscope channel * **Logic**: Enables logic analyzer channel * **Battery**: Enables battery simulation output * **PowerSupply**: Enables power supply output * **ELoad**: Enables electronic load ### `disable(teardown=True)` Disable the net and disconnect from hardware. ```python theme={null} scope.disable() # Disconnect and run teardown scope.disable(teardown=False) # Disconnect without teardown ``` **Parameters:** | Parameter | Type | Default | Description | | ---------- | ------ | ------- | --------------------------------- | | `teardown` | `bool` | `True` | Whether to call teardown function | ## NetType Enum Available net types: | NetType | Role String | Description | | ----------------------- | ----------------- | -------------------------------------------- | | `NetType.Analog` | `analog` | Oscilloscope analog input | | `NetType.Logic` | `logic` | Logic analyzer input | | `NetType.Waveform` | `waveform` | Waveform generator | | `NetType.Battery` | `battery` | Battery simulator | | `NetType.PowerSupply` | `power-supply` | Power supply | | `NetType.ELoad` | `eload` | Electronic load | | `NetType.GPIO` | `gpio` | Digital I/O | | `NetType.ADC` | `adc` | Analog-to-digital converter | | `NetType.DAC` | `dac` | Digital-to-analog converter | | `NetType.Thermocouple` | `thermocouple` | Temperature sensor | | `NetType.WattMeter` | `watt-meter` | Power meter | | `NetType.UART` | `uart` | Serial communication | | `NetType.Debug` | `debug` | Debug probe | | `NetType.Arm` | `arm` | Robotic arm | | `NetType.Usb` | `usb` | USB device | | `NetType.Rotation` | `rotation` | Rotary encoder | | `NetType.Wifi` | `wifi` | WiFi module | | `NetType.Actuate` | `actuate` | Actuator control | | `NetType.PowerSupply2Q` | `power-supply-2q` | Two-quadrant power supply (solar simulation) | ## Properties ### `name` Get the net name. ```python theme={null} print(net.name) # 'VDD' ``` ### `type` Get the net type. ```python theme={null} print(net.type) # NetType.PowerSupply ``` ## Examples ### List and Use Nets ```python theme={null} from lager import Net, NetType # List all available nets nets = Net.list_saved() print("Available nets:") for net in nets: print(f" {net['name']}: {net['role']}") # Get and use a specific net psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.enable() ``` ### Simple Nets (GPIO, ADC, DAC) ```python theme={null} from lager import Net, NetType # GPIO - no enable/disable needed button = Net.get('BUTTON', type=NetType.GPIO) state = button.input() led = Net.get('LED', type=NetType.GPIO) led.output(1) # ADC - no enable/disable needed sensor = Net.get('SENSOR', type=NetType.ADC) voltage = sensor.input() # DAC - no enable/disable needed vref = Net.get('VREF', type=NetType.DAC) vref.output(2.5) ``` ### Complex Nets (Power, Scope) ```python theme={null} from lager import Net, NetType # Power supply - requires enable/disable psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.set_current(0.5) psu.enable() # ... use the power supply ... psu.disable() # Oscilloscope - requires enable/disable scope = Net.get('PROBE', type=NetType.Analog) scope.enable() freq = scope.measurement.frequency() scope.disable() ``` ### Manage Net Configuration ```python theme={null} from lager import Net # Save a new net Net.save_local_net({ 'name': 'NEW_PSU', 'role': 'power-supply', 'channel': 2, 'instrument': 'rigol_dp800', 'address': '192.168.1.100' }) # Rename a net Net.rename_local_net('NEW_PSU', 'MAIN_POWER') # Delete a net Net.delete_local_net('MAIN_POWER') ``` ## Notes * Simple nets (GPIO, ADC, DAC, Thermocouple) work directly without `enable()`/`disable()` calls * Complex nets (Analog, Logic, PowerSupply, Battery, ELoad) require `enable()` before use * Always call `disable()` when finished with complex nets to properly release hardware * Net names must match those configured on the Lager Box * Use `Net.list_saved()` to see all available nets # Python SDK Overview Source: https://docs.lagerdata.com/source/reference/python/overview Introduction to the Lager Python SDK for test automation and hardware control The Lager Python SDK provides a powerful, object-oriented interface for controlling hardware and automating tests on your Device Under Test (DUT). It enables programmatic control of power supplies, sensors, debug probes, and more. ## Import ```python theme={null} from lager import Net, NetType # Convenience import for Net and NetType ``` ## Core Classes | Class | Description | Import | | ------------------------ | -------------------------------------------- | ---------------------------------------------- | | [`binaries`](./binaries) | Execute custom binaries | `from lager.binaries import run_custom_binary` | | [`Net`](./net) | Core class for managing hardware connections | `from lager import Net, NetType` | | [`Central`](./ble) | BLE scanning and connection | `from lager.ble import Central, Client` | ## Net Types The SDK supports various net types for different hardware: | NetType | Description | Hardware | | ------------------------ | ------------------------------- | ------------------------------- | | `NetType.PowerSupply` | Programmable power supply | Rigol DP800, Keithley, Keysight | | `NetType.PowerSupply2Q` | Two-quadrant supply (solar sim) | EA PSI/EL series | | `NetType.Battery` | Battery simulator | Keithley 2281S | | `NetType.ELoad` | Electronic load | Rigol DL3000 | | `NetType.Analog` | Oscilloscope analog input | Rigol MSO5000 | | `NetType.Logic` | Logic analyzer input | Rigol MSO5000 | | `NetType.Waveform` | Waveform generator | Rigol MSO5000 | | `NetType.GPIO` | Digital I/O | LabJack T7, MCC USB-202 | | `NetType.ADC` | Analog-to-digital converter | LabJack T7, MCC USB-202 | | `NetType.DAC` | Digital-to-analog converter | LabJack T7, MCC USB-202 | | `NetType.Thermocouple` | Temperature sensor | Phidget | | `NetType.Rotation` | Rotary encoder | Phidget | | `NetType.WattMeter` | Power meter | Yocto-Watt, Joulescope JS220 | | `NetType.UART` | Serial communication | USB Serial | | `NetType.Debug` | Debug probe | J-Link, pyOCD | | `NetType.Arm` | Robotic arm | Rotrics Dexarm | | `NetType.Usb` | USB port control | Acroname, YKUSH | | `NetType.Wifi` | WiFi module | Lager Box WiFi | | `NetType.Actuate` | Actuator control | Dexarm actuator | | `NetType.SPI` | SPI bus communication | Aardvark, FT232H, LabJack T7 | | `NetType.I2C` | I2C bus communication | Aardvark, FT232H, LabJack T7 | | `NetType.EnergyAnalyzer` | Energy integration measurement | Joulescope JS220 | | `NetType.Webcam` | Video streaming | USB Webcam | ## Quick Start ### List Available Nets ```python theme={null} from lager import Net nets = Net.list_saved() for net in nets: print(f"{net['name']}: {net['role']}") ``` ### Control a Power Supply ```python theme={null} from lager import Net, NetType # Get the power supply net psu = Net.get('VDD', type=NetType.PowerSupply) # Configure and enable psu.set_voltage(3.3) psu.set_current(0.5) psu.enable() # Read measurements print(f"Voltage: {psu.voltage()}V") print(f"Current: {psu.current()}A") # Disable when done psu.disable() ``` ### Read an ADC ```python theme={null} from lager import Net, NetType adc = Net.get('SENSOR', type=NetType.ADC) voltage = adc.input() print(f"Voltage: {voltage}V") ``` ### Control GPIO ```python theme={null} from lager import Net, NetType # Read input button = Net.get('BUTTON', type=NetType.GPIO) state = button.input() # Set output led = Net.get('LED', type=NetType.GPIO) led.output(1) # HIGH led.output(0) # LOW ``` ### Control Debug Probe ```python theme={null} from lager import Net, NetType # Get the debug net debug = Net.get('jlink1', type=NetType.Debug) # Connect and flash firmware debug.connect() debug.flash('firmware.hex') debug.reset() ``` ### Control USB Hub ```python theme={null} from lager import Net, NetType # Get the USB net usb = Net.get('SENSOR_USB', type=NetType.Usb) # Power control usb.enable() # Power on usb.disable() # Power off usb.toggle() # Toggle state ``` ## Complete Example ```python theme={null} from lager import Net, NetType import time # 1. Flash firmware debug = Net.get('jlink1', type=NetType.Debug) debug.connect() debug.flash('firmware.hex') debug.reset() print("Firmware flashed") # 2. Power on USB peripheral usb = Net.get('SENSOR_USB', type=NetType.Usb) usb.enable() print("USB sensor powered on") # 3. Enable main power main_power = Net.get("VDD_MAIN", type=NetType.PowerSupply) main_power.set_voltage(3.3) main_power.enable() print("Main power enabled") # 4. Read sensor sensor = Net.get("TEMP_SENSE", type=NetType.ADC) temperature = sensor.input() print(f"Temperature: {temperature}V") # 5. Clean up main_power.disable() usb.disable() print("Test complete") ``` ## API Reference ### Core Classes | Page | Description | | ----------------------------- | -------------------------------------------------------- | | [Custom Binaries](./binaries) | Execute custom binaries on the Lager Box | | [Net](./net) | Hardware net management and core operations | | [Debug](./debug) | Device flashing, reset, and debug control (includes RTT) | | [USB](./usb) | USB device power control | ### Power & Simulation | Page | Description | | -------------------------- | --------------------------------- | | [Power Supply](./supply) | Programmable power supply control | | [Battery](./battery) | Battery simulation and testing | | [Solar](./solar) | Solar panel simulation | | [Electronic Load](./eload) | Electronic load control | | [Watt Meter](./watt) | Power measurement | ### Measurement | Page | Description | | ------------------------- | ---------------------------------------------- | | [Scope](./scope) | Oscilloscope waveform capture and measurements | | [Logic Analyzer](./logic) | Digital signal capture and protocol decode | | [ADC](./adc) | Analog-to-digital conversion | | [Thermocouple](./tc) | Temperature measurement | ### I/O & Communication | Page | Description | | ------------------ | ---------------------------------- | | [GPIO](./gpio) | Digital input/output control | | [DAC](./dac) | Digital-to-analog conversion | | [I2C](./i2c) | I2C bus communication | | [SPI](./spi) | SPI bus communication | | [UART](./uart) | UART net serial communication | | [Serial](./serial) | Native pyserial support | | [BLE](./ble) | Bluetooth Low Energy communication | | [WiFi](./wifi) | WiFi configuration | ### Utilities | Page | Description | | ------------------ | ---------------------------------- | | [Robot Arm](./arm) | Robotic arm control | | [Webcam](./webcam) | Webcam streaming and video capture | ## Error Handling ```python theme={null} from lager import Net, NetType, InvalidNetError try: net = Net.get('INVALID_NET', NetType.Analog) except InvalidNetError as e: print(f"Net not found: {e}") except Exception as e: print(f"Error: {e}") ``` ## Notes * Always call `disable()` when finished with power-related nets * Simple nets (GPIO, ADC, DAC) don't require `enable()`/`disable()` calls * Complex nets (PowerSupply, Battery, Analog) require `enable()` before use * Net names must match those configured in the Lager system * Use try/except blocks for robust error handling ## Demo Script For a more comprehensive example combining robot arm control, USB hub power cycling, debug probe flashing, power supply management, and ADC measurement in a single automated workflow, see the [demo script](https://github.com/lagerdata/lager/blob/main/docs/examples/demo_script.py). # Oscilloscope Source: https://docs.lagerdata.com/source/reference/python/scope Python SDK for oscilloscope control The oscilloscope module provides Python interfaces for waveform capture, triggering, and measurements. ## Overview Use the scope module to control oscilloscopes for analog signal capture, triggering on specific events, and automated measurements. ## Import ```python theme={null} from lager import Net, NetType ``` ## Usage ```python theme={null} from lager import Net, NetType # Get scope net scope = Net.get('ANALOG1', type=NetType.Analog) # Enable the channel scope.enable() # Start capture scope.start_capture() # Take measurements freq = scope.measurement.frequency() period = scope.measurement.period() # Stop and disable scope.stop_capture() scope.disable() ``` ## Methods ### Channel Control #### `enable()` Enable the oscilloscope channel. ```python theme={null} scope.enable() ``` #### `disable()` Disable the oscilloscope channel. ```python theme={null} scope.disable() ``` ### Capture Control #### `start_capture()` Start continuous waveform capture. ```python theme={null} scope.start_capture() ``` #### `start_single_capture()` Start single-shot capture (captures one triggered event). ```python theme={null} scope.start_single_capture() ``` #### `stop_capture()` Stop waveform capture. ```python theme={null} scope.stop_capture() ``` ### Measurements Access comprehensive measurements through the `measurement` attribute: #### Voltage Measurements ```python theme={null} # Basic voltage vmax = scope.measurement.voltage_max() # Maximum voltage vmin = scope.measurement.voltage_min() # Minimum voltage vpp = scope.measurement.voltage_peak_to_peak() # Peak-to-peak vavg = scope.measurement.voltage_average() # Average voltage vrms = scope.measurement.voltage_rms() # RMS voltage # Waveform characteristics vtop = scope.measurement.voltage_flat_top() # Flat top voltage vbase = scope.measurement.voltage_flat_base() # Flat base voltage vamp = scope.measurement.voltage_flat_amplitude() # Amplitude # Thresholds vupper = scope.measurement.voltage_threshold_upper() vlower = scope.measurement.voltage_threshold_lower() vmid = scope.measurement.voltage_threshold_mid() # Signal quality overshoot = scope.measurement.voltage_overshoot() preshoot = scope.measurement.voltage_preshoot() ``` #### Timing Measurements ```python theme={null} # Frequency and period freq = scope.measurement.frequency() period = scope.measurement.period() # Rise and fall times rise = scope.measurement.rise_time() fall = scope.measurement.fall_time() # Pulse widths pos_width = scope.measurement.pulse_width_positive() neg_width = scope.measurement.pulse_width_negative() # Duty cycles pos_duty = scope.measurement.duty_cycle_positive() neg_duty = scope.measurement.duty_cycle_negative() # Time at voltage extremes t_vmax = scope.measurement.time_at_voltage_max() t_vmin = scope.measurement.time_at_voltage_min() # Slew rates pos_slew = scope.measurement.positive_slew_rate() neg_slew = scope.measurement.negative_slew_rate() ``` #### Counting Measurements ```python theme={null} # Edge counts pos_edges = scope.measurement.positive_edge_count() neg_edges = scope.measurement.negative_edge_count() # Pulse counts pos_pulses = scope.measurement.positive_pulse_count() neg_pulses = scope.measurement.negative_pulse_count() ``` #### Area Measurements ```python theme={null} area = scope.measurement.waveform_area() period_area = scope.measurement.waveform_period_area() ``` #### Statistical Measurements ```python theme={null} variance = scope.measurement.variance() pvrms = scope.measurement.voltage_rms_period() # Period RMS voltage ``` #### Delay and Phase Measurements ```python theme={null} # Delay measurements (between channels) rr_delay = scope.measurement.delay_rising_rising_edge() rf_delay = scope.measurement.delay_rising_falling_edge() fr_delay = scope.measurement.delay_falling_rising_edge() ff_delay = scope.measurement.delay_falling_falling_edge() # Phase measurements rr_phase = scope.measurement.phase_rising_rising_edge() rf_phase = scope.measurement.phase_rising_falling_edge() fr_phase = scope.measurement.phase_falling_rising_edge() ff_phase = scope.measurement.phase_falling_falling_edge() ``` #### Measurement Options Most measurements accept optional parameters: ```python theme={null} # Keep measurement displayed on scope freq = scope.measurement.frequency(display=True) # Enable cursor measurement mode vpp = scope.measurement.voltage_peak_to_peak(measurement_cursor=True) ``` ## Streaming (PicoScope) For PicoScope devices, streaming capabilities are available: ### `stream_start(channel, volts_per_div, time_per_div, trigger_level, trigger_slope, capture_mode, coupling)` Start streaming acquisition. **Parameters:** * `channel` (str): Channel to enable - `"A"`, `"B"`, `"1"`, `"2"` * `volts_per_div` (float): Vertical scale * `time_per_div` (float): Horizontal scale in seconds * `trigger_level` (float): Trigger level in volts * `trigger_slope` (str): `"rising"`, `"falling"`, `"either"` * `capture_mode` (str): `"auto"`, `"normal"`, `"single"` * `coupling` (str): `"dc"`, `"ac"` ```python theme={null} scope.stream_start( channel="A", volts_per_div=1.0, time_per_div=0.001, trigger_level=0.5, trigger_slope="rising", capture_mode="auto", coupling="dc" ) ``` ### `stream_stop()` Stop streaming acquisition. ```python theme={null} scope.stream_stop() ``` ### `stream_capture(output, duration, samples)` Capture data to file. **Parameters:** * `output` (str): Output file path * `duration` (float): Capture duration in seconds * `samples` (int): Number of samples (optional) ```python theme={null} scope.stream_capture( output="waveform.csv", duration=5.0 ) ``` ## Complete Example ```python theme={null} from lager import Net, NetType import time def measure_pwm_signal(): """Measure PWM signal characteristics.""" # Get scope net pwm_net = Net.get('PWM_OUTPUT', type=NetType.Analog) try: # Enable channel pwm_net.enable() # Configure trigger pwm_net.trigger_settings.set_mode_normal() pwm_net.trigger_settings.set_coupling_DC() pwm_net.trigger_settings.edge.set_source(pwm_net) pwm_net.trigger_settings.edge.set_slope_rising() pwm_net.trigger_settings.edge.set_level(1.65) # 50% of 3.3V # Start capture pwm_net.start_capture() time.sleep(0.5) # Wait for stable capture # Take measurements frequency = pwm_net.measurement.frequency() period = pwm_net.measurement.period() print(f"PWM Frequency: {frequency:.2f} Hz") print(f"PWM Period: {period*1000:.3f} ms") # Calculate duty cycle from pulse width if available # ... finally: pwm_net.stop_capture() pwm_net.disable() if __name__ == "__main__": measure_pwm_signal() ``` ### Trace Settings Configure vertical and horizontal scale through `trace_settings`: ```python theme={null} # Vertical scale (V/div) scope.trace_settings.set_volts_per_div(1.0) volts = scope.trace_settings.get_volts_per_div() # Vertical offset scope.trace_settings.set_volt_offset(0.5) offset = scope.trace_settings.get_volt_offset() # Horizontal scale (s/div) scope.trace_settings.set_time_per_div(0.001) # 1ms/div time_scale = scope.trace_settings.get_time_per_div() # Horizontal offset scope.trace_settings.set_time_offset(0.0) time_offset = scope.trace_settings.get_time_offset() ``` ### Advanced Trigger Settings Access advanced trigger configuration through `trigger_settings`: ```python theme={null} # Trigger mode scope.trigger_settings.set_mode_auto() scope.trigger_settings.set_mode_normal() scope.trigger_settings.set_mode_single() mode = scope.trigger_settings.get_mode() # Trigger coupling scope.trigger_settings.set_coupling_DC() scope.trigger_settings.set_coupling_AC() scope.trigger_settings.set_coupling_low_freq_reject() scope.trigger_settings.set_coupling_high_freq_reject() coupling = scope.trigger_settings.get_coupling() # Edge trigger settings scope.trigger_settings.edge.set_source(scope) scope.trigger_settings.edge.set_slope_rising() scope.trigger_settings.edge.set_slope_falling() scope.trigger_settings.edge.set_slope_both() scope.trigger_settings.edge.set_level(1.65) # Get status status = scope.trigger_settings.get_status() ``` ### Cursor Control Access cursor functions through the `cursor` attribute: ```python theme={null} # Set cursor positions scope.cursor.set_a(x=100, y=50) scope.cursor.set_b(x=200, y=50) # Get cursor positions ax, ay = scope.cursor.get_a() bx, by = scope.cursor.get_b() # Move cursors relatively scope.cursor.move_a(x_del=10, y_del=5) scope.cursor.move_b(x_del=-10, y_del=0) # Read cursor measurements x_delta = scope.cursor.x_delta() # Time difference y_delta = scope.cursor.y_delta() # Voltage difference inv_x = scope.cursor.frequency() # Frequency # Get individual values ax_val = scope.cursor.a_x() ay_val = scope.cursor.a_y() bx_val = scope.cursor.b_x() by_val = scope.cursor.b_y() # Hide cursor scope.cursor.hide() ``` ## Supported Hardware | Manufacturer | Model Series | Features | | ------------ | ------------ | -------------------------------------------- | | Rigol | MSO5000 | Multi-channel, mixed-signal, protocol decode | | PicoScope | Various | Streaming support | ## Notes * Use `NetType.Analog` for oscilloscope channels (1-4) * Use `NetType.Logic` for digital channels (D0-D15) on MSO scopes * Streaming features are only available on PicoScope devices * Configure trigger before starting capture for reliable measurements * Measurement methods return `float` on success, or `None` if the measurement is invalid (e.g., no signal, no trigger, wrong channel). On Rigol hardware, the instrument returns 9.9E+37 for invalid measurements, which is automatically converted to `None`. * For protocol triggering (UART, I2C, SPI, CAN), see the [Logic Analyzer](./logic) documentation # Serial Source: https://docs.lagerdata.com/source/reference/python/serial Native pyserial support for serial communication Native `pyserial` support for serial communication with your DUT. ## Import ```python theme={null} import serial ``` ## Methods | Method | Description | | -------------- | ------------------------ | | `Serial()` | Create serial connection | | `readline()` | Read a line | | `read()` | Read specified bytes | | `read_until()` | Read until delimiter | | `write()` | Write data | | `open()` | Open connection | | `close()` | Close connection | | `is_open` | Check connection state | ## Method Reference ### `serial.Serial(port, baudrate, **kwargs)` Create a serial connection. ```python theme={null} import serial ser = serial.Serial('/dev/ttyUSB1', 115200) # With additional parameters ser = serial.Serial( port='/dev/ttyUSB1', baudrate=115200, timeout=60, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE ) ``` **Parameters:** | Parameter | Type | Description | | ---------- | ------- | --------------------------------------------------- | | `port` | `str` | Serial port path (e.g., `/dev/ttyUSB1`) | | `baudrate` | `int` | Baud rate in bits per second | | `timeout` | `float` | Read timeout in seconds | | `bytesize` | `int` | Data bits (5, 6, 7, or 8) | | `parity` | `str` | Parity (`PARITY_NONE`, `PARITY_EVEN`, `PARITY_ODD`) | | `stopbits` | `int` | Stop bits (1, 1.5, or 2) | **Returns:** `Serial` object ### `readline()` Read a line from the serial port. ```python theme={null} line = ser.readline() print(f"Received: {line.decode('utf-8').strip()}") ``` **Returns:** `bytes` - The received line ### `read(size)` Read a specified number of bytes. ```python theme={null} data = ser.read(10) ``` | Parameter | Type | Description | | --------- | ----- | ----------------------- | | `size` | `int` | Number of bytes to read | **Returns:** `bytes` - The received data ### `read_until(expected=b'\n', size=None)` Read until expected sequence is found. ```python theme={null} line = ser.read_until(b'\n') ``` | Parameter | Type | Description | | ---------- | ------- | ---------------------- | | `expected` | `bytes` | Sequence to read until | | `size` | `int` | Maximum bytes to read | **Returns:** `bytes` - Data up to expected sequence ### `write(data)` Write data to the serial port. ```python theme={null} ser.write(b'AT+VER\r\n') ``` | Parameter | Type | Description | | --------- | ------- | ------------- | | `data` | `bytes` | Data to write | **Returns:** `int` - Number of bytes written ### `close()` Close the serial connection. ```python theme={null} ser.close() ``` ### `is_open` Check if connection is open. ```python theme={null} if ser.is_open: print("Connected") ``` **Returns:** `bool` - True if connection is open ## Examples ### Basic Communication ```python theme={null} import serial import time ser = serial.Serial('/dev/ttyUSB1', 115200, timeout=60) # Send command ser.write(b'AT+VER\r\n') time.sleep(0.1) # Read response response = ser.readline() print(f"Response: {response.decode('utf-8').strip()}") ser.close() ``` ### Interactive Session ```python theme={null} import serial def interactive_session(port, baudrate=115200): ser = serial.Serial(port, baudrate, timeout=1) print(f"Connected to {port}") while True: try: # Read any available data if ser.in_waiting: data = ser.read(ser.in_waiting) print(data.decode('utf-8'), end='') # Get user input command = input() if command.lower() == 'quit': break ser.write(f"{command}\r\n".encode('utf-8')) except KeyboardInterrupt: break ser.close() ``` ### Data Logging ```python theme={null} import serial import time from datetime import datetime def log_serial(port, log_file, duration=60): ser = serial.Serial(port, 115200, timeout=1) with open(log_file, 'w') as f: start = time.time() while time.time() - start < duration: if ser.in_waiting: data = ser.read(ser.in_waiting) timestamp = datetime.now().isoformat() f.write(f"[{timestamp}] {data.decode('utf-8')}") f.flush() time.sleep(0.1) ser.close() ``` ### Command/Response Pattern ```python theme={null} import serial import time class SerialDevice: def __init__(self, port, baudrate=115200): self.ser = serial.Serial(port, baudrate, timeout=60) def send_command(self, command, timeout=5): self.ser.reset_input_buffer() self.ser.write(f"{command}\r\n".encode('utf-8')) response = "" start = time.time() while time.time() - start < timeout: if self.ser.in_waiting: line = self.ser.readline().decode('utf-8').strip() response += line + "\n" time.sleep(0.1) return response def close(self): self.ser.close() # Usage device = SerialDevice('/dev/ttyUSB1') response = device.send_command('AT+VER') print(response) device.close() ``` ## Hardware Integration | Connection | Description | | ------------ | ------------------------------------------ | | Raw UART | Direct TX/RX line connections | | USB CDC | Virtual serial over USB | | Flow Control | Hardware (RTS/CTS) and software (XON/XOFF) | ## Notes * Lager supports native `pyserial` for serial communication * Serial ports are designated during Lager setup configuration * Supports both raw UART and USB CDC connections * Default baud rate is 115200 * Always handle `SerialException` errors appropriately * Use `decode('utf-8')` for string conversion # Solar Simulation Source: https://docs.lagerdata.com/source/reference/python/solar Control solar panel simulation nets **Coming Soon:** The Solar Simulator Python API for EA PSI/EL series two-quadrant power supplies is currently under development. The Net-based API (`Net.get('solar1', type=NetType.PowerSupply2Q)`) and associated methods are documented for preview purposes, but full testing and validation are pending hardware availability. Check back in a future release for production-ready functionality. Simulate solar panel characteristics for testing solar-powered devices. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ------------------- | --------------------------------------- | | `enable()` | Connect and start solar simulation mode | | `disable()` | Disconnect and stop solar simulation | | `irradiance(value)` | Set or read irradiance (W/m²) | | `mpp_current()` | Read maximum power point current | | `mpp_voltage()` | Read maximum power point voltage | | `voc()` | Read open-circuit voltage | | `temperature()` | Read simulated cell temperature | | `resistance(value)` | Set or read panel resistance | Solar methods return string values from the instrument. Convert to float if needed for calculations. ## Method Reference ### `Net.get(name, type=NetType.PowerSupply2Q)` Get a solar simulation net by name. ```python theme={null} from lager import Net, NetType solar = Net.get('SOLAR', type=NetType.PowerSupply2Q) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------------- | | `name` | `str` | Name of the solar net | | `type` | `NetType` | Must be `NetType.PowerSupply2Q` | **Returns:** Solar simulation Net instance ### `enable()` Connect to the instrument and start solar simulation mode. ```python theme={null} from lager import Net, NetType solar = Net.get('SOLAR', type=NetType.PowerSupply2Q) solar.enable() ``` ### `disable()` Disconnect from the instrument and stop solar simulation. ```python theme={null} solar.disable() ``` ### `irradiance(value=None)` Set or read the irradiance level. ```python theme={null} # Set irradiance solar.irradiance(1000) # Standard test condition (1000 W/m²) # Read current irradiance irr = solar.irradiance() print(f"Irradiance: {irr}") # Returns string ``` | Parameter | Type | Description | | --------- | ----------------- | ---------------------------------------------------------- | | `value` | `float` or `None` | Irradiance in W/m² (0-1500). If None, reads current value. | **Returns:** `str` - Current irradiance value ### `voc()` Read the open-circuit voltage. ```python theme={null} voc_str = solar.voc() print(f"Voc: {voc_str}") voc = float(voc_str) # Convert to float for calculations ``` **Returns:** `str` - Voltage value ### `mpp_voltage()` Read the maximum power point voltage. ```python theme={null} v_mpp = solar.mpp_voltage() print(f"MPP voltage: {v_mpp}") ``` **Returns:** `str` - Voltage value ### `mpp_current()` Read the maximum power point current. ```python theme={null} i_mpp = solar.mpp_current() print(f"MPP current: {i_mpp}") ``` **Returns:** `str` - Current value ### `resistance(value=None)` Set or read the dynamic panel resistance. ```python theme={null} # Set resistance solar.resistance(5.0) # Read resistance r = solar.resistance() print(f"Resistance: {r}") ``` | Parameter | Type | Description | | --------- | ----------------- | ------------------------------------------------- | | `value` | `float` or `None` | Resistance in ohms. If None, reads current value. | **Returns:** `str` - Resistance value ### `temperature()` Read the simulated cell temperature. ```python theme={null} temp = solar.temperature() print(f"Cell temp: {temp}") ``` **Returns:** `str` - Temperature value ## Examples ### Basic Solar Simulation ```python theme={null} from lager import Net, NetType import time solar = Net.get('SOLAR_INPUT', type=NetType.PowerSupply2Q) # Start simulation solar.enable() # Set standard test conditions (1000 W/m²) solar.irradiance(1000) time.sleep(1) # Read panel characteristics (returns strings) voc = solar.voc() v_mpp = solar.mpp_voltage() i_mpp = solar.mpp_current() print(f"Voc: {voc}") print(f"MPP: {v_mpp}V @ {i_mpp}A") # Calculate max power (convert to float first) v = float(v_mpp) i = float(i_mpp) print(f"Max Power: {v * i:.2f}W") # Clean up solar.disable() ``` ### Test Multiple Irradiance Levels ```python theme={null} from lager import Net, NetType import time solar = Net.get('SOLAR', type=NetType.PowerSupply2Q) solar.enable() conditions = [200, 500, 800, 1000, 1200] for irr in conditions: solar.irradiance(irr) time.sleep(1) # Read and convert values voc = float(solar.voc()) v_mpp = float(solar.mpp_voltage()) i_mpp = float(solar.mpp_current()) print(f"Irradiance: {irr} W/m²") print(f" Voc: {voc:.2f}V") print(f" MPP: {v_mpp:.2f}V @ {i_mpp:.3f}A") print(f" Power: {v_mpp * i_mpp:.2f}W") print() solar.disable() ``` ### MPPT Tracking Test ```python theme={null} from lager import Net, NetType import time solar = Net.get('SOLAR', type=NetType.PowerSupply2Q) dut_current = Net.get('DUT_CURRENT', type=NetType.ADC) solar.enable() solar.irradiance(1000) # Monitor MPPT tracking for i in range(30): i_mpp = float(solar.mpp_current()) actual = dut_current.input() efficiency = (actual / i_mpp) * 100 if i_mpp > 0 else 0 print(f"Target: {i_mpp:.3f}A, Actual: {actual:.3f}A, Eff: {efficiency:.1f}%") time.sleep(1) solar.disable() ``` ## Supported Hardware | Manufacturer | Model | Features | | ------------ | ------------- | --------------------------- | | EA | PSI/EL series | Two-quadrant, PV simulation | | EA | PSB 10060-60 | Bidirectional | | EA | PSB 10080-60 | Bidirectional | ## Notes * Solar simulation requires bidirectional (two-quadrant) power supplies * Standard Test Conditions (STC): 1000 W/m², 25°C, AM1.5 * The I-V curve is automatically generated based on irradiance * Call `enable()` before using solar-specific methods * Always call `disable()` when finished * **Return values are strings** - convert to float for calculations # SPI Source: https://docs.lagerdata.com/source/reference/python/spi Communicate with SPI devices over the serial peripheral interface Perform full-duplex SPI (Serial Peripheral Interface) communication with devices connected to a Lager Box. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | -------------- | ------------------------------------------- | | `config()` | Configure SPI bus parameters | | `read()` | Read words from a device (sends fill bytes) | | `read_write()` | Simultaneous full-duplex read and write | | `transfer()` | Transfer with automatic padding/truncation | | `write()` | Write words to a device (discards response) | | `get_config()` | Get raw net configuration | ## Method Reference ### `Net.get(name, type=NetType.SPI)` Get an SPI net by name. ```python theme={null} from lager import Net, NetType spi = Net.get('MY_SPI_NET', type=NetType.SPI) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------- | | `name` | `str` | Name of the SPI net | | `type` | `NetType` | Must be `NetType.SPI` | **Returns:** SPI Net instance ### `config(mode, bit_order, frequency_hz, word_size, cs_active, cs_mode)` Configure SPI bus parameters. Only explicitly-provided parameters are changed; omitted parameters retain their stored values. ```python theme={null} spi.config(mode=0, frequency_hz=1_000_000) spi.config(mode=3, bit_order="lsb", word_size=16) spi.config(cs_mode="manual") ``` | Parameter | Type | Description | | -------------- | --------------- | -------------------------------------------------------- | | `mode` | `int` or `None` | SPI mode 0-3 (see SPI Modes table below) | | `bit_order` | `str` or `None` | `"msb"` (most significant bit first) or `"lsb"` | | `frequency_hz` | `int` or `None` | Clock frequency in Hz | | `word_size` | `int` or `None` | Bits per word: `8`, `16`, or `32` | | `cs_active` | `str` or `None` | Chip select polarity: `"low"` or `"high"` | | `cs_mode` | `str` or `None` | `"auto"` (hardware CS) or `"manual"` (user-managed GPIO) | #### SPI Modes | Mode | CPOL | CPHA | Clock Idle | Sample Edge | | ---- | ---- | ---- | ---------- | ----------- | | 0 | 0 | 0 | Low | Rising | | 1 | 0 | 1 | Low | Falling | | 2 | 1 | 0 | High | Falling | | 3 | 1 | 1 | High | Rising | ### `read(n_words, fill, keep_cs, output_format)` Read data from an SPI device. Sends fill bytes while receiving data (full duplex). ```python theme={null} data = spi.read(n_words=4) data = spi.read(n_words=4, fill=0x00) ``` | Parameter | Type | Description | | --------------- | ------ | --------------------------------------------------- | | `n_words` | `int` | Number of words to read | | `fill` | `int` | Fill value sent while reading (default `0xFF`) | | `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) | | `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` | **Returns:** `list[int]` - Received words as integers (when `output_format="list"`) ### `read_write(data, keep_cs, output_format)` Perform simultaneous full-duplex SPI read and write. Sends data while simultaneously receiving the response. ```python theme={null} # Send JEDEC Read ID command and read 3 response bytes response = spi.read_write([0x9F, 0x00, 0x00, 0x00]) manufacturer_id = response[1] device_id = (response[2] << 8) | response[3] ``` | Parameter | Type | Description | | --------------- | ----------- | --------------------------------------------------- | | `data` | `list[int]` | Words to transmit | | `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) | | `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` | **Returns:** `list[int]` - Received words (same length as transmitted data) ### `transfer(n_words, data, fill, keep_cs, output_format)` Perform SPI transfer with automatic padding or truncation. If data is shorter than `n_words`, it is padded with the fill value. If longer, it is truncated. ```python theme={null} # Send 1-byte command, read 3 response bytes (4 total) response = spi.transfer(n_words=4, data=[0x9F]) # data [0x9F] is padded to [0x9F, 0xFF, 0xFF, 0xFF] ``` | Parameter | Type | Description | | --------------- | --------------------- | --------------------------------------------------- | | `n_words` | `int` | Total number of words to transfer | | `data` | `list[int]` or `None` | Words to transmit (padded/truncated to `n_words`) | | `fill` | `int` | Fill value for padding (default `0xFF`) | | `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) | | `output_format` | `str` | `"list"` (default), `"hex"`, `"bytes"`, or `"json"` | **Returns:** `list[int]` - Received words ### `write(data, keep_cs)` Write data to an SPI device, discarding the response. Convenience method for write-only operations. ```python theme={null} # Send Write Enable command spi.write([0x06]) # Send Page Program with address and data spi.write([0x02, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]) ``` | Parameter | Type | Description | | --------- | ----------- | ------------------------------------------------- | | `data` | `list[int]` | Words to transmit | | `keep_cs` | `bool` | Keep CS asserted after transfer (default `False`) | ### `get_config()` Get the raw net configuration dictionary. ```python theme={null} cfg = spi.get_config() print(cfg['name']) print(cfg['params']) ``` **Returns:** `dict` - Full net configuration including name, role, instrument, and params ## Output Formats The `output_format` parameter controls how data is returned. Hex formatting is word-size-aware: | Format | Return Type | 8-bit Example | 16-bit Example | | --------- | ----------- | ---------------------- | ------------------- | | `"list"` | `list[int]` | `[222, 173]` | `[57005]` | | `"hex"` | `str` | `"de ad"` | `"dead"` | | `"bytes"` | `str` | `"222 173"` | `"57005"` | | `"json"` | `dict` | `{"data": [222, 173]}` | `{"data": [57005]}` | ## Examples ### Read SPI Flash JEDEC ID ```python theme={null} from lager import Net, NetType spi = Net.get('flash_spi', type=NetType.SPI) spi.config(mode=0, frequency_hz=1_000_000) # JEDEC Read ID: send 0x9F, read 3 response bytes response = spi.read_write([0x9F, 0x00, 0x00, 0x00]) print(f"Manufacturer: 0x{response[1]:02x}") print(f"Device ID: 0x{(response[2] << 8) | response[3]:04x}") ``` ### Read Flash Memory ```python theme={null} from lager import Net, NetType spi = Net.get('flash_spi', type=NetType.SPI) spi.config(mode=0, frequency_hz=1_000_000) # Read 32 bytes starting at address 0x001000 # Command: 0x03 (Read), followed by 3-byte address response = spi.transfer( n_words=4 + 32, data=[0x03, 0x00, 0x10, 0x00], ) # First 4 bytes are command echo; data starts at index 4 data = response[4:] print(f"Read {len(data)} bytes: {' '.join(f'{b:02x}' for b in data)}") ``` ### Multi-Part Transaction with keep\_cs ```python theme={null} from lager import Net, NetType spi = Net.get('flash_spi', type=NetType.SPI) # Part 1: Send address with CS held low spi.write([0x03, 0x00, 0x10, 0x00], keep_cs=True) # Part 2: Read data while CS is still asserted data = spi.read(n_words=32, keep_cs=False) print(f"Read {len(data)} bytes") ``` ### Write to SPI Flash ```python theme={null} from lager import Net, NetType spi = Net.get('flash_spi', type=NetType.SPI) spi.config(mode=0, frequency_hz=1_000_000) # Step 1: Write Enable spi.write([0x06]) # Step 2: Page Program at address 0x001000 payload = [0xDE, 0xAD, 0xBE, 0xEF] spi.write([0x02, 0x00, 0x10, 0x00] + payload) # Step 3: Wait for write to complete (poll status register) import time while True: status = spi.read_write([0x05, 0x00]) if not (status[1] & 0x01): # WIP bit cleared break time.sleep(0.01) print("Write complete") ``` ### 16-bit Word Mode ```python theme={null} from lager import Net, NetType spi = Net.get('dac_spi', type=NetType.SPI) spi.config(mode=1, word_size=16, frequency_hz=500_000) # Send 16-bit DAC command (channel A, gain 1x, active, value 0x0800) spi.write([0x3800]) # Read back 16-bit status register status = spi.read_write([0x0000]) print(f"Status: 0x{status[0]:04x}") ``` ## Supported Hardware | Adapter | Description | | ---------------- | ---------------------------------------------------- | | LabJack T7 | Uses GPIO pins (FIO/EIO) for CLK, MOSI, MISO, and CS | | Aardvark I2C/SPI | Dedicated USB SPI adapter with GPIO bit-bang | ## Notes * Net must be configured as `NetType.SPI` * All SPI operations are full duplex; data is sent and received simultaneously * `write()` performs a full-duplex transfer but discards the received data * `keep_cs=True` holds the chip select line asserted between calls for multi-part transactions * `transfer()` pads short data arrays with the fill value or truncates long arrays to `n_words` * LabJack T7 supports up to 56 bytes per transaction and a maximum of approximately 800 kHz * Aardvark uses GPIO bit-bang mode; actual speed is limited by USB round-trip time regardless of `frequency_hz` * Configuration changes persist to `saved_nets.json` for subsequent commands * LSB-first mode (`bit_order="lsb"`) uses software bit reversal on LabJack T7 # Power Supply Source: https://docs.lagerdata.com/source/reference/python/supply Control programmable power supply nets Control programmable power supplies to set voltage, current, and protection thresholds for your DUT. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ------------------ | ---------------------------------------------- | | `set_voltage()` | Set output voltage | | `set_current()` | Set output current limit | | `voltage()` | Read measured voltage | | `current()` | Read measured current | | `power()` | Read measured power | | `enable()` | Enable power output | | `disable()` | Disable power output | | `set_ovp()` | Set over-voltage protection threshold | | `set_ocp()` | Set over-current protection threshold | | `get_ovp_limit()` | Get over-voltage protection limit | | `get_ocp_limit()` | Get over-current protection limit | | `is_ovp()` | Check if OVP fault is active | | `is_ocp()` | Check if OCP fault is active | | `clear_ovp()` | Clear over-voltage protection fault | | `clear_ocp()` | Clear over-current protection fault | | `state()` | Print comprehensive power state | | `get_full_state()` | Print extended state with setpoints and limits | ## Method Reference ### `Net.get(name, type=NetType.PowerSupply)` Get a power supply net by name. ```python theme={null} from lager import Net, NetType psu = Net.get('VDD', type=NetType.PowerSupply) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ----------------------------- | | `name` | `str` | Name of the power supply net | | `type` | `NetType` | Must be `NetType.PowerSupply` | **Returns:** Power supply Net instance ### `set_voltage(value)` Set the output voltage. ```python theme={null} psu.set_voltage(3.3) # Set to 3.3V ``` | Parameter | Type | Description | | --------- | ------- | ----------------------- | | `value` | `float` | Target voltage in volts | ### `set_current(value)` Set the output current limit. ```python theme={null} psu.set_current(0.5) # Set limit to 0.5A ``` | Parameter | Type | Description | | --------- | ------- | --------------------- | | `value` | `float` | Current limit in amps | ### `voltage()` Read the measured output voltage. ```python theme={null} v = psu.voltage() print(f"Voltage: {v}V") ``` **Returns:** `float` - Measured voltage in volts ### `current()` Read the measured output current. ```python theme={null} i = psu.current() print(f"Current: {i}A") ``` **Returns:** `float` - Measured current in amps ### `power()` Read the measured output power. ```python theme={null} p = psu.power() print(f"Power: {p}W") ``` **Returns:** `float` - Measured power in watts ### `enable()` Enable the power output. ```python theme={null} psu.enable() ``` ### `disable()` Disable the power output. ```python theme={null} psu.disable() ``` ### `set_ovp(limit)` Set over-voltage protection threshold. OVP must be greater than or equal to the configured voltage. When the measured voltage exceeds this threshold, the output is automatically disabled. ```python theme={null} psu.set_ovp(3.6) # Trip at 3.6V ``` | Parameter | Type | Description | | --------- | ------- | ---------------------- | | `limit` | `float` | OVP threshold in volts | ### `set_ocp(limit)` Set over-current protection threshold. When the measured current exceeds this threshold, the output is automatically disabled. ```python theme={null} psu.set_ocp(1.0) # Trip at 1.0A ``` | Parameter | Type | Description | | --------- | ------- | --------------------- | | `limit` | `float` | OCP threshold in amps | ### `get_ovp_limit()` Get the configured OVP limit. ```python theme={null} ovp = psu.get_ovp_limit() print(f"OVP limit: {ovp}V") ``` **Returns:** `float` - OVP threshold in volts ### `get_ocp_limit()` Get the configured OCP limit. ```python theme={null} ocp = psu.get_ocp_limit() print(f"OCP limit: {ocp}A") ``` **Returns:** `float` - OCP threshold in amps ### `is_ovp()` Check if an over-voltage fault is active. ```python theme={null} if psu.is_ovp(): print("OVP fault detected!") ``` **Returns:** `bool` - True if OVP fault is active ### `is_ocp()` Check if an over-current fault is active. ```python theme={null} if psu.is_ocp(): print("OCP fault detected!") ``` **Returns:** `bool` - True if OCP fault is active ### `clear_ovp()` Clear over-voltage protection fault. ```python theme={null} psu.clear_ovp() ``` ### `clear_ocp()` Clear over-current protection fault. ```python theme={null} psu.clear_ocp() ``` ### `state()` Print comprehensive power supply state including channel, enabled status, mode (CV/CC), measured voltage/current/power, and protection status. ```python theme={null} psu.state() ``` **Example output:** ``` Channel: CH1 Enabled: ON Mode: CV Voltage: 3.3000 Current: 0.1520 Power: 0.5016 OCP Limit: 1.0000 OCP Tripped: NO OVP Limit: 3.6000 OVP Tripped: NO ``` ### `get_full_state()` Print extended state including all measurements, configured setpoints, protection limits, and hardware maximum ratings. ```python theme={null} psu.get_full_state() ``` **Example output:** ``` Channel: CH1 Enabled: ON Mode: CV Voltage: 3.3000 Current: 0.1520 Power: 0.5016 Voltage_Set: 3.3000 Current_Set: 1.0000 OCP Limit: 1.0000 OCP Tripped: NO OVP Limit: 3.6000 OVP Tripped: NO Voltage_Max: 30.0000 Current_Max: 3.0000 ``` Additional fields beyond `state()`: * **Voltage\_Set / Current\_Set** - Configured setpoints * **Voltage\_Max / Current\_Max** - Hardware channel ratings ## Examples ### Basic Power Control ```python theme={null} from lager import Net, NetType # Get power supply net psu = Net.get('VDD', type=NetType.PowerSupply) # Configure output psu.set_voltage(3.3) psu.set_current(0.5) # Enable output psu.enable() # Read measurements print(f"Voltage: {psu.voltage():.2f}V") print(f"Current: {psu.current():.3f}A") print(f"Power: {psu.power():.3f}W") # Disable when done psu.disable() ``` ### With Protection Thresholds ```python theme={null} from lager import Net, NetType import time psu = Net.get('VDD', type=NetType.PowerSupply) # Configure voltage/current psu.set_voltage(5.0) psu.set_current(0.5) # Set protection thresholds psu.set_ovp(5.5) # Trip at 5.5V psu.set_ocp(0.6) # Trip at 0.6A # Enable output psu.enable() print("Power enabled") # Monitor for faults time.sleep(1) if psu.is_ocp(): print("OCP fault! Clearing...") psu.clear_ocp() if psu.is_ovp(): print("OVP fault! Clearing...") psu.clear_ovp() # Clean up psu.disable() ``` ### Monitor Power Consumption ```python theme={null} from lager import Net, NetType import time psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.set_current(1.0) psu.enable() # Log power consumption for sample in range(10): v = psu.voltage() i = psu.current() p = psu.power() print(f"V={v:.2f}V, I={i:.3f}A, P={p:.3f}W") time.sleep(1) psu.disable() ``` ### Full State Inspection ```python theme={null} from lager import Net, NetType psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.set_ovp(3.6) psu.set_ocp(0.5) psu.enable() # Print comprehensive state psu.get_full_state() psu.disable() ``` ## Supported Hardware | Manufacturer | Model Series | Channels | Features | | ------------ | -------------- | -------- | ----------------------------------- | | Rigol | DP832 / DP832A | 3 | Ch1-2: 30V/3A, Ch3: 5V/3A | | Rigol | DP821 | 2 | Ch1: 60V/1A, Ch2: 8V/10A | | Rigol | DP811 / DP811A | 1 | 20V/10A or 40V/5A | | Keithley | 2281S | 1 | 20V/6A/120W, battery simulator mode | | Keysight | E36200 series | 2 | E36233A: 30V/20A per channel | | Keysight | E36300 series | 3 | E36311A/12A/13A | | EA | PSI/EL series | 1 | Two-quadrant operation | ## Notes * Net must be configured as `NetType.PowerSupply` * Call `enable()` to turn on the output after setting voltage/current * Always call `disable()` when finished * Protection faults automatically disable output; use `clear_ovp()` or `clear_ocp()` after addressing the fault * OVP must be >= the voltage setpoint; setting a lower OVP will raise an error * Voltage and current limits depend on hardware capabilities * Multi-channel supplies: each channel is configured as a separate net * `state()` and `get_full_state()` print to stdout; use `voltage()`, `current()`, `power()` to get values in code # Thermocouple Source: https://docs.lagerdata.com/source/reference/python/tc Read temperature from thermocouple sensors Read temperature measurements from thermocouple sensors. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | -------- | ---------------------- | | `read()` | Read temperature in °C | ## Method Reference ### `Net.get(name, type=NetType.Thermocouple)` Get a thermocouple net by name. ```python theme={null} from lager import Net, NetType tc = Net.get('TEMP_SENSOR', type=NetType.Thermocouple) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------------ | | `name` | `str` | Name of the thermocouple net | | `type` | `NetType` | Must be `NetType.Thermocouple` | **Returns:** Thermocouple Net instance ### `read()` Read the temperature from the thermocouple. ```python theme={null} temp = tc.read() print(f"Temperature: {temp}°C") ``` **Returns:** `float` - Temperature in degrees Celsius ## Examples ### Single Reading ```python theme={null} from lager import Net, NetType probe = Net.get('OVEN_PROBE', type=NetType.Thermocouple) temp = probe.read() print(f"Temperature: {temp:.1f}°C") ``` ### Continuous Monitoring ```python theme={null} from lager import Net, NetType import time tc = Net.get('FURNACE_TC', type=NetType.Thermocouple) print("Monitoring temperature. Press Ctrl+C to exit.") while True: try: temp = tc.read() print(f"Temperature: {temp:.2f}°C") time.sleep(1) except KeyboardInterrupt: print("Monitoring stopped.") break ``` ### Temperature Logging ```python theme={null} from lager import Net, NetType import time import csv tc = Net.get('DUT_TEMP', type=NetType.Thermocouple) with open('temp_log.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['time', 'temperature']) start = time.time() for i in range(100): elapsed = time.time() - start temp = tc.read() writer.writerow([elapsed, temp]) time.sleep(1) print("Logging complete") ``` ### Thermal Protection ```python theme={null} from lager import Net, NetType import time tc = Net.get('BOARD_TEMP', type=NetType.Thermocouple) psu = Net.get('VDD', type=NetType.PowerSupply) MAX_TEMP = 85.0 # °C psu.set_voltage(3.3) psu.enable() while True: temp = tc.read() print(f"Board temp: {temp:.1f}°C") if temp > MAX_TEMP: print("OVER TEMPERATURE! Shutting down.") psu.disable() break time.sleep(1) ``` ## Hardware Integration | Hardware | Features | | -------- | ----------------------------- | | Phidget | K-type thermocouple interface | ## Notes * Thermocouple nets work directly without `enable()`/`disable()` calls * Temperature is returned in degrees Celsius * Reading rate depends on thermocouple hardware * Net names must match those configured on the Lager Box # UART Net Source: https://docs.lagerdata.com/source/reference/python/uart High-level UART serial communication through Lager nets Access UART serial ports through the Lager net abstraction for simplified device path resolution and connection management. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ---------------- | ------------------------------------------- | | `get_path()` | Get the device path (e.g., `/dev/ttyUSB0`) | | `connect()` | Connect and return a pyserial Serial object | | `get_baudrate()` | Get the configured baudrate | | `get_config()` | Get the raw net configuration | ## Method Reference ### `Net.get(name, type=NetType.UART)` Get a UART net by name. ```python theme={null} from lager import Net, NetType uart = Net.get('DUT_SERIAL', type=NetType.UART) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ---------------------- | | `name` | `str` | Name of the UART net | | `type` | `NetType` | Must be `NetType.UART` | **Returns:** `UARTNet` instance ### `get_path()` Get the device path for the UART net. ```python theme={null} uart = Net.get('DUT_SERIAL', type=NetType.UART) device_path = uart.get_path() print(f"Device: {device_path}") # e.g., "/dev/ttyUSB0" ``` **Returns:** `str` - Device path like `/dev/ttyUSB0` **Raises:** `FileNotFoundError` if the UART device is not connected ### `connect(**overrides)` Connect to the UART serial port with pyserial. ```python theme={null} uart = Net.get('DUT_SERIAL', type=NetType.UART) # Connect with default settings ser = uart.connect() # Connect with custom baudrate ser = uart.connect(baudrate=9600, timeout=1.0) # Use the pyserial connection ser.write(b'AT\r\n') response = ser.readline() ``` **Parameters:** | Parameter | Type | Description | | ---------- | ------- | ----------------------------------------- | | `baudrate` | `int` | Baud rate (default from config or 115200) | | `timeout` | `float` | Read timeout in seconds | | `bytesize` | `int` | Data bits (5, 6, 7, or 8) | | `parity` | `str` | Parity (`'N'`, `'E'`, `'O'`) | | `stopbits` | `float` | Stop bits (1, 1.5, or 2) | **Returns:** `serial.Serial` - Connected pyserial object ### `get_baudrate()` Get the configured baudrate for this net. ```python theme={null} uart = Net.get('DUT_SERIAL', type=NetType.UART) baudrate = uart.get_baudrate() print(f"Baudrate: {baudrate}") ``` **Returns:** `int` - Configured baudrate (default: 115200) ### `get_config()` Get the raw net configuration dictionary. ```python theme={null} uart = Net.get('DUT_SERIAL', type=NetType.UART) config = uart.get_config() print(config) ``` **Returns:** `dict` - Configuration dictionary ## Properties | Property | Type | Description | | ------------ | ------ | ------------------------------------------- | | `name` | `str` | Net name | | `usb_serial` | `str` | USB serial number for device identification | | `channel` | `str` | Channel/port number | | `params` | `dict` | Serial parameters (baudrate, etc.) | ## Examples ### Basic UART Communication ```python theme={null} from lager import Net, NetType # Get the UART net uart = Net.get('DUT_SERIAL', type=NetType.UART) # Connect with default settings ser = uart.connect() # Send command ser.write(b'AT\r\n') # Read response response = ser.readline() print(f"Response: {response.decode('utf-8').strip()}") # Clean up ser.close() ``` ### Using Device Path Directly ```python theme={null} from lager import Net, NetType import serial # Get the UART net uart = Net.get('DUT_SERIAL', type=NetType.UART) # Get device path for manual pyserial usage device_path = uart.get_path() print(f"Using device: {device_path}") # Create your own serial connection ser = serial.Serial( port=device_path, baudrate=115200, timeout=5, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE ) # Use the connection ser.write(b'Hello\r\n') ser.close() ``` ### Interactive Console ```python theme={null} from lager import Net, NetType uart = Net.get('DUT_SERIAL', type=NetType.UART) ser = uart.connect(baudrate=115200, timeout=0.1) print("Interactive console (Ctrl+C to exit)") print("-" * 40) try: while True: # Read any available data if ser.in_waiting: data = ser.read(ser.in_waiting) print(data.decode('utf-8', errors='ignore'), end='') # Get user input (non-blocking would need additional handling) # This is a simple example except KeyboardInterrupt: print("\nExiting...") finally: ser.close() ``` ### Command/Response Pattern ```python theme={null} from lager import Net, NetType import time def send_command(ser, command, timeout=2.0): """Send command and wait for response.""" ser.reset_input_buffer() ser.write(f"{command}\r\n".encode('utf-8')) response = "" start = time.time() while time.time() - start < timeout: if ser.in_waiting: data = ser.read(ser.in_waiting) response += data.decode('utf-8', errors='ignore') # Check for response terminator if '\n' in response: break time.sleep(0.01) return response.strip() # Usage uart = Net.get('DUT_SERIAL', type=NetType.UART) ser = uart.connect(timeout=1.0) # Query firmware version version = send_command(ser, "AT+VER") print(f"Firmware: {version}") # Query status status = send_command(ser, "AT+STATUS") print(f"Status: {status}") ser.close() ``` ### Production Test with UART ```python theme={null} from lager import Net, NetType import time def uart_loopback_test(net_name): """Test UART by sending data and verifying echo.""" uart = Net.get(net_name, type=NetType.UART) ser = uart.connect(baudrate=115200, timeout=1.0) test_data = b"LOOPBACK_TEST_12345" try: # Clear buffers ser.reset_input_buffer() ser.reset_output_buffer() # Send test data ser.write(test_data) time.sleep(0.1) # Read response response = ser.read(len(test_data)) # Verify if response == test_data: print(f"PASS: Loopback test successful") return True else: print(f"FAIL: Expected {test_data}, got {response}") return False finally: ser.close() # Run test uart_loopback_test('DUT_SERIAL') ``` ### Multi-Net UART Test ```python theme={null} from lager import Net, NetType # Test multiple UART nets UART_NETS = ['UART1', 'UART2', 'DEBUG_SERIAL'] results = {} for net_name in UART_NETS: try: uart = Net.get(net_name, type=NetType.UART) path = uart.get_path() baudrate = uart.get_baudrate() ser = uart.connect() ser.write(b'AT\r\n') response = ser.readline() ser.close() results[net_name] = { 'path': path, 'baudrate': baudrate, 'status': 'OK' if response else 'NO_RESPONSE' } except Exception as e: results[net_name] = {'status': 'ERROR', 'error': str(e)} # Print results for name, result in results.items(): print(f"{name}: {result}") ``` ## UART vs Serial Module The Lager Python SDK provides two ways to work with serial communication: | Feature | UART Net (`NetType.UART`) | Serial (`pyserial`) | | -------------------- | ------------------------------- | ------------------- | | **Device Discovery** | Automatic via USB serial number | Manual device path | | **Configuration** | Stored in Lager config | Manual in code | | **Integration** | Full Lager net system | Standalone | | **Best For** | Production tests, multi-device | Quick prototyping | Use **UART Net** when: * Device paths may change between reboots * You need to identify devices by USB serial number * You're using Lager's net configuration system * Running automated production tests Use **raw pyserial** when: * You know the exact device path * You need maximum flexibility * You're doing quick debugging ## Hardware Integration | Hardware | Description | | ------------------- | --------------------------------- | | USB-Serial Adapters | FTDI, CP2102, CH340, etc. | | UART Bridges | Multi-port USB-UART converters | | Built-in UART | Native UART on Lager Box hardware | ## Notes * UART nets resolve device paths using USB serial numbers for consistent identification * The `connect()` method returns a standard pyserial `Serial` object * Default baudrate is 115200 if not specified in configuration * Device paths are cached after first resolution * Use `get_path()` if you need the raw device path for other tools * Serial parameters from net configuration can be overridden in `connect()` # USB Control Source: https://docs.lagerdata.com/source/reference/python/usb Control USB device power state Control the power state of USB devices and ports on your testbed using USB hubs with per-port power control. ## Import ```python theme={null} from lager import Net, NetType # For exception handling from lager import ( USBBackendError, LibraryMissingError, DeviceNotFoundError, PortStateError ) ``` ## Methods | Method | Description | | ----------- | ------------------------------------------------------------------------- | | `enable()` | Enable (power on) USB port | | `disable()` | Disable (power off) USB port | | `toggle()` | Toggle USB port power state; returns the resulting state (`True`=enabled) | | `state()` | Read the current power state without changing it (`True`=enabled) | ## Exception Classes | Exception | Description | | --------------------- | --------------------------------- | | `USBBackendError` | Base class for USB hub errors | | `LibraryMissingError` | Required vendor SDK not installed | | `DeviceNotFoundError` | USB hub not found | | `PortStateError` | Error changing port state | ## Method Reference ### `Net.get(name, type=NetType.Usb)` Get a USB net by name. ```python theme={null} from lager import Net, NetType usb = Net.get('DUT_USB', type=NetType.Usb) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------- | | `name` | `str` | Name of the USB net | | `type` | `NetType` | Must be `NetType.Usb` | **Returns:** USB Net instance ### `enable()` Enable (power on) the USB port. ```python theme={null} from lager import Net, NetType usb = Net.get('CAMERA_USB', type=NetType.Usb) usb.enable() print("USB port powered on") ``` ### `disable()` Disable (power off) the USB port. ```python theme={null} from lager import Net, NetType usb = Net.get('CAMERA_USB', type=NetType.Usb) usb.disable() print("USB port powered off") ``` ### `toggle()` Toggle the power state of the USB port. Returns the resulting state (`True` if now enabled, `False` if now disabled). ```python theme={null} from lager import Net, NetType usb = Net.get('SENSOR_USB', type=NetType.Usb) now_on = usb.toggle() # On -> Off or Off -> On ``` ### `state()` Read the current power state of the USB port **without changing it**. Returns `True` if the port is currently enabled (powered on), `False` if disabled. The value is read live from the hub, so it always reflects the real port state. ```python theme={null} from lager import Net, NetType usb = Net.get('SENSOR_USB', type=NetType.Usb) if not usb.state(): usb.enable() # only power on if it isn't already ``` ## Examples ### Basic Power Control ```python theme={null} from lager import Net, NetType # Get USB net usb = Net.get('CAMERA', type=NetType.Usb) # Power on USB device usb.enable() print("Camera powered on") # Power off USB device usb.disable() print("Camera powered off") ``` ### Power Cycle Device ```python theme={null} from lager import Net, NetType import time def power_cycle(net_name, delay=2): """Power cycle a USB device.""" usb = Net.get(net_name, type=NetType.Usb) print(f"Power cycling {net_name}...") usb.disable() time.sleep(delay) usb.enable() print(f"{net_name} restarted") power_cycle('DUT_USB') ``` ### Error Handling ```python theme={null} from lager import Net, NetType from lager import ( USBBackendError, LibraryMissingError, DeviceNotFoundError, PortStateError ) try: usb = Net.get('SENSOR_USB', type=NetType.Usb) usb.enable() print("Sensor powered on") except LibraryMissingError: print("USB hub SDK not installed") except DeviceNotFoundError: print("USB hub not found - check connection") except PortStateError as e: print(f"Port error: {e}") except USBBackendError as e: print(f"USB error: {e}") ``` ### Automated Test Setup ```python theme={null} from lager import Net, NetType import time def setup_test(): """Power on all USB peripherals for testing.""" usb_devices = ['PROGRAMMER', 'SENSOR', 'DEBUGGER'] for name in usb_devices: try: usb = Net.get(name, type=NetType.Usb) usb.enable() print(f"{name} powered on") except Exception as e: print(f"Warning: {name} - {e}") time.sleep(1) # Wait for USB enumeration # Enable main power psu = Net.get('VDD', type=NetType.PowerSupply) psu.set_voltage(3.3) psu.enable() return True def teardown_test(): """Power off all USB peripherals.""" # Disable main power first psu = Net.get('VDD', type=NetType.PowerSupply) psu.disable() # Power off USB peripherals for name in ['PROGRAMMER', 'SENSOR', 'DEBUGGER']: try: usb = Net.get(name, type=NetType.Usb) usb.disable() except Exception: pass ``` ### USB Device Reset ```python theme={null} from lager import Net, NetType import time def reset_usb_device(net_name, reset_time=2, settle_time=3): """ Reset a USB device by power cycling. Args: net_name: USB net name reset_time: Time to keep power off (seconds) settle_time: Time to wait after power on (seconds) """ usb = Net.get(net_name, type=NetType.Usb) print(f"Resetting {net_name}...") # Power off usb.disable() print(f" Power off for {reset_time}s") time.sleep(reset_time) # Power on usb.enable() print(f" Power on, waiting {settle_time}s for enumeration") time.sleep(settle_time) print(f" {net_name} reset complete") # Usage reset_usb_device('DUT_USB', reset_time=2, settle_time=5) ``` ### Toggle for Quick State Change ```python theme={null} from lager import Net, NetType import time # Get USB net usb = Net.get('LED_USB', type=NetType.Usb) # Quick on/off cycle using toggle for i in range(5): usb.toggle() time.sleep(0.5) ``` ## Supported Hardware | Hardware | Features | | -------------------------- | ------------------------------------------------- | | Acroname BrainStem USB Hub | Individual port power control, current monitoring | | YKUSH USB Hub | Per-port power switching | ## Notes * USB nets must be configured on the Lager Box with hub serial number and port mapping * Power state changes take effect immediately * Allow time for USB enumeration after powering on (\~1-3 seconds) * Power cycling can be useful for device reset/recovery * The `toggle()` function is useful for quick state changes * Use exception handling for robust error recovery # Watt Meter Source: https://docs.lagerdata.com/source/reference/python/watt Read power consumption Measure power consumption from watt meter nets. Supports Yocto-Watt, Joulescope JS220, and Nordic PPK2 hardware. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Hardware | Description | | ---------------- | ----------------------------- | ------------------------------------------------------ | | `read()` | All | Read power in watts | | `read_current()` | Joulescope JS220, Nordic PPK2 | Read current in amps | | `read_voltage()` | Joulescope JS220, Nordic PPK2 | Read voltage in volts | | `read_all()` | Joulescope JS220, Nordic PPK2 | Read current, voltage, and power in a single operation | ## Method Reference ### `Net.get(name, type=NetType.WattMeter)` Get a watt meter net by name. ```python theme={null} from lager import Net, NetType power = Net.get('POWER_METER', type=NetType.WattMeter) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | --------------------------- | | `name` | `str` | Name of the watt meter net | | `type` | `NetType` | Must be `NetType.WattMeter` | **Returns:** Watt meter Net instance ### `read()` Read the current power consumption. Available on all watt meter hardware. ```python theme={null} watts = power.read() print(f"Power: {watts}W") ``` **Returns:** `float` - Power in watts ### `read_current()` Read the current in amps. **Joulescope JS220 and Nordic PPK2.** ```python theme={null} amps = power.read_current() print(f"Current: {amps}A") ``` **Returns:** `float` - Current in amps ### `read_voltage()` Read the voltage in volts. **Joulescope JS220 and Nordic PPK2.** ```python theme={null} volts = power.read_voltage() print(f"Voltage: {volts}V") ``` **Returns:** `float` - Voltage in volts ### `read_all()` Read current, voltage, and power in a single atomic measurement. **Joulescope JS220 and Nordic PPK2.** This is more efficient than calling `read_current()`, `read_voltage()`, and `read()` separately, as all values come from the same sample window. ```python theme={null} measurements = power.read_all() print(f"Current: {measurements['current']}A") print(f"Voltage: {measurements['voltage']}V") print(f"Power: {measurements['power']}W") ``` **Returns:** `dict` with keys: | Key | Type | Description | | ----------- | ------- | ---------------- | | `"current"` | `float` | Current in amps | | `"voltage"` | `float` | Voltage in volts | | `"power"` | `float` | Power in watts | ## Examples ### Basic Power Reading ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.WattMeter) watts = power.read() print(f"Power consumption: {watts:.3f}W") ``` ### Joulescope Full Measurement ```python theme={null} from lager import Net, NetType power = Net.get('DEVICE_POWER', type=NetType.WattMeter) # Read all measurements at once (Joulescope JS220 and Nordic PPK2) data = power.read_all() print(f"Voltage: {data['voltage']:.3f}V") print(f"Current: {data['current']:.6f}A") print(f"Power: {data['power']:.3f}W") ``` ### Power Profiling ```python theme={null} from lager import Net, NetType import time power = Net.get('DEVICE_POWER', type=NetType.WattMeter) measurements = [] # Take 60 seconds of measurements for i in range(60): watts = power.read() measurements.append(watts) print(f"[{i:3d}s] {watts:.3f}W") time.sleep(1) # Statistics avg = sum(measurements) / len(measurements) max_p = max(measurements) min_p = min(measurements) print(f"\nAverage: {avg:.3f}W") print(f"Maximum: {max_p:.3f}W") print(f"Minimum: {min_p:.3f}W") ``` ### Battery Life Estimation ```python theme={null} from lager import Net, NetType import time power = Net.get('POWER', type=NetType.WattMeter) # Average over multiple readings readings = [] for _ in range(10): readings.append(power.read()) time.sleep(0.1) avg_power = sum(readings) / len(readings) # Estimate battery life (1000mAh @ 3.7V = 3.7Wh) battery_wh = 3.7 hours = battery_wh / avg_power if avg_power > 0 else float('inf') print(f"Average power: {avg_power:.3f}W") print(f"Est. battery life: {hours:.1f} hours") ``` ### Power Limit Verification ```python theme={null} from lager import Net, NetType import time def verify_power_limits(min_watts, max_watts): power = Net.get('DEVICE_POWER', type=NetType.WattMeter) # Average multiple readings readings = [] for _ in range(10): readings.append(power.read()) time.sleep(0.1) avg = sum(readings) / len(readings) if min_watts <= avg <= max_watts: print(f"PASS: {avg:.3f}W in range [{min_watts}, {max_watts}]") return True else: print(f"FAIL: {avg:.3f}W outside range [{min_watts}, {max_watts}]") return False # Test verify_power_limits(0.1, 5.0) ``` ### Sleep Current Verification (Joulescope) ```python theme={null} from lager import Net, NetType import time power = Net.get('DEVICE_POWER', type=NetType.WattMeter) # Put device in sleep mode (external trigger) # set_device_sleep_mode() time.sleep(2) # Wait for stabilization # Use read_all() for atomic measurement (Joulescope JS220) data = power.read_all() sleep_current_ua = data['current'] * 1e6 print(f"Sleep voltage: {data['voltage']:.3f}V") print(f"Sleep current: {sleep_current_ua:.1f}uA") print(f"Sleep power: {data['power']:.6f}W") if sleep_current_ua < 100: print("PASS: Sleep current below 100uA") else: print("FAIL: Sleep current exceeds limit") ``` ## Supported Hardware | Manufacturer | Model | Measurement | Features | | -------------------- | ---------- | ----------------------- | ------------------------------------------- | | Yoctopuce | Yocto-Watt | Power only | Instantaneous reading | | Joulescope | JS220 | Power, voltage, current | 0.1s averaged, atomic multi-measurement | | Nordic Semiconductor | PPK2 | Power, voltage, current | 0.3s averaged, source mode constant voltage | ### Hardware Feature Comparison | Feature | Yocto-Watt | Joulescope JS220 | Nordic PPK2 | | ------------------ | ------------- | ------------------- | ------------------- | | `read()` (power) | Yes | Yes | Yes | | `read_current()` | No | Yes | Yes | | `read_voltage()` | No | Yes | Yes | | `read_all()` | No | Yes | Yes | | Measurement method | Instantaneous | 0.1s averaged | 0.3s averaged | | Device selection | Channel-based | Serial number-based | Serial number-based | ## Notes * Power is returned in watts (W) * Joulescope JS220 averages measurements over 0.1 seconds for higher precision * Nordic PPK2 averages measurements over 0.3 seconds; operates in source mode (supplies a configurable voltage 0.8–5V), so voltage readings reflect the configured value, not an independent measurement * `read_current()`, `read_voltage()`, and `read_all()` are available on the Joulescope JS220 and Nordic PPK2; calling them on a Yocto-Watt will raise an error * For current on Yocto-Watt: calculate from power and known voltage (I = P / V) * Use multiple readings and averaging for stability * Allow settling time after device state changes # Webcam Source: https://docs.lagerdata.com/source/reference/python/webcam Webcam streaming and video capture for visual inspection Stream video from webcams attached to the Lager Box for visual inspection, automated vision testing, and remote monitoring. ## Import ```python theme={null} from lager import Net, NetType ``` ## Methods | Method | Description | | ------------------ | ----------------------------------- | | `start(box_ip)` | Start a webcam stream | | `stop()` | Stop the webcam stream | | `get_info(box_ip)` | Get info about the stream | | `get_url(box_ip)` | Get just the URL for the stream | | `is_active()` | Check if stream is currently active | ## Method Reference ### `Net.get(name, type=NetType.Webcam)` Get a webcam net by name. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) ``` **Parameters:** | Parameter | Type | Description | | --------- | --------- | ------------------------ | | `name` | `str` | Name of the webcam net | | `type` | `NetType` | Must be `NetType.Webcam` | **Returns:** Webcam Net instance ### `start(box_ip)` Start a webcam video stream. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) result = webcam.start(box_ip='') print(f"Stream URL: {result['url']}") print(f"Port: {result['port']}") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | --------------------------------------- | | `box_ip` | `str` | Lager Box IP address for URL generation | **Returns:** `dict` with keys: * `url` - Full stream URL (e.g., `http://:8081/`) * `port` - Port number for the stream * `already_running` - Boolean indicating if stream was already active **Raises:** `RuntimeError` if device is already in use or not found ### `stop()` Stop the webcam stream. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) stopped = webcam.stop() if stopped: print("Stream stopped") else: print("Stream was not running") ``` **Returns:** `bool` - True if stopped successfully, False if not running ### `get_info(box_ip)` Get information about the stream. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) info = webcam.get_info(box_ip='') if info: print(f"URL: {info['url']}") print(f"Port: {info['port']}") print(f"Device: {info['video_device']}") else: print("Stream not active") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | -------------------- | | `box_ip` | `str` | Lager Box IP address | **Returns:** `dict` or `None` - Stream info dict or None if not running ### `get_url(box_ip)` Get just the URL for the stream. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) url = webcam.get_url(box_ip='') if url: print(f"Stream at: {url}") ``` **Parameters:** | Parameter | Type | Description | | --------- | ----- | -------------------- | | `box_ip` | `str` | Lager Box IP address | **Returns:** `str` or `None` - Stream URL or None if not running ### `is_active()` Check if the stream is currently active. ```python theme={null} from lager import Net, NetType webcam = Net.get('camera1', type=NetType.Webcam) if webcam.is_active(): print("Stream is running") else: print("Stream is stopped") ``` **Returns:** `bool` - True if stream is running, False otherwise ## Examples ### Start Multiple Cameras ```python theme={null} from lager import Net, NetType # Lager Box IP BOX_IP = '' # Start multiple camera streams cameras = ['overview', 'microscope', 'solder_station'] for name in cameras: try: webcam = Net.get(name, type=NetType.Webcam) result = webcam.start(BOX_IP) print(f"{name}: {result['url']}") except RuntimeError as e: print(f"{name}: Failed - {e}") ``` ### Stream Management ```python theme={null} from lager import Net, NetType BOX_IP = '' def start_camera(name): """Start a camera stream.""" try: webcam = Net.get(name, type=NetType.Webcam) result = webcam.start(BOX_IP) if result['already_running']: print(f"Stream '{name}' was already running at {result['url']}") else: print(f"Started '{name}' at {result['url']}") except RuntimeError as e: print(f"Failed to start '{name}': {e}") def stop_camera(name): """Stop a camera stream.""" webcam = Net.get(name, type=NetType.Webcam) if webcam.stop(): print(f"Stopped '{name}'") else: print(f"Stream '{name}' was not running") def check_camera(name): """Check camera status.""" webcam = Net.get(name, type=NetType.Webcam) if webcam.is_active(): info = webcam.get_info(BOX_IP) print(f"{name}: Running at {info['url']}") else: print(f"{name}: Stopped") # Usage start_camera('main') check_camera('main') stop_camera('main') ``` ### Visual Inspection Test ```python theme={null} from lager import Net, NetType import time BOX_IP = '' def visual_inspection_test(camera_name, inspection_callback): """ Start camera stream and wait for operator inspection. Args: camera_name: Webcam net name inspection_callback: Function to handle the stream URL Returns: bool: True if inspection passed """ # Start stream webcam = Net.get(camera_name, type=NetType.Webcam) result = webcam.start(BOX_IP) stream_url = result['url'] print(f"Visual inspection stream: {stream_url}") # Notify external system (could open browser, send to UI, etc.) inspection_callback(stream_url) # Wait for inspection (in real usage, this would wait for operator input) print("Waiting for visual inspection...") time.sleep(10) # Placeholder # Stop stream webcam.stop() # Return result (would come from operator in real usage) return True # Usage def handle_url(url): print(f"Open in browser: {url}") result = visual_inspection_test('inspection_cam', handle_url) print(f"Inspection result: {'PASS' if result else 'FAIL'}") ``` ### Camera Discovery and Testing ```python theme={null} from lager import Net, NetType import os BOX_IP = '' def discover_cameras(): """Find all available video devices.""" cameras = [] for i in range(10): # Check video0 through video9 device = f'/dev/video{i}' if os.path.exists(device): cameras.append(device) return cameras def test_camera(net_name): """Test if a camera net works.""" try: webcam = Net.get(net_name, type=NetType.Webcam) result = webcam.start(BOX_IP) print(f"{net_name}: OK - {result['url']}") webcam.stop() return True except RuntimeError as e: print(f"{net_name}: FAIL - {e}") return False # Discover and test all cameras print("Discovering cameras...") devices = discover_cameras() print(f"Found {len(devices)} video devices") # Test configured nets (assumes you have webcam nets configured) test_camera('camera1') test_camera('camera2') ``` ### Already Running Detection ```python theme={null} from lager import Net, NetType BOX_IP = '' webcam = Net.get('camera1', type=NetType.Webcam) # Start stream first time result1 = webcam.start(BOX_IP) print(f"First start: already_running = {result1['already_running']}") # Try to start again - should detect it's already running result2 = webcam.start(BOX_IP) print(f"Second start: already_running = {result2['already_running']}") # Check if active print(f"Is active: {webcam.is_active()}") # Cleanup webcam.stop() ``` ## Web Interface Each stream provides a web interface at its URL with: * Live MJPEG video stream * Zoom controls (+, -, Reset) * FPS display * Sidebar with links to other active streams ### API Endpoints | Endpoint | Method | Description | | ----------------- | ------ | --------------------------- | | `/` | GET | HTML page with video viewer | | `/stream` | GET | Raw MJPEG video stream | | `/api/zoom` | GET | Get current zoom level | | `/api/zoom/in` | POST | Increase zoom | | `/api/zoom/out` | POST | Decrease zoom | | `/api/zoom/reset` | POST | Reset zoom to 1.0x | | `/api/fps` | GET | Get current FPS | | `/api/streams` | GET | List all active streams | | `/test` | GET | Health check endpoint | ## Hardware Requirements | Requirement | Description | | ------------- | -------------------------- | | USB Webcams | UVC-compatible cameras | | Video Devices | `/dev/video*` device files | | OpenCV | Required for video capture | ## Notes * Webcam nets must be configured on the Lager Box with video device path * Streams run on ports starting from 8081 * Each stream uses a separate port, automatically allocated * Streams persist until explicitly stopped or the process dies * Dead stream processes are automatically cleaned up * Only one stream can use a video device at a time * Default resolution is 640x480 at 30 FPS * JPEG quality is set to 80 for bandwidth/quality balance * Streams are accessible via HTTP from any network the Lager Box is on * Zoom is digital (crop and scale), not optical # WiFi Source: https://docs.lagerdata.com/source/reference/python/wifi WiFi network management Manage WiFi network connections on Lager Boxes. WiFi operations are box-level functions — they manage the box's own wireless interface, not a test net on a PCB. ## Import ```python theme={null} from lager.protocols.wifi import scan_wifi, connect_to_wifi, get_wifi_status, disconnect_wifi ``` ## Function Reference | Function | Description | | -------------------------------------------- | ---------------------------------- | | `scan_wifi(interface)` | Scan for available WiFi networks | | `connect_to_wifi(ssid, password, interface)` | Connect to a WiFi network | | `get_wifi_status()` | Get current WiFi connection status | | `disconnect_wifi(interface)` | Disconnect from WiFi network | ### `scan_wifi(interface='wlan0')` Scan for available WiFi networks. ```python theme={null} from lager.protocols.wifi import scan_wifi result = scan_wifi() networks = result.get('access_points', []) for network in networks: print(f"{network['ssid']}: {network['strength']}%") ``` **Parameters:** | Parameter | Type | Default | Description | | ----------- | ----- | --------- | ---------------------------- | | `interface` | `str` | `'wlan0'` | Network interface to scan on | **Returns:** `dict` with key `access_points` containing a list of network dicts, each with: * `ssid` - Network name * `strength` - Signal strength as percentage (0-100) * `security` - Security type ('Open' or 'Secured') ### `connect_to_wifi(ssid, password, interface='wlan0')` Connect to a WiFi network. ```python theme={null} from lager.protocols.wifi import connect_to_wifi result = connect_to_wifi('MyNetwork', 'secret123') if result['success']: print(f"Connected: {result['message']}") else: print(f"Failed: {result['error']}") ``` **Parameters:** | Parameter | Type | Default | Description | | ----------- | ----- | --------- | ------------------------------------------------- | | `ssid` | `str` | | Network name | | `password` | `str` | | Network password (empty string for open networks) | | `interface` | `str` | `'wlan0'` | Network interface to use | **Returns:** `dict` with keys: * `success` - Boolean indicating connection success * `message` - Success message (when `success` is True) * `error` - Error message (when `success` is False) ### `get_wifi_status()` Get current WiFi connection status for all interfaces. ```python theme={null} from lager.protocols.wifi import get_wifi_status interfaces = get_wifi_status() for name, info in interfaces.items(): print(f"{name}: {info['state']} - {info['ssid']}") ``` **Returns:** `dict` keyed by interface name, each value containing: * `interface` - Interface name * `ssid` - Connected network name or 'Not Connected' * `state` - 'Connected' or 'Disconnected' ### `disconnect_wifi(interface='wlan0')` Disconnect from the current WiFi network. ```python theme={null} from lager.protocols.wifi import disconnect_wifi result = disconnect_wifi() if result['success']: print(f"Disconnected: {result['message']}") else: print(f"Failed: {result['error']}") ``` **Parameters:** | Parameter | Type | Default | Description | | ----------- | ----- | --------- | ------------------------------- | | `interface` | `str` | `'wlan0'` | Network interface to disconnect | **Returns:** `dict` with keys: * `success` - Boolean indicating disconnect success * `message` - Success message (when `success` is True) * `error` - Error message (when `success` is False) ## Router Internet Access Control The `Wifi` net type controls internet access via an Asus router's parental control feature. This is a separate concern from box-level WiFi management — it blocks/unblocks a device's internet access by MAC address. ```python theme={null} from lager import Net, NetType # Requires a wifi net configured with router credentials wifi = Net.get('wifi1', type=NetType.Wifi) wifi.disable() # Block internet access (parental control) wifi.enable() # Restore internet access ``` ## Examples ### Scan and Connect ```python theme={null} from lager.protocols.wifi import scan_wifi, connect_to_wifi, get_wifi_status import time # Scan for networks result = scan_wifi() networks = result.get('access_points', []) # Find target network for network in networks: if network['ssid'] == 'TestNetwork': print(f"Found: {network['strength']}% signal") break # Connect result = connect_to_wifi('TestNetwork', 'password123') if result['success']: print(f"Connection successful: {result['message']}") else: print(f"Connection failed: {result['error']}") # Wait for connection to stabilize time.sleep(5) # Verify status interfaces = get_wifi_status() for name, info in interfaces.items(): if info['state'] == 'Connected': print(f"Connected to {info['ssid']} on {name}") ``` ### Network Verification Test ```python theme={null} from lager.protocols.wifi import scan_wifi def verify_network_visible(expected_ssid): result = scan_wifi() networks = result.get('access_points', []) ssids = [n['ssid'] for n in networks] if expected_ssid in ssids: print(f"PASS: {expected_ssid} is visible") return True else: print(f"FAIL: {expected_ssid} not found") return False ``` ### Signal Strength Test ```python theme={null} from lager.protocols.wifi import scan_wifi def check_signal_strength(ssid, min_strength=50): """Check if signal strength meets minimum threshold (0-100%)""" result = scan_wifi() networks = result.get('access_points', []) for network in networks: if network['ssid'] == ssid: strength = network['strength'] if strength >= min_strength: print(f"PASS: {ssid} signal {strength}%") return True else: print(f"FAIL: {ssid} signal {strength}% below {min_strength}%") return False print(f"FAIL: {ssid} not found") return False ``` ### Connection Test ```python theme={null} from lager.protocols.wifi import connect_to_wifi, get_wifi_status import time def test_wifi_connection(ssid, password): result = connect_to_wifi(ssid, password) if not result['success']: print(f"FAIL: Connection error - {result['error']}") return False time.sleep(5) interfaces = get_wifi_status() for name, info in interfaces.items(): if info['state'] == 'Connected' and info['ssid'] == ssid: print(f"PASS: Connected to {ssid}") return True print(f"FAIL: Not connected to {ssid}") return False ``` ## Hardware Requirements | Requirement | Description | | ------------------ | ------------------------- | | WiFi Hardware | USB adapter or built-in | | Permissions | Root/sudo access required | | Supported Security | WPA2, WPA3, Open | ## Notes * Lager Box must have WiFi hardware * Root/sudo access required for most operations * WPA2/WPA3 networks supported * Open networks require empty password string (`''`) * Interface defaults to 'wlan0' * `get_wifi_status()` takes no parameters and returns all interfaces * Router management (enable/disable) requires Asus router with parental control and a configured wifi net # Authentication Source: https://docs.lagerdata.com/source/reference/rust/auth Using the Rust crate with boxes behind an authenticating gateway Plain (ungated) boxes need none of this: no header is sent and none of this code runs. This page applies to deployments that place an authenticating reverse proxy (a *gateway*) in front of a box, which rejects unauthenticated traffic with 401 + an `X-Gateway-Auth-Url` header — the same contract the Lager CLI speaks. The crate handles gated boxes transparently, in two modes. ## CLI session reuse (zero config) If you've run `lager login ` on the machine, the crate picks up that session automatically: * Reads the CLI's token store (`~/.lager_gateway_auth`, overridable via `LAGER_GATEWAY_AUTH_FILE`). * Attaches `Authorization: Bearer` to every request — including debug-service traffic and UART Socket.IO handshakes. * Refreshes expired access tokens transparently. * Learns which auth server fronts a box from the gateway's discovery header on first contact, and retries the denied request within the same call. No code changes needed — `LagerBox::from_env()` just works: ```sh theme={null} lager login https://auth.example.com LAGER_BOX_HOST=192.168.1.42 cargo test ``` ## Pinned token (CI) On machines with no CLI login (CI runners), supply a token directly: ```rust theme={null} let lager = lager::LagerBox::builder("192.168.1.42") .bearer_token(std::env::var("MY_CI_TOKEN").unwrap()) .build()?; ``` or set the `LAGER_GATEWAY_TOKEN` environment variable — no code change: ```yaml theme={null} env: LAGER_BOX_HOST: ${{ vars.LAGER_BOX_HOST }} LAGER_GATEWAY_TOKEN: ${{ secrets.LAGER_GATEWAY_TOKEN }} ``` A pinned token is attached verbatim to every request and is never refreshed or written to the token store. If the gateway rejects it, the call fails immediately. ## Errors When a gateway asks for auth and no usable credential exists, calls fail with `Error::AuthRequired`, which names the auth server to log into: | Gateway response | Crate behavior | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | 401 (no/expired credential) | Resolve or refresh a token and retry once; otherwise `Error::AuthRequired` with the `lager login ` fix | | 403 (no access grant for this box) | `Error::Box { status: 403, .. }` — ask your admin for access | | 503 (gateway can't reach its auth server) | `Error::Box { status: 503, .. }` — retry shortly | ## Environment variables | Variable | Meaning | | ------------------------- | --------------------------------------------------------------------- | | `LAGER_GATEWAY_TOKEN` | Pinned bearer token for every request (CI). | | `LAGER_GATEWAY_AUTH_FILE` | Overrides the CLI token store path (default `~/.lager_gateway_auth`). | The full client/gateway contract (discovery header, auth server endpoints, store schema, retry semantics) is specified in the monorepo at [`docs/reference/gateway-auth-contract.md`](https://github.com/lagerdata/lager/blob/main/docs/reference/gateway-auth-contract.md). # Debug Probes & UART Source: https://docs.lagerdata.com/source/reference/rust/debug-and-uart Flash firmware, stream RTT logs, and drive serial from Rust tests The two workflows embedded developers usually evaluate first: driving a debug probe (flash / erase / reset / memory reads / RTT) and streaming UART. Both are fully supported by the crate. ## Flashing and memory access `DebugNet` drives a J-Link/OpenOCD debug probe through the box's debug service (port 8765, published on the box host): ```rust theme={null} let lager = lager::LagerBox::from_env()?; let debug = lager.debug("debug1"); debug.connect()?; debug.erase()?; debug.flash("firmware.hex")?; // .hex/.elf/.bin inferred from extension debug.reset(false)?; // false = run after reset let head = debug.read_memory(0x0800_0000, 16)?; println!("vector table: {head:02x?}"); ``` `flash_bin(path, address)` places a raw binary at an explicit address, and `flash_bytes` uploads an in-memory image — useful when your test builds firmware variants on the fly. `info()` and `status()` report probe and target state. ## RTT log streaming `rtt()` returns a blocking byte stream implementing `std::io::Read`, so asserting on target output is plain std I/O: ```rust theme={null} use std::io::{BufRead, BufReader}; let mut lines = BufReader::new(debug.rtt()?).lines(); assert!(lines.next().transpose()?.unwrap().contains("boot ok")); ``` ## Tunneled debug service If the debug service is reached through an SSH tunnel rather than directly, point the crate at it: ```rust theme={null} let lager = lager::LagerBox::builder("192.168.1.42") .debug_service_url("http://127.0.0.1:8765") .build()?; ``` or set the `LAGER_DEBUG_SERVICE_URL` environment variable. ## Streaming UART Enable the `uart` feature: ```toml theme={null} lager = { package = "lager-net", version = "0.2", features = ["uart"] } ``` A `Uart` session streams over the box's Socket.IO `/uart` namespace. `wait_for` accumulates output until a needle appears (bytes after the needle stay buffered for the next read), which makes boot assertions one-liners: ```rust theme={null} use std::time::Duration; let mut uart = lager.uart("DUT_CONSOLE")?; // Reboot the DUT and assert on its boot banner. debug.reset(false)?; let banner = uart.wait_for(b"boot ok", Duration::from_secs(10))?; println!("{}", String::from_utf8_lossy(&banner)); // Talk to the DUT's shell. uart.write_str("version\r\n")?; let reply = uart.read(Duration::from_secs(2))?; uart.stop()?; ``` The session reports adapter re-enumeration (hub power-cycle, DUT reflash) via `last_status()` — `"reconnecting"` / `"reconnected"` — and keeps streaming across it, so power-cycling tests don't need to reopen the port. # Net Types Source: https://docs.lagerdata.com/source/reference/rust/net-types Every net handle and box-level capability the Rust crate exposes A `LagerBox` hands out lightweight, cloneable handles keyed by net name. Constructing a handle does no I/O — the first method call does — so handles are cheap to create in test setup. Method-level documentation for every handle lives on [docs.rs/lager-net](https://docs.rs/lager-net); the tables below map each net type to its constructor and highlights. ## Instrument nets | Handle | Constructor | Highlights | | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | | [`Supply`](https://docs.rs/lager-net/latest/lager/nets/supply/) | `lager.supply(name)` | `set_voltage`, `set_current`, `enable`/`disable`, OCP/OVP, `state()` | | [`Battery`](https://docs.rs/lager-net/latest/lager/nets/battery/) | `lager.battery(name)` | `set_soc`, `set_voc`, model/capacity/mode, `state()` | | [`Eload`](https://docs.rs/lager-net/latest/lager/nets/eload/) | `lager.eload(name)` | `set(EloadMode::Cc, 0.5)`, `setpoint`, `state()` | | [`Solar`](https://docs.rs/lager-net/latest/lager/nets/solar/) | `lager.solar(name)` | `set`/`stop` PV simulation, `irradiance`, `voc`, `mpp_voltage`/`mpp_current`, `resistance`, `temperature` | | [`Gpio`](https://docs.rs/lager-net/latest/lager/nets/gpio/) | `lager.gpio(name)` | `input`, `output`, `toggle`, `wait_for_level` (hardware-timed) | | [`Adc`](https://docs.rs/lager-net/latest/lager/nets/adc/) / [`Dac`](https://docs.rs/lager-net/latest/lager/nets/dac/) | `lager.adc(name)` / `lager.dac(name)` | `read()`; `set(volts)` | | [`Thermocouple`](https://docs.rs/lager-net/latest/lager/nets/thermocouple/) | `lager.thermocouple(name)` | `read()` in °C | | [`WattMeter`](https://docs.rs/lager-net/latest/lager/nets/watt/) | `lager.watt_meter(name)` | `power/current/voltage/all(duration)` | | [`EnergyAnalyzer`](https://docs.rs/lager-net/latest/lager/nets/energy/) | `lager.energy_analyzer(name)` | `read_energy`, `read_stats` | | [`Spi`](https://docs.rs/lager-net/latest/lager/nets/spi/) | `lager.spi(name)` | `configure`, `read`, `write`, `read_write`, `transfer` | | [`I2c`](https://docs.rs/lager-net/latest/lager/nets/i2c/) | `lager.i2c(name)` | `configure`, `scan`, `read`, `write`, `write_read` | | [`UsbPort`](https://docs.rs/lager-net/latest/lager/nets/usb/) | `lager.usb(name)` | `enable`/`disable`/`toggle`/`state` | | [`Arm`](https://docs.rs/lager-net/latest/lager/nets/arm/) | `lager.arm(name)` | `position`, `move_to`/`move_by`, `go_home`, motor enable/disable, `set_acceleration` | | [`Webcam`](https://docs.rs/lager-net/latest/lager/nets/webcam/) | `lager.webcam(name)` | `start`/`stop` MJPEG stream, `url`, `status` | | [`Router`](https://docs.rs/lager-net/latest/lager/nets/router/) | `lager.router(name)` | `system_info`, interfaces/clients/leases, `block_internet`, generic `command(action, params)` | | [`DebugNet`](https://docs.rs/lager-net/latest/lager/nets/debug/) | `lager.debug(name)` | `connect`, `flash`, `erase`, `reset`, `read_memory`, `info`/`status`, `rtt` (blocking) | | [`Uart`](https://docs.rs/lager-net/latest/lager/nets/uart/) | `lager.uart(name)?` *(feature `uart`)* | streaming `read`, `write`, `wait_for(b"boot ok", ...)` | See **[Debug probes & UART](/source/reference/rust/debug-and-uart)** for worked examples of the last two. ## Box-level capabilities These drive the box's own hardware and take no net name: | Handle | Constructor | Highlights | | ------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------- | | [`Ble`](https://docs.rs/lager-net/latest/lager/nets/ble/) | `lager.ble()` | `scan`/`scan_named`, `info`/`connect` (GATT enumeration), `disconnect` | | [`Wifi`](https://docs.rs/lager-net/latest/lager/nets/wifi/) | `lager.wifi()` | `status`, `scan`, `connect(ssid, password)`, `delete` | | [`Blufi`](https://docs.rs/lager-net/latest/lager/nets/blufi/) | `lager.blufi()` | `scan`, `connect`, `provision(device, ssid, password)`, `wifi_scan`, `status`, `version` | ## Discovery and box health ```rust theme={null} let lager = lager::LagerBox::from_env()?; // All nets configured on the box. for net in lager.nets()? { println!("{} ({})", net.name, net.role); } // Box health and capability probing. lager.health()?; let status = lager.status()?; assert!(status.capabilities.net_command, "box image too old — run: lager box update"); ``` `status().capabilities` also reports `net_command_roles` (the newer arm/webcam/router roles), `ble_command`, `wifi_command`, and `blufi_command`, so a suite can skip tests a box image doesn't support instead of failing on them. ## Not yet available `Scope` (oscilloscope / logic analyzer) is a documented stub whose methods return `Error::NotSupportedByBox` until the box exposes those workflows over its HTTP API. `Rotation`/`Actuate` nets and net CRUD are out of the crate's scope for now — see the crate's [`MISSING_ENDPOINTS.md`](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md). # Rust SDK Overview Source: https://docs.lagerdata.com/source/reference/rust/overview Write your entire hardware-in-the-loop test suite in Rust and run it with cargo test The Lager Rust crate gives embedded developers first-class access to Lager nets, so a hardware-in-the-loop (HIL) test suite can live next to your firmware and run with `cargo test` — no Python required. The crate is a pure HTTP/JSON client of the Lager Box API: power supplies, battery simulators, e-loads, solar simulators, GPIO, ADC, DAC, thermocouples, watt meters, energy analyzers, SPI, I2C, USB hub ports, robot arms, webcams, routers, and streaming UART — plus the box-level capabilities (its own BLE adapter, WiFi interface, and BluFi ESP32 provisioning). Debug-probe nets (flash / erase / reset / memory reads / RTT) talk to the box's debug service. The package publishes on crates.io as [**`lager-net`**](https://crates.io/crates/lager-net) (the bare `lager` name is taken by an unrelated crate), but the library target is named `lager`, so your code reads `use lager::LagerBox;`. Full API reference lives on [docs.rs/lager-net](https://docs.rs/lager-net). ## Quickstart Add the crate to your firmware project's dev-dependencies: ```toml theme={null} # Cargo.toml [dev-dependencies] lager = { package = "lager-net", version = "0.2" } ``` Write a test: ```rust theme={null} // tests/boot.rs use lager::{LagerBox, Level}; #[test] fn dut_boots_at_3v3() -> lager::Result<()> { let lager = LagerBox::from_env()?; // reads LAGER_BOX_HOST let supply = lager.supply("supply1"); let boot_ok = lager.gpio("boot_ok"); supply.set_voltage(3.3)?; supply.enable()?; // Hardware-timed wait on the box; returns elapsed seconds. let t = boot_ok.wait_for_level(Level::High, 5.0)?; println!("booted in {t:.3}s"); supply.disable() } ``` Run it: ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test ``` `LagerBox::connect("hostname-or-ip")` also works, with an optional `host:port` or full URL. ## Features | Feature | Default | What you get | | ---------- | ------- | ------------------------------------------------------------------------------------------------ | | `blocking` | yes | `LagerBox` on [`ureq`](https://crates.io/crates/ureq) — tiny dependency tree, no tokio | | `async` | no | `AsyncLagerBox` on [`reqwest`](https://crates.io/crates/reqwest)/tokio; same methods, `.await`ed | | `uart` | no | `Uart` streaming sessions over the box's Socket.IO `/uart` namespace | Both clients execute the exact same request builders and response parsers, so the two transports cannot drift apart. ```toml theme={null} lager = { package = "lager-net", version = "0.2", features = ["async"] } ``` ## Errors Everything returns `lager::Result` with a single `Error` enum: | Variant | Meaning | | ------------------------- | -------------------------------------------------------------- | | `Connection` | Box unreachable (network/Tailscale/box offline) | | `Timeout` | The box stalled past the (already widened) budget | | `Box { status, message }` | The box refused or the hardware failed | | `UnsupportedByBox` | HTTP 501: the box image predates this endpoint; update the box | | `AuthRequired` | The box's gateway wants a bearer token and none is available | | `NotSupportedByBox` | The net type is a documented stub (see the note below) | ## Requirements * A Lager Box with software new enough to serve `POST /net/command` — check `lager.status()?.capabilities.net_command`, or run `lager box update`. * Rust 1.75+. Oscilloscope / logic-analyzer workflows are not exposed on the box HTTP API yet, so `Scope` ships as a documented stub whose methods return `Error::NotSupportedByBox`. Support lands when the box API does; the endpoint sketch is tracked in the crate's [`MISSING_ENDPOINTS.md`](https://github.com/lagerdata/lager-rs/blob/main/MISSING_ENDPOINTS.md). ## Next steps * **[Net types](/source/reference/rust/net-types)** — every handle the crate exposes * **[Testing guide](/source/reference/rust/testing)** — structuring a `cargo test` HIL suite, parallelism, CI * **[Debug probes & UART](/source/reference/rust/debug-and-uart)** — flashing, RTT, and streaming serial * **[Authentication](/source/reference/rust/auth)** — boxes behind an authenticating gateway # Testing with cargo test Source: https://docs.lagerdata.com/source/reference/rust/testing Structuring a Rust hardware-in-the-loop suite, parallelism, and CI The crate is designed so your HIL suite is just ordinary Rust integration tests: files under `tests/`, run by `cargo test`, living in the same repository as your firmware. ## Structure Point tests at a box with the `LAGER_BOX_HOST` environment variable and construct the client with `LagerBox::from_env()`: ```rust theme={null} // tests/power.rs use lager::LagerBox; #[test] fn dut_draws_less_than_100ma_idle() -> lager::Result<()> { let lager = LagerBox::from_env()?; let supply = lager.supply("DUT_POWER"); supply.set_voltage(3.3)?; supply.enable()?; let current = supply.state()?.current.expect("supply reports current"); assert!(current < 0.1, "idle draw {current} A"); supply.disable() } ``` ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test ``` Group tests by net or by DUT feature — one file per concern (`boot.rs`, `power.rs`, `sensors.rs`) keeps `cargo test ` filtering useful. ## Parallel tests and instrument safety `cargo test` runs tests on multiple threads by default. That is safe at the instrument level: the box serializes access per physical instrument — every net command runs under a per-device lock in the box's single-owner hardware service — so parallel tests can never interleave I/O on one instrument (e.g. a LabJack shared across GPIO/ADC/SPI nets, or a Keithley shared by supply and battery roles). Tests sharing a *net* still observe each other's state changes (one test's `disable()` is visible to another test reading the same supply). Either partition nets across tests, or serialize: ```sh theme={null} cargo test -- --test-threads=1 ``` ## Timeouts Timeout budgets mirror the Lager CLI: quick commands use 10 s, and operations that block on the box for a caller-controlled duration (watt/energy integration windows, `wait_for_level`) widen or drop the client timeout automatically, so a healthy long measurement is never aborted mid-flight. ## Hermetic tests vs. hardware tests Tests that always need real hardware should be marked `#[ignore]` so a plain `cargo test` (on a laptop with no box, or in a PR check) stays green: ```rust theme={null} #[test] #[ignore = "requires a Lager box"] fn flash_and_boot() -> lager::Result<()> { // ... } ``` Then opt in explicitly where a box is available: ```sh theme={null} LAGER_BOX_HOST=192.168.1.42 cargo test -- --ignored ``` This is the convention the crate itself uses: its own suite is hermetic (`cargo test` runs against a mock box), and its hardware smoke tests run with `cargo test --test hardware -- --ignored`. ## CI A minimal GitHub Actions job, assuming the runner can reach the box (e.g. a self-hosted runner on the lab network or a Tailscale-connected runner): ```yaml theme={null} jobs: hil: runs-on: [self-hosted, lab] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Run HIL suite env: LAGER_BOX_HOST: ${{ vars.LAGER_BOX_HOST }} # Only needed for boxes behind an authenticating gateway: LAGER_GATEWAY_TOKEN: ${{ secrets.LAGER_GATEWAY_TOKEN }} run: cargo test -- --ignored ``` For gateway-fronted boxes, see **[Authentication](/source/reference/rust/auth)** for how the token is attached and refreshed. # Version 0.10.0 Source: https://docs.lagerdata.com/source/release-notes/v0.10.0 March 17, 2026 ## Features * **`lager router` command group** — manage routers as Lager nets * **`lager router add-net`** — register a router (MikroTik hAP or compatible) as a net on a Lager Box * **`lager router connect`** — verify connectivity to a router net * **`lager router interfaces`** / **`lager router wireless-interfaces`** — inspect network and wireless interfaces * **`lager router wireless-clients`** — list currently connected wireless clients * **`lager router dhcp-leases`** — list devices that have received IP addresses from the router * **`lager router system-info`** — query router CPU, memory, and uptime * **`lager router reboot`** — reboot a router net * **`lager router enable-interface`** / **`lager router disable-interface`** — toggle wireless interfaces on/off * **`lager router block-internet`** — drop all forwarded traffic for network isolation testing * **`lager router reset`** — restore a router to a clean baseline (removes test firewall rules, bandwidth limits, and access list entries; optionally re-applies a baseline SSID and WPA2 password) * **`lager router run`** — make arbitrary REST API GET calls against the router ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.10.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.10.0/) # Version 0.11.0 Source: https://docs.lagerdata.com/source/release-notes/v0.11.0 March 18, 2026 ## Features * **Nordic PPK2 instrument support** — The Nordic Semiconductor Power Profiler Kit II is now a supported watt-meter and energy-analyzer instrument, alongside the Joulescope JS220 and Yocto-Watt * `lager watt --box ` reads instantaneous power (watts) from a PPK2 * `lager energy read --box --duration ` integrates energy (J, Wh) and charge (C, Ah) over a configurable duration * `lager energy stats --box --duration ` computes mean/min/max/std statistics for current, voltage, and power * PPK2 devices are auto-detected by `lager instruments` and can be added with `lager nets add-all` * Full Python API via `Net.get(name, type=NetType.WattMeter)` and `Net.get(name, type=NetType.EnergyAnalyzer)` ## Bug Fixes * Fixed webcam MJPEG stream returning 404 for dashboard `/stream/{netName}` requests * Fixed `Net.get()` not resolving instrument location when saved net config uses `address` instead of `location` ## Improvements * `lager energy` command now uses consistent argument order: `lager energy --options` * Cleaner formatted output for energy read and stats commands ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.11.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.11.0/) # Version 0.12.0 Source: https://docs.lagerdata.com/source/release-notes/v0.12.0 March 20, 2026 > Historical note: `lager boxes connect` was later removed from open-source Lager once downstream control planes became the canonical box bootstrap and orchestration layer. ## Features * **Command-in-progress lock** — When a `lager` command is running on a Lager Box, all other commands are automatically blocked with a clear error message, including from the same user. Locks auto-expire after 30 minutes to handle crashed CLI processes * **User lock (`lager boxes lock/unlock`)** — Explicitly lock a Lager Box so only you can run commands on it. Other users see a lock error until you unlock. The user who locked it can still run commands * `--force-command` global flag to bypass command-in-progress locks * `lager boxes` list now shows "locked by" and "busy" columns when applicable * `lager python --kill`, `--kill-all`, and `--reattach` skip lock checks so you can always manage running processes ## Improvements * Hardcoded control plane URL for the historical `lager boxes connect` flow, removing the `--url` flag ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.12.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.12.0/) # Version 0.13.0 Source: https://docs.lagerdata.com/source/release-notes/v0.13.0 March 20, 2026 > Historical note: `boxes connect` was a temporary migration seam. Current open-source Lager no longer exposes that command; downstream control planes own box install/bootstrap. ## Features * `--force-command` flag is now available on all subcommands that target a Lager Box (not just as a global flag). Place it anywhere after the subcommand name: `lager python script.py --box lab-box --force-command` * `--force-command` added to `hello`, `install`, `uninstall`, and the historical `boxes connect` command ## Improvements * `lager python --detach` now keeps the command lock until the detached process finishes on the Lager Box, preventing others from accidentally interfering with running scripts. The lock is automatically released when the script completes * All commands that acquire locks now automatically support `--force-command` via shared decorator ## Bug Fixes * Updated locking documentation to reflect current behavior (detach keeps lock, local flag syntax, correct `lager boxes` output format) ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.0/) # Version 0.13.2 Source: https://docs.lagerdata.com/source/release-notes/v0.13.2 March 21, 2026 ## Improvements * Updated `.gitignore` to exclude local AI assistant configuration directories ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.2/) # Version 0.13.3 Source: https://docs.lagerdata.com/source/release-notes/v0.13.3 March 21, 2026 ## Bug Fixes * **`lager python --detach` now holds command lock**: Detached Python scripts now correctly keep the Lager Box marked as busy while the script runs. Previously, the command lock was released immediately after detaching, allowing other commands (e.g. `lager hello`) to run against a busy box. The lock is automatically released when the detached process finishes or is killed. ## Installation ```bash theme={null} pip install lager-cli==0.13.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.3/) # Version 0.13.4 Source: https://docs.lagerdata.com/source/release-notes/v0.13.4 March 23, 2026 ## Bug Fixes * Removed the automatic command-in-progress lock (ephemeral lock) that fired on every CLI command. The feature had multiple corner cases — supply commands never released the lock, long-running commands blocked all other commands on the same box, and detached processes left stale locks * Removed `--force-command` flag from all commands (no longer needed) ## Improvements * User lock (`lager boxes lock` / `lager boxes unlock`) is unchanged and remains the recommended way to reserve a Lager Box ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.13.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.13.4/) # Version 0.14.0 Source: https://docs.lagerdata.com/source/release-notes/v0.14.0 March 24, 2026 ## Features * `lager install-wheel path/to/wheel --box X` installs a local Python wheel file on a Lager Box. Automatically uninstalls any previously installed version of the package before installing, so the version number does not need to be bumped on every rebuild. The package name is parsed from the wheel filename per the wheel specification. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.0/) # Version 0.14.1 Source: https://docs.lagerdata.com/source/release-notes/v0.14.1 March 24, 2026 ## Bug Fixes * `lager update --version v0.14.0` (and any version tag) now works correctly. Previously, version tags were incorrectly resolved as remote branch refs (`origin/v0.14.0`), causing the update to fail. Tags are now resolved directly. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.1/) # Version 0.14.2 Source: https://docs.lagerdata.com/source/release-notes/v0.14.2 March 30, 2026 ## Bug Fixes * `lager debug erase` and `lager debug flash` now correctly pass the JLinkScript to J-Link during the connect step. Previously only `gdbserver` passed the script, causing erase/flash to fail on MCUs that require a JLinkScript to load the correct flash algorithm (e.g. DA1469x with external QSPI flash) * For DA1469x targets, erase now uses address-range erase instead of chip erase, and no longer halts after erase when flashing * Fixed crash when running RTT after flashing * Improved J-Link process management: stale PID files are now cleaned up, and JLinkGDBServer is stopped before chip erase operations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.2/) # Version 0.14.3 Source: https://docs.lagerdata.com/source/release-notes/v0.14.3 March 31, 2026 ## Bug Fixes * Supply net current limit no longer gets automatically reset to 1A on TUI startup or any CLI command * OVP value now correctly displays in `lager supply state` output ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.3/) # Version 0.14.4 Source: https://docs.lagerdata.com/source/release-notes/v0.14.4 March 31, 2026 ## Changes * `lager debug flash` now erases flash by default before programming, ensuring a clean boot state. Use `--no-erase` to skip. The `--erase` flag is retained for backwards compatibility. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.14.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.14.4/) # Version 0.15.0 Source: https://docs.lagerdata.com/source/release-notes/v0.15.0 April 2, 2026 ## Features * `lager boxes lock` now accepts a `--user` flag to lock as a specific username, useful when running inside a Docker container where the effective user would otherwise be `root` ## Improvements * `lager boxes` now shows a warning when any Lager Box is locked as `root`, with instructions to use `--user` or `lager defaults add --user` * `LAGER_USER` environment variable is now the highest-priority source when determining the lager user for lock operations (before `~/.lager` config and the OS username) * Lock output and error messages now display the user's email address when available. External tools that lock boxes using the `::` lock format will have the email extracted and shown rather than the raw lock string * `lager update` SSH operations now use `StrictHostKeyChecking=accept-new` to avoid host-key prompts on first connection to a new Lager Box * `lager update` Docker rebuild step now correctly passes the explicit SSH key file when one is in use * `lager update` stop/remove step now targets the `lager` and `pigpio` containers by name instead of stopping all running containers ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.0/) # Version 0.15.1 Source: https://docs.lagerdata.com/source/release-notes/v0.15.1 April 7, 2026 ## Bug Fixes * DA1469x post-flash reset now uses GDB-based reset instead of J-Link Commander register writes, fixing unreliable behavior on DA1469x targets after flashing ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.1/) # Version 0.15.2 Source: https://docs.lagerdata.com/source/release-notes/v0.15.2 April 8, 2026 ## New Features * `lager install --version` now accepts a release tag (e.g. `v0.15.0`) in addition to a git branch, so you can install a box at a pinned version directly: `lager install --ip --version v0.15.0`. This replaces the old `--branch` flag, which only accepted branches. ## Bug Fixes * Reverted DA1469x post-flash reset to use J-Link Commander register writes (restores 0.15.0 behavior). The GDB-based reset introduced in 0.15.1 caused regressions on DA1469x targets. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.15.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.15.2/) # Version 0.16.0 Source: https://docs.lagerdata.com/source/release-notes/v0.16.0 April 13, 2026 ## Features * **Lager MCP server** — a Model Context Protocol server now runs on the Lager Box on port 8100 (FastMCP, streamable-http), allowing AI agents to discover a bench and understand how its nets are wired to the DUT. The server has moved from the CLI to the box and is started automatically inside the Docker container. * **Net metadata** — nets now support `description`, `dut_connection`, `test_hints`, and `tags` fields. New `lager nets` CLI commands and TUI flows let you edit this metadata interactively so AI agents (and humans) can reason about what each net is for. * **Capability graph and heuristic engine** — a new engine maps test types to the nets available on a bench, giving agents a principled way to pick the right instrument for a given task. * **Auto-generated MCP API reference** — the MCP API reference is now generated from driver introspection at Docker image build time. The build fails fast if a driver is renamed, so the agent-facing surface stays in sync with the code. ## Improvements * Every MCP tool call is wired through an `@audited` decorator that records the call via `audit.log_tool_call`, giving downstream control planes a consistent audit trail. * `quick_io` writes now pass through a `preflight_check` that enforces voltage, current, and dangerous-action constraints before touching hardware. * The `bench.json` parser is now defensive: a single malformed entry no longer breaks `discover_bench`. * MCP errors no longer return raw tracebacks to agents. `NetType()` inputs are validated against the enum. * `plan_firmware_test` uses a regex-based pattern split instead of the previous unsafe `get_pattern` split. * New integration test `test_agent_loop` plus unit tests for the bench loader, capability graph, heuristic engine, safety preflight, and MCP schemas. ## Security * The `run_lager` MCP passthrough tool is now gated behind the `LAGER_MCP_ALLOW_RUN_LAGER` environment flag and is **off by default**. Operators must opt in explicitly before agents can invoke arbitrary `lager` commands on a box. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.0/) # Version 0.16.1 Source: https://docs.lagerdata.com/source/release-notes/v0.16.1 April 13, 2026 ## Bug Fixes * **`bench_loader` null-value crash** — the MCP bench loader no longer crashes when `bench.json` or `saved_nets.json` contains explicit `null` values for list or dict fields such as `test_hints`, `tags`, `aliases`, `params`, `net_overrides`, `dut_slots`, `interfaces`, or `channels`. Previously, `dict.get(key, default)` only substituted the default when the key was absent, so a literal `"test_hints": null` would return `None` and break downstream iteration. All affected sites now coerce an explicit `null` to the same empty default as an absent key. Regression tests added in `test/mcp/unit/test_bench_loader.py::TestNullTolerance`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.1/) # Version 0.16.10 Source: https://docs.lagerdata.com/source/release-notes/v0.16.10 May 1, 2026 ## Bug Fixes * **`lager debug connect` no longer hides the real Segger error behind an `AttributeError` when J-Link cannot reach the target.** When J-Link's multi-speed retry loop in `box/lager/debug/api.py:connect_jlink` exhausted without ever reaching the target, `status['logfile']` could be set to `None` instead of being absent — so `status.get('logfile', 'No log available')` returned `None`, which was then passed into `clean_logfile_content` and crashed with `AttributeError: 'NoneType' object has no attribute 'replace'`. The crash masked the real Segger "Connecting to target failed" message that operators need to see in the dashboard. Two changes: `connect_jlink` now uses `status.get('logfile') or 'No log available'` so the literal fallback fires for both missing and `None` values, and `clean_logfile_content` itself returns `''` when given `None` as defense in depth for any future caller. ## Internal * Bumped seven transitive Rust dependencies in `box/oscilloscope-daemon/Cargo.lock` (`quinn-proto`, `rustls-webpki`, `time`, `bytes`, `tracing-subscriber`, `rand` 0.8 and 0.9 lines) to clear ten Dependabot security advisories on the daemon's QUIC/TLS stack. Lockfile-only change; the daemon binary is built and deployed separately from the lager-cli pip package, so this has no runtime effect on existing boxes until the daemon is rebuilt. Verified with a full release build + libps2000 link on a Picoscope-equipped box. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.10 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.10/) # Version 0.16.2 Source: https://docs.lagerdata.com/source/release-notes/v0.16.2 April 17, 2026 ## Bug Fixes * **Keysight E36313A USB PID** — corrected the USB product ID for the Keysight E36313A power supply (`2a8d:1202`) in the `SUPPORTED_USB` tables used by the box's USB scanner and the CLI's `query_instruments` path. The PID was previously a placeholder (`????`), so the instrument was not recognized on plug-in. A matching udev rule was added so PyVISA can open the device directly via libusb (`MODE=0666`, with the `usbtmc` driver unbound on `bind` to prevent "Resource busy" errors). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.2/) # Version 0.16.3 Source: https://docs.lagerdata.com/source/release-notes/v0.16.3 April 24, 2026 ## Improvements * **`user` column in `lager boxes`** — the table output for `lager boxes` (and `lager boxes list`) now includes a `user` column between `ip` and `version`. Lager Boxes added with `--user` show the configured SSH username; Lager Boxes added without `--user` show the default (`lagerdata`). Makes it easy to see, at a glance, which Lager Boxes are configured for a non-default SSH user. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.3/) # Version 0.16.4 Source: https://docs.lagerdata.com/source/release-notes/v0.16.4 April 27, 2026 ## Bug Fixes * **`/instruments/list` returning empty from worker threads** — when the box's HTTP server handled `/instruments/list` on a `ThreadingHTTPServer` worker, the USB scan's `signal.SIGALRM`-based timeout raised "signal only works in main thread of the main interpreter". The error was silently swallowed and the endpoint returned `[]`, so connected devices (e.g. LabJack T7) appeared to be missing. The scanner now falls back to a no-timeout direct call when it isn't on the main thread; the underlying serial and sysfs reads already have their own I/O timeouts. The CLI path was not affected because it runs `query_instruments.py` as a subprocess. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.4/) # Version 0.16.5 Source: https://docs.lagerdata.com/source/release-notes/v0.16.5 April 27, 2026 ## Bug Fixes * **Keysight E36xxx supplies reporting `Enabled: OFF` after `enable`** — running `lager supply state` (or any other read-only supply command) immediately after a successful `lager supply enable` would report the output as `OFF` on Keysight E36200/E36300 series supplies. The `KeysightE36000` driver constructor was unconditionally calling `disable_output()` as a "safe default" on every connect, so each fresh CLI invocation silently turned the output off before running its query. The disable is now gated behind the explicit `reset=True` flag, so constructing a driver for a read (or for `enable`) no longer mutates output state. * **EA PSB supplies briefly dropping output on re-enable** — `lager supply enable` on an EA PSB 10060-60 / 10080-60 caused a brief (\~500ms) output drop when the output was already on. `EA.enable()` always ran `_clear_latched_events()` first, which writes `OUTPut OFF` and waits 200ms before turning the output back on. `enable()` is now idempotent: if `OUTPut?` reports the output is already on, the call returns immediately without toggling. The off→on path that needs latched-protection clearing is unchanged. * **`lager supply tui` closing silently on Rigol DP821** — the supply TUI was closing after \~5 seconds with no visible error whenever a direct supply command (e.g. `state`) had been run beforehand. The WebSocket supply monitor on the box was opening its own pyvisa session, conflicting with the cached VISA session held by `hardware_service.py` on port 8080. Instruments that don't tolerate concurrent USB sessions (Rigol DP821 reproduces this) hung silently and the TUI's 15-second wait for `supply_driver_ready` always timed out. The monitor thread now releases the cached handle (via `localhost:8080/cache/clear`) before opening its own session. As a defensive bonus, `get_channel_limits()` and the session-store block now emit a visible `error` event on any failure during init, and the CLI captures the TUI's exit reason and prints it red to stderr after Textual's alt-screen tears down — so failures no longer disappear with the screen restore. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.5/) # Version 0.16.6 Source: https://docs.lagerdata.com/source/release-notes/v0.16.6 April 27, 2026 ## Bug Fixes * **`lager battery tui` now works for the first time.** The OLD WebSocket battery monitor crashed at module load with `ImportError: cannot import name '_resolve_net_and_driver' from 'lager.power.battery.dispatcher'` — that symbol existed at module level only in the supply dispatcher, never the battery one. Nobody had reported the bug because nobody had tested the battery TUI before. Incidentally fixed by the VISA-ownership unification below; the battery monitor now also emits a `battery_driver_ready` event mirroring `supply_driver_ready` for client symmetry. * **Concurrent SCPI access on the same instrument now serializes correctly.** Two `/invoke` requests against the same cached driver in `box/lager/hardware_service.py` could race on the SCPI bus and produce `Query INTERRUPTED` pyvisa errors. Added a per-`(device_name, address)` lock that wraps every driver call. Multi-channel devices (e.g., Rigol DP821) correctly share one lock since they share one VISA session. ## Improvements * **VISA session ownership unified.** The supply and battery WebSocket monitors no longer open their own pyvisa sessions in monitor threads. They now route every driver call through `hardware_service.py:/invoke` via the existing `Device` HTTP proxy. `hardware_service.py` (port 8080) is the sole owner of pyvisa sessions per `(device_name, address)`. The v0.16.5 `POST /cache/clear` band-aid is removed — this is the architectural fix that replaces it. Net effect: TUIs are more robust, no longer trip on stale cached sessions, and `Query INTERRUPTED` errors during simultaneous TUI + CLI activity are gone. * **Battery handlers consolidated.** \~670 lines of duplicate battery-handler code in `box/lager/box_http_server.py` (parallel to the modular `box/lager/http_handlers/battery.py`) were deleted. `box_http_server.py` now imports and registers the modular versions, matching what was already done for supply. * **Test hygiene.** Two unit tests in `test/unit/cli/test_performance_improvements.py` had been silently failing since the `.lager` config format was migrated to JSON-only. Tempfiles now use `{"LAGER": {...}}` JSON; full unit suite is back to 141/141 passing. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.6 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.6/) # Version 0.16.7 Source: https://docs.lagerdata.com/source/release-notes/v0.16.7 April 28, 2026 ## Bug Fixes * **`lager uart ` no longer returns `404 — UART net not found`.** The v0.16.6 battery-handler consolidation (commit `f277402`) deleted the two-line UART handler registration in `box/lager/box_http_server.py` as collateral damage. The imports stayed in place, so the file still parsed cleanly; the `/uart/nets/list` Flask route just was never registered, so every UART CLI command 404'd. Restored the `register_uart_routes(app)` / `register_uart_socketio(socketio)` calls alongside the supply and battery registrations. * **`lager supply state` (and other one-shot supply/battery commands) no longer fail with `[Errno 16] Resource busy` immediately after exiting the TUI.** Root cause: `/supply/command` and `/battery/command` returned 404 when no active WebSocket session was found, forcing the CLI's `_run_backend` into a direct-pyvisa subprocess fallback (`cli/impl/power/supply.py` → dispatcher) that opened its own pyvisa session against the same USB device `hardware_service.py` was still caching. Both endpoints now build a transient `Device` proxy via `resolve_net_proxy()` when no active WS session exists, routing through `hardware_service.py:/invoke` like the WS monitor already does. There is now exactly one pyvisa session per `(device_name, address)` regardless of TUI lifecycle. This completes v0.16.6's "VISA session ownership unified" promise. * **Concurrent TUI + CLI access on the same supply no longer cascades `Resource busy` errors across subsequent commands.** Previously, a single transient kernel-level USB-claim collision (the kind that can momentarily occur when two pyvisa-issued USB transfers overlap on the same device) would be mis-classified as a stale pyvisa session: `_is_visa_session_error()` matched the substring `'resource'` inside `'Resource busy'`, the retry path then popped the live cache entry and called `module.create_device()` on the same address, and the new open hit `Resource busy` again because the original session was still alive in the same process. The result was that an isolated USB-busy error turned into a chain of failures across every following command. Removed `'resource'` from `_VISA_SESSION_ERROR_KEYWORDS` in `box/lager/hardware_service.py`; retry now fires only for genuine stale-session signals (`'session'`, `'closed'`, `'invalid'`). An isolated USB-busy collision is still possible on heavily-contended USB transfers but is now returned to the caller cleanly without disturbing the cache, so the next command immediately succeeds. ## Known Limitations * **Keithley 2281S dual-role nets must be used one at a time.** When the same physical Keithley 2281S has both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`) configured, `box/lager/hardware_service.py` caches them under two different keys (`("keithley", address)` vs `("keithley_battery", address)`) and tries to open two pyvisa sessions on the same USB device — the second hits `[Errno 16] Resource busy`. Workaround: configure only the role you need on the Keithley 2281S, or restart the box's lager container between switching roles. The proper fix (shared pyvisa Resource between supply and battery driver instances, or a merged dual-role driver class) is targeted for v0.16.8. * **Concurrent battery TUI + CLI on the Keithley 2281S can surface `[Errno 16] Resource busy`.** Running `lager battery tui` in one terminal while running `lager battery state` (or any other one-shot battery CLI command against the same net) in another terminal can fail with `Resource busy`, even when only the battery role is configured on the Keithley (so this is distinct from the dual-role limitation above). v0.16.7's Bug-B retry-classification fix prevents this from cascading across subsequent commands but does not eliminate the initial collision; the underlying contention appears to live in the Keithley pyvisa session itself rather than in `hardware_service.py`'s lock. Workaround: do not invoke battery CLI commands while a battery TUI is open against the Keithley 2281S — close the TUI first, or run TUI-only or CLI-only on the Keithley battery net. Root-cause investigation tracked for v0.16.8. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.7 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.7/) # Version 0.16.8 Source: https://docs.lagerdata.com/source/release-notes/v0.16.8 April 28, 2026 ## Features * **SEGGER J-Link Flasher PRO support** — the J-Link Flasher PRO (USB `1366:0105`) is now recognized as a supported `debug` instrument. Plugging one into a Lager Box and running `lager instruments` will list it as `J-Link_Flasher_Pro`. Both the box-side scanner (`box/lager/http_handlers/usb_scanner.py`) and the CLI-side scanner (`cli/impl/query_instruments.py`) were updated. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.8 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.8/) # Version 0.16.9 Source: https://docs.lagerdata.com/source/release-notes/v0.16.9 April 29, 2026 ## Bug Fixes * **Keithley 2281S dual-role nets now switch cleanly between supply and battery roles.** When the same physical Keithley 2281S is configured with both a `power-supply` net (e.g. `supply1`) and a `battery` net (e.g. `battery1`), the box now opens exactly one pyvisa session per VISA address and both driver classes wrap that one session — instead of each driver opening its own session and the second hitting `[Errno 16] Resource busy`. Implemented as a process-wide shared-resource cache (`_visa_resources` keyed by VISA address) in `box/lager/hardware_service.py`, plus a `raw_resource=` factory kwarg on `box/lager/power/supply/keithley.py:create_device` and `box/lager/power/battery/keithley.py:create_device`. Both Keithley driver constructors track an `_owns_resource` flag so `close()` does not release the underlying USB claim while the sibling driver still needs it. SCPI serialization moved to a per-address lock (was per `(device_name, address)` cache key) so a supply command followed by a battery command against the same Keithley serialize correctly on the USB bus. This resolves the **sequential** half of the dual-role known limitation in v0.16.7 — a script can now alternate `lager supply ` and `lager battery ` commands against the same Keithley without restarting the box service. The instrument's two operating modes (Power Supply via `:ENTR:FUNC POW`, Battery Simulator via `:ENTR:FUNC BATT`) remain mutually exclusive in firmware, so genuinely *concurrent* supply + battery operation against one Keithley is still not supported by the hardware itself; see Known Limitations. * **Concurrent battery TUI + CLI on the Keithley 2281S no longer fails with `[Errno 16] Resource busy`.** The stale-VISA-session retry path in `box/lager/hardware_service.py:/invoke` now calls `_close_device(old_device, cache_key)` before invoking `module.create_device(net_info)`. Previously the popped driver instance stayed alive in the Python process and kept libusb's USB claim, so the recreated session's `pyvisa.ResourceManager().open_resource(addr)` failed with `Resource busy` — surfaced as `Could not open instrument at ...`. Closing the old session before opening a new one fixes this for any driver whose retry path fires; for Keithley shared-resource drivers the underlying pyvisa session is also reopened so both supply and battery drivers get a fresh handle. This resolves the concurrent battery TUI + CLI known limitation documented in v0.16.7. * **Keithley 2281S supply commands no longer crash with `TypeError`.** `box/lager/http_handlers/supply.py` is modeled on multi-channel drivers (Rigol DP800) and calls supply-driver methods with a `channel=` kwarg or positional channel. The Keithley 2281S supply driver follows the `SupplyNet` abstract (no `channel` parameter — the 2281S is single-channel), so the very first call hit `TypeError: Keithley2281S.output_is_enabled() got an unexpected keyword argument 'channel'`. The handler treated that as a hardware failure and triggered `/cache/clear`, which tore down the shared pyvisa session this release had just opened for dual-role mode. `Keithley2281S.output_is_enabled` now accepts (and ignores) a `channel=None` kwarg, and six new public OCP/OVP wrapper methods (`set_overcurrent_protection_value`, `enable_overcurrent_protection`, `set_overvoltage_protection_value`, `enable_overvoltage_protection`, `clear_overcurrent_protection_trip`, `clear_overvoltage_protection_trip`) delegate to the existing private `_set_ocp` / `_set_ovp` and public `clear_ocp` / `clear_ovp` methods so the supply handler can call them without `AttributeError`. No new SCPI logic — the wrappers exist purely so a single-channel driver can satisfy the multi-channel calling convention used elsewhere. * **`lager battery state` no longer collides with the shared pyvisa session.** The battery CLI sends `action='print_state'` (matching the dispatcher function name), but `/battery/command` previously only recognized `action='state'` (matching the supply handler). The mismatched action returned HTTP 400, the CLI's `_run_backend` fell through to the python:5000 dispatcher path, and that subprocess opened a *second* pyvisa session against the same Keithley — colliding with the shared session that hardware\_service had just opened in the previous `lager supply` command and surfacing as `Could not open instrument at USB0::...: failed to set configuration [Errno 16] Resource busy`. `/battery/command` now accepts both `'state'` and `'print_state'`, keeping the CLI on the WebSocket → hardware\_service path so the shared pyvisa session this release introduces is actually reused for sequential supply→battery CLI workflows. * **`lager python` no longer wipes hardware\_service's cache on every script exit.** `cli/commands/development/python.py` was POSTing `/cache/clear` on script normal exit, Ctrl+C, and BrokenPipeError — a v0.16.5 band-aid that pre-dates Phase 2's per-address shared session. With v0.16.9, hardware\_service is the single owner of the pyvisa session for each USB device and that session is *meant* to persist for the container's lifetime. Clearing it on every script exit defeated the design and re-introduced the very `[Errno 16] Resource busy` race that Phase 2 set out to eliminate. The clears are removed; if you really need to force a reload (e.g., a script that opens its own pyvisa session out-of-band), you can still `curl -X POST http://:8080/cache/clear` manually. * **Resilient first open against libusb's release-interface timing race.** `hardware_service._get_or_open_visa_resource` now retries `open_resource()` on `[Errno 16] Resource busy` with an exponential backoff (`0.2, 0.5, 1.0, 2.0` s) before giving up. pyvisa-py + libusb on Linux releases the USB interface asynchronously, so opening the same device too quickly after a close (e.g. after a manual `/cache/clear` or a TUI exit) could fail the first time and succeed the second. The retries hide the kernel's catch-up window without masking genuine "device unplugged" failures. * **`POST /cache/clear` preserves shared pyvisa sessions; new `POST /cache/clear_all` for the old behavior.** The endpoint still drops cached driver wrappers from `device_cache` (so a wedged driver gets a fresh load on the next `/invoke`), but the per-VISA-address shared session that this release relies on is no longer torn down. This was the missing piece that caused V.5/V.6 hardware verification to fail even after the script-exit clear was removed from `lager python` — older clients (`lager-cli` ≤ 0.16.7) still POST `/cache/clear` on every script exit, and that was nuking the shared session out from under hardware\_service. With the endpoint now safe under Phase 2, those older clients no longer break dual-role workflows. If you actually need to force-close a shared session (e.g. you unplugged the instrument), `POST /cache/clear_all` does what `/cache/clear` used to do. * **Cross-role concurrent use on a single Keithley 2281S now fails fast with a clear error.** Running `lager supply tui` and a concurrent `lager battery ` command (or vice-versa) against the **same** physical Keithley used to surface as cryptic SCPI timeouts or `[Errno 16] Resource busy` errors, because the 2281S's Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`) entry functions are mutually exclusive in firmware and the two clients were fighting over the entry function on every poll. The box now tracks the active monitoring sessions per role (`box/lager/http_handlers/state.py:conflicting_other_role_session`), records the resolved VISA address when a TUI starts, and refuses an opposite-role command at `/supply/command`, `/battery/command`, `start_supply_monitor`, and `start_battery_monitor` with a message that names the conflicting net and explains the hardware limitation. Sequential CLI cross-role workflows are unaffected and continue to work cleanly via Phase 2's shared pyvisa session. ## Known Limitations * **Concurrent supply and battery operation on the same Keithley 2281S is not supported by the instrument itself.** The 2281S has two mutually-exclusive entry functions — Power Supply (`:ENTR:FUNC POW`) and Battery Simulator (`:ENTR:FUNC BATT`). Each Lager driver flips the entry function to its preferred mode before every SCPI command, so running a supply TUI in one terminal while running a battery CLI command in another terminal causes the two clients to fight over the entry function on every poll, producing intermittent SCPI errors and Resource busy events. **Configure either the supply role or the battery role on the Keithley 2281S, not both — or use them strictly sequentially in a single workflow** (which v0.16.9's shared-pyvisa-session work makes fast and clean). This is a property of the instrument, not Lager. ## Internal * Drivers that share a single pyvisa session per VISA address are listed in `box/lager/hardware_service.py:_SHARED_VISA_DEVICE_NAMES` (currently `keithley`, `keithley_battery`). Adding a future dual-role instrument means adding its supply and battery `device_name` strings here and giving each `create_device` factory the `raw_resource=` kwarg pattern. * `Keithley2281S.__init__` and `KeithleyBattery.__init__` accept a new `_owns_resource` kwarg (default `True` for backward compatibility). When `False`, `close()` drops the wrapper reference without closing the underlying pyvisa session. * Single-role drivers (Rigol DP800/DP821, Keysight E36xxx, EA PSB, etc.) are unchanged — they continue to use the legacy per-driver-opens-its-own-session path. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.16.9 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.16.9/) # Version 0.17.0 Source: https://docs.lagerdata.com/source/release-notes/v0.17.0 May 5, 2026 ## Features * **Concurrent J-Link probes on a single Lager Box.** Two J-Link probes plugged into one Box can now run independent debug sessions side-by-side. The box-side service in `box/lager/debug/service.py` resolves each debug net's J-Link USB serial from its VISA address and allocates a deterministic per-probe slot (read from `saved_nets.json` via `NetsCache`). Slot N owns a three-port window — GDB `2331+3N`, SWO `2332+3N`, telnet `2333+3N` — plus RTT base `9090+2N`. The auxiliary `-swoport` and `-telnetport` are passed explicitly to `JLinkGDBServer` so its defaults of `2332`/`2333` can't collide with another slot's GDB port. The service passes `-select USB=` to `JLinkGDBServer` and `-SelectEmuBySN ` to `JLinkExe`, writes per-serial PID and log files, and narrows `pkill` so disconnecting one probe no longer tears down the other. The CLI's `--gdb-port` default is now `None` rather than `2331`, so the box's allocator is honored unless an explicit port is requested; the effective `gdb_port` returned by the box is printed on connect. `start_box.sh` publishes the widened `2331-2342` Docker port range and `secure_box_firewall.sh` admits the same range, so existing hardened boxes need a firewall refresh to use the new slots. Nets without a parseable serial (legacy single-probe setups) fall back to slot 0 / GDB 2331 / RTT 9090 / `/tmp/jlink_gdbserver.pid` and continue to work unchanged. * **RIGOL DP811 power supply detection.** `lager instruments` now lists the DP811 alongside the DP821 and DP832. The DP811 shares VID:PID `1ab1:0e11` with its siblings, so it is identified by USB serial prefix (`DP8H` or `DP81` → `Rigol_DP811`) in both `box/lager/http_handlers/usb_scanner.py` and `cli/impl/query_instruments.py`, and is added to the serial-disambiguated bucket so a generic VID:PID match cannot misclassify it as a DP821. * **Multiple concurrent viewers per webcam stream.** Each `/stream` connection used to open its own `cv2.VideoCapture` against `/dev/videoN`, which V4L2 serves exclusively — the second viewer either failed or got blank frames. The streamer subprocess in `box/lager/automation/webcam/service.py` now spins up a single daemon capture thread on the first viewer that owns the device and broadcasts encoded JPEG frames to a shared buffer guarded by a `threading.Condition`. Any number of viewers can now subscribe to the same stream concurrently. Stop and re-start each webcam to pick up the regenerated streamer script. ## Bug Fixes * **`LabJackADC.input()` no longer inherits sticky AIN register state from a previous tool.** ADC reads previously called `ljm.eReadName` with zero AIN register configuration, inheriting whatever the previous tool left in `AIN_RANGE`, `AIN_NEGATIVE_CH`, `AIN_RESOLUTION_INDEX`, and `AIN_SETTLING_US`. T7 register state persists in device RAM until USB power-cycle, so if a previous tool left an AIN in differential mode with a floating negative channel, every read saturated at \~10.10 V regardless of the actual signal — indistinguishable from a real wiring fault. Safe defaults (`RANGE=10.0`, `NEGATIVE_CH=199`, `RESOLUTION_INDEX=0`, `SETTLING_US=0`) are now written once per `(handle, channel)` tuple before the first `eReadName`, cached in a class-level set. Config-write failures are logged but do not raise. ## Improvements * **Webcam capture forces MJPEG so two cameras can share a USB 2.0 bus.** Default OpenCV negotiation picked YUYV (uncompressed, \~150 Mbps at 640×480 30fps), which doesn't leave room for a second camera on the same bus — the kernel rejected `VIDIOC_STREAMON` with "Not enough bandwidth for altsetting". MJPEG is roughly 5× smaller and fits two cameras comfortably; the FourCC is now set before width/height/fps so negotiation honors it. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.17.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.17.0/) # Version 0.18.0 Source: https://docs.lagerdata.com/source/release-notes/v0.18.0 May 12, 2026 ## Features * **`lager box config` — declarative per-box provisioning.** A new top-level command tree that replaces ad-hoc SSH-and-edit workflows with a single JSON manifest at `/etc/lager/box_config.json` per Lager Box. The file declares mounts, named Docker volumes, container environment variables, host apt packages, kernel sysctl settings, in-container pip packages, cargo crates, and npm packages — and `lager box config apply` reconciles the box to match. Re-applying the same config is a no-op via SHA-256 comparison against the last applied snapshot, so it's safe to wire into CI. The full operator surface: `init`, `show`, `validate`, `diff`, `apply` (with `--dry-run` and `--yes`), `audit`, `status`, `edit` (round-trips through `$EDITOR`/`nano`/`vi` with shim-side validation on save), `copy --from --to`, `import FILE`, `export FILE`, and `repair`. Multi-box fanout via `--box A,B,C` on `show` and `apply` for fleet operations. Every section has CRUD verbs: `mount add/remove/list`, `pip add/remove/list`, `apt add/remove/list`, `cargo add/remove/list`, `npm add/remove/list`, `sysctl set/unset/list`, `env set/unset/list`, `volume add/remove/list`. * **npm support inside the container.** A new `npm_packages` first-class field on `box_config.json` lets you declare Node.js global packages alongside the existing pip and cargo lists. Scoped packages (`@types/node`) and versioned packages (`lodash@4.17.21`) are both supported. The container Dockerfile now ships `nodejs npm` and sets `NPM_CONFIG_PREFIX=/home/www-data/.npm-global` (pre-created and chowned to the `www-data` runtime user) so `npm install -g` works without root. * **Rust toolchain baked into the container image.** rustup is now installed into `/opt/rust` (owned by `www-data`) with `RUSTUP_HOME`, `CARGO_HOME`, and `PATH` set in the Dockerfile, so `cargo install` runs cleanly from the post-bounce loop. No more manual rust installation per Lager Box. `cargo_packages` entries accept both `name` and `name@version`. * **Audit log of every config mutation.** Every `add`/`set`/`remove`/`unset`/`apply` operation is recorded to `/etc/lager/box_config.audit.log` (JSONL, append-only) with an ISO-8601 timestamp. `lager box config audit` reads it back. Filters compose: `--tail 20`, `--since 1h`, `--verb apt-add`, `--json`. Useful for "what changed today" or "every apt operation ever." * **Automatic rollback on failed bounces.** When `lager box config apply`'s container restart fails (for example, because docker rejected a malformed mount), the previously applied snapshot is restored to `/etc/lager/box_config.json` via SSH `sudo cp` and a re-bounce brings the box back up on the prior good config. Sysctl values are reverse-diffed to their previous state in the same pass. The restore goes through direct SSH file ops rather than the in-container shim because the container is necessarily dead by the time the rollback fires. `lager box config repair --box X` exposes the same recovery as a standalone command for situations that automatic rollback can't reach — for example, when an operator hand-edits the JSON to invalid syntax outside the CLI. * **Sudoers auto-bootstrap.** `lager install` (on new boxes) and `lager update` (on existing boxes) now install `/etc/sudoers.d/lager-box-config` with the narrow NOPASSWD grants `lager box config apply` needs: `apt-get` with `SETENV:` for `DEBIAN_FRONTEND`, path-scoped `tee`/`rm`/`sysctl --system` for the sysctl conf, `mkdir`/`chown` for mount auto-prep, and a path-scoped `cp` for the rollback snapshot restore. A marker file at `/etc/lager/.boxcfg-sudoers-v2` lets `lager update` skip re-bootstrapping once the current rule shape is in place. Operators never type a sudoers snippet by hand. ## Bug Fixes * **`lager update` container startup timeout raised from 5 to 10 minutes.** First-time docker builds with cargo and npm layers were timing out on slower Lager Boxes. `_bounce_container`'s SSH ceiling was also bumped from 300s to 900s for the same reason — covers cargo crate compilation + pip and npm install loops with headroom. * **SSH user resolution.** The new shared SSH runner used by `lager box config` was calling `get_box_user(box_ip)` even though that helper keys by box *name*, so every Lager Box with a stored custom SSH user silently fell back to `lagerdata`. The runner now reverse-resolves the name via `get_box_name_by_ip` before the lookup, and uses `~/.ssh/lager_box` via `-i` to match the rest of the CLI's SSH conventions. * **`DEBIAN_FRONTEND=noninteractive` actually propagates on apt installs.** Default Ubuntu sudoers' `env_reset` strips `DEBIAN_FRONTEND` set as a `sudo VAR=value cmd` argument unless `SETENV:` is granted. Packages with debconf prompts (`iptables-persistent` and similar) were hanging on a prompt that never showed. The new sudoers rule grants `SETENV:` only on `/usr/bin/apt-get` so the env var propagates. * **`cargo` found inside the container during apply.** `start_box.sh`'s cargo install loop used `bash -lc` (login shell), which re-sourced `/etc/profile` and reset `PATH` — wiping the Dockerfile's `ENV PATH=/opt/rust/cargo/bin:...`. Switched to `bash -c` (non-login) so the docker `ENV` is honored. Same fix applied to the npm install loop. * **Real exit codes captured from pip/cargo/npm install loops.** The previous `if ! cmd; then _rc=$?` pattern in `start_box.sh` captured `$?` *after* bash's `!` inversion — so `_rc` was always `0` even on real failures, and error messages reported `(rc=0)` for non-zero exits. Refactored to `if cmd; then : else _rc=$?` so error codes propagate accurately. * **Env values with whitespace, `$`, backticks, or single quotes survive the bounce.** The docker-args renderer used to emit `--env 'KEY=hello world'` to stdout, which `start_box.sh` interpolated unquoted into `docker run` — bash variable expansion does not re-parse quotes, so values got word-split and the literal quote characters leaked through. The renderer now writes a bash-sourceable file declaring `BOX_CONFIG_MOUNTS`, `BOX_CONFIG_ENV`, and `BOX_CONFIG_HOST_PATHS` arrays via `shlex.quote`; `start_box.sh` sources that file and uses `"${BOX_CONFIG_MOUNTS[@]}"` so each element preserves its content verbatim. * **`lager box config edit` no longer rejects valid saves with non-zero editor exit.** Some vim plugins return `1` from `:wq` even when the save succeeded. The command now compares tempfile contents before and after the editor exits — content changed AND non-zero rc means "user saved, proceed"; content unchanged AND non-zero rc means "abort." Bonus: `nano` is preferred over `vi` as the fallback when `$EDITOR` is unset. ## Improvements * **`lager box config show` reads as a tree.** Bold uppercase `HOST` / `CONTAINER` group headers with horizontal-rule underlines, bold section labels indented two spaces, and `├── /└── ` branches under each section. Mount paths align around `->`; env/sysctl keys align around `=`; empty sections render as `(none)` leaves so operators discover what's configurable. The header carries a color-coded `[Up To Date]` / `[Unapplied Changes!]` marker driven by a `hash` vs `applied-hash` comparison. * **`apply` shows the pending diff inline before confirming.** When `--yes` is not passed, the confirm prompt is preceded by a per-field diff of what's about to change — closes the most common pre-apply workflow ("run diff first, then apply") into a single command. * **Tightened sudoers rule.** `tee`, `rm`, and `sysctl --system` in the recommended sudoers grant are now path-locked to the exact files and flags `apply` invokes, so a compromised `lagerdata` account cannot escalate to root via those binaries. `apt-get` and `mkdir`/`chown` stay unscoped because the package list and host paths are user-defined. * **flock against the in-container shim.** Two concurrent `lager box config X` invocations against the same Lager Box used to do read-modify-write on `box_config.json` and silently drop one mutation. The shim now `flock`s `/etc/lager/box_config.lock` around the whole dispatch. * **Post-apply consistency check.** After the bounce + API-ready probe but before recording the new applied-hash, the apply path re-runs `validate` + `show` against the box. If either drifts from what was bounced (the JSON was hand-edited mid-apply, say), `applied-hash` is left untouched and the operator is told to re-run apply. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` After upgrading, run `lager update --box ` on each existing Lager Box to deploy the matching box-side code and pick up the sudoers rule. ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.0/) # Version 0.18.1 Source: https://docs.lagerdata.com/source/release-notes/v0.18.1 May 13, 2026 ## Bug Fixes * **J-Link GDB attach no longer halts the target.** Dropped the `-ir` flag from `JLinkGDBServer` and switched GDB into non-stop async mode before target attach, so attaching gdbserver/rtt no longer halts the target CPU on \~15% of attempts. ## Improvements * **`lager usb` enable / disable / toggle \~2.6x faster.** Routes through a new Flask handler on the box's port 9000 instead of spawning a fresh Python subprocess per call (mirrors the supply/battery fast-path from 0.17.x). Backward compatible — falls back to the slow path against older box images. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.1/) # Version 0.18.2 Source: https://docs.lagerdata.com/source/release-notes/v0.18.2 May 13, 2026 ## Features * **`lager box update` is now the canonical update command.** The top-level `lager update` keeps working as a hidden alias for existing scripts but prints a deprecation notice. Same flag set; runs through the same flow. * **`--check` flag for dry-run updates.** `lager box update --box X --check` reports what would change (code, deps, container) without touching the box. Useful for CI gating and pre-deploy checks. * **Docker cache auto-invalidates when `Dockerfile` or `requirements.txt` change.** No more remembering to pass `--force` after a deps bump — the next update detects the drift and rebuilds. ## Bug Fixes * **Updates now take effect on the first run.** Fixes a cluster of bugs behind the recurring "had to run `lager update` 2–3 times before it stuck" reports: stale `/etc/lager/version` after the early-exit branch, the flatten heuristic re-fetching on every run, the 5-second post-restart sleep racing against an unready service, silent flatten failures producing broken images, and the cache-invalidation early-exit skipping rebuilds when only deps had changed. * **Branch switches with conflicting root-level files no longer fail.** Adds `git checkout -f` so a previous flatten step that clobbered a tracked file (e.g. `README.md`) doesn't block the switch with "local changes would be overwritten." * **Git errors are now shown.** `git checkout` and `git reset --hard` failures used to surface only "Failed to checkout version X" without git's underlying error message. ## Improvements * **Consecutive no-op `lager box update` runs are \~10× faster.** SSH calls multiplex through a single OpenSSH ControlMaster connection — \~20s → \~1.6s. * **Cleaner output.** Single green summary line, progress bar adapts to terminal width, elapsed time appears on the bar itself. * **`--all`, `--force`, `--skip-restart` flags removed.** `--all` will return as its own command if needed; `--force` is obsoleted by auto cache-invalidation; `--skip-restart` had no real workflow. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.2/) # Version 0.18.3 Source: https://docs.lagerdata.com/source/release-notes/v0.18.3 May 15, 2026 ## Features * **`lager box update --version ` rolls back.** Previously the update flow only counted commits the box was *behind* the target and treated any "ahead" state as in-sync, so you could not downgrade a box to an earlier branch or tag without manually `git reset --hard`-ing on the box. Now diverges in both directions; an explicit second confirmation prompt (skippable via `--yes`) gates the destructive direction so a typo'd `--version` argument can't silently downgrade. `--check` reports "will roll back N commit(s)" / "will switch (N ahead / M behind)". ## Bug Fixes * **Update no longer aborts on git ≥2.36 with `fatal: 'cli/__init__.py' is not a directory`.** Cone-mode sparse-checkout (default since git 2.36) rejects single-file patterns; the pre-batching version of the sparse-checkout step ran in a separate SSH call whose exit was never checked, so the failure was silently swallowed. The new batched pull script chained it with `&&`, propagating the failure and aborting the whole pull. Now treats the `cli/__init__.py` add as best-effort. Affects boxes on newer git (e.g. git 2.43 on Debian bookworm-backports). * **`/etc/lager/version` and the end-of-run summary report the box's actual code, not the CLI's version.** Previously a rollback or branch switch could write the running CLI's version (e.g. `0.18.3`) into the version file even when the code on the box was older (e.g. `v0.18.2`). The CLI now reads `__version__` from `cli/__init__.py` at the box's post-pull HEAD via `git show`, which works even on cone-mode boxes where the file isn't in the working tree. ## Improvements * **No-op `lager box update` runs \~3× faster (\~5s → \~1.6s).** A single SSH probe collapses \~11 separate `test`/`cat`/`git`/`diff`/`stat` round-trips (git-repo check, remote URL, layout, current commit, build-cache hashes, udev rule state, sudoers ownership, box-config sudoers state, `/etc/lager/version`) into one structured shell script. Combined with merging fetch+rev-list, sparse-checkout+checkout+reset, flatten+verify, post-build directory setup, and verify+J-Link presence into single calls. * **Typical `lager box update` \~6× faster on boxes with cargo/npm packages in `box_config.json` (\~1:40 → \~17s).** Adds two named Docker volumes — `lager-cargo` at `/opt/rust/cargo` and `lager-npm-global` at `/home/www-data/.npm-global` — to `start_box.sh`'s `docker run`, so user-installed cargo crates and global npm packages survive container recreation. Without them, every update reinstalled them from scratch (`cargo install` recompiled from source, \~50–60s per update). With them, the second-and-onward run sees "already installed" and finishes in seconds. The CLI wipes both volumes alongside `docker rmi lager` whenever the build-hash changes, so a Dockerfile rustup/node bump can't leave a stale toolchain in the volume. First update on each existing box is the same speed as today (the volumes seed themselves); the win shows up from the second update on. * **`--verbose` output cleanup.** Probe results print as one tidy block instead of a dozen "Checking X... OK" lines; consistent step labels between the progress bar and verbose log; noise lines dropped (e.g. "Checking remote URL" only prints when it actually migrates SSH→HTTPS); a single label for the build step instead of two. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.3/) # Version 0.18.4 Source: https://docs.lagerdata.com/source/release-notes/v0.18.4 May 20, 2026 ## Bug Fixes * **`lager python` scripts no longer miss tight response deadlines under streaming back-pressure.** A running script's stdout/stderr were drained from their kernel pipes *inline* on the same generator that forwards bytes back to the CLI over HTTP, so any stall on that socket (slow link, Nagle, retransmit) stopped pipe drainage. Once the 64 KiB kernel pipe filled, the script blocked on its next `print()`. For scripts with tight timing budgets — for example a DA14695 ROM-bootloader handshake that must reply within 50–120 ms of each received byte — this stretched response windows enough to fail roughly 90% of the time, even though the same script run directly on the host succeeded every time. Output is now drained on background threads into a bounded queue so HTTP-write latency can no longer back-pressure the script, stdout/stderr pipe buffers are enlarged to 1 MiB, and the interpreter runs unbuffered (`python -u`). The wire format and public API are unchanged. * **Removed a potential deadlock when launching a `lager python` script.** The per-script scheduling-priority boost was applied inside the `fork()`/`exec()` window via a `preexec_fn`, which Python documents as unsafe in a multithreaded service. It is now applied from the parent process after the script starts, with identical effect and no window in which a concurrent request could deadlock the launch. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.4/) # Version 0.18.5 Source: https://docs.lagerdata.com/source/release-notes/v0.18.5 May 22, 2026 ## Bug Fixes * **`lager debug ... flash` and `... erase` no longer fail with a 500 (`filedescriptor out of range in select()`) on a Lager Box that has been running for a long time.** The box debug service is a long-lived process. Its GDB controller helper rebuilds a fresh `gdb-multiarch` connection on every retry attempt — retries that happen routinely while a debugger connection is coming up during flash and RTT — but a *failed* attempt was never registered for cleanup, so each one leaked the `gdb-multiarch` subprocess and the pipe handles to it. After enough leaks the service crossed the operating system's 1024 file-descriptor limit for `select()`, at which point the tool that performs erase and flash crashed instead of running. Failed attempts are now closed immediately, and the erase/flash path uses `poll()` instead of `select()` so it is no longer bound by the 1024 limit even if descriptors run high. Recovering a box that has already hit this no longer needs anything beyond restarting the debug service. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.18.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.18.5/) # Version 0.19.0 Source: https://docs.lagerdata.com/source/release-notes/v0.19.0 May 23, 2026 ## Features * **OpenOCD debug backend.** Non-Segger debug probes are now first-class peers of J-Link under `lager debug` — same `connect` / `gdbserver` / `flash` / `erase` / `reset` / `memrd` / RTT command surface, same multi-probe slot allocator, same Net Manager TUI. The Lager Box dispatches each debug net to the right backend automatically based on the probe's USB vendor ID. Auto-detected OpenOCD probes: * **ST-Link V2 / V2-1 / V3** (STMicroelectronics, VID `0483`) * **Raspberry Pi Debug Probe** (RP2040 Picoprobe / CMSIS-DAP, VID `2e8a`) * **FTDI FT232H** (`0403:6014`, mapped to `c232hm.cfg`) * **FTDI FT2232H** (`0403:6010`, mapped to `olimex-arm-usb-ocd-h.cfg`) * **ARM DAPLink / NXP MK20 CMSIS-DAP** (VID `0d28`) * **Atmel EDBG / mEDBG** (VID `03eb`) * **Olimex ARM-USB-OCD-H** (VID `15ba`) **FTDI FT4232H** is supported via a user-supplied OpenOCD config — the chip exposes four channels and Lager can't guess which one carries SWD without it. Other open-hardware probes whose VIDs aren't on the auto-list (Black Magic Probe, Glasgow, etc.) can also be used by setting `debug_backend: openocd` on the net and supplying an `openocd_config`. Probes already on a J-Link USB ID stay on the J-Link backend, so existing nets are unaffected. OpenOCD nets can run concurrently with J-Link nets on the same Lager Box; the existing J-Link multi-probe slot stride is reused, and OpenOCD adds its own per-slot telnet (`4444 + slot`) and TCL/RPC (`6666 + slot`) ports. * **DA1469x flash programming over OpenOCD via the Apache Mynewt RAM-resident flash\_loader.** Mainline OpenOCD has no QSPI flash driver for the Dialog/Renesas DA1469x family, so before this release `lager debug SWD flash` against an FT4232H rig connected to a DA1469x silently did nothing despite a green `Flashed!` log line. The Lager Box now ports the upstream GDB-script protocol (`flash.gdb` / `erase.gdb` / `flash_loader.gdb`) to a pure OpenOCD TCL/RPC implementation: it brings the loader up in RAM, drives the `fl_cmd` command struct, programs in chunks, and software-resets on success. The CLI side is unchanged — `lager debug SWD flash --bin ,0x16000000` and `lager debug SWD erase` just work — and absolute XIP addresses are accepted with a clear error if the user passes a flash-relative offset by mistake. The two loader artefacts (`flash_loader.elf` + `flash_loader.elf.bin`) are dropped into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` once per box; `lager update` no longer wipes them out, and `start_box.sh` creates the directory tree on every container start so an operator can `scp` the pair in without first `mkdir -p`. Validated end-to-end on hardware after a few bring-up fixes for the loader's double-buffer pointer rotation, the `fl_cmd_rc` post-loop handshake, and the CLI's absolute-XIP-to-flash-offset translation. The J-Link DA1469x flash path is unchanged; this release adds a working OpenOCD path for the same target. * **Concurrent multi-probe slots extended to OpenOCD.** A Lager Box can now run up to four debug probes simultaneously across any mix of J-Link and OpenOCD adapters. Each probe gets a deterministic per-slot port window — GDB on `2331+3·slot`, RTT base on `9090+2·slot`, OpenOCD telnet on `4444+slot`, OpenOCD TCL on `6666+slot`. The legacy single-probe configuration (slot 0: GDB 2331, RTT 9090, OpenOCD telnet 4444, OpenOCD TCL 6666) is preserved exactly as before. `lager python` scripts that resolve a debug net via `Net.get(name, NetType.Debug).connect()` now share the same slot pool as the HTTP debug service, so concurrent scripts no longer collide on slot-0 ports. * **`lager nets add --openocd-config ` and an `openocd_config` field on `nets add-batch`.** Parallels the existing `--jlink-script` flag — the user's `.cfg` is stored on the saved net and materialised on the box before each `openocd` spawn. Required for FT4232H, supported on every other adapter as an escape hatch for vendor-supplied configs. * **`lager nets set-script` / `show-script` / `remove-script` now work for both backends.** The script-routing trio is backend-agnostic: it detects the target backend from the probe VID + the file's extension and content, and writes to the right slot on the saved net (`jlink_script` for J-Link probes, `openocd_config` for OpenOCD). Pass `--backend jlink|openocd` to override; ambiguous cases are refused with a clear hint instead of silently guessing. `SCRIPT_PATH='-'` reads from stdin. A debug net carries either field but never both, and any switch clears the other slot with a yellow stderr notice. * **`--jlink-version ` on `setup_and_deploy_box.sh`.** Pin the J-Link tools version installed on a new Lager Box at deploy time, instead of taking whatever Segger ships at the moment of the box build. The deployment options table in the README is also corrected to match the current flag set. * **Documentation: ST-Link, RP2040, and FTDI listed under Debug & Flashing**, the OpenOCD RTT `chunk_size` knob is documented in the `lager nets` reference, and `lager nets` documents the new `--openocd-config` flag and the unified `set-script` / `show-script` / `remove-script` commands. ## Bug Fixes * **The Net Manager TUI no longer lets you assign two roles to a single Keithley 2281S (or EA PSB).** A single physical Keithley 2281S can run as a `power-supply` *or* a `battery` but never both — its two firmware entry functions are mutually exclusive. The Add Net wizard's duplicate-detection was checking per-role, so once a `supply` net was saved on a Keithley the wizard kept offering a `battery` row for the *same* VISA address (and vice-versa). The two saved nets fought for the entry function on every command, surfacing as `[Errno 16] Resource busy` — the same hardware constraint that the v0.16.7 known-limitations entry and the v0.16.9 hardware-service shared-session work were spent papering over. The TUI now treats `_SINGLE_CHANNEL_INST` chips as one-net-per-(instrument, address) regardless of role, hiding the second-role row entirely once any role binds the chip; the user-visible message tightens from "Only one net per role may be added per ..." to "Only one net may be added per ...". The same hardening applies to EA PSB (`solar` / `supply`). Direct CLI paths (`lager nets add` / `add-batch`) are unaffected, so power users keep an escape hatch. * **FTDI adapters whose EEPROM was never programmed now work end-to-end.** A FT4232H with no readable USB serial caused a chain of silent failures on the prior release: the UART scanner emitted bare interface indices (`"0"`/`"1"`/`"2"`/`"3"`) into the saved net's `pin` field, and the box-side UART dispatcher then failed at first use with `UART bridge with serial 2 not found`; the debug-probe regex rejected the empty serial slot in the VISA address and silently fell back to J-Link for what was actually an OpenOCD-backed FTDI, so `lager debug gdbserver` came back as the canned "Failed to connect to debugger" checklist with no real cause; and the `nets show` output labelled the overloaded `pin` field as "Channel:" regardless of role, hiding misconfigurations. Fixed across the whole stack: the scanner now matches `/dev/ttyUSB*` paths by sysfs node instead of by USB serial, the legacy `["0","1","2","3"]` static channel fallback is removed, the VISA regex tolerates the empty serial slot, the TUI refuses to persist a UART net with an unprogrammed-EEPROM placeholder pin (with an actionable message pointing at the EEPROM), and `nets show` is now role-aware (`Pin/serial:` for UART, `Device:` for debug). On `lager debug gdbserver` failures the CLI surfaces the box's structured error directly instead of falling back to the generic checklist. * **OpenOCD flash failures are no longer reported as success.** OpenOCD's TCL/RPC channel returns `program ...`'s stdout as plain text even when the underlying flash write or verify failed, so a bad flash looked successful to callers — hence the long-standing "Flashed!" line that didn't actually flash. The box debug service now scans the response for `program_error` markers (`** Programming Failed **`, `** Verify Failed **`, etc.) and any `Error:` lines, and surfaces them up to the CLI. Side effect: `Erase complete!` / `Flashed!` no longer print on rigs whose `target.cfg` declares no flash bank — those calls now fail fast with the underlying error. * **Custom OpenOCD configs uploaded via `lager nets set-script` were silently stored in the wrong slot.** `set-script` previously routed every upload to the `jlink_script` field regardless of which backend the probe used, so OpenOCD configs uploaded that way were ignored at run time. The new backend-detection in `set-script` writes to the correct slot, and the in-box `DebugNet` Python API also picks up `openocd_config` correctly — it was being looked up under the wrong key (`openocd_config_path`) and never decoded to disk, so custom OpenOCD configs had no effect when scripts ran `Net.get(name, NetType.Debug).connect()`. Same shape of bug for `jlink_script` on the in-box API path. * **Custom OpenOCD configs failed to start with "adapter driver is not configured".** Lager was emitting `-c "adapter serial "` and `-c "transport select swd"` *before* `-f `, but those `-c` commands require an adapter driver that only gets set inside the user's cfg, so OpenOCD bailed out before the cfg ever loaded. The user cfg now occupies the same command-line slot the auto-detected interface cfg would, and the auto `transport select` is suppressed when a user cfg is supplied (vendor cfgs almost always call it themselves, and OpenOCD errors on duplicate sets). * **Off-box GDB clients can now reach OpenOCD's gdb / telnet / TCL ports.** OpenOCD ≥ 0.11 defaults `bindto` to `127.0.0.1`, so `docker run -p 2331-2342:2331-2342` forwarded traffic to a listener that wasn't accepting it and clients timed out without an error. OpenOCD now binds all interfaces by default, matching `JLinkGDBServer`. The TCL/RPC channel remains 127.0.0.1-only on the wire because the box-side service drives it locally. * **The Net Manager TUI's "script attached" indicator covers OpenOCD configs.** `has_script` was computed only from `jlink_script`, so debug nets carrying only an `openocd_config` (the new normal for FT4232H rigs) showed no indicator even though one was attached. Now checks both fields. * **Hardened Lager Boxes admit OpenOCD telnet and TCL traffic.** `secure_box_firewall.sh`'s `LAGER_PORTS` allowlist was scoped to the J-Link-only port window, so on a box hardened with this script any external client reaching OpenOCD's telnet (`4444-4447`) or TCL (`6666-6669`) port was silently dropped while J-Link sessions kept working. The allowlist now mirrors the slot pool published by `start_box.sh`. ## Improvements * **`DebugNet.connect()` and `.status()` are now symmetric across J-Link and OpenOCD.** `connect()` accepts `force=False` (restart the daemon if already running) and `ignore_if_connected=False` (return the existing status instead of raising) on both backends. `status()` always returns a dict containing at minimum `running`, `pid`, and `backend` keys regardless of which backend handled the probe — backend-specific extras pass through unchanged, so consumers writing portable code can rely on the three guaranteed fields. * **OpenOCD speed-fallback ladder.** `connect_jlink` already walked `[requested, 4000, 1000, 500, 100]` kHz when the requested speed didn't take; OpenOCD's `adapter speed` is set once at daemon startup with no built-in retry, so a vendor cfg expecting 500 kHz against Lager's 4 MHz default would die silently at the first SWD transaction. The same ladder is now applied at the `DebugNet` layer for the OpenOCD branch. * **VID/PID-based FTDI dispatch.** The original VID-only FTDI mapping fell over the moment a Lager Box had both an FT232H and an FT2232H plugged in. The dispatcher now keys on the full VID/PID pair and refuses ambiguous cases with a hint pointing at `lager nets set-script --backend openocd` for the FT4232H path. * **Cleaner debug-script command surface.** The short-lived `set-openocd-config` / `show-openocd-config` / `remove-openocd-config` aliases (which existed only on this branch and never shipped in a tagged release) are removed in favour of the unified `set-script` / `show-script` / `remove-script` trio with `--backend openocd`. There's nothing to migrate; existing CLI usage is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` After upgrading, run `lager update --box ` on each existing Lager Box to deploy the OpenOCD backend, the new firewall ports, and the matching box-side code. To use the new DA1469x OpenOCD flash path, drop the `flash_loader.elf` and `flash_loader.elf.bin` pair into `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` on the box. ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.0/) # Version 0.19.1 Source: https://docs.lagerdata.com/source/release-notes/v0.19.1 May 25, 2026 ## Bug Fixes * **`lager debug ... flash` and the DA1469x flash loader now quote the firmware path before handing it to OpenOCD.** OpenOCD parses its TCL commands word-by-word, so a `program` or `load_image` argument with a space in it was being chopped into two TCL words and the underlying flash op either failed loudly with `wrong # args` or hit the wrong file. In practice the path comes from `tempfile.NamedTemporaryFile()` or the fixed `~/third_party/customer-binaries/openocd/flash-loaders/da1469x/` tree (no spaces), so the bug never bit in normal operation; the fix is defensive and aligns `box/lager/debug/openocd.py`'s `OpenOcdRpc.program()` and `OpenOcdRpc.load_image()` with the existing quoting pattern in `OpenOcdRpc.rtt_setup()`. Notable for operators who relocate the flash-loader tree via `LAGER_FLASH_LOADERS_DIR=/path/with spaces/`. * **DA1469x flash\_loader ELF parser now reports a clear error on a truncated symbol-table name instead of a Python `ValueError` traceback.** `box/lager/debug/da1469x_loader.py`'s ELF32 symbol walker used `bytes.index(b'\x00', ...)` to locate the null terminator for each name in the string table, which raised an unwrapped `ValueError` if the strtab itself was truncated. Switched to `bytes.find()` with an explicit error message that names the offending offset; `_resolve_loader_symbols()` still rewraps it as `Da1469xLoaderError` so the call site error type is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.1/) # Version 0.19.2 Source: https://docs.lagerdata.com/source/release-notes/v0.19.2 May 25, 2026 ## Features * **`--ip` now accepts DNS hostnames in addition to IP addresses** on `lager boxes add`, `lager boxes edit`, `lager install`, and `lager uninstall`. Lets a Lager Box sit behind a DNS name (e.g. `box.example.com`) or a Tailscale MagicDNS short name (e.g. `box-1.tailXYZ.ts.net`) instead of requiring the operator to look up and pin a numeric address. Validation is purely syntactic — IPv4/IPv6 (incl. Tailscale `100.x.x.x`) take the existing `ipaddress.ip_address` fast path; everything else is checked against RFC 1123 hostname rules (1–63 char alphanumeric/hyphen labels, ≤253 chars total, single-label allowed for MagicDNS), with actual resolution deferred to SSH/HTTP. The shared validator lives in the new `cli/address_utils.py` (covered by 34 unit tests in `test/unit/cli/test_address_utils.py`); the four call sites all share one error path that prints a "Valid formats:" cheatsheet on failure (`install` / `uninstall` previously printed only the bare error). Inputs that already carry a scheme, port, or path (e.g. `http://...`, `host:5000`, `host/api`) are rejected with a specific message instead of the previous generic "not a valid IP" — the rest of the CLI composes `http://{addr}:port/...` itself, so an embedded one of those would conflict. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.19.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.19.2/) # Version 0.2.17 Source: https://docs.lagerdata.com/source/release-notes/v0.2.17 November 21, 2025 ## Features ### Concurrent CLI Commands * Enabled concurrent CLI commands while supply TUI is running * Improved multi-tasking capabilities with TUI interfaces ## Bug Fixes * Fixed `--line-ending` flag in UART WebSocket client * UART communication patches and improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.17 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.17/) # Version 0.2.18 Source: https://docs.lagerdata.com/source/release-notes/v0.2.18 November 24, 2025 ## Features ### Automatic Security Configuration * Added automatic security configuration to `lager update` * Enhanced Lager Box security during update process * Automated firewall and security settings ## Improvements * Updated Keysight E36300 device support * Various testing and stability improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.18 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.18/) # Version 0.2.19 Source: https://docs.lagerdata.com/source/release-notes/v0.2.19 November 24, 2025 ## Features ### Lager Binaries Command * Added `lager binaries` command for managing binary files * Enhanced binary file handling capabilities ## Bug Fixes * Fixed `lager update` functionality ## Improvements * Added progress bar to `lager update` for better visibility * Added verbose flag to `lager update` for detailed output * Proper firewall configuration during updates * Cleaned up UART implementation ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.19 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.19/) # Version 0.2.20 Source: https://docs.lagerdata.com/source/release-notes/v0.2.20 November 26, 2025 ## Features ### Python Script Enhancements * Nets now working inside `lager python` scripts * Enhanced Python script execution environment * Added support for UART in Python scripts ### Oscilloscope Web UI * Added HTTP server for oscilloscope web UI on port 8080 * Added oscilloscope-streamer with WebSocket support * Exposed oscilloscope visualization interface ## Bug Fixes * Fixed `python --add-file` command functionality * Fixed `lager update` udev rules path * Fixed LabJack device open timeout causing indefinite hangs ## Improvements * Improved Lager Box deployment scripts * Enhanced `lager update` to include all necessary files in sparse checkout * Moved oscilloscope-streamer to gateway/oscilloscope-daemon/ * Code cleanup and organization improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.20 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.20/) # Version 0.2.21 Source: https://docs.lagerdata.com/source/release-notes/v0.2.21 December 2, 2025 ## Features ### Phidget Thermocouple Expansion * Phidget thermocouple now supports 4 channels (previously limited to fewer channels) * Enhanced multi-channel temperature measurement capabilities ### Keysight Python Support * Added Keysight device support to `lager python` scripts * Enabled Keysight instruments in Python execution environment ## Improvements * Added dependencies for BLE test suite integration * Enhanced `lager update` to include BLE test dependencies ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.21 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.21/) # Version 0.2.22 Source: https://docs.lagerdata.com/source/release-notes/v0.2.22 December 2, 2025 ## Features ### Hardware Invocation Service * Added hardware invocation service for Device proxy pattern * Enabled remote device method calls through proxy interface * Improved hardware abstraction layer ### Keysight Python Support * Added Keysight power supply support to `lager python` scripts * Enhanced Python script execution with Keysight devices ## Bug Fixes * Fixed hardware service to extract low-level device from SupplyNet wrappers * Fixed hardware service: added import paths for supply/battery/eload modules * Fixed hardware service: handle unhashable types in net\_info cache key * Fixed PowerSupply Net initialization in get\_from\_saved\_json * Fixed Net class: changed self.net.channel to self.channel ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.22 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.22/) # Version 0.2.23 Source: https://docs.lagerdata.com/source/release-notes/v0.2.23 December 2, 2025 ## Bug Fixes * Fixed multi-channel USB resource sharing for Keysight devices * Fixed Keysight device compatibility issues ## Improvements * Enhanced Keysight device support * Improved resource management for multi-channel instruments ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.23 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.23/) # Version 0.2.24 Source: https://docs.lagerdata.com/source/release-notes/v0.2.24 December 4, 2025 ## Features ### New Command * Added `lager boxes add-all` command for bulk Lager Box management ## Bug Fixes * Fixed UART data corruption issues * Fixed NetType.Analog handling for Rigol oscilloscopes in get\_from\_saved\_json ## Improvements * Updated CLI commands for better usability * Enhanced oscilloscope integration ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.24 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.24/) # Version 0.2.25 Source: https://docs.lagerdata.com/source/release-notes/v0.2.25 December 4, 2025 ## Improvements * Updated `lager devenv` command functionality * Enhanced development environment setup ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.25 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.25/) # Version 0.2.26 Source: https://docs.lagerdata.com/source/release-notes/v0.2.26 December 5, 2025 ## Features ### Webcam Improvements * Improved webcam interface with enhanced controls * Reduced zoom latency for better responsiveness * Cleaned up webcam sidebar interface ## Bug Fixes * Fixed `lager exec` command * Fixed net.py module functionality * Added usb\_net\_wrapper.py for better USB network handling ## Improvements * Enhanced webcam user experience * Streamlined webcam code organization * Optimized zoom operations ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.26 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.26/) # Version 0.2.27 Source: https://docs.lagerdata.com/source/release-notes/v0.2.27 December 5, 2025 ## Improvements * Internal maintenance release * Minor bug fixes and stability improvements ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.27 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.27/) # Version 0.2.28 Source: https://docs.lagerdata.com/source/release-notes/v0.2.28 December 5, 2025 ## Bug Fixes * Fixed `lager exec` command execution ## Improvements * Updated exec command functionality * Enhanced remote execution capabilities ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.28 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.28/) # Version 0.2.29 Source: https://docs.lagerdata.com/source/release-notes/v0.2.29 December 6, 2025 ## Bug Fixes * Fixed `lager python download` command functionality * Fixed `lager exec` command execution * Fixed `lager devenv` environment setup ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.29 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.29/) # Version 0.2.30 Source: https://docs.lagerdata.com/source/release-notes/v0.2.30 December 8, 2025 ## Features ### MCC USB-202 DAQ Support * Added support for Measurement Computing USB-202 DAQ device * Implemented ADC, DAC, and GPIO functionality for USB-202 * Added USB-202 to instrument detection system * Enabled USB-202 configuration through lager nets ## Bug Fixes * Fixed USB-202 VISA address parsing * Fixed USB-202 GPIO toggle functionality * Updated USB-202 channel naming for consistency ## Improvements * Improved `lager update` interface and performance * Streamlined update process for faster execution * Cleaned up GPIO interface code * Enhanced update interface user experience ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.30 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.30/) # Version 0.2.31 Source: https://docs.lagerdata.com/source/release-notes/v0.2.31 December 10, 2025 ## Features ### Oscilloscope Support * Added support for PicoScope and Rigol oscilloscopes * Implemented voltage measurements for oscilloscopes * Added cursor measurement modes and autoscale functionality ## Bug Fixes * Fixed critical bug in Device proxy: properly handle serialized enum dictionaries * Fixed critical channel parameter bug when channel=None in Device proxy wrapper * Fixed autoscale infinite recursion bug in RigolMSO5000 mapper * Fixed asyncio deprecation warning in PicoScope commands * Fixed oscilloscope measurement channel parameter bugs * Fixed cursor timeout issues in oscilloscope operations * Fixed trigger validation in oscilloscope commands * Fixed mux.connect() error in oscilloscope interface ## Improvements * Removed accidentally committed Rust build artifacts from repository * Added missing clear\_measurement and disable\_cursor\_measure\_mode methods to RigolMso5000 * Improved get\_measure\_item error handling with better logging * Enhanced oscilloscope command reliability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.31 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.31/) # Version 0.2.32 Source: https://docs.lagerdata.com/source/release-notes/v0.2.32 December 11, 2025 ## Features ### J-Link Debugger Integration * J-Link debugger installation now automated during Lager Box deployment * J-Link automatically installed and configured during `lager update` operations * Improved debug workflow reliability across all Lager Boxes ### Flexible Deployment * Lager Boxes can now be deployed with custom usernames instead of requiring `lagerdata` username * Enhanced deployment flexibility for different organizational setups ## Bug Fixes * Fixed webcam documentation links in Mintlify docs ## Improvements * Cleaned up webcam functionality and code organization * Streamlined deployment process with better error handling * Reorganized deployment documentation for improved clarity * Enhanced J-Link integration stability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.32 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.32/) # Version 0.2.33 Source: https://docs.lagerdata.com/source/release-notes/v0.2.33 December 15, 2025 ## Features ### Improved Hello Command * `lager hello` now displays the actual Lager Box hostname instead of the Docker container ID * Format changed from "Hello from DUT " to "Hello from ()" * Mounted host's `/etc/hostname` file into container for hostname access ### Documentation * Added Release Notes section to Mintlify documentation * Created release notes pages for versions v0.2.32 through v0.2.17 * Updated RELEASE\_PROCESS.md with comprehensive release notes instructions ## Bug Fixes * Fixed eload Net API and multi-channel USB caching issues * Fixed cache clearing to use inline requests instead of test\_utils import * Fixed Keithley mapper voltage handling when output is disabled * Fixed OVP conflicts in voltage tests by raising OVP to 10V * Fixed Keithley enable state issues with proper state reset ## Improvements ### Lager Update Enhancements * Reduced password prompts to one input per `lager update` * Improved third\_party directory mounting to container * Streamlined update process with better error handling * Cleaned up update code and output ### Testing & Caching * Auto-clear hardware service cache after `lager python` scripts finish * Added cache clearing utilities to all test files * Increased voltage settling time from 0.3s to 1.0s for more accurate readings * Removed unnecessary clear cache terminal output ### Power Supply & E-Load API * Updated Python API for power supplies and e-loads * Fixed multi-channel USB resource caching * Improved Keithley device handling ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.33 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.33/) # Version 0.2.35 Source: https://docs.lagerdata.com/source/release-notes/v0.2.35 December 17, 2025 ## Features ### Python API Function Renaming * Renamed 14 Python API functions for improved clarity and consistency * Updated documentation to reflect new function names ## Bug Fixes ### ARM/Robot API * Fixed serial port hangs by adding proper timeout handling * Fixed position polling with buffer clearing and reduced frequency * Fixed CLI to close serial port after commands complete * Fixed movement commands to not wait for 'ok' response * Removed problematic reset\_input\_buffer() calls that caused hangs ### Battery API * Fixed Battery API mapper to properly delegate to Keithley methods * Fixed device name conflict issues * Fixed class alias placement ### Other Fixes * Fixed Webcam API * Fixed `lager update` command * Fixed GDB `read_memory` response parsing ## Improvements ### Code Cleanup * Removed unused modules and deprecated code * Updated documentation and Python API ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.35 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.35/) # Version 0.2.36 Source: https://docs.lagerdata.com/source/release-notes/v0.2.36 December 18, 2025 ## Breaking Changes ### Terminology Restructure: Gateway/DUT to Box This release standardizes terminology across the entire codebase, replacing "gateway" and "DUT" (Device Under Test) with the unified term "box". **CLI Changes:** * The `--dut` option is now `--box` (hidden alias kept for backward compatibility) * The `--gateway` option is now `--box` where applicable * All help text and error messages now use "box" terminology **Configuration Changes:** * The `DUTS` key in `.lager` configuration files is now `BOXES` (backward compatible - old format is still read) * Box storage functions renamed (e.g., `load_duts()` → `load_boxes()`) **Directory Structure:** * `gateway/` directory renamed to `box/` * `gateway/lager/lager/` flattened to `box/lager/` * `gateway_http_server.py` renamed to `box_http_server.py` * `start_lager.sh` renamed to `start_box.sh` * Deployment scripts renamed: * `setup_and_deploy_gateway.sh` → `setup_and_deploy_box.sh` * `secure_gateway_firewall.sh` → `secure_box_firewall.sh` * `verify_gateway_security.sh` → `verify_box_security.sh` **Migration:** * Existing `.lager` configuration files will continue to work * The `--dut` CLI option works as a hidden alias for `--box` * Update any scripts or automation to use the new `--box` option ## Features ### Unified Box Terminology * Consistent "box" terminology throughout CLI, Python API, and documentation * Simplified mental model for users - one term for all hardware targets * Cleaner codebase with consistent naming conventions ### Flattened Directory Structure * Removed redundant `gateway/lager/lager/` nesting * More intuitive project navigation * Cleaner import paths ## Improvements ### Documentation * All documentation updated with "box" terminology * Unified help text and error messages * Updated training data for AI assistants ### Code Quality * Removed deprecated modules and unused code * Standardized naming conventions throughout codebase * Improved code organization and readability ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.2.36 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Migration Guide ### For CLI Users Replace `--dut` with `--box` in your commands: ```bash theme={null} # Before lager hello --dut my-device lager supply voltage --dut my-device 3.3 # After lager hello --box my-device lager supply voltage --box my-device 3.3 ``` ### For Script Authors Update any automation scripts to use the new flag names: ```bash theme={null} # Before BOX_NAME="my-device" lager hello --dut $BOX_NAME # After BOX_NAME="my-device" lager hello --box $BOX_NAME ``` ### For Box Administrators Update deployment commands: ```bash theme={null} # Before cd deployment ./setup_and_deploy_gateway.sh # After cd deployment ./setup_and_deploy_box.sh ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.2.36/) # Version 0.20.0 Source: https://docs.lagerdata.com/source/release-notes/v0.20.0 May 27, 2026 This release is a direct response to the 2026-05-26 "battery net not responding" incident on a Keithley 2281S, where root-causing one EBUSY took \~2 hours across `lsof`, `dmesg`, bare `pyvisa` probes, and hardware-service introspection. The biggest items below — `lager diagnose`, the `usbtmc` blacklist, automatic ENODEV recovery, and cross-process device locks — collectively eliminate the most common failure modes that drove that session, and surface the rest (e.g. wedged instrument firmware that only mains-power-cycling can fix) with a single one-line diagnosis. ## Features * **`lager diagnose --box [--type ]` — single-shot net diagnosis.** Polls three box-side endpoints in parallel (USB enumeration + USB-TMC interface-class detection + holder detection + `dmesg` + `lsmod` for usbtmc, bare `pyvisa` `*IDN?` probe, hardware-service in-process session cache) and classifies the net into one actionable bucket with the next step the user should take: `HOST-SIDE: usbtmc kernel module loaded` (→ `lager box update`), `HOST-SIDE: USB device claimed by multiple processes` (→ names the PIDs), `HOST-SIDE: USB device busy`, `TRANSIENT: device disappeared from USB`, `TRANSIENT: device enumerated as USB-TMC but pyvisa probe couldn't reach it` (→ stale libusb context recovery hint), `INSTRUMENT WEDGED` (→ mains-side power-cycle), `NOT ENUMERATED`, `NOT USB-TMC` (LabJack/Picoscope/Acroname use vendor SDKs), or `HEALTHY` (with the IDN string). `--type` is auto-detected from the box's saved nets if omitted. Backwards-compatible against pre-0.20 boxes (per-endpoint 404 fallbacks). * **`usbtmc` kernel-module blacklist shipped with the box image** at `/etc/modprobe.d/blacklist-usbtmc.conf`. Without this, the kernel auto-binds the `usbtmc` driver to USB-TMC-class instruments (Keithley 2281S, Keysight, Rigol scopes) and claims interface 0; pyvisa-py's libusb backend then can't `set_configuration()` and returns `[Errno 16] Resource busy`. The blacklist is the only durable fix. Deployed by `setup_and_deploy_box.sh` (new boxes) and refreshed by `lager box update` (existing boxes). * **Cross-process device locks for USB-TMC drivers** via the new `lager.util.device_lock` module. Generalizes the long-standing EA-solar/supply `DeviceLockManager` pattern (`fcntl.flock` on a lockfile keyed by VISA address) and adopts it in the Keithley battery + supply, Rigol DP800, Rigol DL3021 eload, Keysight E36000, and Rigol MSO5000 scope drivers. Guards against a second box-side `pyvisa` client racing the hardware service for the libusb interface-0 claim. Fails open if the locking infrastructure itself errors, so a transient filesystem hiccup can't take legitimate work offline. * **Version-skew warning** prints once per CLI session to stderr when the CLI's minor version is ahead of the box's by one or more. The 2026-05-26 session started with a 0.19.2 CLI talking to a 0.18.3 box and the first error was opaque — this single line would have cut diagnosis time by hours. Cached per-process by box IP; fails open on any error so a flaky network can never break a working command. * **Actionable error messages for `[Errno 16/19/110]`** in `lager battery` and `lager supply` commands. Errno 16 EBUSY → "USB device busy — another process holds the libusb interface" with a `Try: lager diagnose ` hint. Errno 19 ENODEV → "Instrument disappeared from USB (re-enumeration)" with a `Hw service should auto-recover; if not: sudo docker restart lager` hint. Errno 110 ETIMEDOUT → "Instrument did not respond to SCPI — firmware may be wedged" with a "mains-side power-cycle required" hint. Raw error remains available via `LAGER_DEBUG=1`. * **`lager update` verbose status block now includes `modprobe.d:`** alongside the existing `udev rules:` line. * **`lager diagnose` command-specific docs** at `docs/diagnose.md` covering the three endpoints, the classification decision tree, sample sessions for each bucket, and the `--type` semantics. ## Bug Fixes * **`lager battery ` and `lager supply ` no longer return `[Errno 19] No such device` until `docker restart lager`** after a USB re-enumeration of the instrument (mains power-cycle, accidental unplug, USB hub port toggle). The hardware-service retry path was gated on a keyword tuple that did not match libusb's ENODEV signature — the existing retry never fired. The tuple is extended, a dedicated `_is_enodev_error()` helper is added, and on ENODEV the `/invoke` retry now evicts every sibling `device_cache` entry on the same VISA address and force-closes the shared `pyvisa` session pool entry. Live-verified on a Keithley 2281S via a USB driver unbind/bind sequence. * **`lager diagnose` host-side holder detection now works on the actual box image.** The original `/diagnose/usb` endpoint shelled out to `sudo lsof /dev/bus/usb/` to find competing libusb claims, but neither `sudo` nor `lsof` ship in the lager container; the subprocess silently exited 127 and the endpoint always returned `lsof: []`. As a result the `HOST-SIDE: USB device claimed by multiple processes` and `HOST-SIDE: USB device busy` classifications could never fire in production. Replaced with a `/proc/*/fd/*` walk that reads `/proc//comm` for the process name. No external tools, no permission gymnastics. * **`lager diagnose` classifier no longer misclassifies a healthy USB-TMC instrument as `NOT USB-TMC`** when pyvisa's fresh-probe path can't reach it (most common cause: a stale libusb context inside `box_http_server` after a USB re-enumeration; hw\_service runs in a separate process and recovers transparently). `/diagnose/usb` now reads the device's sysfs interface descriptors and surfaces `is_usbtmc` for USB-TMC class 0xFE / subclass 0x03 devices. The classifier disambiguates: enumerated USB-TMC + fresh-probe failure → new `TRANSIENT` bucket with a concrete recovery hint; enumerated non-USB-TMC → existing `NOT USB-TMC` hint preserved. * **`lager diagnose` VISA-side error mapping catches all three libusb "device not reachable" message variants.** pyvisa-py emits `[Errno 19] No such device` (libusb's standard ENODEV after a re-enumeration), `[Errno 2] Entity not found` (authorized=0 or denied open), and `No device found.` (generic vendor-not-matched-or-stale path). All three now map to `error_class: nodev` so the classifier consistently returns `TRANSIENT` instead of falling through to `UNCLEAR`. * **`lager diagnose` VISA section renders all five fields on endpoint-returned errors.** The pre-fix renderer short-circuited on any `error` key in the dict, collapsing the section to a single `error:` line and dropping the `error_class` and `elapsed_ms` context the user needs to interpret the failure. * **`lager diagnose` prints an actionable message when the box is unreachable** instead of wrapping the raw urllib3 traceback. Now reads `Box '' unreachable at :5000 (connection refused). The lager container may be stopped. Check with: lager ssh --box -- "sudo docker ps"`. Connection-refused and timeout cases are tailored separately. * **`/diagnose/visa` correctly consults hw\_service's session pool across processes.** `box_http_server` (port 9000) and `hardware_service` (port 8080) are separate processes; the original implementation imported `_visa_resources` from `lager.hardware_service` and saw its own empty copy of the dict rather than hw\_service's live state. The fresh probe then always ran and hit EBUSY on healthy boxes with a cached session. Now consulted via HTTP at `localhost:8080/diagnose/dispatcher`. * **`device_lock` no longer truncates the lock file before acquiring.** The pre-fix `open(path, 'w')` erased the existing holder's PID at open time, leaving the file empty under contention even when our own acquire later timed out. Now opens via `os.open(O_RDWR|O_CREAT)` and only truncates + writes the PID after a successful flock acquisition. * **`_dmesg_usb_tail` is robust against missing passwordless sudo.** The pre-fix shell pipeline used `sudo dmesg` (could hang on password prompt), `2>&1 | grep` (merged stderr into stdout where grep filtered it), and a final `tail` (whose rc masked upstream failures). Now uses `sudo -n dmesg` (fails fast on password prompt), does the filtering in Python, and the rc reflects what actually happened. * **`lager update` Step 5b (new) re-detects the `modprobe_d/` source dir post-pull.** The update probe runs before the `git pull`; on the very first deploy that introduces the directory, the pre-pull probe correctly reports the source path empty and the install step would short-circuit. Re-detects via a fresh SSH round-trip if the pre-pull probe came up empty. ## Improvements * **TUI WebSocket-failure messages call out the specific next step** instead of `WebSocket connection failed: Failed to connect to WebSocket server`. `lager battery tui` and `lager supply tui` now probe `http://:9000/health` on connect failure and emit one of four actionable messages depending on the response (box reachable but pre-0.20, services partially up, connect-timeout via Tailscale, container not running). Original WS error preserved in parentheses. * **Documented "TUIs are laptop-only"** in `box/lager/README.md`. Running TUIs directly on the box was the suspected culprit of that incident (a second `pyvisa-py` client competing with hardware-service for interface 0). The OS-level `device_lock` makes this case detect-and-fail-clean instead of silent EBUSY, but the right answer is still to launch TUIs from the laptop CLI. * **`lager diagnose` output labels clarified.** The header line reads `NetType: ` instead of `resolved role: ` to align with terminology elsewhere in the CLI. The USB section prints `usb-tmc class: yes/no` (newly surfaced from `/diagnose/usb`) so the user can see whether the classifier is treating the device as USB-TMC. The existing kernel-module-status line is renamed from the ambiguous `usbtmc:` to `usbtmc kmod:` so the two related fields are visually distinct. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.20.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.20.0/) # Version 0.20.1 Source: https://docs.lagerdata.com/source/release-notes/v0.20.1 May 27, 2026 ## Features * **New `--force` option on `lager update`.** Re-runs an update even when Lager thinks your box is already up to date, and rebuilds the box cleanly from scratch. Reach for this if a previous update didn't finish and the box is acting strangely — `lager update --box --force`. ## Bug Fixes * **Updates no longer hang or fail on some networks.** On certain setups a box couldn't reach GitHub while updating, which made `lager update` appear to freeze for around 15 minutes and then fail. Boxes now connect reliably during an update, and a brief network hiccup is retried automatically instead of stopping the whole update. ## Improvements * **Updating a box is much faster — about 30 seconds instead of \~15 minutes.** Lager now reuses the work from your last update instead of rebuilding everything from scratch each time. (A longer update still happens when the box's software dependencies actually change.) * **Use `lager update` to update a box.** This is now the one command to remember. The older `lager box update` has been removed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.20.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.20.1/) # Version 0.21.0 Source: https://docs.lagerdata.com/source/release-notes/v0.21.0 May 28, 2026 ## Features * **Pause a running script with `lager.pause()`.** Drop `lager.pause("why")` anywhere in a `lager python` script and it stops at that line mid-run, so you can check the bench from another terminal before it continues — useful for a long test that reaches a known trouble spot. A paused script doesn't lock the box, so your other `lager` commands (read a supply, toggle a GPIO, check a net) keep working while it waits. * **Resume however suits you.** Press **Enter** in the script's terminal, run **`lager python --continue --box `** from anywhere, or just walk away — it auto-resumes after 5 minutes by default so an unattended run never hangs. The pause prints the `id` and the exact resume commands. * **Inspect the paused script with a live Python console.** Add `pause(interactive=True)` and connect with **`lager python --console --box `** to get a Python prompt running inside the paused script — read any of its variables, evaluate expressions, or call its functions. This is also how you read a device the script is holding open (e.g. a LabJack), since the console runs in the same process. ## Improvements * **The built-in `breakpoint()` now works in `lager python` scripts.** It previously errored out; calling `breakpoint()` now triggers the same interactive pause as `lager.pause()`. * **Tune or disable the auto-resume.** Set a longer (or shorter) wait with `pause("...", timeout=1800)` or `lager python ... --env LAGER_BREAKPOINT_TIMEOUT=1800`; use `timeout=0` to wait indefinitely; set `LAGER_BREAKPOINTS=off` to turn every breakpoint into a no-op for a clean run. See the [Breakpoints guide](/source/reference/python/breakpoints) for the full reference and a worked example. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.0/) # Version 0.21.1 Source: https://docs.lagerdata.com/source/release-notes/v0.21.1 May 29, 2026 ## Improvements * **Help that actually shows you what to type.** Every net command now displays the real `lager [NET_NAME] [COMMAND] --box [BOX_NAME]` usage pattern instead of a generic placeholder, and each one comes with copy-pasteable examples. `lager --help` is now grouped into sections instead of one long alphabetical list, so it's far easier to find the command you want. * **Clearer errors that tell you the fix.** When something goes wrong — a Box you can't reach, a bad config, an SSH or login failure, an instrument that's busy or unplugged, or a command missing its net name — Lager now prints a short message describing the problem and what to do about it, instead of a raw Python traceback. (Need the full technical detail? Re-run with `--debug`.) ## Bug Fixes * **Fixed a couple of rough edges** in the command-line tooling: a broken internal entry point and an incorrect "defaults set" hint. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.1/) # Version 0.21.2 Source: https://docs.lagerdata.com/source/release-notes/v0.21.2 May 29, 2026 ## Bug Fixes * **Fixed erratic input in `lager nets tui`.** Since 0.21.0, the interactive Net-Manager TUI could drop keystrokes and feel unresponsive — navigation and the rename/edit dialogs would lag or miss input. The TUI now correctly opts out of the `lager.pause()` stdin watcher that 0.21.0 added, so it no longer competes with the terminal for your keypresses. Breakpoint resume in `lager python` is unchanged. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.2/) # Version 0.21.3 Source: https://docs.lagerdata.com/source/release-notes/v0.21.3 May 29, 2026 ## Bug Fixes * **Fixed erratic input in `lager supply tui` and `lager battery tui`.** This completes the 0.21.2 fix. Since 0.21.0, the `lager.pause()` breakpoint feature added a background thread that watches `stdin` so you can press Enter to resume a paused `lager python` script. 0.21.2 stopped `lager nets tui` from competing with that thread, but the power TUIs (and the `lager supply`/`lager battery`/`lager arm` confirmation prompts) hit the same problem through a different path — validating the net before launch left a stray `stdin` reader that then stole keypresses. Both TUIs now feel responsive again, and `y`/Enter confirmations are no longer intermittently swallowed. * **Hardened the breakpoint watcher against future regressions.** The stdin watcher now only starts for a genuine interactive foreground run, so any command that captures script output internally can no longer leak a competing reader. Breakpoint resume in `lager python` is unchanged, including when piping output (e.g. `lager python script.py | tee log.txt`). ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.21.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.21.3/) # Version 0.22.0 Source: https://docs.lagerdata.com/source/release-notes/v0.22.0 June 1, 2026 ## Changes * **Pin to a release with its tag.** `lager update --version X.Y.Z` and `lager install --version X.Y.Z` now resolve a release number to the matching `vX.Y.Z` tag instead of a same-named git branch. You can pass the bare number (`0.21.3`) or the tag form (`v0.21.3`) — both resolve to the tag, including pre-releases like `v0.22.0-rc1`. Branch targets such as `main`, `staging`, or a feature branch are unchanged and still pin to that branch. Existing `--version X.Y.Z` pins keep working unchanged. ## Bug Fixes * **Tag pins fetch reliably on every box.** Updating to a tag now fetches it with an explicit refspec so the tag is created as a local ref on the box. Previously, on a box that didn't already have the tag, `lager update --check` could report "update state unknown" and the checkout could fail. ## Deprecations * **Per-release version branches are deprecated.** Releases no longer publish a `X.Y.Z` branch alongside the `vX.Y.Z` tag — the tag is the single source of truth for pinning. Pin with the tag going forward. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.0/) # Version 0.22.1 Source: https://docs.lagerdata.com/source/release-notes/v0.22.1 June 2, 2026 ## Improvements * **Standardized Lager Box references to placeholders across the repository.** All `--help` output, command docstrings, source comments, the CHANGELOG, release notes, and documentation now use the `` placeholder (and ``) for box names and addresses; test fixtures use a neutral `test-box` token. This is a documentation/metadata change only — no functional or API changes. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.1/) # Version 0.22.2 Source: https://docs.lagerdata.com/source/release-notes/v0.22.2 June 3, 2026 ## Bug Fixes * **Multi-output power supplies now apply every command to the selected channel.** On Keysight E363xx and Rigol DP800 series supplies, the channels of a single instrument share one USB session, and the shared driver stayed bound to whichever channel was opened first. Commands that don't name a channel — setting voltage or current, enabling/disabling the output, and reading state — were applied to that first channel instead of the one you selected. On the Keysight E36312A this looked like a limits problem: a voltage setpoint above 6V on CH2 or CH3 (25V channels) was rejected, because the write was actually reaching CH1 (6V max). Each command now targets the correct channel. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.22.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.22.2/) # Version 0.23.0 Source: https://docs.lagerdata.com/source/release-notes/v0.23.0 June 4, 2026 ## Features * **`lager box config udev add/list/remove` — add your own USB device rules.** Grant a USB device read/write access from inside the Lager Box container by vid:pid, e.g. `lager box config udev add 1209:0001 --box ` followed by `lager box config apply`. This fixes the common case where a freshly-plugged device is owned by root, so tools like `dfu-util` fail to open it ("No DFU capable USB device available"). Pass `--usbtmc` for SCPI/USBTMC instruments to also unbind the kernel `usbtmc` driver (needed for PyVISA/libusb access). Rules persist in the box config and are installed on the host on every `apply` — no more waiting for a new release to support a device. * **`lager box config reset` — erase the box config to empty.** A single command that clears the config to a clean slate (unlike `init`, which seeds the default `box-tools` volume). Pass `--apply` to erase **and** restart the container in one step — handy before a test run. * **`lager box config restart` — restart the container without changing config.** Brings up a fresh container with the same configuration, useful for per-test isolation. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.23.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.23.0/) # Version 0.24.0 Source: https://docs.lagerdata.com/source/release-notes/v0.24.0 June 5, 2026 ## Features * **DUT context for AI agents — the MCP server now understands the board, not just the bench.** New `DUTContext`, `SubSystem`, and `DocRef` models capture a device-under-test's purpose, summary, MCU, key peripherals, and schematic/datasheet references (by URL or synced `repo_path`). New `discover_dut()` and `cite_schematic()` tools and `lager://dut/overview.md` resources surface it, and `discover_bench(net)` now returns the parent subsystem and relevant doc refs. Schematics are referenced, never stored — the agent fetches and analyzes them with its own tools. * **`lager box dut show | edit | add-doc`** — author and inspect DUT context (subsystems, documentation references) stored in the Lager Box's `bench.json`. * **`DebugNet.session()` and `DebugNet.rtt_defmt(elf=...)`.** `session()` is a context manager that scopes connect-on-entry and guaranteed teardown so the safe connect/disconnect ordering is encoded once. `rtt_defmt()` opens an RTT session and pipes it through `defmt-print`, yielding decoded log lines instead of raw bytes — so on-box `lager python` tests can assert directly on defmt-encoded firmware logs. `defmt-print` is now bundled in the Lager Box image. * **MCP slash-command prompts** (`write_lager_test`, `explore_bench`, `assess_test_feasibility`) and a new "AI Agents (MCP)" documentation tab with a server overview and DUT-context guide. ## Improvements * **The MCP server is now a read-only discovery & planning surface.** Its purpose is to let an agent learn the bench and DUT well enough to write and run a test; execution happens in the test script via `lager python … --box `. `discover_bench`/`discover_dut` echo the real address the client connected on and return a ready-to-run command, and `discover_bench` now reports instrument channels, capabilities, firmware, and authored specs/ranges. * **Reconnect-aware RTT and self-healing reset/read\_memory on both J-Link and OpenOCD backends.** The RTT reader transparently re-attaches to the same port after a socket drop (it only re-attaches to an already-running server, never starts one), and `reset`/`erase`/`read_memory` reconnect automatically when no server is running — so scripted flash → attach → reset loops no longer thrash. A DA1469x guard avoids auto-starting an unhalted server. * **Simplified agent-facing net metadata to `purpose` / `notes` / `tags`**, replacing the overlapping `description` / `dut_connection` / `test_hints` fields. `lager nets describe` now takes `--purpose` / `--notes` / `--tag`, and the Net Manager TUI edit dialog matches. * **The MCP server auto-reloads bench config on change** (it watches the mtimes of `bench.json`, `saved_nets.json`, and `box_id`), so DUT/net edits are picked up on an agent's next request without a reload call or service restart. It also warns when a subsystem references a net that doesn't exist. ## Bug Fixes * **Debug connect no longer burns its retries when the GDB remote rejects non-stop mode.** JLinkGDBServer rejects `set non-stop on`, which previously exhausted all connect retries and skipped target verification and RTT control-block auto-detection. The connect now detects that specific rejection and retries once with an all-stop controller; OpenOCD keeps non-stop, and any other target error still fails loudly. * **`lager box dut add-doc` / `edit` now save `bench.json` without requiring passwordless sudo**, and no longer raise a `KeyError` on a Lager Box whose `bench.json` has no DUT block. * **`lager box dut edit` and `lager box config edit` now honor `$EDITOR`/`$VISUAL` flags** (e.g. `subl -w`, `code -w`, `vim -p`) by parsing the editor string with `shlex.split()` instead of treating it as a single program name. ## Breaking Changes * **The MCP server no longer exposes hardware I/O or mutation tools.** The `quick_io`, `install_dependency`, `run_python`, `pip`, `logs`, `defaults`, and `binaries` tools, the safety/preflight engine, the `run_lager` CLI passthrough, and the audit subsystem have been removed. Agents now execute tests via `lager python path/to/test.py --box ` rather than over MCP. Per-bench safety constraints are now advisory metadata surfaced in `discover_bench`, not enforced. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.24.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.24.0/) # Version 0.25.0 Source: https://docs.lagerdata.com/source/release-notes/v0.25.0 June 11, 2026 ## Features * **Custom serial-device assignment — RS-232 instruments become first-class.** Instruments the Lager Box cannot identify by USB enumeration (first case: the Rigol DP711 power supply, reached through a generic Prolific USB-serial cable that enumerates as the cable, not the PSU) can now be assigned to their cable once with `lager nets assign`. From then on the instrument scans, nets, and drives exactly like an auto-detected device. * **`lager nets assign`** — `--list` shows assignable devices, current assignments, and unassigned USB-serial cables; `lager nets assign Rigol_DP711 --serial ` (or `--port ` for serial-less clone cables) stores the assignment on the Lager Box, durable across reboots and replugs. `--baud` overrides the catalog default when the instrument's front panel differs; `--as-net [NAME]` creates the net in the same step; `--remove` unassigns. * **Assign Device flow in the Net Manager TUI** — the interactive twin of `nets assign`: pick a cable, pick the instrument (with optional baud override), and name its net in a follow-up dialog. Assignments can be removed from the same screen. * **Rigol DP711 (DP700-series) support** — single-channel RS-232 power supply driver with the DP800-compatible method surface, addressed by a durable `serial://:/serial/` (or `/port/

`) identity that re-resolves to the live tty at open time — surviving tty renumbering, port moves, and replugs — with stale-session self-healing. * **Generic `POST /net/command` endpoint** on the Lager Box HTTP server for Tier-1 instruments (GPIO, ADC, DAC, thermocouple, watt-meter, e-load), giving them the same warm in-process path the supply/battery endpoints use instead of a subprocess per call. The `netCommand` capability is advertised in `/status` only when the route actually registers. ## Improvements * **Nets live and die with their assignment.** Removing (or replacing) a cable assignment deletes the saved nets bound to its address and reports them; pre-existing generic-UART nets on the cable are retired at assign time so a terminal session can never fight the instrument driver for one tty. A baud-only re-assign keeps existing nets. * **The scanner reports assigned instruments, not their cables.** `lager instruments`, the TUI, and the Lager Box's `/instruments/list` show the catalog instrument at its durable `serial://` address; the cable's generic UART record is suppressed while assigned, and assigned ttys are excluded from the Dexarm G-code handshake probe. * **Backend JSON parsing is robust to doubled output for objects, not just arrays** — both the CLI and TUI parsers now take the first complete JSON value, fixing a latent recovery bug for the known "double execution" Lager Box output. ## Bug Fixes * **Manually-added supply and battery nets are driveable again.** `lager nets add`/`delete`/`add-batch` now normalize the legacy role tokens `supply` → `power-supply` and `batt` → `battery`; the short tokens were previously saved verbatim, producing nets that listed fine but that no supply/battery command could drive. The tokens remain accepted as input aliases, `delete` reaches legacy nets saved under either spelling, and channel validation for supplies now actually runs on `nets add`. * **Documentation and error hints no longer reference the nonexistent `lager nets create` family** — renamed to the real `add`/`add-all`/`add-batch` commands across the docs, READMEs, and four runtime error messages, and the documented role vocabulary now matches what saved nets actually carry. * **The DP700 driver reports a missing cable as a device-not-found error** when unplugged mid-session, instead of a raw Python traceback. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.25.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.25.0/) # Version 0.26.0 Source: https://docs.lagerdata.com/source/release-notes/v0.26.0 June 11, 2026 ## Security * **Key authorization is rate limited.** Bad-token attempts against the Lager Box `/authorize-key` endpoint are limited per client IP (5 attempts per 60-second window, then HTTP 429); the window resets on a successful authorization. * **The Lager Box web service `SECRET_KEY` persists across restarts.** Generated once and stored at `/etc/lager/secret_key` (mode 0600) instead of regenerated on every boot, so sessions survive container restarts. * **`org_secrets.json` is held at owner-only permissions.** The on-box secrets file is tightened to mode 0600 at load time, and the boot-time permission fix is best-effort so an unexpected owner can no longer abort container startup. * **Instrument device nodes are scoped to a dedicated `lager` group.** The shipped udev rules grant `MODE="0660", GROUP="lager"` instead of world-writable 0666; `lager update` creates the group on the Lager Box host when missing and the container joins it automatically. User-added udev rules (`lager box config udev add`) default to the same scoping — run `lager update` on a Lager Box before applying new user udev rules so the group exists. ## Features * **Per-connect J-Link script override — `DebugNet.connect(script=...)`.** Pass a path on the Lager Box or a base64 blob to swap the J-Link script for one session phase (for example, a halt-in-place reset script for a memory read-back, then the stock script to reboot the target). The bytes are copied to the shared script path so `flash`/`reset`/`read_memory` pick the new script up immediately; an already-running gdbserver adopts it on relaunch (`force=True`). Invalid input is ignored and the net's saved script stays in effect. * **Opt-in cache-coherent post-program verify for DA1469x QSPI images (experimental).** Set `LAGER_DA1469_UNCACHED_VERIFY=1` to read programmed `.bin` bytes back through the uncached QSPI mirror after a cache-controller flush: a matching image suppresses J-Link's stale-cache false "verification failed" report from no-reset attaches, a real mismatch is reported with its first differing address, and an inconclusive read-back leaves the original output untouched. `LAGER_DA1469_UNCACHED_VERIFY_BYTES` caps the compare (0 = whole file). Default off; flash output is unchanged when unset. ## Bug Fixes * **`lager debug ... gdbserver --rtt` no longer leaves the target halted** on probes whose J-Link GDB server rejects non-stop mode: the RTT control-block scan implicitly halts the core in the all-stop fallback, and the core is now resumed after the scan. Non-stop and OpenOCD paths are unchanged. * **`lager box config` host-side operations no longer dead-end on Lager Boxes with customer-managed SSH users.** The dedicated `~/.ssh/lager_box` key previously replaced ssh's default identity list, so a user whose own key was authorized (via `ssh-copy-id`) failed every host-side call with `Permission denied (publickey,password)` even though `lager ssh` worked. The SSH runner now retries once without the dedicated key on an auth failure, so default identities get their chance. * **SSH transport failures are reported as SSH failures.** An unreachable or hung Lager Box host during mount pre-flight was misread as "path missing" (producing a wrong manual fix) or crashed with a raw traceback; it is now classified separately with the real user\@ip and actionable fixes, `mount add` persists the mount, and `apply` warns and continues. * **Mount pre-flight runs after the confirm prompt and after apt/sysctl/udev provisioning**, so a mount of a file installed by an apt package in the same config (for example `/usr/bin/dfu-util`) works in a single `apply`, and the host is no longer mutated before the operator confirms. `apply --skip-restart` no longer runs the pre-flight at all. * **Leaked file handles closed** in project packaging (`zip_dir`) and the gdb `--debugfile` read; **bare `except:` clauses replaced with specific exceptions** across the CLI so interrupts and unexpected errors surface instead of being silently swallowed. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.26.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.26.0/) # Version 0.27.0 Source: https://docs.lagerdata.com/source/release-notes/v0.27.0 June 12, 2026 ## Features * **Custom LabJack pin selection for i2c/spi nets.** `lager nets add` now accepts `--sda`/`--scl` (i2c) and `--cs`/`--sck`/`--mosi`/`--miso` (spi). Any DIO pin (FIO0-FIO7, EIO0-EIO7, CIO0-CIO3, MIO0-MIO2) or raw DIO number can be assigned per signal; omit `--cs` for 3-pin SPI with manual chip select. Defaults are unchanged, and pins already used by another saved LabJack net warn without blocking. * **Net TUI pin-picker dialog.** Adding a LabJack i2c/spi net in the TUI opens a pin dialog with the historical defaults preselected (I2C: SDA=FIO4/SCL=FIO5; SPI: CS=FIO0/SCK=FIO1/MOSI=FIO2/MISO=FIO3). Duplicate pins block the save; pins claimed by saved nets warn live. ## Bug Fixes * **Net TUI buttons no longer need multiple clicks** (a 0.25.0 regression): box round-trips ran on the UI thread and froze the interface for seconds per call, worst right after launch and on Assign Device. All box calls — assign flows, add/save, delete, rename, delete-all, edit details — now run in the background with busy indicators and disabled controls while in flight. * **Fixed a `signal only works in main thread` crash** when TUI actions ran Lager Box scripts from worker threads; the Ctrl+C handler is now only installed for interactive command-line runs. ## Improvements * `lager i2c` and `lager spi` display custom LabJack pin assignments with canonical pin names (for example `EIO0` instead of a raw DIO number). * Net TUI startup is faster: the saved-nets list is no longer fetched a second time while the first screen paints. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.27.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.27.0/) # Version 0.27.1 Source: https://docs.lagerdata.com/source/release-notes/v0.27.1 June 12, 2026 ## Features * **`lager authorize --box [BOX]`** sets up passwordless SSH to a Lager Box in one step: it creates `~/.ssh/lager_box` if needed, copies the key to the box (one password prompt), and verifies it works. Running it again against an already-authorized box just confirms it. When a command fails with `Permission denied (publickey,password)`, the error now points you straight at `lager authorize`. ## Bug Fixes * **`lager nets` and `lager instruments` no longer cut off data.** UART serial-port paths (for example `/dev/ttyUSB0`) and full VISA/USB addresses now display in full instead of being truncated. * **Clearer SSH error reporting in `lager box dut` and `lager box config`.** A failed connection now shows the real cause and the fix to run, instead of a raw error line or a misleading "no snapshot" message. ## Improvements * **Cleaner `--help` usage lines.** Command groups now read `COMMAND [OPTIONS]`, and `lager nets` / `lager authorize` show `--box [BOX_NAME]`, consistent with commands like `lager supply`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.27.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.27.1/) # Version 0.28.0 Source: https://docs.lagerdata.com/source/release-notes/v0.28.0 June 13, 2026 ## Features * **`lager python` automatically reserves the box while it runs.** Every run acquires the box lock at start and releases it on exit, `Ctrl+C`, crash, or kill, with a server-side TTL + heartbeat reap as the backstop. Lock identity is CI-aware, so parallel CI matrix jobs queue against a shared box instead of colliding: collisions fail fast on a dev machine and wait (up to `LAGER_LOCK_WAIT`) in CI. `--detach` holds an eternal lock you release with `lager boxes unlock`. The box-mutating admin commands — `lager install`, `uninstall`, `update`, `install-wheel` — also hold the lock across their destructive steps so a concurrent test is never killed mid-run. Tune or disable via `LAGER_AUTO_LOCK_DISABLE`, `LAGER_LOCK_WAIT`, `LAGER_LOCK_TTL`, `LAGER_LOCK_HEARTBEAT`, and `LAGER_LOCK_HOLDER`. * **Explicit `lager boxes lock` reservations are never disturbed** by any auto-locking command — they keep their no-expiry semantics on new and old box servers alike. ## Bug Fixes * **The supply TUI now works on slow instruments (e.g. Keithley 2281S).** Previously its live readout could stay stuck at `00.000` with repeated "Hardware service unreachable" errors and timed-out commands. The box-side monitor now reads the full display state in a single call per update and paces itself to the instrument, so readings populate and commands respond promptly — even when the instrument is shared with the battery simulator. * **TUI error messages now say what actually failed** (a timeout, a refused connection, or a device error) instead of an empty message, and a reachable box with a failed supply/battery session points you at the instrument (check it is on and shows in `lager instruments`) instead of suggesting an outdated box image. * **`lager battery tui` with a non-battery net now exits with an error code** instead of reporting failure but exiting successfully. ## Improvements * **Pinned `textual` and `python-socketio` to compatible ranges** so a fresh `pip install lager-cli` can't pull in a newer, breaking version of either library. Existing installs are unaffected. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.0 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.0/) # Version 0.28.1 Source: https://docs.lagerdata.com/source/release-notes/v0.28.1 June 15, 2026 ## Features * **`spi`, `i2c`, and `energy-analyzer` instruments now run on the Lager Box's warm path.** These roles previously went through the box's per-command Python executor; they now run in the long-lived box server alongside gpio/adc/dac/eload/thermocouple/watt-meter, removing an interpreter spawn and device re-open on every command. Behavior and dashboard logs are unchanged, and energy-analyzer read durations are clamped to 0.1–30s. The rollout is back-compatible — an older Lager Box automatically falls back to the previous path. * **WebSocket transport support added to the Lager Box.** The box image now bundles `simple-websocket`, so clients can use a native WebSocket connection instead of long-polling, negotiated automatically. ## Bug Fixes * **`lager ssh` now uses the key set up by `lager authorize`.** Authorized Lager Boxes were still dropping to a password prompt because `lager ssh` didn't offer `~/.ssh/lager_box`; it now does when that key exists, while leaving the password fallback intact for boxes that haven't been authorized. * **UART devices are now opened exclusively.** A second user of a serial port — another dashboard session, or `lager uart` while a Workbench session is live — now fails fast with a clear "device in use" message instead of silently interleaving reads on the same port. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.1 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.1/) # Version 0.28.2 Source: https://docs.lagerdata.com/source/release-notes/v0.28.2 June 17, 2026 ## Features * **Save your devenv container setup in the project.** You can now store container settings in your project's `.lager` file instead of retyping them or keeping shell aliases. Use `devenv set`/`unset`/`show` for basic settings (image, shell, user, group, ports, and more), `devenv mount add`/`remove`/`list` for folders to share into the container, and `devenv env set`/`unset`/`list` for environment variables. Both `devenv terminal` and `lager exec` use these settings automatically, so they travel with the repo for everyone on the team. * **Add settings for a single run.** `devenv terminal` and `lager exec` take `-v HOST:CONTAINER` to share a folder. `devenv terminal` also takes `-e FOO=BAR` to set a variable and `--passenv NAME` to forward one from your shell. Paths can use `~` and `${PROJECT_ROOT}`, so saved settings work on any machine. * **Preview a session without launching it.** `devenv terminal --info` prints the exact `docker` command it would run, then exits without starting anything. * **Skip the reset before a memory read.** `lager debug memrd --no-reset` skips the reset-and-halt the Lager Box normally does before reading a DA1469x — useful on a blank chip where you don't want to reboot it. ## Bug Fixes * **Reading memory from a running DA1469x now works.** Live firmware turns off the debug port, so reads used to fail. The Lager Box now resets and halts the chip first. This reboots the device under test — pass `--no-reset` to skip it. * **Some memory reads returned wrong values.** Reads of certain chip registers now return the correct values. * **`devenv terminal --group` now works.** The group setting was being ignored before; it is now applied. ## Improvements * **More predictable devenv settings.** When the same setting is given both on the command line and in `.lager`, the command line now wins. The container entrypoint can also be saved in `.lager`. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.2 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.2/) # Version 0.28.3 Source: https://docs.lagerdata.com/source/release-notes/v0.28.3 June 18, 2026 ## Features * **`lager diagnose` now works on J-Link debug nets.** Point it at a `debug` net — `lager diagnose --box ` — and it walks the whole debug-probe stack and tells you exactly what's wrong and what to do about it: the probe isn't on USB (cable/power/hub), the J-Link software isn't installed on the Lager Box, the probe is held by another process or its firmware is wedged (power-cycle it), a debug server is wedged, the target board is unpowered, the target is locked by readout/IDCODE protection, the net's device/MCU name is wrong, or the probe is fine but can't reach the target over SWD/JTAG (wiring, reset, or speed). When a debug session is already running for the probe, diagnose reports from that session instead of interrupting it. OpenOCD/ST-Link probes get basic coverage (probe detection and debug-server state). Previously `lager diagnose` only covered USB-TMC instruments such as power supplies, electronic loads, and scopes. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.3 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.3/) # Version 0.28.4 Source: https://docs.lagerdata.com/source/release-notes/v0.28.4 June 22, 2026 ## Improvements * **`lager update` is much faster.** Box image rebuilds now reuse a build cache for the Rust (`defmt-print`) and Python package layers, so a from-scratch rebuild reuses already-downloaded packages and compiled artifacts instead of redoing them — a cold rebuild that used to take around 20 minutes now finishes in a few minutes, and a warm rebuild takes seconds. `lager update` also checks up front that the box's Docker supports BuildKit and tells you how to upgrade if it doesn't. * **`lager update` asks for the sudo password at most once.** The udev, modprobe, sudoers, and box-config setup steps used to each prompt separately, so a box that needed several of them could ask for the password multiple times in one run. They now run in a single step — at most one prompt, and none at all on a fully set-up Lager Box. ## Bug Fixes * **A repeat `lager update` no longer rebuilds when nothing changed.** The box keeps a record of its build inputs so it can skip an unnecessary rebuild, but that record couldn't be updated and went stale, so every update rebuilt the image and restarted the box (about 30 seconds). It is now written reliably, so an unchanged box finishes in about a second. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.4 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.4/) # Version 0.28.5 Source: https://docs.lagerdata.com/source/release-notes/v0.28.5 June 24, 2026 ## Bug Fixes * **`lager update` fails fast with a clear fix when a box is missing buildx.** The BuildKit work in 0.28.4 made the box image require Docker's `buildx` plugin, which a stock `docker.io` install (for example on Ubuntu) doesn't bundle. The up-front check used to pass on the Docker version alone and then the build died minutes later with a confusing "buildx component is missing or broken". `lager update` now verifies buildx is actually present, checks it before stopping the box's container (so a box that can't build is never taken offline), and tells you the exact command to install it. * **A stale SSH connection no longer breaks `lager update`.** A leftover SSH control socket from an earlier interrupted run could be silently reused and surface as "Permission denied (publickey,password)" on the box state check, even when the key worked fine. `lager update` now clears any leftover connection before it starts. * **`lager update` accepts a first-seen box host key.** Its SSH calls now auto-trust a brand-new box's host key the same way the key-setup step does, so updating a box that isn't yet in `known_hosts` no longer fails with "Host key verification failed". ## Improvements * **Provisioning a box installs buildx automatically.** New box setup now installs the Docker `buildx` plugin (falling back to the official buildx binary when distro packages don't provide a working one), so a freshly provisioned box is ready to build the box image and never hits the update preflight error. ## Installation To install this version: ```bash theme={null} pip install lager-cli==0.28.5 ``` To upgrade from a previous version: ```bash theme={null} pip install --upgrade lager-cli ``` ## Resources [View Release on PyPI](https://pypi.org/project/lager-cli/0.28.5/) # Version 0.29.0 Source: https://docs.lagerdata.com/source/release-notes/v0.29.0 June 29, 2026 ## Features * **`lager boxes add` now requires `--user` (breaking change).** The implicit `lagerdata` default has been removed — you must specify the box's login user explicitly when adding a box. * **Read a USB net's state without changing it.** The new `lager usb state` reports whether a port is on or off, read-only. * **Run a one-off command on a Lager Box over SSH.** `lager ssh --box -- ` 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.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.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 | Flasher ARM | `lager debug` | | STMicroelectronics | ST-Link/V2, V2-1, V3 | `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 — there are too many wiring variants on quad-MPSSE boards — so you must pass an `openocd_config` pointing 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 via 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. * Target chip configuration is auto-selected from the device name for STM32, nRF5x, RP2040/RP2350, ATSAM, LPC, i.MX RT, and ESP32 families; anything else can be driven via a custom OpenOCD target config. *** # 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` | **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` | | 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 | | WiFi | `lager wifi` | Scan, connect, disconnect, status | **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)