Build a driver
A Driver speaks one protocol, and most devices need only a Device File for an existing driver, so build a driver only when no existing driver speaks your device's protocol. This tutorial builds a driver for a simulated bench lamp and runs the conformance suite on the driver.
The contract
- A driver implements
connect,readand_write, and never overrideswrite,action,safe_state,attachor_consume. The base class consumes the kernel's Grant, then calls_write. - Values arrive checked and in the channel's unit, so a driver converts only to what the protocol needs.
discoverandidentifysend only read-only commands.- A failure raises a
DriverErrorwhose message says what happened and what to try, and a driver never returns a made-up value. - Every limit that
describeproposes is markedsuggested.
The driver contract has the full rules.
The driver
The lamp takes a duty byte from 0 to 255, and because the channel is in percent, _write converts the value.
from openhal.drivers.base import Driver, DriverError, DriverInfo, Health, Reading
class LampSimulator:
def __init__(self) -> None:
self.duty = 0
self.online = True
class LampDriver(Driver):
info = DriverInfo(name="x-lamp", protocols=("in-memory",), hazard_default="low", summary="A simulated bench lamp.")
async def connect(self) -> None:
self.sim = LampSimulator()
async def read(self, channel: str) -> Reading:
if not self.sim.online:
raise DriverError("The lamp stopped answering. Check its cable, then reconnect.")
return Reading(channel, round(self.sim.duty / 255 * 100, 1), "percent")
async def _write(self, channel: str, value: float | bool | str) -> Reading | None:
if not self.sim.online:
raise DriverError("The lamp stopped answering. Check its cable, then reconnect.")
self.sim.duty = round(float(value) / 100 * 255)
return await self.read(channel)
async def health(self) -> Health:
return Health(connected=self.sim.online)Names that start with x- are reserved for drivers outside the repository.
Run the conformance suite
A conformance case pairs the driver with a confirmed device file and a hook that breaks the connection.
import asyncio
from openhal.conformance import ConformanceCase, run
from openhal.drivers.base import Driver
from openhal.spec import loads
from lamp import LampDriver
FILE = loads(
"""
device: Bench lamp
id: bench-lamp
connect: { driver: x-lamp }
write:
brightness: { unit: percent, min: 0, max: 80, max_change_per_s: 40 }
safe_state: { brightness: 0 }
hazard: low
confirmed: { by: tutorial, at: 2026-10-24 }
"""
)
async def unplug(driver: Driver) -> None:
assert isinstance(driver, LampDriver)
driver.sim.online = False
report = asyncio.run(run(ConformanceCase(name="x-lamp", file=FILE, make=LampDriver, break_connection=unplug)))
print(report.summary())
assert report.okx-lamp: 11 passed, 0 failed, 1 skippedThe suite sends a brightness of 160 % and checks that the kernel blocks the value before _write runs. The skipped check needs a hook that slows the device down, and Conformance lists every check.
Add it to the repository
A driver in the repository is a package under openhal/drivers/<name>/ with the driver, its simulator, a conformance case, a catalog entry, an example device file, tests and a docs page, and the suite picks up every case.
$ uv run pytest tests/conformance -q -k mockPassing the suite marks a driver tested on simulator, and a person running the driver's hardware test plan on a real device marks the driver verified.