Pure-Python user-space driver for the NI GPIB-USB-HS adapter (with an Agilent/Keysight 34401A wrapper). No NI-488.2, no linux-gpib, no kernel module — works on macOS/Apple Silicon and Linux.
Project description
ni-gpib-usb-hs
A pure-Python, user-space driver for the National Instruments GPIB-USB-HS
adapter itself — it works with any GPIB instrument at any address, not
just one model. No NI-488.2. No linux-gpib. No kernel module. Just
pyusb + libusb
talking to the adapter directly.
from ni_gpib_usb_hs import NIUSBGPIB
with NIUSBGPIB() as gpib:
print(gpib.query(22, "*IDN?")) # or any instrument at any address
A ready-to-use wrapper for the Agilent/HP/Keysight 34401A multimeter ships alongside the driver as a worked example — swap in your own instrument's SCPI commands and it works the same way (see Quickstart):
from ni_gpib_usb_hs import Agilent34401A
with Agilent34401A(addr=22) as dmm:
print(dmm.idn()) # HEWLETT-PACKARD,34401A,0,10-5-2
print(dmm.voltage_dc()) # 0.0000334
Why this exists
The GPIB-USB-HS is a great, cheap way to get a modern host talking to old GPIB bench gear — except:
- NI ships no driver for Apple Silicon. NI-488.2/NI-VISA for macOS is Intel-only, and the kernel extension doesn't even load on macOS 13+.
linux-gpibis the usual fallback on Linux/Raspberry Pi, but it can be a pain to build (out-of-tree kernel module, autotools, Python-binding install paths that vary by distro) — and on the hardware this was built against, a genuine, boxed NI GPIB-USB-HS reproducibly failed every addressed GPIB transfer underlinux-gpib'sni_usbkernel driver (deterministicNIUSB_ADDRESSING_ERROR/ USB-110timeouts), even againstgit masterand after ruling out cabling, the instrument, USB autosuspend, and every config knob.
The interesting part: the same adapter, driven directly over libusb with a
from-scratch reimplementation of NI's wire protocol, worked immediately — clean
register reads/writes, clean addressed transfers, real *IDN? and READ?
replies from a 34401A. The fault was in the kernel driver's interrupt-endpoint
handling on that particular host, not the hardware. This project is that
reimplementation, cleaned up: synchronous bulk request/response only, no
interrupt-endpoint monitoring, no kernel module — which means it also runs
identically on macOS (incl. Apple Silicon) and Linux.
Install
# libusb (native library) — pyusb needs this
brew install libusb # macOS
# sudo apt install libusb-1.0-0 # Debian/Ubuntu/Raspberry Pi OS
pip install ni-gpib-usb-hs
Want the latest unreleased code instead? Install from source — see Development below.
Linux permissions
On Linux, plain USB device nodes are root-owned by default. Install the udev
rule so your user can access the adapter without sudo:
sudo cp udev/99-ni-gpib-usb-hs.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
# log out/in (or add yourself to the "plugdev" group) for group membership to apply
macOS needs no special permissions.
Quickstart
python examples/idn.py 22 # *IDN? whatever is at GPIB address 22
python examples/measure_dc.py 22 # a few different ways to read DC volts
Talk to any GPIB instrument
NIUSBGPIB is a thin, general-purpose GPIB controller — it just addresses a
device and moves bytes, so it works with anything on the bus (SCPI or not):
from ni_gpib_usb_hs import NIUSBGPIB
with NIUSBGPIB() as gpib:
print(gpib.query(22, "*IDN?"))
gpib.write(5, "OUTP ON")
print(gpib.query(5, "MEAS:VOLT?"))
The bundled 34401A wrapper
Agilent34401A is a convenience wrapper around NIUSBGPIB for one specific
instrument's command set (also works with HP 34401A / Keysight 34401A, which
share it):
from ni_gpib_usb_hs import Agilent34401A
with Agilent34401A(addr=22) as dmm:
# one-shot, autoranged
print(dmm.voltage_dc())
print(dmm.resistance_4wire())
print(dmm.frequency())
# fixed-range, low-noise, repeated reads (faster than MEASure? each time)
dmm.configure_dc_volts(rng=10, nplc=10)
for _ in range(5):
print(dmm.read())
print("mean of 10:", dmm.read_average(10))
print(dmm.errors()) # drains SYSTem:ERRor? queue
Writing a wrapper for your own instrument
If you have a different instrument, ni_gpib_usb_hs/agilent34401a.py
is a ~180-line template: a small class holding a GPIB address and an
NIUSBGPIB, with methods that just call self.write(...)/self.query(...)
with your instrument's SCPI commands. Or skip the wrapper entirely and use
NIUSBGPIB directly, as above — plenty of use cases don't need one.
Hardware
- Tested against a genuine, Hungary-made NI GPIB-USB-HS (USB ID
3923:709b), talking to an HP/Agilent 34401A. - Should work with any instrument on the bus, since the driver just implements the standard NI-USB wire protocol (register read/write, command, addressed write/read) — the 34401A wrapper is a convenience layer on top.
- Not tested against the GPIB-USB-HS+ (different USB PID) or the older GPIB-USB-B; PRs welcome if you have one.
How it works
The adapter exposes a Cypress FX2 USB front-end in front of an NI TNT4882 GPIB
controller chip. NIUSBGPIB:
- Performs the vendor control-transfer "ready" handshake (serial number + poll-ready).
- Bulk-writes a 26-entry TNT4882 register init sequence (reset, handshake mode, T1 delay, interrupt-mask setup, controller address, system-controller bit).
- Issues GPIB command bytes (UNL/talk-address/listen-address) over a bulk OUT/IN request-response pair to address instruments.
- Issues write/read bulk transfers for the actual data, with EOI handling and NI's chunked read-response framing.
Every operation is a synchronous bulk OUT then bulk IN — there is no async interrupt-endpoint status monitoring, which keeps the implementation small and sidesteps whatever the kernel driver's interrupt path was tripping over.
See ni_gpib_usb_hs/controller.py for the full
implementation (well-commented, ~300 lines).
Scope / limitations
- One controller, one instrument addressed at a time. No serial poll, no SRQ, no parallel poll.
- No secondary GPIB addressing.
- This is enough to drive the vast majority of SCPI bench instruments (anything
you'd script with
*IDN?/MEAS?/READ?style commands), but if you need interrupt-driven SRQ handling or multi-controller bus sharing, uselinux-gpibor NI-488.2 instead.
Development
git clone https://github.com/embeddedci-com/ni-gpib-usb-hs
cd ni-gpib-usb-hs
python -m venv .venv && source .venv/bin/activate
pip install -e .
python examples/idn.py
Publishing (maintainers)
This repo publishes to PyPI via Trusted Publishing (OIDC) — no API tokens stored in CI. One-time setup, then every GitHub Release publishes automatically.
One-time setup:
- Create the project on PyPI (either publish once manually, or use PyPI's "pending publisher" flow to register the name before any upload exists).
- On PyPI → your project → Settings → Publishing, add a trusted publisher:
- Owner:
embeddedci-com(or your GitHub org/user) - Repository name:
ni-gpib-usb-hs - Workflow name:
publish.yml - Environment name:
pypi
- Owner:
- On GitHub → repo → Settings → Environments, create an environment named
pypi(matches the workflow). Optionally add required reviewers for extra safety before a publish runs.
Every release:
- Bump
versioninpyproject.tomlandni_gpib_usb_hs/__init__.py. - Commit, tag, and push:
git commit -am "release: v0.1.0" git tag v0.1.0 git push && git push --tags
- On GitHub, Releases → Draft a new release, pick the tag, publish it.
The
Publish to PyPIworkflow (.github/workflows/publish.yml) runs automatically and uploads the build to PyPI.
To test the packaging without publishing, run the build job locally:
python -m pip install build
python -m build # produces dist/*.whl and dist/*.tar.gz
Or use workflow_dispatch to trigger publish.yml manually from the Actions
tab if you ever need to re-run a publish.
Credits
The GPIB-USB-HS wire protocol implemented here (register layout, command
framing, TNT4882 init sequence) is derived from the
linux-gpib project's ni_usb kernel
driver (drivers/gpib/ni_usb/ni_usb_gpib.c), © Frank Mori Hess and
contributors, licensed GPL-2.0. This project is a from-scratch, user-space
reimplementation of that protocol in Python — no code was copied — released
under the same license (GPL-2.0-only) in respect of that heritage.
License
GPL-2.0-only. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ni_gpib_usb_hs-0.1.0.tar.gz.
File metadata
- Download URL: ni_gpib_usb_hs-0.1.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74781526ae869a23c045ef35bbebe06fcb129820d83205e4875dd52956e67546
|
|
| MD5 |
cd488ada10442b47548ffb2695fc6953
|
|
| BLAKE2b-256 |
6202c3f372f69d21b8c0084213b889efc94d40be6fc946a7a93d245c30170a9f
|
Provenance
The following attestation bundles were made for ni_gpib_usb_hs-0.1.0.tar.gz:
Publisher:
publish.yml on embeddedci-com/ni-gpib-usb-hs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ni_gpib_usb_hs-0.1.0.tar.gz -
Subject digest:
74781526ae869a23c045ef35bbebe06fcb129820d83205e4875dd52956e67546 - Sigstore transparency entry: 2043459974
- Sigstore integration time:
-
Permalink:
embeddedci-com/ni-gpib-usb-hs@6187dd88f8632cef1b84fb31d7e14412d4abe7dd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/embeddedci-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6187dd88f8632cef1b84fb31d7e14412d4abe7dd -
Trigger Event:
release
-
Statement type:
File details
Details for the file ni_gpib_usb_hs-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ni_gpib_usb_hs-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c8f91b12d6057a15ead9ec2bedaa84f190afdea3d5b4da42c2e6e037cbded84
|
|
| MD5 |
6b521b5a41fe7d38bd094c56934cd2e9
|
|
| BLAKE2b-256 |
054163cb93f492dc315c288cd4a40b84f3e45596360831b6b06e32bdc338d125
|
Provenance
The following attestation bundles were made for ni_gpib_usb_hs-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on embeddedci-com/ni-gpib-usb-hs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ni_gpib_usb_hs-0.1.0-py3-none-any.whl -
Subject digest:
2c8f91b12d6057a15ead9ec2bedaa84f190afdea3d5b4da42c2e6e037cbded84 - Sigstore transparency entry: 2043459990
- Sigstore integration time:
-
Permalink:
embeddedci-com/ni-gpib-usb-hs@6187dd88f8632cef1b84fb31d7e14412d4abe7dd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/embeddedci-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6187dd88f8632cef1b84fb31d7e14412d4abe7dd -
Trigger Event:
release
-
Statement type: