Quick Start#

This guide shows how to build and use a PlestyLib device API with the Thorlabs PM100D power meter as an example.

Install#

pip install plesty-lib
# or 
uv add plesty-lib
# of if you want the plesty-sdk (optional dependency) installed as well
pip install "plesty-lib[dev]"
# or
uv add "plesty-lib[dev]"

Before You Implement a Device API#

Collect the following information first:

  1. Which physical interface is used (USB, Ethernet, Serial, and so on)?

  2. Which communication protocol is provided (SCPI, vendor SDK, custom binary protocol)?

  3. Where is the protocol reference (manual, command table, SDK docs)?

For PM100D, the typical setup is USB + SCPI, with commands described in the vendor manual.

Implement a PM100D Device Class#

When the device uses VISA + SCPI, inherit from BaseVisaScpiDevice and register parameters through the Parameter system.

from plesty.lib.device.base_visa_scpi_device import BaseVisaScpiDevice


class PowermeterDevice(BaseVisaScpiDevice):
    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")

Use the Device Locally#

Manual Connect/Disconnect#

from powermeter_device import PowermeterDevice

address = "USB0::0x1313::0x8078::P0000001::INSTR"
sensor_type = "S155C"

powermeter = PowermeterDevice(address, sensor_type=sensor_type)
powermeter.connect()

try:
    print(powermeter.identity())
    for name in powermeter.get_config_list():
        value = powermeter.query(name)
        print(f"{name}: {value}")
finally:
    powermeter.disconnect()

Use the Device Asynchronously#

import asyncio

from powermeter_device import PowermeterDevice
from plesty.lib.device.async_wrapper import AsyncWrapperSafe

address = "USB0::0x1313::0x8078::P0000001::INSTR"
sensor_type = "S155C"


async def main() -> None:
    async with AsyncWrapperSafe(PowermeterDevice(address, sensor_type=sensor_type)) as powermeter:
        print(await powermeter.identity())

        for name in await powermeter.get_config_list():
            value = await powermeter.query(name)
            print(f"{name}: {value}")


asyncio.run(main())

Access the Device Remotely#

Host Machine (Run Server)#

import asyncio

from powermeter_device import PowermeterDevice
from plesty.lib.service import build_server

address = "USB0::0x1313::0x8078::P0000001::INSTR"
sensor_type = "S155C"


async def main() -> None:
    device = PowermeterDevice(address, sensor_type=sensor_type)
    server = build_server(device, fixed_threading=False, address="tcp://*:5555")
    await server.run()


asyncio.run(main())

Remote Machine (Run Client)#

from plesty.lib.service import build_client

client = build_client(address="tcp://<host-ip>:5555", timeout=5000)

for name in client.get_config_list():
    value = client.query(name)
    print(f"{name}: {value}")

Connection Settings (Env-Driven)#

Credential and connection information — where to reach the device and which port to serve it on — belongs in the environment, not hard-coded. EnvSettings is a small, dependency-free helper that loads environment variables (optionally seeded from a .env file) with typed access. It is deliberately generic: each device module chooses its own variable names and defaults; the helper just loads and casts them.

from plesty.lib.utils import EnvSettings

env = EnvSettings.load()  # reads ./.env, then the process env (process env wins)
# By default load() also registers the parsed `.env` values into os.environ
# (without overriding existing ones), so they are visible to os.getenv and child
# processes too. Pass export=False for a side-effect-free load.

# Precedence per value: CLI argument (override) > env var > default.
# Credentials use require() — never hard-code them as a default in source.
host = env.require("AMC300_HOST", override=args.host)
port = env.require("AMC300_PORT", cast=int, override=args.port)
# The serving port is not a credential, so a conventional default is fine.
tcp_port = env.get("AMC300_TCP_PORT", default=5555, cast=int, override=args.tcp_port)

device = PowermeterDevice(host, port)
server = build_server(device, address=f"tcp://*:{tcp_port}")

Pass a parsed CLI argument as override (it wins when not None, so unset flags fall through to the environment). require() raises KeyError with a clean message when a mandatory credential is set neither by argument nor environment. The same .env then drives both the server and its tests. Flexible operational tuning (non-credential parameters) is configured separately, e.g. via a YAML file.

Next Reading#

  1. Parameter system

  2. Traffic manager

  3. Command solver