Command Solver

Command solvers translate standardized device operations into protocol-specific command payloads, and optionally help decode protocol responses.

In PlestyLib, command solving is separated from transport I/O:

  1. Device class defines high-level operations (query, write, function calls).

  2. Command solver converts these operations to wire commands.

  3. Traffic manager sends commands and receives raw responses.

This separation keeps each layer focused and easier to test.

What It Does

A command solver typically handles:

  1. Command string construction from parameter metadata.

  2. Value serialization rules (for example, bool to 1 and 0 in SCPI).

  3. Query command formatting.

  4. Optional response decoding/validation helpers.

Solver Types in PlestyLib

The base interfaces are in plesty.lib.solver:

  1. CmdSolver: parameter-centric command generation.

  2. OpSolver: operation/message-centric translation.

CmdSolver Interface

CmdSolver defines:

  1. get_write_cmd(cfg_param, value) -> str | None

  2. get_query_cmd(cfg_param) -> str | None

Use this for command families based on registered parameters (ConfigParameter.command, dtype, ranges, and so on).

OpSolver Interface

OpSolver defines:

  1. solve_request(request: dict) -> dict

Use this for protocol layers that operate on operation messages rather than direct parameter commands.

Built-in Example: SCPISolver

SCPISolver (in plesty.lib.solver.scpi) is a CmdSolver implementation used by SCPI devices.

Behavior highlights:

  1. get_write_cmd builds <CMD> <VALUE>.

  2. get_query_cmd builds <CMD>?.

  3. Supports scalar and list/tuple serialization.

  4. Additional helpers: <CMD>:MIN? and <CMD>:MAX?.

  5. solve_func dispatches schema-driven operations via FuncMeta.

solve_func — schema-driven operation dispatch

SCPISolver.solve_func(op_name, send_fn, func_meta) -> dict | None

Sends the SCPI command stored in a FuncMeta object, casts the raw string response to func_meta.output_dtype, and returns {output_key: cast_value}.

Returns None when func_meta is None, signalling that the caller must handle the operation through its own custom logic.

Parameter

Type

Description

op_name

str

PLESTY operation name — used only in error messages

send_fn

Callable[[str], Any]

Sends a raw SCPI string and returns the device response (e.g. tm.send_command)

func_meta

FuncMeta | None

Injected by FunctionSystem from the request dict; None for custom operations

Raises ValueError if the response cannot be cast to func_meta.output_dtype.

Example — wiring solve_func in a custom OpSolver:

from plesty.lib.solver.scpi import SCPISolver
from plesty.lib.solver import OpSolver


class MySCPIOpSolver(OpSolver):
    def __init__(self, send_fn):
        self._send = send_fn
        self._scpi = SCPISolver()

    def solve_request(self, request: dict) -> dict:
        op_name = request["op"]
        func_meta = request.get("func_meta")  # injected by FunctionSystem
        result = self._scpi.solve_func(op_name, self._send, func_meta)
        if result is not None:
            return result
        # fallback: custom dispatch for complex operations
        raise NotImplementedError(f"No handler for operation '{op_name}'")

How It Integrates with Base Devices

For SCPI devices, base classes call solver methods internally:

  1. BaseVisaScpiDevice._write_ and _query_ use self.scpi_solver.get_write_cmd(...) and get_query_cmd(...).

  2. BaseTCPScpiDevice._write_ and _query_ follow the same pattern.

This means your concrete device class usually only needs to register parameters and choose the correct base device.

Demo 1: Use SCPISolver Directly

from plesty.lib.solver.scpi import SCPISolver
from plesty.lib.device.params import ConfigParameter

solver = SCPISolver()

cfg = ConfigParameter(
	name="WAVELENGTH",
	dtype=int,
	command="SENS:CORR:WAV",
)

write_cmd = solver.get_write_cmd(cfg, 1064)
query_cmd = solver.get_query_cmd(cfg)
min_cmd = solver.get_param_min_cmd(cfg)
max_cmd = solver.get_param_max_cmd(cfg)

print(write_cmd)  # SENS:CORR:WAV 1064
print(query_cmd)  # SENS:CORR:WAV?
print(min_cmd)    # SENS:CORR:WAV:MIN?
print(max_cmd)    # SENS:CORR:WAV:MAX?

Demo 2: Create a Custom CmdSolver

Use a custom solver when your protocol syntax is not SCPI-compatible.

from typing import Any

from plesty.lib.solver import CmdSolver
from plesty.lib.device.params import ConfigParameter


class KVTextSolver(CmdSolver):
	"""Example protocol: SET <key>=<value> and GET <key>."""

	def get_write_cmd(self, cfg_param: ConfigParameter, value: Any) -> str | None:
		if not cfg_param.command:
			return None
		return f"SET {cfg_param.command}={value}"

	def get_query_cmd(self, cfg_param: ConfigParameter) -> str | None:
		if not cfg_param.command:
			return None
		return f"GET {cfg_param.command}"

Demo 3: Operation-Style Solver Example

ICEBLOCKSolver is an operation-style solver that builds compact JSON messages with transmission IDs and includes helpers for response decoding and status checks.

from plesty.lib.solver.iceblock import ICEBLOCKSolver

solver = ICEBLOCKSolver()
msg = solver.get_startLink_cmd("192.168.1.20")
print(msg)

ok = solver.status_ok('{"message":{"parameters":{"status":"ok"}}}')
print(ok)  # True

Best Practices

  1. Keep transport details in traffic managers, not in solvers.

  2. Keep device business logic in device classes, not in solvers.

  3. Put all protocol text formatting in the solver layer.

  4. Return None for unsupported/missing command metadata and let caller handle it.

  5. Keep serialization deterministic for easier testing.