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.readandio.writefrom the serial, USB, and FTDI transportsfirmware.command.started,.completed, and.failedfrom the Hamilton USB driverprecise_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 |
|---|---|
|
|
|
|
|
incubator fetch/take-in/nest transfer; temperature, humidity, CO2, and O2 control when supported |
|
resource pickup/move/drop; tip pickup/drop; 96-head tip pickup/drop; aspirate; dispense |
|
|
|
set temperature, wait for temperature, deactivate |
|
|
|
|
|
|
|
|
|
lifecycle, fault/home/freedrive, joint/cartesian/rail/gripper motion, pick/drop, park |
|
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.