# Quick Start This guide shows how to build and use a PlestyLib device API with the Thorlabs PM100D power meter as an example. ## Install ```bash 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](device/param_system.md). ```python 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 ### Context Manager (Recommended) ```python from powermeter_device import PowermeterDevice address = "USB0::0x1313::0x8078::P0000001::INSTR" sensor_type = "S155C" with PowermeterDevice(address, sensor_type=sensor_type) as powermeter: print(powermeter.identity()) for name in powermeter.get_config_list(): value = powermeter.query(name) print(f"{name}: {value}") cfg = powermeter.get_config(name) if not cfg.read_only: powermeter.write(name, value) ``` ### Manual Connect/Disconnect ```python 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 ```python 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) ```python 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) ```python from plesty.lib.service import build_client client = build_client(address="tcp://: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. ```python 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](device/param_system.md) 2. [Traffic manager](device/traffic_manager.md) 3. [Command solver](device/cmd_solver.md)