DiamondLightSource / DiamondLightSource/fastcs
Example 4 — SCPI device: annotated attributes + per-attribute filler data
- Dominant language
- Python
- Stars
- 6
- Forks
- 8
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 3
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 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
Contributor guide
Research direction
Start with docs/explanations/decisions/0013-declarative-procedural-split-and-controller-filler.md and 0014, then inspect the temperature-controller simulator and the blocked dependency #394. Implement the examples in src/fastcs/demo/scpi.py and src/fastcs/demo/temperature_scpi.py with unit tests covering valid metadata and the invalid string precision case. Done means uv run --locked tox -e pre-commit,type-checking,tests is green.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100