FuncMeta & SCPISolver.solve_funcSchema-driven operation dispatch — map a JSON command entry to a typed device response in zero hand-written logic.
plesty-libThe classic PLESTY device OpSolver was a single solve_op() method containing a long chain of if/elif blocks. Each branch manually called send_command("MEAS:POW?"), stripped whitespace, cast to float, and returned a dict.
For devices with many query operations (power meters, spectrometers, lock-in amplifiers) this produced 30–60 near-identical code paths. Any typo in a command string — "MEAS.POW?" instead of "MEAS:POW?" — was a runtime bug that only surfaced when that specific operation was called. There was no schema to cross-check against. There was no type contract. And because each developer wrote their own variant, modules were inconsistent with each other.
A lightweight @dataclass that links a PLESTY operation name to its device-protocol command. The FunctionSystem builds one automatically from any schema operation that has a "command" field, then injects it into the request dictionary before calling solve_op().
| Field | Type | Description |
|---|---|---|
| name | str | Operation name as registered in the function schema (e.g. "get_power") |
| command | str | Raw device command string to send (e.g. "MEAS:POW?") |
| output_key | str | Key under which the parsed value is returned (e.g. "power") |
| output_dtype | type | Python type used to cast the raw string response. Defaults to float. |
from dataclasses import dataclass, field @dataclass class FuncMeta: """Link a PLESTY operation to its device-protocol command.""" name: str command: str output_key: str output_dtype: type = field(default=float)
solve_func DoesWhen func_meta is None — meaning the schema entry has no "command" field — solve_func returns None and the caller's solve_op() handles the operation with custom logic.
def solve_func( self, op_name: str, send_fn: Callable[[str], Any], func_meta: "FuncMeta | None", ) -> "dict[str, Any] | None": if func_meta is None: return None # caller handles custom logic raw = send_fn(func_meta.command) # send SCPI command try: value = func_meta.output_dtype(str(raw).strip()) # cast response except (ValueError, TypeError) as exc: raise ValueError( f"solve_func: cannot cast response {raw!r} to" f" {func_meta.output_dtype.__name__} for op '{op_name}'" ) from exc return {func_meta.output_key: value} # return typed dict
"command" field to your operation schemaIn schema_func.json, any operation with a "command" key is automatically handled by solve_func. Operations without "command" fall through to your OpSolver.
// plesty/mydevice/schema_func.json { "operations": { "get_power": { "command": "MEAS:POW?", // ← FuncMeta built from this "output_key": "power", "output_dtype": "float", "description": "Measure optical power in Watts." }, "get_wavelength": { "command": "SENS:CORR:WAV?", // ← also handled automatically "output_key": "wavelength", "output_dtype": "float", "description": "Read the correction wavelength in nm." }, "get_zeroed_power": { // no "command" field — falls through to OpSolver.solve_op() "description": "Return zero-corrected power reading." } } }
FunctionSystem builds FuncMeta automaticallyAt device initialisation, FunctionSystem scans your schema. For every operation with a "command" field it constructs a FuncMeta instance and registers it. No code change needed — this happens inside plesty-lib.
When solve_op() is called, FunctionSystem injects the matching FuncMeta (or None) into request["func_meta"].
solve_func first in your OpSolverPass request.get("func_meta") to solve_func. If it returns a dict, you're done. If it returns None, handle the operation manually.
class _MyDeviceOpSolver(OpSolver): def __init__(self, tm: TrafficManager) -> None: self._tm = tm self._scpi = SCPISolver() def solve_op(self, op_name: str, request: dict) -> dict: # Step 1: try schema-driven dispatch result = self._scpi.solve_func( op_name, self._tm.send_command, request.get("func_meta"), ) if result is not None: return result # done — no Python method needed # Step 2: handle custom operations if op_name == "get_zeroed_power": self._tm.send_command("SENS:CORR:COLL:ZERO:INIT") raw = self._tm.send_command("MEAS:POW?") return {"power": float(raw.strip())} raise NotImplementedError(f"Unknown operation: {op_name}")
"command" and implement in OpSolverOperations requiring multi-step SCPI exchange, binary responses, or conditional device state should not have a "command" field in the schema. solve_func will return None for them, and your if block in solve_op handles them with full custom logic.
class Pm100dOpSolver(OpSolver): def solve_op(self, op_name, request): if op_name == "get_power": raw = self._tm.send_command("MEAS:POW?") return {"power": float(raw.strip())} elif op_name == "get_wavelength": raw = self._tm.send_command("SENS:CORR:WAV?") return {"wavelength": float(raw.strip())} elif op_name == "get_zeroed_power": # custom multi-step logic self._tm.send_command( "SENS:CORR:COLL:ZERO:INIT") raw = self._tm.send_command("MEAS:POW?") return {"power": float(raw.strip())} ... # 27 more elif blocks
class _Pm100dOpSolver(OpSolver): def solve_op(self, op_name, request): # Simple ops: handled automatically result = self._scpi.solve_func( op_name, self._tm.send_command, request.get("func_meta"), ) if result is not None: return result # Only complex ops reach here if op_name == "get_zeroed_power": self._tm.send_command( "SENS:CORR:COLL:ZERO:INIT") raw = self._tm.send_command("MEAS:POW?") return {"power": float(raw.strip())} raise NotImplementedError(op_name)
Omit "command" from the schema entry and implement the logic in solve_op() when your operation:
| Field | Description |
|---|---|
| name: str | Operation name matching the schema key. |
| command: str | Raw SCPI command to send (e.g. "MEAS:POW?"). |
| output_key: str | Dict key for the returned value (e.g. "power"). |
| output_dtype: type | Cast target for the raw string response. Default: float. |
| Parameter / Return | Description |
|---|---|
| op_name: str | Operation name — used only in error messages. |
| send_fn: Callable | Callable that sends a command string and returns the device response. |
| func_meta: FuncMeta | None | Injected by FunctionSystem. None = no schema command, caller must handle. |
| → dict | None | {output_key: cast_value} on success; None when func_meta is absent. |
| raises ValueError | When the raw response cannot be cast to output_dtype. |