Tutorial 13: Hardware abstraction (optional)¶
Prerequisites¶
Tutorial 12: Post-processing. This page is for developers adding a new device type.
What you'll learn¶
- The four-method contract every device must satisfy.
- How devices are opened, handed to a task, and closed.
- The shape of adding a new device type.
1. Why an abstraction layer¶
A task asks for a capability ("I need a Bpod"), not a specific connection. The hardware layer turns that request into a real, opened device and hands the task only a handle. This is what lets the same task run on any rig: the rig's setup file says where the device is, the hardware layer connects it, and the task never sees a port string. Adding a new kind of device means satisfying one small contract, with no changes to task code.
2. The device contract¶
Every device implements the same structural interface, DeviceProtocol. It has a
name attribute and four methods:
| Member | Responsibility |
|---|---|
name |
the key the device is exposed under (for example "bpod") |
preflight() |
check the device is reachable before connecting; raise a clear error if not |
connect() |
open the hardware connection (with retries as appropriate) |
disconnect() |
close the connection gracefully |
handle |
the raw object handed to the task (a property) |
The split between preflight and connect is deliberate: preflight catches
"the device is not there" with a readable message before any connection state is
created, so failures are clean.
3. How devices are wired in¶
The HardwareManager takes a list of devices and manages their lifecycle as a
context manager. For each device it calls preflight(), then connect(), and on
exit calls disconnect() in reverse order. It returns a dict mapping each
device's name to its handle:
from murineshiftwork.hardware.manager import HardwareManager
with HardwareManager([BpodDevice(port)]) as devices:
# devices == {"bpod": <raw Bpod handle>}
run_task(bpod=devices["bpod"], ...)
# disconnect() has now run for every opened device
The task receives only devices["bpod"], the raw handle, and is unaware of the
device class or its port.
4. Adding a new device type (shape)¶
To support a new device, the work is:
- Write a class that satisfies
DeviceProtocol: setname, and implementpreflight,connect,disconnect, and thehandleproperty. - Construct it from the rig's setup config (the device's
typeand port live in the setup YAML underdevices). - Include it in the list passed to
HardwareManagerfor tasks that require it.
Because the contract is small and structural, a new device participates in preflight, connection, and cleanup automatically once it implements the four members. No task or framework code needs to know the new type by name.
You now know¶
Every device satisfies a four-member DeviceProtocol (name, preflight,
connect, disconnect, handle), and HardwareManager opens, hands off, and
closes devices uniformly. Adding a device type means implementing that contract
and constructing it from the setup config, with no changes to task code.
Next¶
Tutorial 14: Valve calibration (optional). For how devices fit the wider system, see Architecture.