Skip to main content

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, or ScpiProtocolError individually, or catch the base ScpiError for 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 ScpiDevice as 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 a ScpiDevice, 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


Download files

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

Source Distribution

scpi_core-0.1.2.tar.gz (12.1 kB view details)

Uploaded Source

Built Distribution

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

scpi_core-0.1.2-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

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

Hashes for scpi_core-0.1.2.tar.gz
Algorithm Hash digest
SHA256 8d76decd300d33dccf58a7fd38ddcd586629fc10d209732fea84964baea45afc
MD5 061676cae0148e9ec7523c450393e9f1
BLAKE2b-256 503e75aa8e737b2d257f33e10b315ed002301cc478782f2bc022d50c9b2e9ff9

See more details on using hashes here.

Provenance

The following attestation bundles were made for scpi_core-0.1.2.tar.gz:

Publisher: publish.yml on ril3y/scpi-core

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

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

Hashes for scpi_core-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a26067579037ff558cf7436ff666a4052136ee8528974b569879da0e2452d23e
MD5 f81e27e5fb6b2d298ca8e637a6112a72
BLAKE2b-256 75a0101f8224cc00883a1c3391ce5fee2b5b59f213fe2979f07606a087d2fbcd

See more details on using hashes here.

Provenance

The following attestation bundles were made for scpi_core-0.1.2-py3-none-any.whl:

Publisher: publish.yml on ril3y/scpi-core

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