The Driver
Every instrument in PIEC is represented by a driver — a Python class that wraps the communication with a physical (or virtual) instrument into a consistent interface that measurement code can work with.
Driver inheritance hierarchy
Within piec.drivers, drivers are organized as a strict 3-level class hierarchy.
Each level inherits from the one above it, adding specificity:
Level 1 — Instrument (base) / Scpi (convenience)
The base Instrument class defines the minimum requirements of any
instrument driver, wrapping PyVISA for resource management. The Scpi
class optionally extends this with vetted implementations for
Standard Commands for Programmable Instruments (SCPI)-compliant
devices (e.g., *IDN?, *RST, *CLS).
Level 2 — Instrument-type interface
Examples: Oscilloscope, Awg, Lockin, SourceMeter, Dmm
Defines the required methods and parameter standards that all
drivers of that category must implement. Measurement code talks
to this interface, making drivers interchangeable.
Level 3 — Specific instrument model
Examples: KeysightDSOX3024a, Agilent33220a, SR830, Keithley2400
Inherits from a Level 2 class and a Level 1 base (usually Scpi)
and implements the hardware-specific logic using that instrument's
exact command set.
This design means you can swap out one oscilloscope driver for another without changing any measurement code, as long as both implement the same Level 2 interface.
Each instrument category also provides a VirtualInstrument (e.g.,
VirtualScope, VirtualAwg) that returns simulated responses, enabling
development and testing without physical hardware.
What each level provides
- Level 1 — Instrument (
piec.drivers.instrument.Instrument) The foundation for all drivers. It:
Accepts an address string (VISA resource string, COM port, or
'VIRTUAL') at initialization.Opens and manages the connection via PyVISA (or falls back to a virtual backend).
Provides the
AutoCheckMetaframework for automatic parameter validation and state tracking based on class attributes. Setting a class attribute toNonebypasses this validation, allowing for custom driver-side handling.Exposes
read(),write(), andquery()methods used by all subclasses.
- Level 1 — Scpi (
piec.drivers.scpi.Scpi) A convenience class, not a separate structural level. It inherits from
Instrumentand provides vetted implementations of standard IEEE 488.2 SCPI commands that most SCPI-compliant instruments share:idn()— sends*IDN?and returns the instrument identification string.reset()— sends*RSTto restore factory defaults.clear()— sends*CLSto clear the status registers and error queue.wait()— sends*WAIto wait for pending operations.error()— sends*ESR?to read the error status register.
Drivers should always cross-check the instrument manual. If the instrument does not support a standard
Scpimethod (or uses a different command string), the Level 3 driver must override that method.- Level 2 — Instrument-type interface classes
Each category (
Oscilloscope,Awg,Dmm,Lockin,SourceMeter, etc.) defines the set of methods and class attributes that a measurement class can rely on. For example, anOscilloscopeis expected to have methods for setting the timebase, configuring channels, and capturing a waveform. These files contain no specific SCPI command strings — only the “vocabulary” of the instrument type. Level 2 interfaces also support optional functionality. Methods can be decorated with@optional, or custom Level 3 driver methods can be automatically treated as optional. This allows measurement routines to skip unsupported methods gracefully across different hardware.- Level 3 — Specific model drivers
These are the classes you instantiate in your code. They inherit from a Level 2 category class and a Level 1 base (usually
Scpi), then translate the generic interface into the exact SCPI strings (or vendor API calls) that the hardware understands:from .awg import Awg from ..scpi import Scpi class Keysight81150a(Awg, Scpi): AUTODETECT_ID = "81150A" # Implementation ...
Note
To see a complete implementation template, refer to the
src/piec/drivers/example/directory. It contains theExampleLevel 2 interface and theSpecificExampleLevel 3 driver. Please read the adding a driver guide before writing custom drivers.For the full list of available drivers, see Supported Instruments.
Folder structure
The driver hierarchy maps directly to the folder structure within src/piec/drivers/:
Level 1 base files (e.g.,
instrument.py,scpi.py) sit directly in the rootdrivers/directory.Level 2 interface files are located in a folder named after the instrument category, and the Python file shares this name (e.g.,
oscilloscope/oscilloscope.py).Level 3 specific model files are placed alongside their Level 2 interface in the same category folder (e.g.,
oscilloscope/k_dsox3024a.py).
Virtual instruments
Each instrument category provides a VirtualInstrument class (e.g.,
VirtualScope, VirtualAwg, VirtualLockin) that returns simulated
responses. Virtual instruments allow you to develop and test measurement code
without any physical hardware connected — simply pass 'VIRTUAL' as the address:
from piec.drivers.oscilloscope.virtual_oscilloscope import VirtualScope
scope = VirtualScope()
print(scope.idn()) # Returns a simulated identification string
Using a driver
Import the specific driver class and instantiate it with the instrument’s address:
from piec.drivers.awg.k_81150a import Keysight81150a
awg = Keysight81150a('GPIB0::8::INSTR')
print(awg.idn()) # Confirms connection
For help connecting or finding an instrument’s address, see Connecting to the Instrument.
Parameter validation
Every piec driver has built-in parameter validation that checks method arguments against
the instrument’s known capabilities before sending any command to hardware. This is
controlled by the check_params flag at instantiation and is off by default:
# Default — no validation performed (fastest for scripting)
awg = Keysight81150a('GPIB0::8::INSTR')
# Validation enabled — arguments are checked on every method call
awg = Keysight81150a('GPIB0::8::INSTR', check_params=True)
When check_params=True, every method call validates its arguments against the driver’s
class attributes before executing:
List (e.g.
waveform = ['SIN', 'SQU', 'RAMP']) — the argument must be one of the listed values.Tuple (e.g.
amplitude = (0.01, 10.0)) — the argument must fall within that numeric range (inclusive).Dictionary (e.g.
frequency = {'waveform': {'SIN': (1e-6, 30e6), ...}}) — the valid range depends on the current value of another parameter. piec resolves the dependency automatically using the current instrument state.None — validation for that parameter is skipped entirely. The driver handles it internally. This is the standard way for a driver to opt out of automatic checking for a specific parameter.
If a value fails validation, a ValueError is raised before any command is sent to the
instrument.
Note
String arguments are automatically lowercased before validation and before being
passed to the driver method, so 'SIN', 'sin', and 'Sin' are all equivalent
from the caller’s perspective.
Validation is case-insensitive regardless of how class attribute values are written —
the validator lowercases both sides before comparing. If a driver needs to send an
uppercase string to the instrument, it should call .upper() on the argument inside
the method body before writing it.
Class attributes
Every driver class exposes its valid parameter ranges as class attributes. You can inspect these directly — without connecting to any hardware — to understand what values an instrument accepts:
from piec.drivers.awg.k_81150a import Keysight81150a
print(Keysight81150a.channel) # [1, 2]
print(Keysight81150a.amplitude) # (0.01, 10.0)
print(Keysight81150a.waveform) # ['SIN', 'SQU', 'RAMP', 'PULS', ...]
Attribute values follow a consistent syntax:
List — a finite set of accepted values (e.g. channel numbers, waveform types)
Tuple — a continuous numeric range
(min, max)Dictionary — a nested structure where the valid range depends on another argument
None — no automatic bounds; the driver validates this parameter itself
These attributes are defined at the Level 2 interface for the instrument category and overridden at Level 3 with the specific model’s real values. The parent Level 2 class defines the vocabulary (attribute names that must exist); the Level 3 driver fills in the actual numbers from the instrument manual.
State tracking
Whenever a set_ method completes successfully, piec automatically records the
value that was set as an instance attribute self._current_<name>. For example,
after calling awg.set_waveform(1, 'sin'), the driver stores
awg._current_waveform = 'sin'.
All state attributes are initialized to None at connection time and updated as
methods are called. They serve two purposes:
Dependent validation — when
check_params=True, if a parameter’s valid range depends on another (e.g.frequencydepends onwaveform), piec looks up the current state automatically so you do not need to pass the dependency explicitly every time.Driver-side logic — driver implementations can read
self._current_<attr>to make decisions based on the last known hardware state without issuing an extra query to the instrument.