instro puts one typed Python API in front of lab instruments from many vendors
An open-source library for hardware test instrumentation
At a glance
- What is it?
- A vendor-agnostic library for power supplies, scopes, DAQ cards, multimeters and PLCs, backed by a Rust workspace that the README never mentions and a hard dependency on the Nominal data platform.
- Who is it for?
- instro earns its place on a bench where the same measurement gets written four times for four vendors, because the driver is the only thing that changes between a Rigol supply and a Keysight one. The costs are concrete: a Rust toolchain sits behind the Python package, vendor runtimes arrive through extras and are documented in a NOTICE file, and pyproject.toml requires the Nominal platform package whether or not you send data anywhere.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One API across supplies, scopes, DAQ cards and PLCs
The pitch in one line from the README is an open-source, vendor-agnostic Python library for interfacing with test equipment. What that means concretely is visible in the supported devices table, which spans far more than a bench: power supplies from B&K Precision, EA Elektro-Automatik, Keysight, Rigol, Siglent and TDK Lambda, multimeters from Agilent and Keithley, waveform generators, electronic loads, oscilloscopes from Keysight, Tektronix and Siglent, an Alicat flow controller, DAQ hardware, an I2C interface, Modbus devices, and EtherNet/IP PLCs from the Allen-Bradley and CompactLogix class.
Each row names a class such as InstroPSU or InstroScope and then the vendors behind it. That is the useful abstraction: you write against the instrument class, and the vendor driver underneath handles the SCPI dialect, the register map or the vendor quirks. A reader with mixed-brand hardware otherwise maintains one script per supply.
Vendor-agnostic here means one API across vendors, not that vendors are optional. The table is explicit that some entries are simulated and some are marked unstable, which is the honest way to signal that driver quality is not uniform across a table this wide. The Keithley 2750 multimeter and the VESC 6 motor controller both carry an unstable label, so treat those two as experiments rather than as supported hardware.
The repository reports 704 stars and 24 forks, and its topics match the table: scpi, instrument-control, data-acquisition, test-and-measurement. It is a narrow library that a narrow audience searches for by vendor name, and the topic list suggests most people arrive already knowing which instruments they need to drive.
The quickstart swaps a driver and nothing else
The README's quickstart is the whole argument in one block. It opens a DAQ device through a driver object, passes that driver into the InstroDAQ class along with a list of publishers, configures one analog channel, and reads from it:
from instro.daq.drivers.labjack import LabJackTSeriesDriver
from instro.daq import InstroDAQ
from instro.daq.types import Direction
from instro.lib.publishers import FilePublisher
pub = FilePublisher(format="jsonl", directory="/tmp/instro/")
daq = InstroDAQ(
name = "myDAQ",
driver = LabJackTSeriesDriver(device_id="1234"),
publishers = [pub],
)
daq.open()The line that matters for anyone with different hardware is a commented one in the original snippet: `#driver = NIDAQDriver(device_id="Dev1") # swap drivers, same code`. Changing from a LabJack T-series to an NI DAQ is a different import and a different device identifier. Everything after that line stays identical, including the channel configuration and the read call.
The publishers list is the other half of the design. FilePublisher writes to jsonl in a directory you name, and a comment on the read call notes that the measurement is written to the publishers rather than only returned to the caller. So data capture is not a separate script written afterward; it is attached at construction time, and adding a different destination later means adding to that list.
The signature style uses aligned equals signs inside the constructor call. That is unusual for a Python codebase and worth knowing before you paste these snippets into a linter whose formatting rule disagrees.
Extras decide which vendor runtimes land on your machine
Installation is two commands, and the choice between them is preference rather than substance:
uv add instropip install "instro[all]"The extras table is where the real decisions sit. The base package is small, and each extra pulls a vendor group: daq for all DAQ drivers, then nidaq for the NI DAQ package, mccdaq for Measurement Computing, labjack for LabJack. i2c pulls the Total Phase Aardvark package, ethernetip adds EtherNet/IP support, contrib holds community-contributed drivers, and unstable holds experimental modules that the supported devices table also flags in two places.
There is an all extra for everything at once. On a rack with one supply and one scope that is the wrong default, since it installs runtimes for instruments you do not own, and vendor runtimes are exactly the kind of dependency that brings its own version constraints.
The pyproject.toml file explains what the vendor support is actually built on. Dependencies include pyvisa for VISA, pymodbus for Modbus devices, pyserial for serial, and fastavro, plus pydantic for typed models. The pyvisa comment in that file is unusually candid: it notes that enabling every pyvisa-py backend gives USB-TMC and TCPIP discovery, that the usb-full bundle ships libusb so no system libusb is needed, that GPIB still requires a native GPIB driver, and that proprietary vendor runtimes are still expected for some instruments.
Two consequences follow. Python support is declared as 3.10 through 3.14, which is a wide floor for a library wrapping binary vendor stacks. And the Apache 2.0 licence is paired with a NOTICE file documenting third-party dependency notices and proprietary vendor runtime requirements, so the licence is permissive but the terms are not self-contained.
A Python package with a Rust workspace underneath
The README is entirely Python. The repository is not. The top level contains Cargo.toml, Cargo.lock, rust-toolchain.toml and a crates directory, and the Cargo workspace declares what is inside it:
[workspace]
resolver = "2"
members = [
"crates/*",
"packages/instro-ethernetip",
"packages/instro-opcua",
]The workspace pins Rust edition 2024 with a minimum of 1.93, and the workspace dependencies include open62541 with mbedtls and x509 features, plus an instro-opcua-test crate and an instro-test-utils crate. That is an OPC UA implementation living in the workspace, alongside the EtherNet/IP package that the README's extras table does mention.
A contributor building this project needs both toolchains. The justfile shows the split plainly, with a Rust target, a Python target, and a combined target that also runs EtherNet/IP packaging checks:
test-rust:
cargo test --workspace --all-features --all-targetsThe same file wires up uv for Python work, mypy for typing, ruff for formatting and import ordering, and clippy lints configured in Cargo.toml that turn unwrap_used, expect_used, indexing_slicing and panic into warnings. This is a project with real static analysis on both sides, which is a good sign for a library where a wrong index can mean a wrong voltage.
The development install is the conventional uv flow:
git clone https://github.com/nominal-io/instro.git
cd instro
uv syncOne more dependency is worth naming because it is invisible from the README prose. pyproject.toml requires nominal at 1.166.0 or later and below 2.0.0. That is the Nominal data platform package, and the README's badge row points to a Nominal SDK, a community forum and a Discord, so instro ships inside that ecosystem. Vendor-agnostic describes the instruments, not the surrounding platform. A reader who wants a standalone measurement script with no platform dependency should confirm what nominal pulls in before installing.
Where the device table and the examples directory disagree
Two places in the repository describe coverage, and they do not line up. The examples directory holds twelve subdirectories: custom, daq, dmm, eload, ethernetip, i2c, modbus, psu, publishers, scope, test_rack_example and vna. There is a vna directory, but no vector network analyser class appears anywhere in the supported devices table. The table's instrument classes cover power supply, multimeter, waveform generator, electronic load, oscilloscope, flow controller, DAQ, I2C, Modbus, motor controller and EtherNet/IP. A vector network analyser is not on that list. The examples tree also includes a custom directory and a test_rack_example directory, neither of which maps to a row in the table, though both read as intentionally open-ended.
The opposite direction runs too. The device table lists EtherNet/IP for PLCs, and the README extras table has an ethernetip package, so EtherNet/IP appears in all three places: examples, extras and devices. OPC UA appears in two places only, the Cargo workspace members and the justfile's workspace dependencies, and in neither the extras table nor the device table, despite the workspace carrying a dedicated packages/instro-opcua package.
Neither gap is necessarily a defect. An example directory can be a scratch space, and a Rust package can exist ahead of its documentation. The README names two docs pages as the authority on both questions, and they are the pages to check first:
https://instro.nominal.io/instrumentation/installation
https://instro.nominal.io/instrumentation/supported-instrumentsIf your instrument is a VNA or an OPC UA server, look in crates and packages and on those pages before concluding the library cannot drive it. If your instrument is on the device table, expect a matching extras entry. Where the two listings disagree, the device table is the one the README names as the supported list.
Release cadence and a changelog date that trails the publish date
Three releases in six days show a project in motion. v1.18.1 landed on 2026-09-04 and fixed read_batch so measurements come from a background daemon fetch rather than a blocking call. v1.19.0 followed on 2026-09-07, adding EA PSB 10000 support for power supplies and electronic loads, plus a fix so an electronic load's cached mode is only set after the driver confirms set_mode. v1.20.0 on 2026-09-09 added JSON config-driven instrument creation for scopes, which is the release that makes instrument definitions declaratively loadable rather than hard-coded in Python.
The dates inside the release notes are not the dates GitHub records. The v1.20.0 entry is headed 2026-09-09 while the release published on 2026-09-11, and v1.19.0 is headed 2026-09-07 while published on 2026-09-08. v1.18.1 is the exception, matching on 2026-09-04. The pattern is consistent with a changelog written at merge time and the tag cut later, which is routine, but it means the two dates are not interchangeable when you are reconstructing a timeline.
The last push was on 2026-09-17, and pyproject.toml declares version 1.20.0, which matches the newest tag. The default branch and the published package therefore agree, with no unreleased major sitting in the tree.
Every release is scoped and conventional. Features carry issue numbers, bug fixes carry commit hashes, and nothing in the notes describes a breaking change, which is convenient for a library pinned into an existing measurement script. The open issue count sits at 98 on a repository this size, so there is a real backlog, and the README documents no deprecation policy and no support window for driver classes.
Editorial conclusion
instro earns its place on a bench where the same measurement gets written four times for four vendors, because the driver is the only thing that changes between a Rigol supply and a Keysight one. The costs are concrete: a Rust toolchain sits behind the Python package, vendor runtimes arrive through extras and are documented in a NOTICE file, and pyproject.toml requires the Nominal platform package whether or not you send data anywhere. Start with the base install and one driver from the supported devices table, then check the extras list before adding hardware. The device table and the examples directory do not fully agree, so verify your specific instrument class against the docs site at instro.nominal.io before committing to it.
Frequently asked questions
Which instruments does instro support?
The supported devices table covers power supplies, multimeters, arbitrary waveform generators, electronic loads, oscilloscopes, a flow controller, DAQ hardware, an I2C interface, Modbus devices, a motor controller and EtherNet/IP PLCs. Each row lists the class and the vendors behind it, including B&K Precision, EA, Keysight, Rigol, Siglent, Tektronix, Keithley, Agilent and Alicat.
How do I install instro with drivers for a specific vendor?
The base install is uv add instro or pip install instro, and vendor drivers arrive through extras such as nidaq, mccdaq, labjack, i2c and ethernetip. Use the all extra to install every driver at once. The justfile's install recipe accepts the same extra names for a development checkout.
Does instro need a Rust toolchain to use it from Python?
The Python package installs through uv or pip without a Rust build step. A Rust workspace with crates and packages directories sits in the repository and is used for tests, packaging checks and the EtherNet/IP and OPC UA packages, so contributors building from source need cargo alongside uv.
What licence does instro use and are there extra terms?
The project uses Apache License 2.0, declared in pyproject.toml alongside the LICENSE file. The README also points at a NOTICE file documenting third-party dependency notices and proprietary vendor runtime requirements, so some instruments still need vendor-supplied runtime components under their own terms.
Can instro read from two different brands with the same code?
Yes, that is the design. The quickstart shows the same InstroDAQ call and the same channel configuration and read call with either a LabJackTSeriesDriver or an NIDAQDriver, and only the import and the device identifier change. The same pattern applies across the instrument classes in the device table.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nominal-io-instro)