Skip to content

serial

tested on simulator

Tier Tier 1
Protocol line-based text over USB CDC serial, UART, RFC 2217, raw TCP (socket://)
Covers Arduino and other microcontroller sketches, bench instruments with ASCII command sets, sensor modules with text output, lab equipment with RS-232 ports
Hazard default medium
Conformance control
Tested with simulated pyserial port and a real pty opened with pyserial

Estimated reach. Tens of thousands of models in practice. The driver has no built-in command set. Any device whose serial protocol is one text command and one text reply per line can be described in a device file. That covers most hobby microcontroller sketches and a large share of RS-232 lab and industrial equipment. The real limit is how many devices have documented commands, not the driver.

The serial driver talks to any device with a line-based text protocol over a serial port. The driver has no command set of its own, and every query, reply pattern and command template comes from the device file, so one driver covers Arduino sketches, RS-232 instruments and sensor modules alike.

Driver nameserial
Librarypyserial 3.5 (BSD-3-Clause)
Extrauv tool install --compile-bytecode "openhal-hub[serial] @ git+https://github.com/sohan-bhat/OpenHAL"
TransportsUSB CDC and UART device paths, socket://host:port, rfc2217://host:port
Hazard defaultmedium
Safe statefrom the device file only (the protocol has no emergency behavior)
Self-describeno

What it supports

  • Read channels of type number, bool, enum and string, either queried (map.query) or pushed by the device (map.listen).
  • Write channels of every type, with an optional readback query (map.get).
  • Actions whose arguments fill a command template.
  • Unit conversion between the device's unit and the channel unit, plus a linear scale and offset for raw counts.
  • Discovery that lists serial ports with their USB ids without opening any port.
  • port: auto, which picks the one listed port whose USB id matches match.usb (and match.serial_number if given).

Setup

  1. Install the extra with uv tool install --compile-bytecode "openhal-hub[serial] @ git+https://github.com/sohan-bhat/OpenHAL".
  2. Find the port with openhal discover or python -m serial.tools.list_ports -v.
  3. Write a device file, where each read channel needs a map with query or listen and each write channel and action needs a map with command.
  4. Validate the file with openhal file validate my-device.hal.yaml.
  5. Confirm the limits by hand (spec section 12), because until then the device loads read-only.

Parameters

KeyDefaultMeaning
portrequiredDevice path, pyserial URL or auto
baud9600Baud rate
newlinelfLine ending sent and expected. lf, crlf or cr
encodingasciiascii, utf-8 or latin-1
timeout1 sHow long to wait for each reply, 50 ms to 30 s
settle0 sWait after opening, for boards that reset when the port opens. Bytes received meanwhile are dropped
identifynoneA read-only query sent by identify(), such as ID? or *IDN?
boardnoneFree text recorded in the identity
rtscts, dsrdtrfalseHardware flow control

Channel maps

KeyUsed byMeaning
queryreadLine to send. The next line that is not a stream line is the reply
listenreadRegex for lines the device sends on its own. The latest match is the value
commandwrite, actionTemplate to send. Writes use {value}, actions use their argument names
getwriteReadback query. Without a readback query a read returns the last value sent, and unknown (null) before the first write
parseread, writeRegex for the reply. Group value, else group 1, else the whole match
expectwrite, actionRegex the reply must match. Without expect or error the driver doesn't wait for a reply
errorwrite, actionRegex that marks a refusal, reported as a failed command
scale, offsetread, writechannel value = raw * scale + offset, inverted for writes
unitread, writeThe device's unit when it differs from the channel unit
on, offbool channels, actionsDevice text for true and false. Reads accept common words when unset
lookupenum channelsDevice text to enum value, inverted for writes

Templates accept only the placeholders named above, with an optional format spec such as {value:.1f}. Conversions and nested fields are refused when the file loads. A rendered command that contains a control character is refused before anything is sent, so a string value can't append a second command.

Example device file

openhal/drivers/serial/examples/arduino-fan-board.hal.yaml

device: { name: Arduino fan board, model: Uno R3, manufacturer: Arduino }
id: fan-board
match: { usb: ["2341:0043"], usb_class: cdc }
connect: { serial: auto, baud: 115200, timeout: 1 s, settle: 2 s, identify: "ID?" }
read:
  temperature: { unit: degC, every: 1 s, map: { query: "TEMP?", parse: "^T=(?P<value>-?[0-9.]+)$" } }
write:
  fan_speed:
    unit: percent
    min: 0
    max: 100
    max_change_per_s: 25
    map: { command: "FAN {value:.0f}", scale: 0.39215686, expect: "^OK$", error: "^ERR", get: "FAN?", parse: "^FAN=(\\d+)$" }
safe_state: { fan_speed: 0 }
hazard: low

Known limitations

  • The driver handles one command and one reply line at a time, so multi-line replies and binary protocols need a dedicated driver.
  • Because replies are matched by order, a device that replies out of order or sends unrelated chatter that no listen pattern covers makes the driver match the wrong reply, so add a listen pattern for the chatter.
  • Regexes run with Python re with no per-match timeout, and lines are cut at 4096 characters.
  • Because the protocol has no safe state of its own, always give the device file a safe_state, and prefer firmware that falls back to a safe output when the host stops sending commands.

Hazard defaults

The driver reports a default hazard of medium, because the driver can drive anything, so set hazard in the device file to what the device really is.

Sources