# 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. ```python # 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 ` to change it. `read_only=True` marks a parameter that can be read but not changed, such as the measured power. [Parameter system](../device/param_system.md) 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. ```python 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: ```python 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: ```python 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: ```python # 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: ```python 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](../device/service.md) 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. ```python 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](../device/func_system.md) 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](../device/base_device.md#ready-made-base-devices) | | control an instrument that does not speak SCPI | [Demo 2: Create a Custom CmdSolver](../device/cmd_solver.md#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](../device/traffic_manager.md#demo-2-implement-your-own-serial-traffic-manager) | | list the parameters in a JSON file instead of in code | [Register Parameters from Schema](../device/param_system.md#register-parameters-from-schema) | | add your own operations, like `measure_power` | [Register Operations Programmatically](../device/func_system.md#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](../common/env_file.md) | | let several programs use one instrument at the same time | [Multi-Client Access Control](../device/service.md#multi-client-access-control) | | check that the instrument is plugged in and answering before a measurement | [Preflight: is the instrument actually there?](../concepts/preflight.md) | | use several devices, on one or more computers, as one object | [Composite Device](../device/composite_device.md) | | control what a device writes to its log, and where | [Logging System](../common/logging_system.md) | | decide what happens when the instrument reports an error | [Error Handling](../device/error_handling.md) | | test your device automatically, without the instrument | [Testing a Device](../testing/device.md) |