Writing a driver
A driver implements one protocol and a device file describes one device, so most new devices need only a device file. Write a driver only when no protocol driver can connect. The tutorial builds one with openhal driver new.
The package
openhal/drivers/<name>/
__init__.py NAME and ENTRY only, no heavy imports
driver.py the Driver subclass, with a header docstring about the protocol and its quirks
simulator.py a mock or protocol simulator used by tests and the conformance case
conformance.py def case() -> ConformanceCase
catalog.json this driver's catalog entry (merged into docs/drivers/catalog.json)
examples/ at least one complete example device file
tests/drivers/<name>/test_<name>.py
docs/drivers/<name>.md
docs/hardware-tests/<name>.md__init__.py holds only this, so the registry finds the driver without importing its dependencies.
NAME = "scpi"
ENTRY = "openhal.drivers.scpi.driver:ScpiDriver"The class
class ScpiDriver(Driver):
info = DriverInfo(name="scpi", protocols=("SCPI over VISA",), hazard_default="medium",
summary="Programmable test instruments over USB, LAN and serial.")
params_model = ScpiParams # a Pydantic model with extra="forbid"
@classmethod
async def discover(cls) -> list[Discovered]: ... # read-only, never changes hardware
async def identify(self) -> dict[str, str]: ... # read-only (*IDN?, M115, firmware describe)
async def connect(self) -> None: ...
async def describe(self) -> dict[str, Any] | None: ... # optional self-description
async def read(self, channel: str) -> Reading: ...
async def _write(self, channel: str, value: float | bool | str) -> Reading | None: ...
async def _action(self, name: str, args: dict[str, Any]) -> Any: ...
async def _safe_state(self) -> None: ... # optional protocol-native safe behavior
async def health(self) -> Health: ...
async def close(self) -> None: ...- Never override
write,action,safe_state,attachor_consume. Those methods consume the kernel's grant before calling_write,_actionand_safe_state, and the base class raisesTypeErrorfor an override. - Never call
_write,_actionor_safe_stateyourself. A test fails if anything outsidedrivers/base.pycalls one. - Values arrive checked and in the channel's declared unit. Don't re-check limits. Convert only to what the protocol needs, such as percent to a 0 to 255 PWM byte, and document the mapping.
- Discovery and identification send only documented read-only commands, with short timeouts, and never open a port another process holds.
readreturns the channel's declared unit, withReading.unitset for numeric channels.- Errors raise
DriverErrorwith what happened and what to try next. Timeouts may raiseTimeoutError. Never swallow an error and return a fake value. describe()maps what the protocol exposes, such as SCPI queries, Modbus registers, GATT characteristics, Home Assistant entities, ROS topics, OPC UA nodes or Moonraker objects, into a mapping with any ofdevice,match,read,write,actionsandnotes. Every limit is markedsuggested: true, since a driver never confirms limits.- Safe state. If the protocol has its own emergency behavior, such as GRBL feed hold or outputs off, set
DriverInfo.safe_state_supportto"native", implement_safe_stateand document what_safe_statedoes. If the protocol can't guarantee a safe state, say so in the docs page and the example file'ssafe_state_note. - Bridges such as Home Assistant, ESPHome and ROS 2 expose only the entities the device file selects, and the device file's limits apply even when the bridged system has its own.
- No secrets in device files. A secret parameter takes an environment variable name, such as
token_env: HA_TOKEN. - Optional dependencies go in a
pyproject.tomlextra named after the driver and are imported indriver.py, never in__init__.py.
Tests
Tests cover normal operation for every supported channel type, disconnects, timeouts, malformed responses and every error path. The conformance case must pass, and tests/conformance/test_conformance.py picks up the case on its own. No test touches real hardware.
Docs page
docs/drivers/<name>.md covers support, setup, an example device file, known limitations, hazard defaults and the protocol sources.
Hardware test plan
docs/hardware-tests/<name>.md is a plan for a person with real hardware, from read-only checks to the smallest safe writes, with exact commands, expected output, what to send back on failure and a template for a verified-on-hardware report.
Catalog entry
catalog.json in the driver package holds the entry.
{
"name": "scpi",
"tier": 1,
"status": "implemented",
"protocols": ["SCPI over USB-TMC", "SCPI over LAN (VXI-11, raw socket)", "SCPI over serial"],
"families": ["bench power supplies", "multimeters", "oscilloscopes", "function generators", "electronic loads"],
"hazard_default": "medium",
"tests": "mock and pyvisa-sim",
"estimate": {"devices": "thousands of models", "reasoning": "...", "sources": ["https://..."]}
}Tutorial
The scaffold is a complete driver of about 80 lines on an in-memory simulator, and the scaffold passes the conformance suite as generated. Swap in the real protocol one method at a time, keeping the suite passing.
-
Scaffold the driver from an OpenHAL checkout.
uv run openhal driver new acme-pump --title "Acme pump"The command creates
openhal/drivers/x_acme_pump/,tests/drivers/x_acme_pump/,docs/drivers/x-acme-pump.mdanddocs/hardware-tests/x-acme-pump.md. New drivers are namedx-...and marked experimental. -
Run the conformance suite.
uv run openhal driver test x-acme-pump -
Write the protocol. In
driver.py, replaceconnect,readand_writewith the real transport, and makesimulator.pysend and receive the device's bytes. Keepdiscover()andidentify()read-only. -
Test every error path. Disconnects, timeouts, malformed replies and error replies each get a test and raise
DriverErrorwith the next step. -
Run the suite again, then fill in the docs page, the hardware test plan and the catalog entry's estimate with sources.
-
Promote the driver. After conformance and a person's hardware test run, a maintainer adds the plain name to
KNOWN_DRIVERSinopenhal/spec/models.pyand sets the status to implemented.
openhal driver list shows every driver with its protocols, hazard default, conformance level and status.