Skip to main content

Devices

A device is an instrument Orca can drive: a shaker, a sealer, a plate reader, a liquid handler. You construct one with a name, and nothing else:

from orca.devices.shaker import Shaker
from orca.devices.sealer import Sealer

shaker = Shaker("shaker")
sealer = Sealer("sealer")

The driver comes later. Each device class is a thin wrapper: the class is what your workflow talks to, the driver is what talks to the hardware or the simulator, and the device factory in scope at topology-build time decides which of the two you get. Put the devices in a topology and the same code runs in sim and on the bench.

Device types you can use

ClassUse forImport
ShakerPlate shakers and incubatorsorca.devices.shaker
SealerHeat sealers and de-sealersorca.devices.sealer
CentrifugeCentrifuges (single or multi-plate)orca.devices.centrifuge
ThermocyclerThermocyclers (PCR block + lid)orca.devices.thermocycler
LiquidHandlerDeck-modeled liquid handlers (Hamilton STAR, Opentrons Flex / OT-2)orca.devices.devices
LiquidHandlerProtocolDeckless / protocol-driven handlers (Bravo, VWorks, remote)orca.devices.devices
ReaderPlate readers, imagersorca.devices.devices
StoragePlate stackers, hotelsorca.devices.devices
WasteWaste / discard locationsorca.devices.devices
PlateWasherPlate washers (BioTek, etc.)orca.devices.devices
DelidderLid removal stationsorca.devices.devices

Vendor-specific concrete classes also exist: Venus (Hamilton Venus protocol runner) and A4SSealer, both importable from orca.sdk.devices.

LiquidHandler is the one class that takes more than a name: it models a deck, so it takes a deck layout, and its arguments are keyword-only. See Topology for the full constructor and for LiquidHandlerProtocol's site_names.

HumanTransfer is not a device. It is a transporter subclass for manual moves with operator prompts; see Transporters.

Import note: orca.sdk.devices re-exports Device, ResourcePool, Transporter, Shaker, Sealer, Centrifuge, Thermocycler, Venus, A4SSealer, and HumanTransfer. The handler, reader, storage, waste, washer, and delidder classes come from orca.devices.devices. Import those directly, as the maintained examples do.

Calling a device from a workflow

Each device class implements an Orca-side interface (IShaker, ISealer, ICentrifuge, IThermocycler, ILiquidHandler, IReader, IProtocolRunner) in orca.devices.device_interfaces. Use it as a typed handle inside an action:

from orca.devices.device_interfaces import ILiquidHandler

@orca.action(device=flex_1, inputs=[sample_plate, tips])
async def transfer(ctx: ActionContext):
lh = ctx.device(ILiquidHandler) # typed handle
plate = ctx.plate("sample_plate")
column = [plate.well(f"{row}1") for row in "ABCDEFGH"]
await lh.pick_up_tips(await ctx.next_tips("tips", 8))
await lh.aspirate(column, [50.0] * 8)

Some signatures: IShaker.shake(duration, speed), ICentrifuge.centrifuge(g, duration), ISealer.seal(temperature, duration), IProtocolRunner.run_protocol(protocol_filepath, params). ctx.device() with no argument returns an untyped handle, which is enough for a one-line call.

Sim or real driver

Devices have no hardware-specific code in them. Each device class calls resolve_drivers(kind, name, default_sim) in its constructor, which consults the device factory context in scope to pick the (live, sim) driver pair: the bound factory's drivers, or the default sim driver when none is bound. Swapping the factory context swaps every device's driver without touching the topology or the device classes. See Sim hierarchy for which world a run means, and PyLabRobot for the driver layer underneath.

Advanced

Read this when you are adding a device type, working out why a command is refused, or driving an instrument by hand.

The KIND string

Every device declares a KIND string ("shaker", "liquid_handler", "translator", ...). The kind selects the driver pair from the factory, labels the device in operator surfaces, and is the same string a connected on-prem client advertises. It is advisory at the contract level: the driver's interfaces are the safety gate, not the kind.

What a device can be asked to do

There is no capability registry. A driver interface's abstract methods are the capability contract. A device advertises the interfaces its bound driver declares, and the gateway resolves a command by asking whether it sits on one of them.

Two sets of interface names, and they are not interchangeable. The driver interfaces live in cheshire_drivers.interfaces and all end in Driver: IShakerDriver, ITransporterDriver, ILiquidHandlerDriver. They are what a driver implements and what decides capabilities. The device interfaces live in orca.devices.device_interfaces and carry no suffix: IShaker, ILiquidHandler, IReader. They are the typed handles you pass to ctx.device(...) in an action. A third form, the bare advertised name ("IShaker", "IHomeable"), is the string a driver puts in its interfaces frozenset and the key the gateway resolves against.

Three rules follow from that.

Interface methods are the contract. IShakerDriver, ISealerDriver, ILiquidHandlerDriver and friends live in cheshire-drivers. Their abstract methods (plus a @property an interface declares) are exactly what a device advertising that interface can be sent. Adding a method to an interface adds a capability; nothing else needs updating.

@external governs discoverability, never validity. An interface method marked @external (from cheshire_drivers.driver_introspection) shows up in the operator-facing method catalog. An unmarked one is internal plumbing (deck-occupancy writes, transporter world-sync ops) and is hidden from the catalog. Hidden is not refused: the engine still dispatches those, and they are still part of the contract.

Commands on a vendor object have to be declared. Most driver wrappers hold their vendor object rather than inheriting from it. The PLR wrappers hold one as self._backend; the Opentrons Flex driver holds an OpentronsFlex plus its gripper and its mounts. Nothing on those objects is reachable from the driver class, so each one is declared in a vendor_surfaces: ClassVar[tuple[VendorSurface, ...]]. A vendor surface carries the attribute path to the object, the class whose public async methods are advertised, and a prefix that namespaces them on the wire (gripper.ungrip). The class is declared rather than read off a live attribute because advertisement happens at the handshake, before the device has connected. Undeclared means unadvertised, uncataloged, and uncallable.

Everything else on a concrete driver that is not on any interface is derived automatically as a vendor extra, so a diagnostic helper a vendor driver defines is callable without anyone listing it.

Metadata reads (what a device advertises, what its command timeouts are) read the live driver unconditionally, because that is what describes the deployment's real capability surface. Dispatch reads the driver the resolved run mode selects.

Capabilities that are their own interface

Some abilities do not follow from what a device is for, so they are separate interfaces a driver opts into:

InterfaceWhat it adds
IHomeableDriverhome. An arm and a liquid handler both home; a shaker has nothing to home. A liquid handler that advertises IHomeable can be homed.
ILiquidProbeDriverliquid_probe. The head can sense the liquid surface. A Flex and a STAR read pressure as a tip descends; an OT-2 has no sensor, so it does not declare this and the command is refused at the gateway rather than failing at the deck.
IGantryParkingDriverpark_gantry. Lets an arm ask the handler to get off its own deck. See Transporters.
IPipetteMotionDriver, IGripperMotionDriver, IGripperPositionDriverDirect Cartesian motion for teaching and troubleshooting.
ITempSettableDriver, ITempGettableDriverSet or read a temperature, independently of device kind.

Only the raw console asks for a confirmation

Exactly one thing needs an explicit confirm: the raw vendor console (send_command and a backend's aliases for it). Its catalog entry carries requires_confirm: true, and the controller refuses it without confirm=True (--confirm on the CLI). Every other vendor extra dispatches freely, motion and settings writes included, because each carries a signature and a docstring the operator surface can show first. A confirmation on every command is a confirmation on none. Free to dispatch is not the same as safe: the surface is responsible for telling the operator what a command does.

Device lifecycle

Four verbs, and they are deliberately separate. CLI: operator interventions covers driving them by hand.

VerbWhat it doesCLI
connectOpens the link to the device and confirms it answers. Moves nothing. Safe to call on a device that is already linked.orca device connect <name>
initializeBrings the device up so it accepts commands, and resets driver state. Whether it moves anything is the driver's business.orca device initialize <name>
homeDrives every axis to its reference. Moves the device, sweeping its full envelope through anything in the way. Always a deliberate operator request.orca device send <name> home
disconnectHands the device back to its own controls without moving it. On hardware that holds an exclusive session (an Opentrons robot refuses its touchscreen while a run is open), this is how an operator gets it back without a power cycle.orca device disconnect <name>

Bring-up does not home. A device whose axes lose their reference at power-off has to advertise IHomeable so an operator can make its first move safe. connect exists so a device can be checked for reachability without committing to an initialize that, on most hardware, ends in motion.

Many drivers have no link separate from bring-up, so connect and disconnect default to doing nothing and is_connected tracks is_initialized. A driver holding a real per-connection session overrides all three.

Every one of these verbs takes --mode to say which world it means. Outside a run they default to LIVE, and a device's topology sim_override still ratchets the answer toward sim. See Sim hierarchy.

What a device is holding

A device does not keep its own list of labware. It walks the site Locations it owns, and each site answers from the one position ledger. orca device info <name> reports residents by instance id, not by template name, so two racks of one template on one deck are two distinct rows. See CLI: inspect the system.

Device faults

A command that does not come back clean latches a fault on its device. The fault is device-scoped and operator-cleared, and while it stands the engine will not drive that instrument at all (ADR-016).

The question that decides it is narrow: did the failure leave the instrument somewhere nobody chose? The driver answers it rather than the caller guessing. InstrumentOutcome (in cheshire_drivers.driver_errors) is declared on the error class and travels on the wire:

OutcomeMeaning
REFUSEDThe driver checked its own state and did not act. The same request will succeed later.
REJECTEDNothing acted on it and nothing ever will: a command the agent does not have, a device it does not hold, a payload that will not deserialize.
FAILEDIt ran and stopped part-way.
UNKNOWNDispatched, no answer. Nothing told it to stop.

A refusal that moved nothing is not a device fault. REFUSED and REJECTED leave an untouched instrument, so there is nothing to fault. REFUSED alone is worth waiting on and retrying. An error class that declares nothing gets FAILED, deliberately: a caller told the machine stopped part-way goes and looks, which is safe on a machine that merely refused.

Faults surface on every device read. A faulted device reports status: "faulted" rather than the agent's word for its idle driver, and the fold happens before the status filter, so a query for faulted devices is a real query and a query for ready ones cannot hide a fault.

To clear one: recover the paused thread with RETRY, RETRY_OP or CONTINUE, which says the machine has been looked at; or a clean initialize or home; or orca device clear-fault <name>. The three aborts never clear a fault, because giving up on the work says nothing about whether the jaws are empty. Nothing here checks the machine: clearing a fault removes the record, not the trouble.

Adding a new device type

A new device kind touches three repos, and all three in the same change:

  1. cheshire-drivers: a driver interface (e.g. IMyDeviceDriver) whose abstract methods are the capability contract, plus a real and a sim implementation. Mark operator-facing methods @external. Declare any vendor object the driver holds in vendor_surfaces.
  2. orca-core: an Orca device class wrapping it, with a KIND, and an entry in orca/gateway/registry/capabilities.py:NAME_TO_INTERFACE if the interface is new. Without that entry the gateway cannot resolve the interface name to a class, and every command behind it reads as unsupported.
  3. The hosted layer: the REST and MCP surfaces for the new commands.

See also