A Device
A device is a Python class that controls one instrument. This page writes one for a real power meter, the Thorlabs PM100D, and then uses it in four ways: from a script, from asynchronous code, from another computer, and without the instrument at all.
What you need to know about the instrument
Before writing any code, find three things out:
How it is connected: USB, Ethernet, a serial cable, …
What language it speaks: the commands it accepts and the replies it sends. Many lab instruments speak SCPI, a standard text format: you send
SENS:CORR:WAV?to ask for the wavelength setting, andSENS:CORR:WAV 633to set it to 633. Others need a library from the manufacturer, or a binary format of their own.Where the commands are written down: usually the programming chapter of the instrument’s manual.
The PM100D connects over USB and speaks SCPI. Its manual lists the commands used below.
To reach a USB or Ethernet instrument, plesty-lib uses VISA, a standard
software layer that most instrument makers support. VISA names each instrument
with an address such as USB0::0x1313::0x8078::P0000001::INSTR, which you can read
from the instrument’s settings or from the VISA tools on your computer.
Write the device class
plesty-lib provides a starting class for each common combination of connection
and language. BaseVisaScpiDevice already knows how to open a VISA connection and
exchange SCPI commands, so your class only has to list the instrument’s settings.
In plesty-lib, a setting is called a parameter. You declare each one with
register_config, giving it a name, its type, and the SCPI command that reads and
changes it. A unit and a range are optional: when you give them, plesty-lib refuses
out-of-range values before anything is sent to the instrument.
# powermeter_device.py
from plesty.lib.device.base_visa_scpi_device import BaseVisaScpiDevice
class PowermeterDevice(BaseVisaScpiDevice):
# The wavelength range depends on which sensor head is plugged in.
SENSOR_WAVELENGTH_RANGES = {
"S155C": (800, 1700),
"S120C": (400, 1100),
"S130C": (400, 1100),
}
def __init__(self, address: str, sensor_type: str):
super().__init__(address)
if sensor_type not in self.SENSOR_WAVELENGTH_RANGES:
raise ValueError(f"Unsupported sensor type: {sensor_type}")
min_wl, max_wl = self.SENSOR_WAVELENGTH_RANGES[sensor_type]
self.register_config("SENSOR", read_only=True, command="SYST:SENS:IDN")
self.register_config(
"WAVELENGTH",
dtype=int,
unit="nm",
min_value=min_wl,
max_value=max_wl,
command="SENS:CORR:WAV",
)
self.register_config("DIAMETER", dtype=float, command="SENS:CORR:BEAM")
self.register_config("POWER_RANGE", dtype=float, command="SENS:POW:RANG")
self.register_config("POWER_RANGE_AUTO", dtype=bool, default=True, command="SENS:POW:RANG:AUTO")
self.register_config("AVERAGES", dtype=int, command="SENS:AVER:COUN")
self.register_config("POWER", read_only=True, dtype=float, unit="watt", command="MEAS:SCAL:POW")
From command="SENS:CORR:WAV", the class builds both SCPI messages itself:
SENS:CORR:WAV? to read the parameter and SENS:CORR:WAV <value> to change it.
read_only=True marks a parameter that can be read but not changed, such as the
measured power.
Parameter system explains parameters in full, including how to declare them in a JSON file instead of in code.
Use it from a script
Two methods do most of the work: query(name) reads a parameter from the
instrument, and write(name, value) changes it. Before either can be used, the
device has to open its connection to the instrument, and it should close it
afterwards so that other programs can reach the instrument.
A with block does both for you: it connects when the block starts and
disconnects when the block ends, even if an error happens inside.
from powermeter_device import PowermeterDevice
address = "USB0::0x1313::0x8078::P0000001::INSTR"
with PowermeterDevice(address, sensor_type="S155C") as powermeter:
print(powermeter.identity()) # the instrument's name and serial number
powermeter.write("WAVELENGTH", 1064)
print(powermeter.query("WAVELENGTH")) # 1064
print(powermeter.query("POWER")) # the measured power, in watts
for name in powermeter.get_config_list(): # every parameter you declared
print(f"{name}: {powermeter.query(name)}")
Without a with block, call connect() and disconnect() yourself:
powermeter = PowermeterDevice(address, sensor_type="S155C")
powermeter.connect()
try:
print(powermeter.query("POWER"))
finally:
powermeter.disconnect()
Use it from asynchronous code
A program built on Python’s asyncio runs many tasks at once in one thread, and a
task that waits for a slow instrument would hold up all the others.
AsyncWrapperSafe wraps a device so that each call runs in the background and is
awaited instead:
import asyncio
from plesty.lib.device.async_wrapper import AsyncWrapperSafe
from powermeter_device import PowermeterDevice
async def main() -> None:
device = PowermeterDevice("USB0::0x1313::0x8078::P0000001::INSTR", sensor_type="S155C")
async with AsyncWrapperSafe(device) as powermeter:
print(await powermeter.identity())
print(await powermeter.query("POWER"))
asyncio.run(main())
If your program does not use asyncio, you do not need this section.
Use it from another computer
An instrument is plugged into one computer, but the program that uses it often runs on another. plesty-lib solves this with a server and a client:
The server runs on the computer the instrument is plugged into. It opens the device and waits for requests over the network.
A client runs anywhere on the network. It has the same methods as the device (
query,write, …) and forwards each call to the server.
Start the server on the instrument’s computer:
# serve.py
from plesty.lib.service import build_server, run_server
from powermeter_device import PowermeterDevice
device = PowermeterDevice("USB0::0x1313::0x8078::P0000001::INSTR", sensor_type="S155C")
server = build_server(device, address="tcp://*:5555") # listen on network port 5555
run_server(server.run()) # keep serving until Ctrl-C
Then, on any other computer, open a client with the server computer’s network address:
from plesty.lib.service import build_client
client = build_client(address="tcp://192.168.1.20:5555", timeout=5000) # timeout in ms
print(client.query("POWER"))
The server handles one request at a time, in the order they arrive, on one dedicated thread. Many instrument drivers only work from the thread that opened them, so leave that as it is. Remote Access explains the server’s options and how several clients can share one instrument safely.
Try it without the instrument
plesty-lib comes with DemoDevice, a mock power meter. It keeps its settings in memory and makes up its readings instead of talking to an instrument, so you can try everything on this page without hardware.
from plesty.lib.sim.demo_device import MOCK_ADDRESS, DemoDevice
with DemoDevice(MOCK_ADDRESS) as device:
device.write("wavelength", 633.0)
print(device.query("wavelength")) # 633.0
print(device.measure_power(samples=5)) # {'power': 0.005}
Actions like measure_power are called operations in plesty-lib. A device
declares them next to its parameters, and the part of plesty-lib that handles them
is the function system: Function System shows how to
declare them.
Advanced topics
You want to … |
Read |
|---|---|
control an instrument connected over Ethernet, a serial cable, or a Thorlabs motion controller |
|
control an instrument that does not speak SCPI |
|
reach an instrument over a connection plesty-lib does not support yet |
|
list the parameters in a JSON file instead of in code |
|
add your own operations, like |
|
keep the instrument’s address and the server’s port out of your code, so the same code runs in any lab |
|
let several programs use one instrument at the same time |
|
check that the instrument is plugged in and answering before a measurement |
|
use several devices, on one or more computers, as one object |
|
control what a device writes to its log, and where |
|
decide what happens when the instrument reports an error |
|
test your device automatically, without the instrument |