> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lagerdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Version 0.16.9

> April 29, 2026

## <u>Bug Fixes</u>

* **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 <net>` and `lager battery <net>` 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 <net> 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://<box>: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 <net> tui` and a concurrent `lager battery <net>` 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.

## <u>Known Limitations</u>

* **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.

## <u>Internal</u>

* 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.

## <u>Installation</u>

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/)
