# 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.

## 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 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:

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 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

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.
