Base Device#
BaseDeviceSyncModel is the core abstraction for building a synchronized device API in PlestyLib.
It combines:
Device lifecycle (
init,connect,disconnect)Parameter system (
ConfigSystem)Function system (
FunctionSystem)Standardized read/write/query workflow
Resource ownership and context-manager cleanup
What It Provides#
When your device class inherits from BaseDeviceSyncModel, you get:
Parameter registration, validation, and typed parsing through
ConfigSystem.Function registration and invocation metadata through
FunctionSystem.Built-in high-level methods such as
write,query,state, andsummary.Context manager support (
with device:) that callsinit,connect, anddisconnect.Resource tracking through
ResourceRegistryto avoid duplicate ownership.
Typical Layering#
A concrete device class usually composes three parts:
Device class (inherits
BaseDeviceSyncModel)Traffic manager (transport I/O)
Command/operation solver (protocol translation)
Data flow:
write("KEY", value)validates input against parameter metadata.Device
_write_builds protocol command and sends it via traffic manager.query("KEY")sends protocol query and converts response to target dtype.
Required Methods to Implement#
Your subclass must implement these methods:
connect(self)disconnect(self)_write_(self, key, value)_query_(self, key)check_errors(self)check_operatability(self)
Commonly overridden methods:
init(self, main=None)to create traffic manager and solver objects.identity(self)for model/vendor identification command.query_param_range(self, key)if device can report runtime min/max.query_param_options(self, key)if categorical options are queryable.
Lifecycle#
Recommended lifecycle:
Instantiate device object.
Enter context (
with device:) or callinit()andconnect()manually.Perform
query/writeoperations.Call
disconnect()(automatic when using context manager).
Using context manager is recommended because cleanup is guaranteed:
with MyDevice("dev-id") as dev:
print(dev.query("POWER"))
Minimal Implementation Example#
from typing import Any
from plestylib.device.base_device_sync import BaseDeviceSyncModel
from plestylib.traffic.serial import SerialTrafficManager
from plestylib.solver.scpi import SCPISolver
class MyScpiSerialDevice(BaseDeviceSyncModel):
def __init__(self, port: str):
super().__init__(id=port)
self.port = port
# Register parameters for validation + parsing.
self.register_config("POWER", dtype=float, read_only=True, command="MEAS:POW")
self.register_config("WAVELENGTH", dtype=int, command="SENS:WAV")
def init(self, main=None):
self.traffic_manager = SerialTrafficManager(
port=self.port,
baudrate=9600,
timeout=5,
write_termination="\r",
read_termination="\r\n",
)
self.cmd_solver = SCPISolver()
def connect(self):
return self.traffic_manager.open(parity="none", stopbits="one", bytesize=8)
def disconnect(self):
self.traffic_manager.close()
def _write_(self, key: str, value: str | float | int | bool) -> bool:
cfg = self.get_config(key)
command = self.cmd_solver.get_write_cmd(cfg, value)
return bool(self.traffic_manager.send_command(command))
def _query_(self, key: str) -> str:
cfg = self.get_config(key)
command = self.cmd_solver.get_query_cmd(cfg)
return self.traffic_manager.send_command(command)
def check_errors(self) -> list[str]:
# Replace with device-specific error query if available.
return []
def check_operatability(self) -> bool:
return self.traffic_manager is not None and self.traffic_manager.is_open
State and Synchronization Helpers#
Useful built-in helpers:
state/get_state(): query all registered parameters and return a dictionary.synchronize_param_from_device(keys=None, sync_constraints=False): pull current values from hardware into the model.summary(): generate a textual API overview.
If your device supports querying dynamic constraints, implement:
query_param_rangefor numeric parameters.query_param_optionsfor categorical parameters.
Sync vs Async Use#
BaseDeviceSyncModel is synchronous by design. To use it in async applications, wrap it with one of the provided wrappers.
AsyncWrapperSafe: thread-offloaded calls protected by an async lock (simple, safe default).AsyncDeviceThread: dedicated worker thread with queued calls (better for high-frequency workflows).
Example with AsyncWrapperSafe:
import asyncio
from plestylib.device.async_wrapper import AsyncWrapperSafe
async def main():
async with AsyncWrapperSafe(MyScpiSerialDevice("/dev/ttyUSB0")) as dev:
power = await dev.query("POWER")
print(power)
asyncio.run(main())
Best Practices#
Keep transport logic in traffic managers, not in device classes.
Keep protocol formatting/parsing in solvers.
Register all parameters with correct dtype/range/options for safe validation.
Prefer
with device:for guaranteed cleanup.Implement
check_operatabilityusing a real device health query when possible.Avoid mixing direct synchronous access and async wrapper access on the same device instance.