UART

Uart drives the UART peripheral of an open adapter, from Device.uart(). Both the Supernova and the Pulsar have it. The async twin is AsyncUart.

Bring-up

from pycosmicsdk import Device, UartBaudRate, UartDataSize, UartParity, UartStopBit

with Device.open() as dev:
    uart = dev.uart()
    uart.set_voltage(voltage_mv=3300)
    uart.bring_up(
        baudrate=UartBaudRate.BAUD_115200,
        parity=UartParity.NONE,
        data_size=UartDataSize.BITS_8,
        stop_bit=UartStopBit.ONE,
    )

bring_up is the re-runnable bring-up and the one to reach for: it tries initialize, falls back to configure when the UART is already up, and returns a BringUpResult. On return the UART is up and usable. The contract is identical across the four protocols that have it and is explained once under Bring-up is bring_up(); check settings_applied when the baud rate is something your test depends on:

result = uart.bring_up(baudrate=UartBaudRate.BAUD_9600)
if not result.settings_applied:
    raise result.refusal

Every bring-up argument is keyword-only. baudrate is required; parity, data_size, stop_bit and hw_handshake default to 8-N-1 with handshaking disabled. Baud rates come from the UartBaudRate ladder (600 through 115200) rather than a free integer.

Warning

hw_handshake=UartHwHandshake.ENABLED is believed to have no effect on a Supernova Rev B. Firmware’s UART setup branches on the hardware revision and the Rev B branch does not wire RTS/CTS up; the call still returns successfully, so a script asking for flow control on such a board gets none. The RTS/CTS pins stay muxed to their alternate function, so they look configured on a scope while the peripheral ignores them. The reading is from firmware source alone — no bench run has confirmed it, and confirming it needs a peer that actually asserts RTS/CTS plus enough traffic to force flow control.

This SDK does not refuse the request — refusing one that might work is the worse error (see Refusals fail open) — but it does say so, in three places. dev.capabilities.uart_hw_handshake_effective is False on such a unit, so you can branch on it before asking. initialize and configure log a warning, which is all a call returning None can do. And bring_up reports it: settings_applied is False with the reason in refusal, even on a fresh bring-up where nothing was refused on the wire. It is the one line parameter whose answer is known from the capability table before the call, so a field documented as the one to consult must not affirm it. Rev C Supernovas and Pulsars honour the request and report settings_applied=True as usual.

send_only_mode is bring-up only — but bring_up does take it. send_only_mode=True arms no reception: no RX queue, no dispatcher subscription. It is a host-side setting with no firmware parameter behind it, which is why configure has no such argument — and why bring_up can still honour it on either path, including the fallback onto a live UART: it selects the RX path in both directions, so on a UART whose reception an earlier run armed, True releases the queue and subscription again rather than flipping a flag behind machinery that keeps running. A caller parked in receive/query when that happens is woken with DisconnectError, exactly as deinit wakes it. The choice is never what settings_applied is reporting about. See examples/blocking_api/uart/send_only.py.

Underneath, initialize is not idempotent: a re-run on an already-up UART raises FirmwareError with FW_INTERFACE_ALREADY_INITIALIZED, and the UART stays initialised across reconnects. configure re-configures a live interface; deinit tears the peripheral down without closing the device — UART is the only protocol here that offers one, which is one reason bring_up reports rather than promising to reach any given settings. examples/helpers.py’s uart_bring_up() delegates to bring_up and adds the examples’ console lines.

The rail is the shared multifunction VTARG — the same supply as I2C, SPI and GPIO. get_bus_voltage reads it back, and AsyncUart mirrors it.

Note

set_voltage takes voltage_mv keyword-only, where the I2C, SPI and GPIO setters accept it positionally. So uart.set_voltage(3300) is a TypeError while spi.set_voltage(3300) is fine, and uart.set_voltage(voltage_mv=3300) is the form to use. This is an inconsistency in this SDK rather than anything upstream imposes; the keyword-only form works on all four, so write it that way everywhere and the question does not arise.

Three reception patterns — pick one per interface

This is the part that bites. The RX path is a single internal queue shared by receive and query, and the three patterns below do not mix.

query

Request/response. Drains the queue, sends, waits for a reply. Not concurrency-safe — overlapping calls discard each other’s data and can return each other’s replies. Use it when the exchange is strictly one-at-a-time. examples/blocking_api/uart/query.py.

receive

Pull. Blocks for the next message, with an optional timeout. Safe to call concurrently with competing-consumer semantics: each message goes to exactly one waiter, non-deterministically — UART has no request/response correlation, so there is nothing to correlate on. examples/blocking_api/uart/receive_stream.py.

Device.subscribe with UartRxNotification : Push. Register a callback once and the dispatcher invokes it — on its own thread — for every message received. The natural style for telemetry or reacting to each line a device emits. examples/blocking_api/uart/subscribe.py.

Mixing query with receive is unsound. Pick one pattern per interface.

On the async side, iter_receive is a fourth option with no blocking equivalent: an async iterator that installs its own subscription and queue, so iterating does not compete with receive or query for messages.

async with uart.iter_receive() as stream:
    async for message in stream:
        print(message)

send takes bytes. DeviceCapabilities.max_uart_message_bytes reports the advisory message cap (1024).

Errors

UartError is much narrower than its name suggests. The 0x04xx band maps to it, and upstream declares exactly one code in that band: FW_UART_IN_VCOM_PORT_MODE (0x0401), reported by uartInit alone, and only when the port is held by the USB CDC.

Everything else lands outside the band:

Failure

Code

Exception

Port held by USB CDC

FW_UART_IN_VCOM_PORT_MODE (0x0401)

UartError

Rejected configuration

FW_INVALID_PARAMETER (0x0004)

FirmwareError

Interface not initialised

FW_INTERFACE_NOT_INITIALIZED (0x0006)

FirmwareError

Rejected voltage

FW_VOLTAGE_OUT_OF_RANGE (0x0101)

SystemFirmwareError

Catch FirmwareError to cover every firmware-reported UART failure. A receive/query that runs out of time raises CosmicTimeoutError, which also satisfies except TimeoutError:.

Examples

examples/blocking_api/uart/

query.py (request/response), send_receive.py (separate send and receive calls — a console session), receive_stream.py (pull loop), subscribe.py (push path), send_only.py (send_only_mode=True plus explicit teardown).

examples/async_api/uart/

query.py, send_receive.py, and receive_stream.py — the last built on iter_receive().

Reference