EventBus#

PLR’s EventBus is an optional, in-process source of structured execution events. It is useful when an application needs to persist an execution log, display progress, or correlate a failed operation with device diagnostics without parsing human-readable logs.

Event observation is opt-in. Creating or subscribing to a bus does not change protocol control flow, and a subscriber exception is isolated from the instrument operation that emitted the event.

Subscribe to events#

Install a bus for the scope that should produce events, then subscribe a fast callback. The callback should enqueue or persist the event rather than perform slow work synchronously.

from pylabrobot.events import EventBus, use_event_bus

event_bus = EventBus()
event_bus.subscribe(lambda event: print(event.as_dict()))

with use_event_bus(event_bus):
  await machine.setup()
  # Other instrumented PLR operations emit events in this scope.

Use set_default_event_bus() when one process-wide bus is appropriate. use_event_bus() is preferred for a bounded protocol or task because it is context-local and composes safely with async tasks.

Forward events to external services#

Subscribers can format and forward selected events to external logging, monitoring, or notification services. EventBus itself remains transport-independent: integrations such as Slack live in application code and choose which semantic or diagnostic events they need.

The Slack notifications cookbook demonstrates a small subscriber that forwards completed and failed semantic operations to a Slack webhook. It also shows how to keep the synchronous subscriber fast by submitting network work to a background thread.

Event shape#

Every event has the following JSON-ready representation:

{
  "sequence": 42,
  "name": "incubator.fetch_plate.completed",
  "timestamp": "2026-08-10T12:34:56.789012+00:00",
  "context": {
    "operation": "incubator.fetch_plate",
    "operation_id": "...",
  },
  "data": {
    "device": {"name": "incubator", "type": "Incubator"},
    "resources": [{"name": "plate_1", "type": "Plate"}],
  },
}

Semantic operations emit a correlated lifecycle pair:

<component>.<operation>.started
<component>.<operation>.completed

If the operation raises, the second record is <component>.<operation>.failed, with error_type and error_message. All records for the operation share context.operation_id.

Resource fields describe the direct PLR resource involved in the call. Each reference can include structural ancestors; applications that want to display an owning plate or rack should derive that view from the reference rather than replacing the operated resource in the event.

Geometric targets use PLR’s serialized Coordinate representation, for example {"x": 12.5, "y": 8.0, "z": 42.0, "type": "Coordinate"}.

Quantitative fields use PLR’s default units without repeating the unit in the field name. A suffix is used only when a value deliberately differs from the default, such as rotational speed_rpm instead of PLR’s default linear speed.

Add application context#

Applications may attach their own execution context around PLR calls. This is useful for run or batch identifiers that PLR itself cannot know.

from pylabrobot.events import event_context

with use_event_bus(event_bus), event_context(run_id="run-42", batch_id="batch-2"):
  await incubator.fetch_plate_to_loading_tray("plate_1")

The values are inherited by nested PLR events. Keep this context application-specific; device events continue to describe only what the PLR frontend actually did.

Semantic and diagnostic events#

Instrumented frontend operations emit semantic events such as liquid_handler.aspirate.completed or precise_flex.move_to_location.completed. Some transports also emit lower-level diagnostic events:

  • io.read and io.write from the serial, USB, and FTDI transports

  • firmware.command.started, .completed, and .failed from the Hamilton USB driver

  • precise_flex.firmware_command.* from the PreciseFlex controller

Both event classes can be subscribed to. Semantic events preserve PLR operation meaning; diagnostic events preserve controller and transport activity for debugging.

Current event coverage#

EventBus adoption is incremental. The current implementation instruments the following public frontends. Each listed semantic operation emits started, completed, and failed lifecycle events.

Frontend

Canonical semantic operations

legacy.machines.Machine

machine.setup, machine.stop

legacy.storage.Incubator

incubator.fetch_plate, incubator.take_in_plate

high_res.sample_storage.HighResSampleStorage

incubator fetch/take-in/nest transfer; temperature, humidity, CO2, and O2 control when supported

legacy.liquid_handling.LiquidHandler

resource pickup/move/drop; tip pickup/drop; 96-head tip pickup/drop; aspirate; dispense

legacy.shaking.Shaker

shaker.shake, shaker.stop_shaking

legacy.temperature_controlling.TemperatureController

set temperature, wait for temperature, deactivate

legacy.centrifuge.Centrifuge

centrifuge.spin

legacy.centrifuge.Loader

centrifuge_loader.load, centrifuge_loader.unload

agilent.vspin.VSpin

centrifuge.spin

agilent.vspin.Access2

centrifuge_loader.load, centrifuge_loader.unload

brooks.precise_flex.PreciseFlex

lifecycle, fault/home/freedrive, joint/cartesian/rail/gripper motion, pick/drop, park

manual_operator.ManualOperator

arbitrary acknowledged manual actions; resource moves

Detailed operation references:

Operation reference#

Incubator#

incubator.fetch_plate, incubator.take_in_plate, and incubator.transfer_plate include device, the moved plate in resources, and physical source and destination resource references.

LiquidHandler#

Resource pickup events include the source holder when the moved resource is assigned at invocation time; resource-drop events include their destination. Aspirate and dispense events include direct operated resources plus liquid_operations, one record per channel, with channel, resource, optional owning plate, and volume. Tip events similarly include direct tip locations and per-channel tip_operations.

Shaker and environmental controllers#

Shaker events include speed_rpm and optional duration. Temperature-controller events include target_temperature where applicable. The legacy shaker and temperature frontends are ResourceHolders: when a resource is loaded at operation start, it is included as the direct resource in resources. HighRes sample-store humidity and gas targets are fractions and use an empty resources list because the controller acts on the store environment rather than one plate.

Brooks PreciseFlex#

PreciseFlex motion events identify the controller in device. Cartesian target payloads use a serialized Coordinate in target.location; joint targets use axis-name-to-value mappings. pick and drop describe controller actions. A resource-aware wrapper should emit the separate resource-transfer event when it has PLR resource context.

Agilent BenchCel#

benchcel.downstack, benchcel.upstack, and benchcel.move_plate_between_stacks include the BenchCel in device, the directly moved plate in resources, and the actual PLR stack or loading tray holders in source and destination.

More detail#

The EventBus contributor guide defines the stable naming, resource, and test conventions for driver authors adding coverage. The Event Schema Registry defines canonical operation names and payload fields.