Skip to content
Haiku OSDeep Dive Published Updated 8 min readViews unavailable

Haiku BSerialPort: Reliable Serial Configuration and Stream I/O

Build robust Haiku serial tools with BSerialPort: enumerate devices, configure framing and flow control, handle timeouts, partial I/O, and teardown.

Haiku’s BSerialPort gives an application a Kit-level interface to a serial device: enumerate available names, open one port, configure baud rate and framing, select flow control, and read or write bytes. It is intentionally a byte-stream API. It does not define your device’s packet boundaries, command acknowledgements, retries, or firmware-update protocol. Those belong to the application protocol layered above it.

That boundary matters because serial failures are often misdiagnosed as baud-rate problems. A process can open a port while its peer is disconnected, the electrical interface can be RS-232 while the application expects TTL UART, or a correct byte stream can be parsed with the wrong framing. Treat the physical adapter, port name, line settings, flow-control wiring, and protocol state as separate evidence.

Discover names at runtime

CountDevices() reports the current number of serial devices visible to the API, and GetDeviceName() returns a name for an index. Names are environment-dependent. Do not hard-code a port index and assume it is a permanent identity; USB adapters may appear in another order after reconnect, and a test machine may expose a different set.

Enumerate and present the returned names to an operator, or apply a documented device-selection policy and report exactly which name was chosen. Check the status from GetDeviceName() and provide a buffer size that matches the destination. Enumeration is a snapshot, not a promise that the port remains present until Open() succeeds.

Match framing and flow control with the peer

Configure the serial connection from the device protocol’s documented settings. BSerialPort exposes standard rates through data_rate, seven- or eight-bit data, one or two stop bits, and no/odd/even parity. The names map to wire-level framing. One side using 8-N-1 while the peer uses 7-E-1 produces bytes that may look corrupt even though both sides report a functioning port.

Flow control is an independent choice. B_NOFLOW_CONTROL, B_HARDWARE_CONTROL, and B_SOFTWARE_CONTROL represent different behavior; they are not interchangeable defaults. Hardware flow control relies on modem-control signals being wired and supported by the adapter. Software flow control can reserve in-band control bytes, which may conflict with a binary protocol unless the protocol accounts for them. Set DTR or RTS only when the peer expects those line states.

Some configuration methods return status_t, while others are void. In particular, the API reference notes that setters for some line parameters can fail silently when a driver does not support the requested value. Read the corresponding getter after setting it when the exact configuration matters. A successful call to a void setter is not confirmation that the device accepted the setting.

#include <SerialPort.h>

status_t ConfigurePort(BSerialPort& port, const char* name)
{
    status_t status = port.Open(name);
    if (status < 0)
        return status;

    status = port.SetDataRate(B_115200_BPS);
    if (status != B_OK)
        return status;

    port.SetDataBits(B_DATA_BITS_8);
    port.SetStopBits(B_STOP_BITS_1);
    port.SetParityMode(B_NO_PARITY);
    port.SetFlowControl(B_NOFLOW_CONTROL);

    if (port.DataRate() != B_115200_BPS
        || port.DataBits() != B_DATA_BITS_8
        || port.StopBits() != B_STOP_BITS_1
        || port.ParityMode() != B_NO_PARITY
        || port.FlowControl() != B_NOFLOW_CONTROL) {
        return B_ERROR;
    }

    return port.SetTimeout(2000000);
}

This helper expects the selected settings to match the attached peer. A production owner should close the port if configuration fails after Open(), and should use RAII or a single cleanup path so every successful open is paired with Close(). The two-second timeout is for input waiting in blocking mode; it is not a global deadline for every operation. Open() has a legacy return contract despite its status_t declaration: Haiku’s current implementation returns the nonnegative file descriptor when open succeeds and a negative error value on failure. Test for a negative result rather than comparing it with B_OK. The published API example uses <= 0, but the implementation treats descriptor zero as an open descriptor, so < 0 preserves that edge case.

Understand blocking, timeout, and partial data

SetBlocking() controls whether reads and writes wait for the requested transfer. The API reference distinguishes blocking writes, which wait to write the full buffer, from non-blocking writes, which may return after accepting only some bytes. Code must use the returned count and preserve the unsent suffix instead of assuming that one Write() call equals a complete protocol message.

SetTimeout() configures how long Read() and WaitForInput() wait for input in blocking mode. The documented accepted values are B_INFINITE_TIMEOUT or values from zero through 25,000,000 microseconds. Non-blocking mode ignores this setting. WaitForInput() is a special case: the reference says it waits for available input even when non-blocking mode is selected, while respecting the configured timeout. Do not infer its behavior from the SetBlocking() name alone.

The input buffer is a stream, not a message queue. A read can contain a partial frame, one exact frame, or bytes spanning multiple frames. Preserve incomplete bytes between reads, parse only the returned length, validate declared payload lengths before indexing, and bound how much data can accumulate without a valid delimiter or length field. A timeout means “no input arrived in the configured interval,” not “the peer rejected the previous command.” That decision requires protocol-level state.

For non-blocking output, use a write cursor and retry only the unsent suffix according to a bounded scheduling policy. BSerialPort does not expose a universal output-ready notification API in this class, so do not busy-spin on B_WOULD_BLOCK. For blocking output, a peer holding hardware flow control inactive can delay progress. Keep serial operations off a window’s message loop and ensure the application’s shutdown policy accounts for a worker waiting on I/O.

A bounded request/response pattern

This sketch sends a short command and consumes only the bytes reported by the port. The framing routine is deliberately separate: a real application should decode its own length, delimiter, checksum, and sequence fields rather than treating a read as a whole reply.

status_t ReadAvailable(BSerialPort& port, uint8* buffer,
    size_t capacity, size_t* bytesRead)
{
    if (buffer == nullptr || bytesRead == nullptr || capacity == 0)
        return B_BAD_VALUE;

    *bytesRead = 0;
    ssize_t available = port.WaitForInput();
    if (available < 0)
        return static_cast<status_t>(available);

    size_t request = static_cast<size_t>(available);
    if (request > capacity)
        request = capacity;

    ssize_t count = port.Read(buffer, request);
    if (count < 0)
        return static_cast<status_t>(count);

    *bytesRead = static_cast<size_t>(count);
    return B_OK;
}

The availability count can exceed the caller’s buffer, so the sketch caps the request. If more bytes remain, a later call reads the rest. A production loop must also choose a deadline policy around repeated reads; a stream that sends one byte just before every timeout can otherwise keep a transaction alive indefinitely. Use an overall monotonic deadline in addition to the per-call port timeout.

Do not use ClearInput() as a general recovery action. It discards unread input bytes and can erase the start of a valid response. ClearOutput() discards queued bytes not yet transmitted. Both methods are explicit loss operations; log why they were called and reset the higher-level protocol state at the same time. If you need to drop stale bytes after a known reset sequence, do so at a documented boundary, then wait for a fresh sync marker.

Ownership, reconnect, and diagnostics

An open port is a scarce device resource. Keep its owner explicit, close it on every error path, and do not share one BSerialPort instance between unrelated read/write workers without a serialization design. A reconnect is a new discovery and open event; do not keep assuming the same index or a previous line configuration after the underlying device disappears.

Record the port name, adapter model, peer firmware, baud rate, data/parity/stop bits, flow control, timeout, and raw byte counts. For a protocol failure, capture bounded and redacted hexadecimal samples plus frame-decoder status, not only a generic “serial read failed.” Do not log sensitive payloads by default. Separate Open() failure from configuration mismatch, missing electrical loopback, timeout, framing/checksum error, and a peer that returned a valid negative response.

Use a controlled loopback test to separate local transmit/receive from device behavior, but verify that the test setup electrically connects the correct transmit and receive pins. A successful loopback does not prove that the intended adapter voltage, cable, grounding, or target hardware is correct. Test the exact board and adapter combination at the documented settings, including device disconnect, partial response, timeout, flow-control pause, and shutdown during a blocked read.

Acceptance criteria for a serial integration

Before depending on a serial protocol, verify that enumeration selects the intended port by name, Open() succeeds, each required getter matches the expected configuration, a known command produces a known response, and malformed or truncated frames are rejected without reading beyond the available bytes. Exercise repeated open/close, reconnect with changed enumeration order, zero-length input, timeout, short non-blocking writes, and the longest expected transaction.

Measure transaction latency from command write to complete validated reply, not just the time Write() returns. Record the requested and actual line settings, actual read sizes, timeout count, checksum failures, and reconnect history. These measurements show whether a delay belongs to the serial driver, flow control, peer firmware, or the protocol’s retry policy.

BSerialPort is a useful, deliberately small abstraction over serial devices. Reliable applications preserve its distinction between device discovery, line configuration, byte-stream I/O, and protocol framing. They also treat timeout and returned byte counts as data, not as implementation details that can be ignored on a fast local test.

Related:

Sources:

Comments