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:

  1. How it is connected: USB, Ethernet, a serial cable, …

  2. 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, and SENS:CORR:WAV 633 to set it to 633. Others need a library from the manufacturer, or a binary format of their own.

  3. 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

Ready-Made Base Devices

control an instrument that does not speak SCPI

Demo 2: Create a Custom CmdSolver

reach an instrument over a connection plesty-lib does not support yet

Demo 2: Implement Your Own Serial Traffic Manager

list the parameters in a JSON file instead of in code

Register Parameters from Schema

add your own operations, like measure_power

Register Operations Programmatically

keep the instrument’s address and the server’s port out of your code, so the same code runs in any lab

The .env File

let several programs use one instrument at the same time

Multi-Client Access Control

check that the instrument is plugged in and answering before a measurement

Preflight: is the instrument actually there?

use several devices, on one or more computers, as one object

Composite Device

control what a device writes to its log, and where

Logging System

decide what happens when the instrument reports an error

Error Handling

test your device automatically, without the instrument

Testing a Device