A generic driver generator for devices controlled via (virtual) COM ports using SCPI commands. Creates a python class for controlling your device.
This class is compatible with the ARTIQ experimental control system and, if desired, a network ARTIQ controller is also generated.
Install the package and its dependancies with:
pip install generic-scpi-driver
If you'd like to use the ARTIQ network controller generation, instead install with:
pip install generic-scpi-driver[artiq]
To make a driver, simply inherit from the GenericDriver class. To define commands, call _register_query on the class (not on objects of the class). For example:
from generic_scpi_driver import GenericDriver
class SimpleDriver(GenericDriver):
'''A driver for my simple SCPI device'''
SimpleDriver._register_query("get_identity", "*IDN")
This creates a class called SimpleDriver
which has a constuctor __init__
, a close()
method
and a new method called get_identity()
which takes no parameters and returns a string. You could open a connection to your device like this:
dev = SimpleDriver(
id="COM10",
baud_rate=57600
)
# This sends the command "*IDN\n" to the device and returns the response
identity = dev.get_identity()
For more complex commands, you can specify parameters for the command:
SimpleDriver._register_query(
"set_voltage",
"VOLT",
args=[
GenericDriver.Arg(name="channel"),
GenericDriver.Arg(name="voltage"),
]
)
This would allow you to call:
# Using positional arguments
dev.set_voltage(0, 5.4)
# ...or keyword arguments
dev.set_voltage(channel=0, voltage=5.4)
Parameters can be validated by passing a custom function (which may accept any single parameter and must return a string to be sent to the device, or throw an error):
def check_voltage_in_limits(v):
voltage = float(v)
if voltage > 10:
raise ValueError("Voltage too high")
return str(voltage)
SimpleDriver._register_query(
"set_voltage",
"VOLT",
args=[
GenericDriver.Arg(name="channel", validator=lambda: str(int(x))),
GenericDriver.Arg(name="voltage", validator=check_voltage_in_limits, default=0.0,
]
)
Return values are, by default, the string returned by the SCPI device in response to your command.
If you'd prefer to process these, you can pass a response_parser
function:
SimpleDriver._register_query(
"count_foobars",
"COUN",
response_parser=int,
)
SimpleDriver._register_query(
"list_foobars",
"LIST",
response_parser=lambda x: x.split(","),
)
If your device doesn't give any response at all, you can set
response_parser=None
and the driver won't attempt to listen for a respose.
You can also add error checking to your commands. Pass a function as
response_validator
and it will be called with the output from the device
(not the parsed output of response_parser
) as its input. The
response_validator
's return value will be ignored: it's only job is to raise
an exception if needed. E.g.
def check_for_error(s):
if "error" in s.lower():
raise RuntimeError("Error returned by device: {}".format(s))
SimpleDriver._register_query(
"do_something",
"DOOO",
response_validator=check_for_error,
)
By default, all methods are syncronous. If you'd prefer async operation, pass coroutine=True
to _register_query
. This creates a new thread for the serial call and returns an asyncio
coroutine. Note that you have to call these using an async loop which is a whole topic of python
programming. This is particularly useful for ARTIQ drivers, since ARTIQ handles coroutines
automatically.
The method generation is intended to be quite flexible, but if you really need custom logic there's
nothing to stop you writing your own methods. You can use self.instr
to access the
pyvisa.Resource
for your device. Use the wrappers with_handler
to cause the driver to issue a
VISA .flush()
if an error occurs and with_lock
to ensure that only one method access the device
at a time (only relevant in multi-threaded applications).
from generic_scpi_driver import GenericDriver, with_lock, with_handler
class SimpleDriver(GenericDriver):
'''A driver for my simple SCPI device'''
@with_handler
@with_lock
def do_complex_thing(self):
'''Do something complex'''
response = self.instr.query("COMP 1 2 3")
return int(response) + 5
It can be useful to check on startup if communicatio with a device has been
established successfully. To do this, define a method in the class called
check_connection
. Return value is ignored, but this method will be called
when the object is constucted and has the chance to raise an exception. Example:
from generic_scpi_driver import GenericDriver, with_lock, with_handler
class SimpleDriver(GenericDriver):
'''A driver for my simple SCPI device'''
def check_connection(self):
idn = self.get_identity()
if idn != "My device":
raise RuntimeError(f"Bad device identity: got '{idn}'")
# Note that it's fine to define functions later which get used in methods
# defined previously
SimpleDriver._register_query("get_identity", "*IDN")
The constuctor accepts a keyword parameter simulation=True
to return a simulation device, for running
offline unit tests. This won't work unless you also register a simulator device with a method query
which
takes a string and returns a string. For example:
class Simulator:
def query(s):
if s == "*IDN":
return "Simulator device"
else:
return "ERROR"
class SimpleDriver(GenericDriver):
pass
SimpleDriver._register_simulator(Simulator)
SimpleDriver._register_query("get_identity", "*IDN")
dev = SimpleDriver(id="fake", simulation=True)
dev.get_identity() # returns "Simulator device"
To get a network controller for use by the ARTIQ controller manager, just make a python module like:
from generic_scpi_driver import get_controller_func
from .my_driver import SimpleDriver
# Makes a controller called "SimpleDriver" which listens to port 3300 by default
main = get_controller_func("SimpleDriver", 3300, SimpleDriver)
if __name__ == "__main__":
main()
Register this main
function in your setup.py
like so:
setup(
...
entry_points={
"console_scripts": [
"artiq_simple_device=my_driver_package.my_driver_controller:main",
]
},
)
After installing your package using pip install -e . as normal, you should be able to call
artiq_simple_device
on the command line to launch a controller for your device.
For developing the package, you'll need a few more packages. Install with:
pip install -e .[dev,artiq]
generic_scpi_driver was written by Charles Baynham.