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 example SCPIController + SCPIParam extras 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 the SCPIController's ControllerFiller building 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/Meta extra on the same hint.
- The filler builds the getter/setter and passes
.meta into the constructed attribute, runtime-validating it against the datatype.
- Named
SCPIParam (a binding extra you instantiate, sibling of PvSuffix), not SCPIMeta — the *Meta suffix is reserved for the Unpack-able TypedDicts.
- Show a valid case, and (in a test) that supplying
precision for a str attribute 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
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 viaSCPIParam, drawing on the composition example (controllers.py, #390).Scope
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.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.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.FloatMeta/Metaextra on the same hint..metainto the constructed attribute, runtime-validating it against the datatype.SCPIParam(a binding extra you instantiate, sibling ofPvSuffix), notSCPIMeta— the*Metasuffix is reserved for theUnpack-able TypedDicts.precisionfor astrattribute raises with a message naming the attribute + field.Acceptance
uv run --locked tox -e pre-commit,type-checking,testsgreen.ADR: docs/explanations/decisions/0013-declarative-procedural-split-and-controller-filler.md, 0014
Parent: #388
Blocked by: #394