# Base Device `BaseDeviceSyncModel` is the core abstraction for building a synchronized device API in PlestyLib. It combines: 1. Device lifecycle (`init`, `connect`, `disconnect`) 2. Parameter system (`ConfigSystem`) 3. Function system (`FunctionSystem`) 4. Standardized read/write/query workflow 5. Resource ownership and context-manager cleanup ## What It Provides When your device class inherits from `BaseDeviceSyncModel`, you get: 1. Parameter registration, validation, and typed parsing through `ConfigSystem`. 2. Function registration and invocation metadata through `FunctionSystem`. 3. Built-in high-level methods such as `write`, `query`, `state`, and `summary`. 4. Context manager support (`with device:`) that calls `init`, `connect`, and `disconnect`. 5. Resource tracking through `ResourceRegistry` to avoid duplicate ownership. 6. Per-instance resource locking (`_locked_resources`, `_clients`) for multi-client access control. ## Typical Layering A concrete device class usually composes three parts: 1. Device class (inherits `BaseDeviceSyncModel`) 2. Traffic manager (transport I/O) 3. Command/operation solver (protocol translation) Data flow: 1. `write("KEY", value)` validates input against parameter metadata. 2. Device `_write_` builds protocol command and sends it via traffic manager. 3. `query("KEY")` sends protocol query and converts response to target dtype. ## Required Methods to Implement Your subclass must implement these methods: 1. `connect(self)` 2. `disconnect(self)` 3. `_write_(self, key, value)` 4. `_query_(self, key)` 5. `check_errors(self)` 6. `check_operatability(self)` Commonly overridden methods: 1. `init(self, main=None)` to create traffic manager and solver objects. 2. `identity(self)` for model/vendor identification command. 3. `query_param_range(self, key)` if device can report runtime min/max. 4. `query_param_options(self, key)` if categorical options are queryable. ## Lifecycle Recommended lifecycle: 1. Instantiate device object. 2. Enter context (`with device:`) or call `init()` and `connect()` manually. 3. Perform `query`/`write` operations. 4. Call `disconnect()` (automatic when using context manager). Using context manager is recommended because cleanup is guaranteed: ```python with MyDevice("dev-id") as dev: print(dev.query("POWER")) ``` ## Minimal Implementation Example ```python from typing import Any from plesty.lib.device.base_device_sync import BaseDeviceSyncModel from plesty.lib.traffic.serial import SerialTrafficManager from plesty.lib.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: 1. `state` / `get_state()`: query all registered parameters and return a dictionary. 2. `synchronize_param_from_device(keys=None, sync_constraints=False)`: pull current values from hardware into the model. 3. `summary()`: generate a textual API overview. If your device supports querying dynamic constraints, implement: 1. `query_param_range` for numeric parameters. 2. `query_param_options` for 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. 1. `AsyncWrapperSafe`: thread-offloaded calls protected by an async lock (simple, safe default). 2. `AsyncDeviceThread`: dedicated worker thread with queued calls (better for high-frequency workflows). Example with `AsyncWrapperSafe`: ```python import asyncio from plesty.lib.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()) ``` ## Multi-Client Resource Control `BaseDeviceSyncModel` carries three resource-tracking attributes that support exclusive access control when a device is shared by multiple clients (for example, over a TCP/IP service): | Attribute | Type | Purpose | |---|---|---| | `_resources` | `list` | Hardware resources registered for automatic release on disconnect | | `_locked_resources` | `set` | Resources that are currently allocated to any client | | `_clients` | `dict` | Maps `client_id → [allocated resources]` | These attributes mirror the `ResourceManager` pattern from `plesty.lib.service.resource_manager`. You can compose a `ResourceManager` inside your device subclass and delegate to its methods, or override `_user_permission_check` to enforce access rules based on the per-instance state. The default `_user_permission_check` always returns `True` (no restriction). Override it to implement real guard logic: ```python def _user_permission_check(self, user_resources: list, key: str) -> bool: if not user_resources: return True return any(r in self._locked_resources for r in user_resources) ``` Both `write` and `query` pass a `resources` kwarg through to `_user_permission_check` when the caller supplies it, so access control integrates transparently with the standard operation interface. ## Best Practices 1. Keep transport logic in traffic managers, not in device classes. 2. Keep protocol formatting/parsing in solvers. 3. Register all parameters with correct dtype/range/options for safe validation. 4. Prefer `with device:` for guaranteed cleanup. 5. Implement `check_operatability` using a real device health query when possible. 6. Avoid mixing direct synchronous access and async wrapper access on the same device instance.