Messaging#
Two distinct communication problems#
The ICS has two communication problems, and it is important not to confuse them:
Daemon to daemon |
Driver to device |
|
|---|---|---|
Protocol |
One common envelope, instrument-wide. |
Whatever the device speaks. |
Chosen for ZShooter |
Libby (Bamboo envelope). |
Per device: serial, TCP ASCII, EtherCAT, SNMP, EPICS, MQTT, vendor SDK. |
Who sees it |
Every daemon and every client. |
Only the owning driver. |
Changeable? |
Transport yes, envelope no. |
Freely. It is an implementation detail. |
Device protocols never appear on the instrument bus. If a vacuum gauge speaks MQTT and a motion controller speaks EtherCAT, that difference stops at the driver; both are represented on the bus by identical keyword reads and writes.
Libby#
ZShooter uses Libby for all daemon-to-daemon and client-to-daemon messaging. Libby provides:
the Bamboo message envelope, common to every message on the bus;
pluggable transports, ZeroMQ (peer-to-peer) and RabbitMQ (brokered), selectable per deployment without changing daemon code;
request/response (
rpc) and publish/subscribe (publish/topics) on the same envelope;a typed keyword layer over RPC (see Keywords and addressing);
a ``LibbyDaemon`` base class providing lifecycle, discovery, service registration, and topic subscription;
a CLI that can read and write any keyword on any peer.
A ZShooter daemon is a LibbyDaemon subclass. It declares its peer identity,
registers typed keywords for the hardware it owns, subscribes to any topics it
needs, and calls serve().
The message envelope#
Every message on the instrument bus carries the Bamboo envelope:
Field |
Type |
Meaning |
|---|---|---|
|
int |
Protocol version. Strictly incrementing. |
|
MsgType |
Message type. ZShooter uses |
|
str |
Transaction ID (UUID). Correlates a request with its progress events, its response, and its log records. |
|
str |
The keyword, command, or topic this message concerns. |
|
dict |
JSON object: the value, parameters, result, or error. |
|
str |
Sender’s message timestamp. |
|
str |
Identity of the sending peer. |
|
str or None |
Identity of the receiving peer; |
Binary data, for example a compressed detector frame, travels alongside the
envelope as an optional binary field on the message rather than inside the
JSON payload.
Bamboo also defines ACK, SUBSCRIBE, and CONFIG. ZShooter does not
use ACK: see One request, one reply below.
Envelope and payload division#
The envelope carries routing, identity, and correlation. The payload carries content. The payload does not repeat what the envelope already says.
For keyword traffic, Libby fixes the payload convention:
{} -> show (read current value)
{"value": V} -> modify (apply V, then return the resulting value)
and responses carry {"ok": true, "value": V, "units": "..."}.
Request and response#
One request, one reply#
Every request gets exactly one reply. There is no separate acknowledgement
message. A REQ is answered by a RESP, whether the command succeeded,
was rejected, or failed.
Rejection is fast because validation is cheap and happens before the daemon touches hardware. A command that is going to be refused is refused in milliseconds, which is what a two-stage acknowledgement would otherwise have been used to convey.
For a long-running command the reply arrives at completion, and progress in the meantime is published, not returned. Clients therefore have exactly one thing to wait for and one place to find the outcome.
Message flow#
sequenceDiagram
participant C as Client (GUI / sequencer)
participant D as Device daemon
participant H as Hardware
C->>D: REQ transid=T key=zsvis.motion.slitwidth {"value": 1.0}
D->>D: validate against limits + current state
D-->>C: PUB zsvis.motion.status (state: busy)
D->>H: driver call
D-->>C: PUB transid=T progress
H-->>D: motion complete
D-->>C: RESP transid=T {"ok": true, "value": 1.0, "units": "arcsec"}
D-->>C: PUB zsvis.motion.status (state: idle)
A rejected command produces an immediate RESP carrying the error, and the
daemon never enters busy. A command that is accepted and then fails reports
the failure in its RESP at the point it gives up. The two are
distinguishable without parsing prose, which matters because they mean
different things about the hardware: see State models.
Timeouts#
Because there is no acknowledgement, a client’s timeout must cover the whole
operation, not just its acceptance. This is why every slow keyword advertises
timeout_s in its metadata and why clients read it before issuing a modify.
See Keywords and addressing.
Broadcast#
Commands are point to point: one client asks one daemon to do one thing. Almost everything else is broadcast. Instrument state, telemetry, events, progress, and alerts are published once and consumed by whoever needs them.
Why broadcast is the default#
State has many consumers and one owner. Detector temperature is needed by the detector daemon’s own logic, the sequencer’s readiness check, both GUIs, the FITS header writer, the trending database, and the night log. Serving that by request would mean seven pollers asking the same question and seven different answers in flight at once.
Publication does not depend on being asked. A daemon publishes whether or not anyone is listening. A client that connects mid-night sees the full instrument state without a startup handshake, and a client that restarts recovers by subscribing rather than by interrogating twenty daemons.
It keeps clients out of the command path. A GUI that polls with commands generates command traffic, fills the command log, and competes with the sequencer for a daemon’s attention. A GUI that subscribes costs nothing extra.
Topics#
Broadcast topics use the same namespace as keywords, so there is one vocabulary rather than two:
<group>.<scope>.<name> state and telemetry, per keyword
<group>.<scope>.status compound daemon status
<group>.<scope>.event events from that daemon
<group>.<scope>.alert alerts from that daemon
<group>.<scope>.heartbeat liveness
zsseq.obs.sequence sequence progress
zsseq.obs.instrument aggregated instrument state
<transid> progress for one running command
Subscribers use the same % wildcard convention as keyword queries, so a
status panel can subscribe to zsvis.motion.% and the watchdog to
%.%.heartbeat.
Channels#
Channel |
Cadence |
Retained |
Consumer response |
|---|---|---|---|
Heartbeat |
1 Hz |
No |
Detect absence. Silence is the signal. |
State |
On change, plus periodic refresh |
Last value |
Display; gate decisions on it. |
Telemetry |
Per keyword class |
Historical store |
Trend, diagnose, write to headers. |
Event |
On occurrence |
Log |
Record. |
Alert |
On occurrence |
Until cleared |
Act. See Errors, alerts, and logging. |
Periodic refresh#
State is published on change and on a slow periodic cadence. Publishing only on change is an appealing optimisation that fails in two specific ways:
a subscriber that connects after the last change has nothing, and cannot tell “no change since I connected” from “this daemon is not publishing”;
a dropped publication is invisible, and a consumer can hold a wrong value indefinitely. With periodic refresh the state self-corrects on the next cycle.
Staleness#
Every published value carries the time it was produced, and consumers are expected to use it.
A value is stale when it is older than a per-keyword threshold, typically a small multiple of its publication cadence. Stale is a third state alongside good and bad, and it must be visible:
GUIs display staleness rather than showing an old number as though it were current. A frozen value that looks live is worse than a blank one.
Interlocks treat stale dependencies as blocking, per Safety.
The sequencer does not gate a decision on stale state; it waits or stops.
Staleness is why heartbeats exist separately from state. A daemon can be alive and healthy while one value is stale because its hardware stopped responding, and the two conditions call for different responses.
Delivery expectations#
Broadcast is best effort, and there are no retries at the protocol level.
This is workable because of what is broadcast: a dropped telemetry sample is replaced by the next one, and a dropped state publication is corrected by the next refresh. It is not workable where a single missed message changes behaviour, which is why commands are request and response rather than broadcast, and why alerts are retained until cleared rather than fired once.
Ordering is not guaranteed across topics. A consumer that needs two values to agree with each other reads them from one compound status publication rather than correlating two separate ones.
Subscriber rules#
Subscribe; do not poll. A client that repeatedly asks for a value it could have subscribed to is a defect, not a style preference.
Handle absence explicitly. Every subscriber needs defined behaviour for “this topic has produced nothing for longer than expected”. Blank is honest; a stale value presented as current is not.
Do not republish. A consumer that re-broadcasts state it received creates a second source of truth with a different timestamp. Values derived from several daemons are published by the one daemon that owns the derivation, as the sequencer does for aggregated instrument state.
Transport selection#
The transport is a deployment choice, set per daemon in configuration:
Transport |
Properties |
Suits |
|---|---|---|
ZeroMQ |
Peer-to-peer, no broker, lowest latency. Requires each peer to bind an address and to hold an address book. |
Bench work, single-host development, latency-sensitive paths. |
RabbitMQ |
Brokered. No address book; the broker routes. Easier to scale and to monitor; the broker is a dependency. |
Multi-host summit deployment. |
Switching is a one-line change in a daemon’s configuration. Because both transports carry the same envelope, a client does not need to know which is in use, and the two can coexist during migration.
Note
TBC: deployment transport
The reference transport for the summit deployment is not yet fixed. RabbitMQ is the likely choice given the multi-host layout, but this should be settled by prototyping the vertical slice described in Testing. Recorded as ADR-0003 in Architectural decisions.
Known gaps#
Open items against Libby that ZShooter depends on:
Gap |
Impact on ZShooter |
|---|---|
No |
Failures ride in the |
Peer discovery requires manual key learning |
|
No command cancellation primitive |
Aborting an exposure or halting a stage mid-move is a hard
requirement. ZShooter defines |
No retries at the protocol level |
By design. Retry policy is an application concern and must be defined per command class rather than assumed. |
Envelope field names differ from the COO specification |
Bamboo uses |