Command and Control#
This page describes the contract between a HISPEC daemon and anything that talks to it. It is the instrument-specific expansion of the COO Command-and-Control Design Specification.
The keyword model#
HISPEC has no separate “command” concept. Everything — reading a temperature, moving a stage, homing an axis, stopping a daemon — is a keyword: a typed, named value with a uniform two-shape payload convention.
{} → show: return the current value
{"value": V} → modify: apply V, then return it
A keyword is read-only, write-only or read-write depending on whether it was registered with a getter, a setter, or both. Triggers are write-only keywords whose value is irrelevant — firing is the point.
Each keyword carries metadata that clients can query rather than hardcode:
type, access mode, units, a human description, a validator, and
timeout_s for operations too slow for the default RPC deadline. The CLI uses
timeout_s to extend its own deadline when it modifies a slow keyword, so
homing a stage does not time out on the client side while succeeding on the
hardware.
Two meta-services are auto-registered on every daemon, which makes a daemon self-describing:
libby list hsfei.atcfw.% # every keyword this daemon serves
libby describe hsfei.atcfw.positionvalue
Why keywords rather than commands#
This inherits Keck’s KTL keyword model, and it is the reason the migration
described in Architecture Evolution was tractable: operators, scripts and
documentation at Keck already think in terms of keywords, and a keyword-shaped
interface over a new transport preserves that vocabulary. The cost is that
HISPEC gets no free command lifecycle — the spec’s accepted / running /
completed progression has no direct representation. See
Conformance and Roadmap.
Addressing#
<group>.<peer>.<keyword>
Group and scope must both be explicit; % wildcards are allowed in the keyword
name for list, not in the group or peer. Names are matched case-insensitively
because both client and daemon normalise through the same function.
Client surfaces#
CLI — for interactive work and shell scripts:
libby show hsfei.ms.positionnamed
libby modify hsfei.ms.positionnamed keck_mask
libby list hsfei.ms.%
libby waitfor hsfei.ms.ismoving == false
waitfor is libby’s equivalent of ktl.waitFor: it blocks until a keyword
satisfies a condition, which is how a script sequences a move without polling.
Python — libby.Client is a long-lived in-process handle for reading and
writing keywords and for wait_for. This is the supported path for engineering
scripts.
Direct RPC — libby.rpc(peer, key, payload) underneath both of the above.
There is no GUI client at present.
Validation and safety#
Safety is enforced in the daemon, next to the hardware, as the specification requires. A client may pre-validate for usability but is never the only check.
The enforcement points, in the order a write passes through them:
Type coercion — the keyword’s declared type rejects a value that cannot be coerced.
Validator — a per-keyword callable returning an error string. Motion keywords use it for soft limits (
_check_soft_limits) and named-position keywords for membership in the configured set (_check_named), which is also where the error message lists the available names.Connection check — every hardware-touching method verifies the connection first and returns
{"ok": False, "error": ...}rather than calling into a dead driver.Daemon-specific interlocks — the clearest example is the PI daemon’s
shutdown, which refuses while any stage is moving: stopping a daemon mid-motion would abandon a moving stage with no owner.
Soft limits are re-checked inside set_pos as well as in the validator. The
duplication is deliberate — the method is reachable from paths that do not go
through keyword validation.
Safe-state behaviour#
halt stops motion on every axis a daemon owns and is safe to repeat. on_stop
disconnects from the hardware on the way down.
Beyond that, safe-state handling is still being built out: HISPEC has no instrument-wide safe state, no park positions, and no fault state that inhibits further commands — a daemon that has recorded an error will still accept the next write. The direction is for each daemon to define what safe state means for its own subsystem and to refuse non-recovery commands once faulted; see Stage 1 of the roadmap.
Error reporting#
A HISPEC daemon reports failure through three channels, which is more than the specification requires but less coherent than it asks for:
Channel |
Shape |
Who sees it |
|---|---|---|
Raised exception |
RPC error back to the caller |
the client that made the failing call |
|
last error string in the daemon’s own state |
any client that reads it |
|
last |
any client that reads it |
Log |
journald, or |
whoever is on the host |
Internally, daemon methods return {"ok": bool, ...} dicts, and a
keyword_wrapper helper converts a non-ok result into a RuntimeError at the
keyword boundary so the failure reaches the client rather than being swallowed.
There is no structured error model — no error codes, no machine-readable categories. Errors are strings.
Daemon lifecycle#
stateDiagram-v2
[*] --> Starting: systemd ExecStart
Starting --> Registering: from_config_file() + serve()
Registering --> Serving: keywords registered
Serving --> Connected: hardware connect succeeds
Serving --> Degraded: hardware connect fails
Degraded --> Connected: write isconnected = true
Connected --> Degraded: connection lost / isconnected = false
Connected --> Stopping: shutdown trigger or SIGTERM
Degraded --> Stopping: shutdown trigger or SIGTERM
Stopping --> [*]: on_stop() disconnects
The sequence in on_start is load config → register keywords → attempt
hardware connection. Keywords come first so the daemon is inspectable even when
the connection fails; Degraded above is a description of observable behaviour,
not a state the daemon names or publishes.
Shutdown has two paths: SIGTERM from systemd, and the shutdown keyword, which
lets an engineer stop a daemon over the same interface used for everything else
rather than needing host access. Both land in on_stop, which disconnects the
hardware.
The states in this diagram are not exposed as a keyword. A client infers
them from uptime and isconnected.
Transport#
RabbitMQ, brokered, set once instrument-wide by HispecDaemon. libby
discovery is disabled because the broker provides its own; a daemon is
reachable as soon as it binds, without a discovery handshake.
The wire envelope comes from bamboo and carries protocol version, message
type (REQ/RESP/ACK/PUB/HELLO/CONFIG/SUBSCRIBE), a transaction ID,
the key, source and destination IDs, a timestamp, and the JSON payload with
optional binary attachment. HISPEC does not construct envelopes itself and does
not surface the transaction ID to daemon code — see Conformance and Roadmap for what
that costs.
Pub/sub exists in libby and is unused by HISPEC: no daemon publishes telemetry or events. All monitoring is polled.
Idempotency#
The specification asks each command to declare whether repeated invocation is safe. For HISPEC’s keyword set this follows from the keyword kind:
Keyword kind |
Idempotent |
Note |
|---|---|---|
Any read ( |
yes |
No side effects. |
|
yes, w.r.t. destination |
Re-commanding the same target is safe; commanding a new one during a move is not guarded. |
|
yes |
Absolute values. |
|
yes |
Writing |
|
no |
Writing |
|
yes |
Safe to repeat, by design. |
|
yes |
Second call has nothing to stop. |
|
no |
Destroys accumulated counters. |
This table is documentation of current behaviour, not an enforced contract; nothing in the code marks a keyword idempotent.