PLESTY devices define their capabilities in two JSON schema files. These schemas are the source of truth for parameter validation, documentation generation, and test automation.
schema_param.json
Defines all readable/writable device parameters.
Flat schema (simple devices)
{
"WAVELENGTH": {
"type": "int",
"command": "SENS:CORR:WAV",
"unit": "nm",
"min": 400,
"max": 1700,
"description": "Measurement wavelength in nm"
},
"POWER": {
"type": "float",
"command": "MEAS:SCAL:POW",
"unit": "watt",
"read_only": true,
"description": "Measured optical power"
},
"AUTO_RANGE": {
"type": "bool",
"command": "SENS:POW:RANG:AUTO",
"default": true,
"description": "Enable auto-range mode"
},
"AVERAGES": {
"type": "int",
"command": "SENS:AVER:COUN",
"min": 1,
"max": 1000,
"default": 100,
"description": "Number of averages per measurement"
}
}
Grouped schema (multi-channel devices)
{
"Channel1": {
"command_prefix": "CH1",
"parameters": {
"POWER": {
"type": "float",
"command": "MEAS:POW",
"unit": "watt",
"read_only": true
},
"WAVELENGTH": {
"type": "int",
"command": "SENS:WAV",
"unit": "nm"
}
}
}
}
Grouped parameters resolve to keys like CH1.POWER and CH1.WAVELENGTH.
Supported fields
| Field | Required | Type | Description |
|---|---|---|---|
type |
yes | string | One of: int, float, str, bool |
command |
yes | string | Protocol command string |
unit |
no | string | Physical unit label |
min |
no | number | Minimum allowed value |
max |
no | number | Maximum allowed value |
options |
no | array | Allowed categorical values |
default |
no | any | Default value |
read_only |
no | bool | If true, write raises an error |
write_only |
no | bool | If true, query raises an error |
description |
no | string | Human-readable description |
schema_func.json
Defines device operations (functions with inputs and outputs beyond simple param read/write).
{
"measure_power": {
"command": "MEAS:SCAL:POW?",
"iparams": {},
"oparams": {
"power": {
"type": "float",
"description": "Measured optical power in watts"
}
},
"description": "Take a single power measurement"
},
"measure_power_sequence": {
"iparams": {
"count": {
"type": "int",
"required": true,
"description": "Number of measurements to take"
},
"delay_ms": {
"type": "float",
"default": 100.0,
"description": "Delay between measurements in milliseconds"
}
},
"oparams": {
"powers": {
"type": "float",
"description": "Array of measured power values in watts"
},
"timestamps": {
"type": "float",
"description": "Unix timestamps for each measurement"
}
},
"description": "Take a sequence of power measurements with a fixed delay"
}
}
Each operation has iparams (inputs) and oparams (outputs), and optionally a command:
commandlinks the operation to its raw device-protocol command. TheFunctionSystembuilds aFuncMetafrom it and injects it into the request, so a generic solver (e.g.SCPISolver) can dispatch a single-command, single-scalar-output operation with no hand-written dispatch logic — likemeasure_powerabove.- Complex operations that need multi-step exchanges or custom response parsing omit
commandand implement the logic in a customOpSolver— likemeasure_power_sequenceabove.
iparams entries support the same constraint fields as parameters (required, default, unit, options, range, shape).
Registered operations are callable directly as methods on the device (device.measure_power()) — there is no _call wrapper. Gate d1's test_funcs_mock verifies that all registered operations return a dict with the correct output keys.
Where the files live
The schemas ship inside the package, beside the code that loads them:
plesty/power_meter/
├── __init__.py
├── base_device.py
├── device.py
├── schema_param.json # parameters
└── schema_func.json # operations
plesty init does not create them — you add them during implementation — but
the location matters: a file outside plesty/<module>/ is not part of the
wheel, so the module would import on your machine and fail on a bench. Keeping
them in the package means they install with it.
The names follow the reference devices (schema_param.json,
schema_func.json). Nothing in the platform hard-codes either name — you pass
the path — but a device that spells them differently is one more thing the
next reader has to look up.
Loading schemas in the device class
Resolve the path relative to the module file, never the working directory:
from pathlib import Path
from plesty.lib.device.base_device_sync import BaseDeviceSyncModel
_SCHEMA_DIR = Path(__file__).parent
class MyDevice(BaseDeviceSyncModel):
def __init__(self, address: str):
super().__init__(
id=address,
param_schema=str(_SCHEMA_DIR / "schema_param.json"),
)
self.register_from_op_schema(str(_SCHEMA_DIR / "schema_func.json"))
param_schema takes a path or an already-loaded dict, so a device that has
to adjust the schema before registering it can load the JSON itself and pass
the object.
Or register parameters in code (useful for dynamic ranges that depend on constructor arguments):
class PowermeterDevice(BaseDeviceSyncModel):
def __init__(self, address: str, sensor_type: str):
super().__init__(id=address)
min_wl, max_wl = SENSOR_RANGES[sensor_type]
self.register_config("WAVELENGTH", dtype=int, unit="nm",
min_value=min_wl, max_value=max_wl,
command="SENS:CORR:WAV")
Tip: Prefer schema-driven registration for devices with many parameters — an LLM can generate
schema_param.jsondirectly from the vendor manual.