Device agent
The device agent is a small program you run on the computer your instruments are wired to. It opens one outbound WebSocket to the Orca daemon, takes commands on it, drives the instrument through its driver, and sends the answer back on the same connection.
You need it when the instruments are not on the machine running the daemon, or when you want the vendor libraries to live next to the hardware in their own process. If you are running the quickstart in simulation on one machine, skip this page. Sim drivers are built in-process and no agent is involved.
Lab bench PC Daemon host
+-----------------------------+ +--------------------------+
| shaker (serial) | | Orca runtime |
| arm (TCP) | | scheduler |
| liquid handler (USB) | | |
| | | | GET /ws/devices |
| v | | ^ |
| device agent ----------|--- wss --->|----------+ |
+-----------------------------+ outbound +--------------------------+
The agent always dials out. The daemon never dials in, so the bench PC needs no inbound firewall rule.
Install
The agent is the swarm-client package. It is not on PyPI, so install it from a clone. Python 3.10 or newer.
git clone https://github.com/Cheshire-Labs/swarm-client.git
cd swarm-client
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux, macOS
pip install -e .
The agent also needs cheshire-drivers, which holds the driver classes and the wire protocol. It is not pinned as a dependency, so install it from its own clone into the same virtual environment.
From here on this page calls it the device agent.
Configure
The agent reads one JSON file. It defaults to config.json in the working directory, and --config points it somewhere else.
{
"client_id": "lab-workstation-1",
"site": "boston",
"lab": "molbio",
"workcell": null,
"platform": {
"url": "wss://your-deployment.example.com/ws/devices",
"api_key": "${SWARM_API_KEY}",
"heartbeat_interval": 30.0
},
"devices": []
}
| Field | What it is |
|---|---|
client_id | A label for this agent, written into its own logs. |
site, lab | How the daemon names the connection. It registers the agent as <site>-<lab>-client. |
workcell | Optional extra label. null is fine. |
platform.url | The daemon's /ws/devices endpoint. Required. |
platform.api_key | Sent as the X-API-Key header on the handshake. Required, and must not be empty. |
platform.heartbeat_interval | Seconds between heartbeats. Defaults to 30. |
devices | The list described in the next section. |
Any string value may contain ${VAR}, which is replaced from the environment when the file is read. Keep the key out of the file that way.
Three fields also have an environment fallback, used only when the config file leaves them blank:
| Variable | Falls back for |
|---|---|
SWARM_URL | platform.url |
SWARM_API_KEY | platform.api_key |
SWARM_HEARTBEAT_INTERVAL | platform.heartbeat_interval |
--env path/to/.env loads a .env file before the config is read. Without it the agent uses the process environment.
Which URL
The scheme is checked before the agent dials. ws:// is accepted only for localhost, 127.0.0.1 and ::1. Any other host must be wss://, and the agent refuses to start otherwise.
The framework daemon binds 127.0.0.1 and picks a free port at start, so on a single machine the URL is ws://127.0.0.1:<port>/ws/devices. Pin the port with orca start --port 8000, or read the one it chose from orca status.
Authenticating
The daemon checks the X-API-Key header at the handshake and closes the socket with code 1008 if the check fails. The framework daemon ships an allow-all check, because a single node trusts its own machine, so any non-empty key works there. A hosted deployment installs a real check and the key must be the one it issued.
Declare your devices
Each entry in devices is one instrument. The name is the binding key: an agent device named shaker_1 binds to a topology Shaker("shaker_1"). Get that wrong and the daemon refuses the connection.
"devices": [
{
"type": "shaker",
"name": "shaker_1",
"driver": {
"type": "plr",
"backend": "InhecoThermoShake",
"connection": { "type": "serial", "port": "COM3", "baudrate": 9600 }
}
},
{
"type": "transporter",
"name": "arm_1",
"driver": {
"type": "plr",
"backend": "PreciseFlex",
"connection": { "type": "tcp", "host": "192.168.1.100", "tcp_port": 10100 }
}
}
]
type is the device kind, one of shaker, centrifuge, sealer, transporter, translator, liquid_handler, plate_washer, reader, delidder, storage, waste, thermocycler.
driver.type is how the instrument is driven:
driver.type | Drives |
|---|---|
plr | A real instrument through a PyLabRobot backend, named in backend. |
venus | A Hamilton Venus protocol runner. |
sim | A simulated instrument, for testing the wire with no hardware attached. |
connection takes type of serial, tcp or usb, with port and baudrate for serial and host and tcp_port for TCP.
Two things you do not configure here. Teachpoints stay on the daemon: it resolves them and pushes the coordinates with each move command, so the agent holds no local copy. And the agent builds both a real driver and a sim driver for every device, then the daemon picks between them per run mode, so you do not switch modes in this file.
Run it
python -m swarm_client --config config.json
| Flag | Effect |
|---|---|
--config, -c | Config file path. Defaults to ./config.json. |
--env, -e | Load this .env file before reading the config. |
--verbose, -v | Debug logging. |
--version | Print the version and exit. |
The agent connects, sends its device list, then waits. Devices are left uninitialized on purpose. The daemon calls initialize when it is ready, so it owns the timing.
Confirm the device connected
Ask the daemon, not the agent:
orca device registry list
The client column says whether this agent is reachable. The device column says whether the link is open on the driver being driven. They differ: a device you have taken by hand sits under an agent that is still heartbeating. link names which driver answered, and modes shows which run modes the device is eligible for now that its capabilities are known.
orca device list is the shorter view of the same runtime: name, kind, busy, initialized, mode and fault, for every device the topology declares. Use it once you know the agent is up.
Advanced
Read this when a connection is refused, or when you are working out what happens to a run while an agent is away.
Protocol version
The agent and the daemon both take the wire protocol from cheshire-drivers, currently version 1.3.0. The agent stamps it on its connect message and the daemon compares it against its own before parsing anything else.
A mismatch is fatal and immediate. The daemon closes the socket with code 1002 and the reason Incompatible protocol version: expected <version>. There is no negotiation and no compatibility window, so the agent and the daemon must come from matching builds. Upgrade one, upgrade the other.
Reconnecting
A dropped link is retried automatically. The agent waits 5 seconds, doubles the wait on each failure, and caps it at 60 seconds. It keeps trying until you stop it.
On reconnect the daemon assumes nothing survived. A returning agent may have rebuilt its drivers from scratch, so the runtime forgets what it had cached about that device's bring-up, both the driver's own initialized flag and the system's bring-up record. The next explicit orca device initialize or connect then actually reaches the instrument instead of being skipped as already done.
The runtime also marks an observation gap on every piece of labware sitting at that device, because nobody was watching while the link was down, and it re-reads the deck if the device is a liquid handler. That re-read is a background task, not part of the handshake. What it does not do is move anything. Homing and bring-up stay behind their own verbs.
A command that was in flight when the link dropped leaves a device fault on the device. The workflow will not drive a faulted device. Look at the instrument, put it right, then clear the record with orca device clear-fault <name>.
One instrument, one agent
Device names are claimed at the handshake. If a name in the connect message is already held by another live agent, the daemon refuses the newcomer and closes it with code 1002 and a reason naming the device and its current owner.
This is deliberate and it is not a handover. Two agents wired to one instrument is a misconfiguration: both sockets would stay open, both would believe they own the device, and commands would route to whichever registered last. Stop the incumbent agent first, then start the replacement.
The same handshake also re-checks the advertised drivers against the topology. An agent whose device advertises a contract the topology contradicts, because someone swapped a driver class on the bench PC, is closed with 1002 and the offending device named.
Taking a device by hand
To drive an instrument yourself while a workflow is running, take it out of the workflow's reach rather than stopping the agent:
orca device take-control arm_1 --reason "re-teaching the hotel"
orca device release-control arm_1
A thread that tries to use a held device, or to move labware into or out of it, fails until you release it. Without this a workflow can start a move into the device between two of your own commands. The agent stays connected throughout, which is why orca device registry list can show client true and device false at the same time.
See also
- Devices for the device classes your workflow calls and how capabilities are decided.
- Topology for declaring the devices the agent binds to.
- Transporters for arms, translators and how moves are planned.
- Simulation hierarchy for what each run mode drives, and which ones never reach the agent.
- CLI: inspect for the rest of the
orca deviceverbs.