Skip to main content

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-gpib is 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 under linux-gpib's ni_usb kernel driver (deterministic NIUSB_ADDRESSING_ERROR / USB -110 timeouts), even against git master and 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:

  1. Performs the vendor control-transfer "ready" handshake (serial number + poll-ready).
  2. Bulk-writes a 26-entry TNT4882 register init sequence (reset, handshake mode, T1 delay, interrupt-mask setup, controller address, system-controller bit).
  3. Issues GPIB command bytes (UNL/talk-address/listen-address) over a bulk OUT/IN request-response pair to address instruments.
  4. 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, use linux-gpib or 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:

  1. Create the project on PyPI (either publish once manually, or use PyPI's "pending publisher" flow to register the name before any upload exists).
  2. 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
  3. 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:

  1. Bump version in pyproject.toml and ni_gpib_usb_hs/__init__.py.
  2. Commit, tag, and push:
    git commit -am "release: v0.1.0"
    git tag v0.1.0
    git push && git push --tags
    
  3. On GitHub, Releases → Draft a new release, pick the tag, publish it. The Publish to PyPI workflow (.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ni_gpib_usb_hs-0.1.0.tar.gz (22.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ni_gpib_usb_hs-0.1.0-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

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

Hashes for ni_gpib_usb_hs-0.1.0.tar.gz
Algorithm Hash digest
SHA256 74781526ae869a23c045ef35bbebe06fcb129820d83205e4875dd52956e67546
MD5 cd488ada10442b47548ffb2695fc6953
BLAKE2b-256 6202c3f372f69d21b8c0084213b889efc94d40be6fc946a7a93d245c30170a9f

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for ni_gpib_usb_hs-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c8f91b12d6057a15ead9ec2bedaa84f190afdea3d5b4da42c2e6e037cbded84
MD5 6b521b5a41fe7d38bd094c56934cd2e9
BLAKE2b-256 054163cb93f492dc315c288cd4a40b84f3e45596360831b6b06e32bdc338d125

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page