Adding an Instrument¶
To support a new piece of hardware you write a PyMeasure Instrument class, then
declare it in instruments.yaml. Procedures can then queue it like any other.
Check PyMeasure first
The PyMeasure instrument library
already covers many devices. If yours is there, you may only need a YAML
entry pointing target at the existing class (as Laser Setup does for
ThorlabsPM100USB).
1. Write the instrument class¶
Instruments live in laser_setup/instruments/. A PyMeasure instrument exposes
controls (read/write properties) and measurements (read-only) via
Instrument.control(...) / Instrument.measurement(...), which map to SCPI-like
commands.
import logging
from pymeasure.instruments import Instrument, SCPIMixin
log = logging.getLogger(__name__)
class MyDevice(SCPIMixin, Instrument):
"""Driver for the Acme Widget 9000."""
def __init__(self, adapter, name="Acme Widget 9000", **kwargs):
super().__init__(adapter, name, **kwargs)
# read/write property → 'VOLT?' to read, 'VOLT %g' to set
voltage = Instrument.control(
"VOLT?", "VOLT %g",
"""Output voltage in volts.""",
validator=lambda v, vs: max(min(v, 10.0), 0.0),
)
# read-only measurement
reading = Instrument.measurement(
"MEAS?", """The latest sensor reading.""",
)
def shutdown(self):
self.voltage = 0 # leave the device safe
super().shutdown()
Connection types¶
The adapter argument determines how the device is reached. PyMeasure picks the
backend from the adapter string/object:
- VISA (USB/GPIB): pass a resource string like
USB0::0x1313::0x8078::P0037982::INSTR. Requires a VISA backend. - Serial: pass a COM port; PyMeasure wraps it in a
SerialAdapter. Setread_termination/write_terminationto match the firmware (seeClicker/PT100SerialSensorininstruments/serial.py). - Proprietary: override
__init__to swap in a custom adapter object — theBenthamclass replacesself.adapterwith abendev.Devicefor its USB protocol.
Custom shutdown()
InstrumentManager.shutdown_all() calls each instrument's shutdown().
Implement it to return the device to a safe state (zero the output, turn off
a lamp). It's skipped automatically for debug/fake instruments.
2. Export it (optional)¶
Re-export from laser_setup/instruments/__init__.py so procedures can import it
by name:
3. Declare it in instruments.yaml¶
MyDevice:
adapter: USB0::0x1234::0x5678::SN12345::INSTR
name: Acme Widget 9000
IDN: ACME,WIDGET9000 # used by setup_adapters to recognize it
target: ${class:laser_setup.instruments.mydevice.MyDevice}
kwargs:
# any extra constructor args, e.g. termination for serial devices
timeout: 5000
${class:...} resolves the dotted path to the class at config-load time. The
Instruments helper in procedures/utils.py then exposes Instruments.MyDevice.
4. Use it in a procedure¶
from ..instruments import InstrumentManager, MyDevice
from .utils import Instruments
class MyMeasurement(BaseProcedure):
instruments = InstrumentManager()
widget: MyDevice = instruments.queue(**Instruments.MyDevice)
def startup(self):
self.connect_instruments()
self.widget.voltage = 1.0
def execute(self):
self.emit('results', {'value': self.widget.reading})
5. Test without hardware¶
Because the queue/connect flow falls back to a DebugInstrument under -d, you
can exercise the whole procedure before the device arrives:
For finer control, give the YAML entry kwargs: {debug: true} so it always uses
the debug instrument.
Gotchas¶
- Termination characters must match the firmware for serial devices, or reads hang or return garbage.
- VISA backend required for USB/GPIB devices — install NI-VISA or
pyvisa-py(details). - Instance caching: the manager caches by
f"{Class}/{adapter}". Two procedures with the same class+adapter share one connection, which is great for reuse but means state can carry over — reset instartup(). help():InstrumentManager.help(MyDevice)prints the available controls and measurements — handy while developing.
See Instruments for the runtime lifecycle.