Device-agnostic SCPI instrument communication library with TCP and serial transports
Project description
scpi-core
A device-agnostic SCPI (Standard Commands for Programmable Instruments) communication library for Python.
Why scpi-core?
Most SCPI libraries fall into one of two traps: they're either tightly coupled to a specific vendor's instruments, or they drag in heavy dependencies like NI-VISA just to send ASCII strings over a socket.
scpi-core takes a different approach:
- Device-agnostic -- No vendor-specific code. No hardcoded instrument quirks. This is a transport and protocol layer only. Instrument-specific logic belongs in your driver library, not here.
- Zero required dependencies -- TCP transport works out of the box with the Python standard library. Serial transport is an optional extra (
pip install scpi-core[serial]). - Proper timeout handling -- Every send, receive, and connection operation has configurable timeouts. No infinite hangs when an instrument doesn't respond.
- Clean error hierarchy -- Catch
ScpiTimeoutError,ScpiConnectionError, orScpiProtocolErrorindividually, or catch the baseScpiErrorfor everything. - Transport abstraction -- Swap between TCP, serial, or your own custom transport without changing instrument driver code.
Installation
pip install scpi-core
With serial port support:
pip install scpi-core[serial]
Quick Start
TCP (LAN/LXI instruments)
from scpi_core import TcpTransport, ScpiDevice
transport = TcpTransport("192.168.1.100", port=5555, timeout=5.0)
with ScpiDevice(transport) as dev:
print(dev.idn())
dev.command(":CHAN1:DISP ON")
scale = dev.query_float(":CHAN1:SCAL?")
print(f"Channel 1 scale: {scale} V/div")
Serial (USB-CDC / COM port instruments)
from scpi_core import SerialTransport, ScpiDevice
transport = SerialTransport("COM3", baudrate=115200, timeout=5.0)
with ScpiDevice(transport) as dev:
print(dev.idn())
Typed Queries
# Parse response as float
voltage = dev.query_float(":MEAS:VAVG?")
# Parse response as int
depth = dev.query_int(":ACQ:MDEP?")
# Parse response as bool (handles 0/1 and ON/OFF)
enabled = dev.query_bool(":CHAN1:DISP?")
IEEE 488.2 Common Commands
dev.reset() # *RST
dev.clear_status() # *CLS
dev.wait() # *WAI
dev.opc() # *OPC? -- returns True when complete
dev.save_state(1) # *SAV 1
dev.recall_state(1) # *RCL 1
result = dev.self_test() # *TST? -- 0 = pass
error = dev.check_error() # :SYST:ERR? -- None if no error
Building Instrument Drivers on scpi-core
scpi-core is designed as a foundation layer. Instrument-specific libraries should depend on scpi-core for transport and use composition to build their API.
Pattern: Subsystem Composition
from scpi_core import TcpTransport, ScpiDevice
class ChannelSubsystem:
"""Controls oscilloscope channel settings."""
def __init__(self, device: ScpiDevice):
self._dev = device
def set_scale(self, channel: str, scale: float):
self._dev.command(f":{channel}:SCAL {scale}")
def get_scale(self, channel: str) -> float:
return self._dev.query_float(f":{channel}:SCAL?")
def set_display(self, channel: str, enabled: bool):
state = "ON" if enabled else "OFF"
self._dev.command(f":{channel}:DISP {state}")
class MyOscilloscope:
"""Driver for a specific oscilloscope model."""
def __init__(self, host: str, port: int = 5555):
self._transport = TcpTransport(host, port)
self._dev = ScpiDevice(self._transport)
self.channel = ChannelSubsystem(self._dev)
def __enter__(self):
self._transport.connect()
return self
def __exit__(self, *args):
self._transport.disconnect()
Guidelines for Instrument Libraries
- Do not put vendor-specific code in scpi-core. Keep it in your instrument library.
- Do use
ScpiDeviceas the interface between your subsystems and the transport. - Do use the typed query methods (
query_float,query_int,query_bool) to keep parsing out of your driver code. - Do use the transport abstraction so your driver works over TCP, serial, or any future transport without changes.
- Do not subclass
ScpiDevice. Use composition -- your instrument class has aScpiDevice, it is not one.
Custom Transports
Implement the Transport abstract class to add new communication backends:
from scpi_core.transport import Transport
class MyCustomTransport(Transport):
def connect(self):
...
def disconnect(self):
...
def send(self, data: str) -> None:
...
def receive(self, timeout: float | None = None) -> str:
...
def is_connected(self) -> bool:
...
Then use it with ScpiDevice like any other transport:
transport = MyCustomTransport(...)
dev = ScpiDevice(transport)
API Reference
Transports
| Class | Description | Install |
|---|---|---|
TcpTransport(host, port, timeout) |
SCPI over TCP/LAN | Built-in |
SerialTransport(port, baudrate, timeout) |
SCPI over serial/USB-CDC | pip install scpi-core[serial] |
ScpiDevice Methods
| Method | Description |
|---|---|
command(cmd) |
Send a command (no response) |
query(cmd) |
Send a query, return string response |
query_float(cmd) |
Query and parse as float |
query_int(cmd) |
Query and parse as int |
query_bool(cmd) |
Query and parse as bool (0/1/ON/OFF) |
query_raw(cmd, count) |
Query and read raw bytes |
idn() |
*IDN? |
reset() |
*RST |
clear_status() |
*CLS |
opc() |
*OPC? |
wait() |
*WAI |
self_test() |
*TST? |
save_state(slot) |
*SAV |
recall_state(slot) |
*RCL |
check_error() |
:SYST:ERR? (returns None if no error) |
Errors
| Exception | When |
|---|---|
ScpiError |
Base class for all errors |
ScpiConnectionError |
Connection failed or lost |
ScpiTimeoutError |
Operation timed out |
ScpiProtocolError |
Unexpected/malformed response |
License
MIT
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 scpi_core-0.1.2.tar.gz.
File metadata
- Download URL: scpi_core-0.1.2.tar.gz
- Upload date:
- Size: 12.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d76decd300d33dccf58a7fd38ddcd586629fc10d209732fea84964baea45afc
|
|
| MD5 |
061676cae0148e9ec7523c450393e9f1
|
|
| BLAKE2b-256 |
503e75aa8e737b2d257f33e10b315ed002301cc478782f2bc022d50c9b2e9ff9
|
Provenance
The following attestation bundles were made for scpi_core-0.1.2.tar.gz:
Publisher:
publish.yml on ril3y/scpi-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scpi_core-0.1.2.tar.gz -
Subject digest:
8d76decd300d33dccf58a7fd38ddcd586629fc10d209732fea84964baea45afc - Sigstore transparency entry: 909219282
- Sigstore integration time:
-
Permalink:
ril3y/scpi-core@d63a0e4e804cd8e5ff705f29a181b2daf2b03430 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/ril3y
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d63a0e4e804cd8e5ff705f29a181b2daf2b03430 -
Trigger Event:
release
-
Statement type:
File details
Details for the file scpi_core-0.1.2-py3-none-any.whl.
File metadata
- Download URL: scpi_core-0.1.2-py3-none-any.whl
- Upload date:
- Size: 9.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a26067579037ff558cf7436ff666a4052136ee8528974b569879da0e2452d23e
|
|
| MD5 |
f81e27e5fb6b2d298ca8e637a6112a72
|
|
| BLAKE2b-256 |
75a0101f8224cc00883a1c3391ce5fee2b5b59f213fe2979f07606a087d2fbcd
|
Provenance
The following attestation bundles were made for scpi_core-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on ril3y/scpi-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scpi_core-0.1.2-py3-none-any.whl -
Subject digest:
a26067579037ff558cf7436ff666a4052136ee8528974b569879da0e2452d23e - Sigstore transparency entry: 909219285
- Sigstore integration time:
-
Permalink:
ril3y/scpi-core@d63a0e4e804cd8e5ff705f29a181b2daf2b03430 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/ril3y
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d63a0e4e804cd8e5ff705f29a181b2daf2b03430 -
Trigger Event:
release
-
Statement type: