Instruments & drivers¶
A FLEX driver is an ordinary Python class. It inherits a connection from a protocol base class, declares its knobs as parameters, and adds plain methods for everything else. Nothing needs to be registered before you can use it.
The Instrument base class¶
flex.instrument.Instrument holds everything that does not depend on how the
instrument is connected: name, per-instrument logger (inst.<name>), the
parameter registry, snapshot(), idn(), and context-manager lifecycle
(with ... : closes the connection on exit). Protocol classes add the wire:
query/write for text protocols, call for JSON-RPC.
Parameters¶
add_parameter registers a named, unit-carrying get/set handle:
def add_parameter(self, name, *, get_cmd=None, set_cmd=None,
getter=None, setter=None, get_parser=None,
unit="", vals=None, doc="")
get_cmd/set_cmdare command templates sent through the instrument'squery/write;set_cmdis formatted with the value ("SOUR:VOLT {}"),get_parserconverts the reply string (float).getter/setterare arbitrary callables, for instruments whose methods already exist. Give one style or the other, not both.valsis an optional validator, checked on every set.
A parameter is callable — no value reads, one value writes:
k.voltage = k.add_parameter("voltage", get_cmd="SOUR:VOLT?",
set_cmd="SOUR:VOLT {}", get_parser=float, unit="V")
k.voltage(0.5) # set
k.voltage() # get -> 0.5
Sweeps and measurements consume parameters directly: the column name and unit in the data file come from the parameter (see Experiments & data).
Two validators exist: Numbers(min, max) accepts any real number in range —
including numpy scalars, excluding bool — and Enum(*values) accepts a
fixed set. Both raise on set before anything reaches the hardware.
Protocol base classes¶
flex-protocols provides one base per connection type:
| Class | Connection | Talk to it with |
|---|---|---|
VISAInstrument |
GPIB / USB / RS-232 via VISA / TCPIP INSTR | query(cmd), write(cmd) |
TCPInstrument |
raw TCP socket, line-based text | query(cmd), write(cmd) |
SerialInstrument |
COM port | query(cmd), write(cmd) |
ZMQInstrument |
JSON-RPC 2.0 over a ZMQ REQ socket | call(method, params) |
All four connect eagerly in __init__ and raise if the connection cannot
be opened — constructing a driver instance means you are connected. All take
a timeout in seconds and accept **kwargs passed up to Instrument.
ZMQInstrument specifics¶
ZMQInstrument speaks the LevyLab Instrument-Framework dialect of JSON-RPC
(but works with any JSON-RPC-over-ZMQ endpoint). On connect it sends an ACK
to verify the endpoint (disable with connect_check=False).
call(method, params)returns the JSON-RPCresult.- A JSON-RPC
errorresponse raisesZMQInstrumentError(carries.codeand.data). - A missed reply raises
TimeoutErrorand resets the REQ socket — a REQ socket that missed its reply is otherwise stuck forever, so the next call works again. idn()andhelp()wrap the IF built-insIDNandHELP.
Driver packages and the catalog¶
Drivers live in vendor folders inside flex_drivers (srs/, colby/,
rotrics/, levylab/). The package exports one plain dict:
CATALOG: dict[str, str] = {
"srs.sr7270": "flex_drivers.srs.sr7270:SR7270",
"levylab.lockin": "flex_drivers.levylab.lockin:Lockin",
...
}
Importing flex_drivers imports no driver module (and no zmq/pyvisa); the
values are "module:Class" references resolved lazily.
Two ways to get a driver class:
- Direct import — always works. A driver is just a class:
from flex_drivers.srs.sr7270 import SR7270. No installation state, no enablement, nothing gates imports. - By name, through the catalog: station configs (
driver = "srs.sr7270"),flex instruments --probe, and the dashboard resolve driver names viaCATALOG— and require the name to be enabled first.
flex enable <name> adds the name to [drivers] enabled in the active
config (installing the parent package first if needed). Every name-based
resolution (load_station(), --probe, the dashboard) refuses a driver
that isn't enabled, even if its package is installed — a deliberate gate on
the config-driven path, not on imports: direct imports and CESession (which
only ever connects instruments the Configure Experiments file says are
physically wired up) are unaffected either way.
LevyLab drivers and lv_class¶
Drivers for LevyLab Instrument-Framework apps additionally carry a lv_class
class attribute — the LabVIEW class name the IF app reports:
class Lockin(ZMQInstrument):
lv_class = "Instrument.Lockin.lvclass"
flex_drivers.levylab.lvclass_registry() walks the catalog and builds
{LabVIEW class name: "module:Class"} from these attributes (plus
lv_class_aliases — the five v1 PPMS variants all map to the one PPMS
driver). CESession uses that registry to auto-connect a driver to every
instrument in the Configure Experiments VI file — see the
LevyLab guide. A driver with lv_class = None (e.g.
TransportServer) is never auto-discovered.
SimulatedInstrument¶
flex.SimulatedInstrument behaves like a text-protocol instrument without
hardware: replies maps commands to canned responses, every command sent is
recorded in .sent, and add_sim_parameter(name, initial=..., unit=...)
creates an in-memory readable/settable parameter. It powers the
Quickstart example, tests, and dry runs. For testing ZMQ
drivers against a fake IF app, see
Write a driver.