Adding a Driver
This page explains the recommended workflow for creating a new instrument driver for piec. The majority of the implementation work is handled by an AI coding assistant — your job is to gather the right materials, provide them clearly, and then review and validate the output.
The Code Rules Reference at the bottom of this page defines every architectural rule the driver must follow. Read it before starting.
Step 1: Gather your materials
Before opening an AI session, collect the following:
The instrument programming manual — the PDF (or equivalent) that documents all remote-control commands for the instrument.
The parent class file — identify which instrument category your instrument belongs to (e.g. AWG, oscilloscope, sourcemeter, lock-in amplifier) and locate its interface file in
src/piec/drivers/. Examples:Instrument type
Parent file
Arbitrary Waveform Generator
drivers/awg/awg.pyOscilloscope
drivers/oscilloscope/oscilloscope.pySource Meter
drivers/sourcemeter/sourcemeter.pyLock-in Amplifier
drivers/lockin/lockin.pyDigital Multimeter
drivers/dmm/dmm.pyA convenience base class (optional) — piec provides convenience classes that handle common low-level communication boilerplate. Using one is optional — a driver that inherits only from the instrument-type parent is perfectly valid.
Class
File
Use when
Scpidrivers/scpi.pyThe instrument communicates over VISA (GPIB, USB-TMC, Ethernet) and follows the SCPI-99 standard. Gives you
idn(),reset(),clear(),error(),wait(), etc. for free.Digilentdrivers/digilent.pyThe instrument is an MCC/Digilent DAQ device using the Universal Library (
mcculw). Replaces VISA entirely with UL board-number addressing.# With Scpi convenience class from .awg import Awg from ..scpi import Scpi class RigolDg1000(Awg, Scpi): ... # With Digilent convenience class from .daq import Daq from ..digilent import Digilent class Usb231(Daq, Digilent): ... # No convenience class — implement everything directly from .awg import Awg class MyCustomAwg(Awg): ...
If you are unsure which applies, describe the instrument’s communication interface in the AI prompt and let it determine the right inheritance.
DRIVER_DEVELOPMENT_GUIDE.md— this file (included at the bottom of this page) is the ruleset the AI must follow. Attach it to every session.
Step 2: Create the file skeleton
Before running the AI session, create an empty driver file in the correct location:
src/piec/drivers/<category>/<model_name>.py
For example, a Rigol DG1000 AWG would go in src/piec/drivers/awg/rigol_dg1000.py.
Use src/piec/drivers/example/specific_example.py as a reference for the expected structure — class definition, AUTODETECT_ID, class attributes, and method stubs.
Step 3: Run the AI coding session
Open your preferred AI coding assistant (e.g. Google Gemini, GitHub Copilot, Claude) and attach the following files:
File |
Why |
|---|---|
|
The rules the driver must follow |
|
The parent interface to implement |
|
Optional — include the relevant convenience class file only if your instrument uses it |
Instrument programming manual |
The primary source of all command strings and device capabilities |
Instrument user manual |
Optional — attach alongside the programming manual if you need to extract hardware specs or features not covered there (e.g. channel count, voltage ranges). The programming manual usually covers this. |
Then provide the following prompt, filling in the blanks:
Primary prompt (implementation)
You are tasked with creating a new instrument driver for the piec Python package.
**Instrument:** <Manufacturer> <Model Number> (e.g. Rigol DG1000)
**Instrument type:** <Type> (e.g. Arbitrary Waveform Generator)
**Parent class:** <ClassName> from the attached parent file (e.g. Awg)
**Uses SCPI protocol:** <Yes / No>
**Attached files:**
- DRIVER_DEVELOPMENT_GUIDE.md — read this first. It defines every rule the driver must follow.
- <category>.py — the parent class. Implement every method defined in it.
- scpi.py — include only if the instrument uses SCPI. If it does not, omit this file and implement communication directly.
- <manual filename> — use this as the sole source for all command strings.
**Your task:**
1. Implement all methods from the parent class using commands from the attached manual.
Only use commands you can find in the manual — do not invent or guess command strings.
2. Fill out all class attributes (capabilities and limits) as described in the guide.
3. Set AUTODETECT_ID to the unique model substring returned by the instrument's *IDN? response.
If the manual does not show an example *IDN? response, set it to None.
4. Do not write any # inline comments — use docstrings only.
5. Follow the method naming, signature, and default-parameter rules in the guide exactly.
Verification pass (optional but recommended)
After receiving the initial driver, run a second session with the same attachments plus the generated driver and ask:
Review the attached driver against the instrument manual and the DRIVER_DEVELOPMENT_GUIDE.md rules.
Check for the following and list any discrepancies:
1. Are all class attribute names identical to the argument names used in the methods?
2. Do the class attribute values (ranges, lists) match the manual?
3. Are all command strings sent to the instrument valid according to the manual?
4. Are all methods from the parent class implemented? Have any extra methods been added that are not in the parent?
For each discrepancy, show: the original code, the corrected code, and the section of the manual or guide where you found the issue.
Step 4: Register the driver
Once the driver file is in place:
Add the import to the
__init__.pyin the category folder so piec can discover it.Verify autodetection — the
AUTODETECT_IDstring must be a unique substring of the instrument’s identification response. You can test this by connecting to the instrument and calling.idn()on it.
Step 5: Validate with the test notebook
Each driver category has a Jupyter test notebook in its folder (e.g. drivers/awg/awg_test.ipynb). Use this to verify your driver against the virtual instrument first, then against real hardware.
The notebook follows this structure:
Section 0 — connection and
.idn()checkSections 1–2 — base
Instrumentcommunication testsSections 3+ — driver-specific method tests (one cell per method)
Final section — cleanup and disconnect
When adding a new driver, copy src/piec/drivers/example/example_test.ipynb as a starting template. Add a cell for each method your driver implements. Run each cell from top to bottom sequentially and confirm expected behavior before opening a pull request.
Before contributing
If you are planning to submit this driver as a pull request, we strongly recommend the following before forking the repository and opening a PR:
Test against real hardware. Run the test notebook (Step 5) end-to-end with a physical instrument connected. Every cell should pass without errors or unexpected responses.
Cross-check all command strings. For each method, verify the command sent to the instrument against the manual one more time — AI-generated command strings can be subtly wrong.
Check the autodetect ID. Connect to the instrument, call
.idn(), and confirm thatAUTODETECT_IDis a substring of the actual response string.Run the automated test suite. From the repository root:
pytest tests/ -v. All tests must pass before submitting.
A driver that has only been verified against the virtual instrument is not ready for a pull request. Physical hardware verification is the minimum bar for contribution.
Code Rules Reference
The following is the complete DRIVER_DEVELOPMENT_GUIDE.md. Every rule here is enforced during code review.
Instrument Driver Development Guide
This guide outlines the strict requirements and conventions for creating new instrument drivers within the piec library. Adhering to these rules ensures a globally consistent, interface-compliant, and minimal codebase across all supported instruments.
1. The 3-Level Architecture
PIEC drivers follow a strict 3-level hierarchy to ensure consistency and modularity.
Level 1: The Foundation (Instrument)
All instruments in the library MUST inherit from the base Instrument class. This class handles the core VISA communication and standard PIEC behavior.
Convenience Classes (e.g., Scpi)
Scpi is a convenience class, not a structural level. It provides vetted implementations of standard IEEE 488.2 / SCPI-99 functions (like idn, reset, clear, error, wait, self_test, operation_complete, initialize) that most SCPI-compliant instruments share.
How it works with Level 2 base classes:
Every Level 2 base class (e.g., Oscilloscope, Awg) already defines skeleton versions of these SCPI commands — empty methods with full docstrings but no implementation. This means:
A driver that inherits only from the Level 2 class has the correct interface and can override each skeleton with its own native protocol commands.
A driver that also inherits from
Scpigets the real SCPI implementations for free via MRO — no extra work needed for standard*IDN?,*RST,*CLS, etc.
[!IMPORTANT] Verification: Always cross-check the instrument manual. If your instrument is SCPI-compliant but does not support a standard
Scpimethod (e.g.,*RSTdoesn’t reset properly), or uses a different command string, you MUST override the method in your Level 3 driver.
Level 2: Instrument-Type Interface (example.py, oscilloscope.py)
These files define the Template/Interface for an entire category of instruments.
They list all requirements (methods and attributes) for that type.
They include skeleton versions of the standard SCPI commands (
idn,reset,clear, etc.) so that the interface is complete even withoutScpiinheritance.They contain no specific SCPI command strings — only the “vocabulary” of the instrument type.
Level 3: Specific Instrument Model (agilent_33220a.py)
This is the Actual Implementation of the driver.
Inherits from the Level 2 Category (e.g.,
Awg) and optionally from theScpiconvenience class.Implements the Level 2 interface using specific hardware commands.
Path A — SCPI-compliant instrument (most common):
from .awg import Awg
from ..scpi import Scpi
# Inherits real SCPI implementations (idn, reset, clear, etc.) from Scpi
class Agilent33220a(Awg, Scpi):
AUTODETECT_ID = "33220A"
# Only implement the instrument-specific methods...
Path B — Non-SCPI instrument (proprietary protocol):
from .oscilloscope import Oscilloscope
# No Scpi mixin — override the skeletons with native protocol commands
class MyProprietaryScope(Oscilloscope):
AUTODETECT_ID = "PROPSCOPE"
def idn(self):
return self.instrument.query("ID?")
def reset(self):
self.instrument.write("FACTORY_RESET")
self.set_trigger_sweep("AUTO") # ensure AUTO mode per the contract
self._initialize_state()
# ... override remaining skeletons ...
2. Constructor (__init__)
DO NOT write a custom
__init__method if its only purpose is to callsuper().__init__(resource_name, **kwargs). The baseInstrumentclass handles standard initialization.IF you must write a custom constructor for hardware configuration queries, it must take
*args, **kwargsand pass them exactly tosuper():
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Custom queries here...
3. Autodetection (AUTODETECT_ID)
Every driver MUST (if possible) define a class-level string attribute named AUTODETECT_ID. This is a unique substring expected to be returned by the instrument when queried with an .idn() command.
AUTODETECT_ID = "MODEL_1234"
4. Class Attributes (Capabilities & Limits)
Class attributes define the valid parameters an instrument can accept. The parent base classes (e.g., Oscilloscope, Awg) define a strict vocabulary of these attribute names.
Drivers MUST explicitly assign their supported capabilities using these exact class attribute names (e.g.,
frequency,voltage,waveform).NEVER introduce new vocabulary terms (like
waveform = ['WEIRD_WAVE']) in the child class that do not exist in the parent class’s definitions.
Attribute Formatting Rules: The class attributes must follow a specific syntax based on what kind of parameter they restrict:
Lists (Discrete Sets): If the argument takes a limited number of defined values, use a list of the appropriate type. Examples:
channel = [1, 2] waveform = ['SIN', 'SQU', 'RAMP']
Tuples (Ranges): If the argument accepts any continuous float/int value within a range, use a geometric tuple
(min, max). Examples:amplitude = (0.01, 10.0) # Vpp offset = (-5.0, 5.0)
Dictionaries (Dependent Arguments): If the valid range or options of an argument depend on the state of another argument (e.g., the maximum frequency is restricted depending on the waveform selected), write this as a dictionary. The primary key is the name of the argument it depends on:
frequency = { 'waveform': { 'SIN': (1e-6, 30e6), 'SQU': (1e-6, 10e6), 'DC': None } }
(If a parameter has no known class attribute boundaries, set it to
None.)
5. Method Conventions: set_, configure_, and run_
Function naming strictly determines scope:
set_<property>Methods: Must perform a SINGLE action. For instance,set_frequencyonly changes the frequency. They correspond directly to SCPI writes assigning one explicit hardware parameter.get_<property>Methods: Must RETURN a single, unformatted value (e.g., a status bit, a scalar measurement).read_<property>Methods: Must RETURN formatted or complex data (e.g., an array of waveform points, a multi-value response, or a post-processed string).Formatting: The specific data structure (typically a
pandas.DataFrame) must adhere to the return specification detailed in the parent class’s docstring.
configure_<module>Methods: Must perform MULTIPLE actions by wrapping and calling several individualset_functions. For instance,configure_waveformmight callset_waveform,set_frequency, andset_amplitude.For EVERY
configure_command, initialize all non-essential arguments toNonein the signature, and only invoke the correspondingset_method if the parameter is notNone.
quick_readMethod: A specialized convenience function (common in Oscilloscopes) used to return whatever data is currently ready or displayed on the hardware (e.g., a cursor value or mean measurement). It is used for fast, unformatted polling.run_<routine>Methods: Used for hardware-executed routines where the instrument performs a complete operation internally (at hardware speed) and then returns the results. The key distinction is that arun_method triggers autonomous instrument behavior — unlikeset_(which only writes a parameter) orconfigure_(which just calls multipleset_methods). Examples:run_voltage_sweep(...)— the sourcemeter executes the full I-V sweep internally and returns all data points at once.run_current_sweep(...)— same for current sweep.This is fundamentally different from manually looping
set_source_voltage+quick_readin Python.
6. Method Signatures and Default Parameters
Method signatures must perfectly mirror the parent interface.
DO NOT provide arbitrary magnitude or state defaults in your
set_functions. Parameters likevoltage=0.0,waveform="SIN", orfrequency=1000must be set toNonein the signature.Drivers must enforce explicit parameter assignments, looking like:
def set_voltage(self, channel=1, voltage=None): if voltage is None: raise ValueError("voltage must be provided") self.instrument.write(f"SOUR{channel}:VOLT {voltage}")
EXCEPTIONS:
Structural/targeting defaults like
channel=1.Boolean flag toggles like
on=True,ac=False, orfour_wire=False.Convenience
configure_Methods: These are allowed to retain sensible default values if those defaults are established in the parent interface.
7. Communication & Protocol Convenience
Read variables using
self.instrument.query("SCPI?").Write variables using
self.instrument.write("SCPI").The Role of
Scpi: Inheritance fromScpiis a convenience to avoid rewriting the same basic*IDN?,*RST,*CLS,*ESR?,*WAI,*TST?,*OPC?commands. However, the driver developer is responsible for verifying that the inheritedreset(),clear(), etc., map correctly to the instrument’s manual.When to skip
Scpi: If your instrument does not speak SCPI at all (e.g., it uses a proprietary serial/binary protocol), simply inherit from the Level 2 category class alone. The skeleton methods defined there give you the correct interface — just override each one with your native protocol commands.
8. Automatic State Tracking
The piec framework automatically tracks the “last set” value of any parameter that has a corresponding class attribute.
Whenever a
set_<property>(value=...)method finishes successfully, the decorator updatesself._current_<property>with that value.These attributes are useful for dependent parameter checks (handled by the framework) and for driver-side conditional logic.
Example: If you need to know the current
modeto set the correctvoltagerange, you can accessself._current_mode.
8a. Automatic String Lowercasing
The auto_check_params decorator automatically converts all string arguments to lowercase before they are passed into your driver method. This means:
Driver implementations should always expect lowercase strings (e.g.
'sin', not'SIN').You do not need to call
.lower()inside your methods — the framework handles it.Validation is case-insensitive regardless of how class attribute values are written — the validation function lowercases both sides before comparing, so
'sin','SIN', and'Sin'all pass against['SIN', 'SQU', 'RAMP']or['sin', 'squ', 'ramp']equally.If your instrument requires an uppercase string in its command (e.g. the instrument rejects
FUNC sin), call.upper()on the argument inside your method before writing it to the instrument.
[!CAUTION]
Initial State isNone: Upon first connection, all tracked attributes are initialized toNone. This means the first fewset_calls (where one parameter depends on another) might skip validation or cause errors if your logic expects a value. Always perform a hardware query in__init__(see Rule 2) to synchronize these states immediately.
9. Optional Methods
PIEC has two mechanisms for optional methods, ensuring measurement code never needs to change regardless of which specific driver is connected.
9a. @optional Decorator (Parent Base Classes)
Some standard instrument features are not universally supported across all models. Use @optional in the category base class to mark these:
from ..instrument import Instrument, optional
class Oscilloscope(Instrument):
@optional
def set_channel_impedance(self, channel, channel_impedance):
"""Sets the channel impedance, e.g. 1MOhm, 50Ohm"""
Only use
@optionalin base classes (e.g.,Oscilloscope,Awg), never in specific drivers.If a specific driver supports the feature, override the method as usual.
If it doesn’t, do nothing — calls will print
[OPTIONAL SKIP]and returnNone.
9b. Automatic Optional (Child-Specific Methods)
Any public method that a specific driver defines beyond what the parent class provides is automatically treated as optional. If measurement code calls that method on a different driver that doesn’t have it, it gracefully skips.
# In a Keysight-specific driver:
class KeysightDSOX3024a(Oscilloscope, Scpi):
def read_statistics(self): # Not in parent Oscilloscope — auto-optional
return self.instrument.query(":MEAS:STAT?")
This works because all standard methods exist on every driver via the parent class. Only truly missing child-specific methods trigger the skip mechanism.
10. Argument Mapping
In cases where the Level 2 interface uses a generic argument (e.g., channel=1, mode='CONSTANT') but the hardware expects a different value (e.g., channel='A', mode='FIXED'), the Level 3 driver is responsible for its own internal mapping:
def set_mode(self, channel, mode):
# Map generic PIEC mode to specific hardware command
mode_map = {'CONSTANT': 'FIXED', 'SWEEP': 'SWE'}
hw_mode = mode_map.get(mode)
if hw_mode is None:
raise ValueError(f"Mode {mode} not supported by this instrument")
self.instrument.write(f"SOUR{channel}:FUNC:{hw_mode}")
This ensures the user’s measurement code can remain model-agnostic.
11. Repository Folder Structure
To keep the drivers directory organized, follow this nesting pattern:
Category Folder: (e.g.,
drivers/oscilloscope/)Interface File: Named after the category (e.g.,
oscilloscope.py).Model Drivers: Put directly in the category folder, named after the model (e.g.,
dsox3024a.py).
piec/
drivers/
oscilloscope/
oscilloscope.py (Level 2 Interface)
dsox3024a.py (Level 3 Driver - Keysight)
tds6604.py (Level 3 Driver - Tektronix)