Repository navigation
Example 4 — SCPI device: annotated attributes + per-attribute filler data #405
Description
Part of the tutorial examples (#388). Tutorial 3 — declarative: a NON-introspectable device whose attributes are declared as annotated hints, with each attribute's getter/setter provisioned from per-attribute filler metadata (extras mechanism, #388 decision 3) via ControllerFiller.
Runs on the temperature-controller backend — its text protocol is already SCPI-shaped, so no new sim is needed and, crucially, no introspection is invented. A SCPI device does not describe itself; that is why you hand-annotate, and keeping it non-introspectable preserves the contrast with the introspectable Eiger tutorial (#391).
This tutorial also carries composition + methods (the old "io= pattern" tutorial is gone): it walks the full multi-ramp temperature controller — ramp sub-controllers / ControllerVector, the voltage @scan, the cancel-all @command — declared via SCPIParam, drawing on the composition example (controllers.py, #390).
Scope
- Add
src/fastcs/demo/scpi.py: an exampleSCPIController+SCPIParamextras vocabulary. This lives in the demo (protocol) layer, NOT core FastCS (decision 3: core ships no extras vocabulary for 1.0) — it demonstrates how a third party builds a protocol layer on the filler's(child, extras)yield. - Add
src/fastcs/demo/temperature_scpi.py: the full temperature controller as annotated hints —Annotated[AttrRW[float], SCPIParam("P", precision=3, units="W", description=…)]— with theSCPIController'sControllerFillerbuilding each attribute's getter/setter (and applying.meta) from that static metadata; plus sub-controllers/vector +@scan+@command. - Unit tests against the sim, including the negative case below.
SCPIParam — one spec object per attribute (ADR 0014)
The canonical demo of the common declarative spelling.
SCPIParam(param: str, **kwargs: Unpack[Meta])(Meta= the superset TypedDict) is the single place an attribute's whole spec is written: the command token plus all generic metadata (description,precision,units, limits…). It stores a.meta.- Exclusive spec source for its attribute — the filler does not also merge a separate
FloatMeta/Metaextra on the same hint. - The filler builds the getter/setter and passes
.metainto the constructed attribute, runtime-validating it against the datatype. - Named
SCPIParam(a binding extra you instantiate, sibling ofPvSuffix), notSCPIMeta— the*Metasuffix is reserved for theUnpack-able TypedDicts. - Show a valid case, and (in a test) that supplying
precisionfor astrattribute raises with a message naming the attribute + field.
Acceptance
uv run --locked tox -e pre-commit,type-checking,tests green.
ADR: docs/explanations/decisions/0013-declarative-procedural-split-and-controller-filler.md, 0014
Parent: #388
Blocked by: #394
Activity
coretl commentedon Sep 9, 2026
Opened #427 against
refactor, now that #394 has landed as #426.src/fastcs/demo/scpi.pycarriesSCPIParamandSCPIController, andsrc/fastcs/demo/temperature_scpi.pythe full multi-ramp temperature controller declared as annotated hints, running on the existing temperature sim. Tests include the negative case —precisionon astrattribute raises, naming the attribute and the field.Three points are flagged for a human under "Checks for reviewer" there rather than decided here: whether the two scan-updated attributes should also go through
SCPIParam, whether a per-attribute poll period is wanted (it is not metadata, so it has no place inMeta), and the onecastwhere the declared datatype doubles as the parser for the device's text answer.— overnight agent
Generated by Claude Code