Device and discovery

The narrative version of everything on this page — how to choose a selector, what makes a handle stale, when to reboot — is The device lifecycle.

Discovery

Answers “what is plugged in?” without opening anything: it reads USB-HID enumeration directly, so no adapter is opened and no firmware command is sent. Every field of a DiscoveredDevice is ready to use as an open() argument or a filter key. examples/blocking_api/system/list_devices.py is the runnable version.

pycosmicsdk.list_devices(*, model: DeviceModel | None = None) → list[DiscoveredDevice]

List the Binho host adapters currently attached, without opening any.

Only adapters Device.open() can actually open are listed: selection is by the (vendor id, product id) pairs the CosmicSDK driver accepts (CosmicDriver::binhoDeviceList), so a Binho HID device that is not a supported host adapter is never returned, and the product id doubles as the model marker – which is what makes model a pre-open filter rather than a post-open check.

Pair an entry with Device.open() (or AsyncDevice.open) by serial or by path:

for dev in list_devices(model=DeviceModel.SUPERNOVA):
    with Device.open(serial=dev.serial_number) as device:
        ...

Entries are grouped by model in DeviceModel declaration order and, within a model, follow the host’s HID enumeration order. Neither is stable across replugs, and neither predicts which adapter an argument-less Device.open() picks. Duplicate HID interfaces of one physical adapter are collapsed to a single entry by serial number.

Parameters:

model – Optional host-adapter model to restrict the listing to, e.g. DeviceModel.SUPERNOVA. None lists every supported model.

Returns:

One DiscoveredDevice per attached adapter, empty if none are attached. An entry whose serial_number is empty was found but could not be opened, so its string descriptors are blank and it cannot be selected by serial; a warning naming the likely cause is logged under cosmicpysdk.discovery.

Raises:

ValidationError – If model is not a selectable host-adapter model (i.e. DeviceModel.UNKNOWN).

class pycosmicsdk.DiscoveredDevice(model: DeviceModel, serial_number: str, path: str, manufacturer: str, product_name: str, hw_version: str, fw_version: str)

Bases: object

One attached host adapter, as seen from USB enumeration alone.

Deliberately not DeviceInfo. DeviceInfo reports what an open adapter says about itself, and it reports it raw: the firmware’s USB string descriptors carry a three-character tag, so an opened Supernova reports serial_number="SN-634F...", hw_version="HW-B", manufacturer="MN-Binho LLC". The legacy supernovacontroller shim depends on that raw form. Enumeration sees the same fields untagged, and only the untagged serial is accepted by Device.open(serial=...) – the tagged one raises DeviceNotFoundError. Putting both string conventions behind one type would make open(serial=info.serial_number) a coin flip on where the info came from, so discovery gets its own record.

Two things this shape gains from being its own type: path, which DeviceInfo structurally cannot carry (CosmicSDK does not expose the resolved path of a connected handle, but enumeration has it), and a model taken from the USB product id rather than inferred from a product string.

Every field is ready to use as an open() argument or a filter key; nothing here requires a round trip to the device.

model

Host-adapter model, from the USB product id – the authoritative model marker at enumeration time.

Type:

pycosmicsdk._cpp._pycosmicsdk_cpp.DeviceModel

serial_number

Untagged USB serial number, ready to pass as Device.open(serial=...). Empty if the adapter reports none.

Type:

str

path

USB-HID device path, ready to pass as Device.open(path=...). Valid only until the adapter is replugged.

Type:

str

manufacturer

USB manufacturer string, untagged.

Type:

str

product_name

USB product string, untagged.

Type:

str

hw_version

Hardware revision letter decoded from the USB release number ("B", not "HW-B"), or "?" for an adapter on firmware 3.x, whose encoding carries no revision.

Type:

str

fw_version

Firmware version decoded from the USB release number ("4.5.0", not "FW-4.5.0"). An adapter on firmware 3.x reports "3.x.x": that scheme is a fixed value rather than an encoding, so its minor and patch are not knowable here. An adapter waiting in its bootloader reports "bootloader".

Type:

str

Device

The blocking handle for one open adapter.

Note

The constructor signature in each class heading below is package-private — Device and AsyncDevice are built by open / open, never by calling the class. It is shown because autodoc reads it off the class, not because it is API.

class pycosmicsdk.Device(facade: Any, info: DeviceInfo)

Bases: object

Blocking SDK handle for a single Binho USB host adapter.

Construct with Device.open(). The explicit constructor is package-private; user code never calls it. Use as a context manager for deterministic cleanup; close() is idempotent.

Thread safety: a per-instance threading.RLock serializes every lifecycle mutation and every blocking FFI call. Two threads can share a Device instance safely; sequences of calls that need to appear atomic to other threads (e.g. send + receive) require user-level locking on top.

classmethod open(*, serial: str | None = None, path: str | None = None, model: DeviceModel | None = None, timeout_ms: int = 2000) → Device

Open a Binho host adapter and return a usable Device.

Parameters:
  • serial – Optional device serial number to match. Mutually exclusive with path and model.

  • path – Optional USB-HID device path to match. Mutually exclusive with serial and model. With serial, path and model all set to None, opens the first enumerated Binho adapter.

  • model – Optional host-adapter model to match, e.g. DeviceModel.SUPERNOVA. Opens the first enumerated adapter of that model — use it instead of a hard-coded serial when the scenario needs a specific model but any unit of it will do. Mutually exclusive with serial and path.

  • timeout_ms – Per-call firmware response timeout for the blocking facade. Default 2000 ms matches CosmicSDK.

Returns:

A fully constructed Device in DeviceState.OPEN.

Raises:
  • ValidationError – If more than one of serial, path and model is set, or if model is not a selectable host adapter model.

  • DeviceNotFoundError – If no adapter of the requested model is attached, or if the underlying connect call never returned True. A connect that fails slowly means an adapter is attached but busy, so it is retried for up to two seconds before giving up; the error distinguishes the two cases.

close() → None

Close the device. Idempotent.

Routes through the same cleanup path as the GC finalizer (so there is exactly one teardown sequence). After close(), any subsequent FFI-touching method raises DisconnectError.

__enter__() → Device

Context manager entry — return self.

__exit__(*exc: object) → None

Context manager exit — close the device unconditionally.

property info: DeviceInfo

Cached DeviceInfo snapshot taken at open() time.

Stable for the lifetime of the Device — even after close(). Use fetch_device_info() to issue a fresh firmware query.

property model: DeviceModel

Convenience accessor for info.model.

property capabilities: DeviceCapabilities

Capability set derived from info.model at open() time.

property state: DeviceState

Current lifecycle state. Locked read for memory-model clarity.

property discarded_response_count: int

Responses this connection received and refused, since open().

A reply is discarded when no command is waiting for it, when it carries another command’s transaction id, when it duplicates one already consumed, or when it is too short to hold a response header. The count only rises.

Read it when a command times out and the cause is unclear: a rising count says another client is answering on this adapter, a static one says the device is simply slow. It stays readable after close(), which is when a post-mortem usually wants it.

Note

A non-zero value is not an error. Windows delivers every HID input report to every open handle, so a second process talking to the same adapter makes this climb on a healthy session. Diagnose with it; do not branch on it.

Returns:

The number of responses discarded on this connection.

property is_connected: bool

True while the device is in DeviceState.OPEN.

fetch_device_info() → DeviceInfo

Issue a fresh get_device_info query and return the result.

Distinct from the info property, which returns the snapshot cached at open. Every field is fixed for the life of a connection: reboot() re-enumerates the adapter and marks this handle DeviceState.TERMINAL, so changed info has to be read from a newly opened Device. This method re-issues the query on demand rather than reporting a change.

Returns:

A freshly queried DeviceInfo from the firmware.

Raises:

DisconnectError – If the device is no longer OPEN.

get_version() → str

Return the CosmicSDK library version string.

Returns:

The CosmicSDK library version string.

Raises:

DisconnectError – If the device is no longer OPEN.

reboot(into: ResetTarget) → None

Reset the host adapter into the requested mode.

On success the device re-enumerates over USB and this handle becomes stale: state transitions to DeviceState.TERMINAL and a fresh Device must be opened. Poll list_devices rather than sleeping a fixed interval; on the bench an adapter is openable again 0.7-0.9 s after a RUNTIME reset.

A RUNTIME reset clears latched interface roles (an I2C controller role that survived close) and a wedged SPI bulk payload path; it does not clear the VTARG setpoint. It reaches the device only while its SYS manager still dispatches commands.

Parameters:

into – Reset target. RUNTIME performs a normal device reset; BOOT jumps the device into bootloader mode (used for firmware flashing); ISP is reserved by CosmicSDK and not implemented yet.

Raises:
get_usb_config() → UsbConfig

Read the USB interface configuration stored on the adapter.

Returns:

The WebUSB state, USB CDC state and CDC mode currently saved on the device. The no-change sentinels never appear in a read.

Raises:
  • DisconnectError – If the device is no longer OPEN.

  • ProtocolError – If the response was too short to carry a configuration, or carried a value this SDK does not know. CosmicSDK maps a header-only firmware error response to SDK_ERROR_WRONG_RESPONSE, so firmware predating CMD_SYS_GET_USB_CONFIG also reports itself this way rather than as an unsupported-command error.

  • FirmwareError – On a firmware-reported failure. Reading the configuration saves nothing, so upstream getUsbConfig reports no 0x01xx code and this never surfaces as SystemFirmwareError – unlike set_usb_config, which writes to flash and can report FW_SETTINGS_SAVE_FAILED.

set_usb_config(config: UsbConfig) → None

Save a USB interface configuration to the adapter’s flash.

The firmware writes the configuration to flash and replies; it does not reset. The new configuration takes effect on the next reset or power cycle – call reboot() to apply it now. This handle stays OPEN, and get_usb_config() reads back the saved values.

A firmware-reported rejection changes nothing and leaves the handle OPEN. Any other failure (timeout, disconnect) leaves it unknown whether the adapter took the command, so the handle is marked TERMINAL.

Disabling both interfaces at once is recoverable: CosmicSDK drives the adapter over its USB HID interface, which this command does not touch, so a device left with WebUSB and CDC disabled can still be reopened and reconfigured.

CdcMode has no no-change sentinel, so read the current configuration and modify it rather than composing one from scratch:

current = dev.get_usb_config()
dev.set_usb_config(dataclasses.replace(current, cdc_mode=CdcMode.RS485))
Parameters:

config – The configuration to save. WebusbState.NO_CHANGE and CdcState.NO_CHANGE leave their own field as it is.

Raises:
  • ValidationError – If a field is not a member of its enum. No facade call is made.

  • CapabilityError – If config.cdc_mode is a mode this adapter model does not implement. No facade call is made.

  • DisconnectError – If the device is no longer OPEN.

  • SystemFirmwareError – On a firmware-reported failure — FW_SETTINGS_SAVE_FAILED (0x0108) when the settings write or the flash flush fails. The flush-failure reply is 10 bytes, not the usual 6: firmware appends a raw uint32_t flash diagnostic after the response header. Both decode paths tolerate it — each treats the 6-byte header as a minimum, never as an exact length — but the diagnostic word itself is not surfaced.

subscribe(notification_type: type[N], callback: Callable[[N], None], *, where: Callable[[N], bool] | None = None) → Subscription[N]

Register a callback for notifications of the given type.

The first call lazily builds the NotificationDispatcher and installs one bridge on the facade’s on_notification slot; later calls reuse both. The bridge enqueues notifications and never propagates exceptions back into the C++ polling thread.

Every part of that is host-side: on_notification sets a callback slot and returns, and nothing here reaches the adapter. So subscribing is exempt from the no-answer count in both directions – it is neither evidence that the adapter still answers nor refused on a handle that has stopped answering, which keeps it usable from the diagnostic path a caller writes after a command times out. See _OPERATIONS_THAT_ISSUE_NO_TRAFFIC.

Parameters:
  • notification_type – The concrete Notification subclass to match (e.g. UartRxNotification). Used for both isinstance filtering and type narrowing.

  • callback – Invoked on the dispatcher thread for every matching notification. Keep it short; for heavy work, queue onto your own loop / executor.

  • where – Optional secondary predicate applied after the type check. The dispatcher only invokes callback when isinstance(n, notification_type) and where(n) holds.

Returns:

A Subscription handle. Use unsubscribe() to detach, or use it as a context manager to scope the subscription to a with block.

Raises:

DisconnectError – If the device is no longer OPEN.

i2c(*, bus: I2cBus = I2cBus.A) → I2cController

Return the I2cController for bus, building it on first use.

Controllers are cached per bus on this device, so repeated calls with the same bus return the same object. The cache is dropped on close().

Parameters:

bus – Which I2C bus to control. Defaults to I2cBus.A.

Returns:

The cached or newly built I2cController for bus.

Raises:

CapabilityError – If the device does not expose bus.

i2c_target(*, bus: I2cBus = I2cBus.A) → I2cTarget

Return the I2cTarget for bus, building it on first use.

Targets are cached per bus, under a key distinct from i2c()’s, so repeated calls with the same bus return the same object and the target and controller interfaces for one bus are two objects rather than one. The cache is dropped on close().

The selector is per bus because the command is. i2cTargetInit takes busId as its first parameter, which is what makes this accessor take bus where spi_target() takes nothing – forced by the surface, not chosen for symmetry.

Getting the object is free; :meth:`I2cTarget.initialize` is the act that takes the role, and :meth:`I2cTarget.release` is what gives it back. Two consequences a program hits rather than reads about, both of them now about what can be up at once rather than about the power cycle: on one adapter an I2C target and an ordinary I2C controller cannot both be up, in either order, and only one target protocol can be armed at a time, because the arena is the shared bulk-staging pool. A release clears either. Mind that the target guard is per device while the release is per bus, so freeing a two-bus adapter for a target bring-up takes a release on each. The comment block above I2cTarget in pycosmicsdk._interfaces.i2c carries the firmware citations; read it before arming a target on an adapter somebody else is using.

The capability check is the same one i2c() makes, and it is the only one there is to make: firmware refuses a target bring-up on a bus it does not have with FW_BUS_NOT_SUPPORTED, and the arena’s own gate – which is a bulk transport question rather than a model one – belongs at I2cTarget.initialize(), where it can be waived. So unlike spi_target() this accessor adds no SDK-side hardware gate of its own; a Pulsar has both I2C buses and can be an I2C target on either.

Parameters:

bus – Which I2C bus the target answers on. Defaults to I2cBus.A.

Returns:

The cached or newly built I2cTarget for bus.

Raises:

CapabilityError – If the device does not expose bus.

i3c() → I3cController

Return the I3cController, building it on first use.

I3C has no bus selector: the firmware picks the low- or high-voltage port electrically from the bus voltage, so there is a single interface per device. It is cached, so repeated calls return the same object; the cache is dropped on close().

Returns:

The cached or newly built I3cController.

Raises:

CapabilityError – If the device does not expose I3C.

i3c_target() → I3cTarget

Return the I3cTarget interface, building it on first use.

Target mode runs on the same I3C hardware as i3c() and needs no bus selector, so there is a single interface per device. It is cached separately from the controller interface, so repeated calls return the same object; the cache is dropped on close().

Returns:

The cached or newly built I3cTarget.

Raises:

CapabilityError – If the device does not expose I3C.

spi() → SpiController

Return the SpiController, building it on first use.

SPI has a single controller per device — the chip-select line is a SpiController.configure() parameter, not a separate interface. It is cached, so repeated calls return the same object; the cache is dropped on close().

Returns:

The cached or newly built SpiController.

Raises:

CapabilityError – If the device does not expose SPI.

spi_target() → SpiTarget

Return the SpiTarget interface, building it on first use.

SPI target mode runs on the same SPI hardware as spi() and takes no selector — the chip-select line this target answers on is an SpiTarget.initialize() parameter. It is cached separately from the controller interface, so repeated calls return the same object; the cache is dropped on close().

Getting the object is free; :meth:`SpiTarget.initialize` is the act that takes the role, and :meth:`SpiTarget.release` is what gives it back. Taking the SPI target role is exclusive, so while it is held a controller bring-up is refused with FW_SPI_ROLE_CONFLICT; the release clears that in-session, and reboot() — after which this handle is terminal and a fresh device must be opened — is only the fallback on firmware predating the deinit dispatch. The comment block above SpiTarget in pycosmicsdk._interfaces.spi says why at length; read it before bringing a target up on an adapter somebody else is using.

Refused here as well as at bring-up on a unit whose silicon cannot receive a chip select, because the whole interface is unusable on such a unit rather than one call of it — a caller who cannot obtain the object cannot write the rest of the program against it. That refusal is this SDK’s own: firmware applies no revision check and would answer SUCCESS, after which the adapter arms and then reports nothing ever again. Read capabilities.spi_target_capable to branch on it.

That refusal does not cost such a unit the release. The guard is about a unit’s ability to serve as a target, and spiDeinit is role-agnostic — so on a unit refused here, the peripheral’s role is handed back through SpiController.release() instead. Read that method: it exists for exactly this case.

Returns:

The cached or newly built SpiTarget.

Raises:

CapabilityError – If the device does not expose SPI, or if this hardware revision cannot act as a SPI target.

uart() → Uart

Return the Uart interface, building it on first use.

UART has no bus selector, so there is a single interface per device. It is cached, so repeated calls return the same object; the cache is dropped on close().

Returns:

The cached or newly built Uart interface.

Raises:

CapabilityError – If the device does not expose UART.

gpio() → Gpio

Return the Gpio interface, building it on first use.

One interface covers every GPIO pin; the pin is an argument to each call. It is cached, so repeated calls return the same object — and with it the recorded per-pin configuration. The cache is dropped on close().

Returns:

The cached or newly built Gpio interface.

Raises:

CapabilityError – If the device does not expose GPIO.

AsyncDevice

The asyncio twin. See Blocking and asyncio for how the two relate and where they differ.

class pycosmicsdk.AsyncDevice(facade: Any, engine: AsyncEngine, timeout_ms: int)

Bases: object

Asyncio SDK handle for a single Binho USB host adapter.

Construct with AsyncDevice.open(). The explicit constructor is package-private; user code never calls it. Use the opener as an async context manager for deterministic cleanup; close() is idempotent.

Concurrency: a per-instance threading.RLock serializes lifecycle mutations (open/close/reboot). Protocol commands take it only for the lifecycle guard and to report their outcome to the no-answer count, never across an await — they correlate through the AsyncEngine, so concurrent awaits on one device interleave freely and the firmware processes them FIFO at the wire.

classmethod open(*, serial: str | None = None, path: str | None = None, model: DeviceModel | None = None, timeout_ms: int = 2000) → _AsyncDeviceOpener

Open a Binho host adapter, returning an awaitable async context manager.

The returned opener connects on first await or async with entry; argument validation happens eagerly, here.

Parameters:
  • serial – Optional device serial number to match. Mutually exclusive with path and model.

  • path – Optional USB-HID device path to match. Mutually exclusive with serial and model. With all three None, opens the first enumerated adapter.

  • model – Optional host-adapter model to match, e.g. DeviceModel.SUPERNOVA. Connects to the first enumerated adapter of that model — use it instead of a hard-coded serial when the scenario needs a specific model but any unit of it will do. Mutually exclusive with serial and path.

  • timeout_ms – Per-request firmware response timeout applied to awaited commands. Default 2000 ms matches CosmicSDK.

Returns:

An _AsyncDeviceOpener usable as async with or await.

Raises:

ValidationError – If more than one of serial, path and model is set. Failures that need the USB bus (no adapter of model attached, an unselectable model) surface on connect, not here.

async close() → None

Close the device. Idempotent.

Routes through the same cleanup path as the GC finalizer (so there is exactly one teardown sequence): outstanding requests are failed with DisconnectError and the native handle is disconnected. After close(), any subsequent command raises DisconnectError.

property state: DeviceState

Current lifecycle state. Locked read for memory-model clarity.

property is_connected: bool

True while the device is in DeviceState.OPEN.

property info: DeviceInfo

Cached DeviceInfo snapshot taken at open time.

Stable for the lifetime of the device — even after close(). Use fetch_device_info() to issue a fresh firmware query.

property model: DeviceModel

Convenience accessor for info.model.

property capabilities: DeviceCapabilities

Capability set derived from info.model at open time.

async get_version() → str

Return the CosmicSDK library version string.

Returns:

The CosmicSDK library version string.

Raises:

DisconnectError – If the device is no longer OPEN.

async fetch_device_info() → DeviceInfo

Issue a fresh device-info query and return the result.

Composes five get_usb_string commands (manufacturer, product, serial, firmware, hardware) and assembles the read-only DeviceInfo, deriving model from the product string. Distinct from the info property, which returns the snapshot cached at open.

Returns:

A freshly queried DeviceInfo from the firmware.

Raises:
async reboot(into: ResetTarget) → None

Reset the host adapter into the requested mode.

On success, the device re-enumerates over USB and the current handle becomes stale; state transitions to DeviceState.TERMINAL. Reconnecting requires opening a fresh AsyncDevice.

Parameters:

into – Reset target. RUNTIME performs a normal device reset; BOOT jumps the device into bootloader mode; ISP is reserved by CosmicSDK and not implemented yet.

Raises:
async get_usb_config() → UsbConfig

Read the USB interface configuration stored on the adapter.

Returns:

The WebUSB state, USB CDC state and CDC mode currently saved on the device. The no-change sentinels never appear in a read.

Raises:
  • DisconnectError – If the device is no longer OPEN.

  • ProtocolError – If the response was too short to carry a configuration, or carried a value this SDK does not know — which is also how firmware predating CMD_SYS_GET_USB_CONFIG reports itself, since its error response carries the generic header only.

  • CosmicTimeoutError – If the request exceeds the per-request timeout.

  • FirmwareError – On a firmware-reported failure. Reading the configuration saves nothing, so upstream getUsbConfig reports no 0x01xx code and this never surfaces as SystemFirmwareError – unlike set_usb_config, which writes to flash and can report FW_SETTINGS_SAVE_FAILED.

async set_usb_config(config: UsbConfig) → None

Save a USB interface configuration to the adapter’s flash.

The firmware writes the configuration to flash and replies; it does not reset. The new configuration takes effect on the next reset or power cycle – call reboot() to apply it now. This handle stays OPEN, and get_usb_config() reads back the saved values.

A firmware-reported rejection changes nothing and leaves the handle OPEN. Any other failure (timeout, disconnect) leaves it unknown whether the adapter took the command, so the handle is marked TERMINAL.

Disabling both interfaces at once is recoverable: CosmicSDK drives the adapter over its USB HID interface, which this command does not touch, so a device left with WebUSB and CDC disabled can still be reopened and reconfigured.

CdcMode has no no-change sentinel, so read the current configuration and modify it rather than composing one from scratch:

current = await dev.get_usb_config()
await dev.set_usb_config(dataclasses.replace(current, cdc_mode=CdcMode.RS485))
Parameters:

config – The configuration to save. WebusbState.NO_CHANGE and CdcState.NO_CHANGE leave their own field as it is.

Raises:
  • ValidationError – If a field is not a member of its enum. No facade call is made.

  • CapabilityError – If config.cdc_mode is a mode this adapter model does not implement. No facade call is made.

  • DisconnectError – If the device is no longer OPEN.

  • CosmicTimeoutError – If the request exceeds the per-request timeout.

  • SystemFirmwareError – On a firmware-reported failure — FW_SETTINGS_SAVE_FAILED (0x0108) when the settings write or the flash flush fails. The flush-failure reply is 10 bytes, not the usual 6: firmware appends a raw uint32_t flash diagnostic after the response header. Both decode paths tolerate it — each treats the 6-byte header as a minimum, never as an exact length — but the diagnostic word itself is not surfaced.

async get_analog_measurements() → AnalogMeasurements

Read every voltage rail the adapter measures, in one transaction.

One firmware command samples all six rails at once — the target rail shared by I2C, SPI, UART and GPIO, the I3C high- and low-voltage target rails, and the VCCA, VCCA-I3C and VDDIO2 supply rails — so this lives on the device rather than on a protocol interface: no single protocol owns the reading.

This method is async-only, and that asymmetry is forced: upstream declares getAnalogMeasurements on CosmicAsyncSDK alone, and CosmicSDK neither redeclares it nor exposes the private asyncSDK member it would have to go through, so a blocking Device has no way to read all six rails at once.

The reverse is not true. Upstream’s five blocking per-protocol readers are each implemented on top of this one command, keeping the field they were asked for and discarding the rest (CosmicSDK.cpp, i2cGetBusVoltage and friends). So the per-protocol readers are available on both surfaces — the async ones are built here, on top of this command, exactly as upstream builds the blocking ones.

Returns:

A snapshot of all six rails in millivolts. Rails the connected model does not fit still occupy their fields with whatever the firmware reports; see AnalogMeasurements.

Raises:
  • DisconnectError – If the device is no longer OPEN.

  • ProtocolError – If the response was too short to carry the six measurements — which is also how firmware that does not implement the command reports itself, since its error response carries the generic header only.

  • CosmicTimeoutError – If the request exceeds the per-request timeout.

  • SystemFirmwareError – On a firmware-reported failure.

i2c(*, bus: I2cBus = I2cBus.A) → AsyncI2cController

Return the AsyncI2cController for bus, building it on first use.

Controllers are cached per bus on this device, so repeated calls with the same bus return the same object. The cache is dropped on close(). i2c itself is synchronous (no I/O); await the controller’s methods to talk to the device.

Parameters:

bus – Which I2C bus to control. Defaults to I2cBus.A.

Returns:

The cached or newly built AsyncI2cController for bus.

Raises:

CapabilityError – If the device does not expose bus.

i2c_target(*, bus: I2cBus = I2cBus.A) → AsyncI2cTarget

Return the AsyncI2cTarget for bus, building it on first use.

The asyncio twin of Device.i2c_target(): same cache key (("i2c_target", bus)), same per-bus selector – forced by i2cTargetInit taking busId – and the same capability check. Targets are cached separately from controllers, so repeated calls with the same bus return the same object; the cache is dropped on close(). i2c_target itself is synchronous (no I/O); await the target’s methods to talk to the device.

Getting the object is free; :meth:`AsyncI2cTarget.initialize` is the act that takes the role, and :meth:`AsyncI2cTarget.release` is what gives it back – per bus, while the target guard is per device. The comment block above I2cTarget in pycosmicsdk._interfaces.i2c carries the firmware citations.

Note

The arena is bulk-only and the bulk plane is blocking-only, so an AsyncI2cTarget can arm a target and watch its bus events but cannot seed or read back its arena. That asymmetry lives on the interface, which documents it and which gates its bring-up on the host half of the transport alone; this accessor adds nothing to it.

Parameters:

bus – Which I2C bus the target answers on. Defaults to I2cBus.A.

Returns:

The cached or newly built AsyncI2cTarget for bus.

Raises:

CapabilityError – If the device does not expose bus.

i3c() → AsyncI3cController

Return the AsyncI3cController, building it on first use.

I3C has no bus selector: the firmware picks the low- or high-voltage port electrically from the bus voltage, so there is a single interface per device. It is cached, so repeated calls return the same object; the cache is dropped on close(). i3c itself is synchronous (no I/O); await the controller’s methods to talk to the device.

Returns:

The cached or newly built AsyncI3cController.

Raises:

CapabilityError – If the device does not expose I3C.

i3c_target() → AsyncI3cTarget

Return the AsyncI3cTarget interface, building it on first use.

Target mode runs on the same I3C hardware as i3c() and needs no bus selector, so there is a single interface per device. It is cached separately from the controller interface, so repeated calls return the same object; the cache is dropped on close(). i3c_target itself is synchronous (no I/O); await the interface’s methods to talk to the device.

Returns:

The cached or newly built AsyncI3cTarget.

Raises:

CapabilityError – If the device does not expose I3C.

spi() → AsyncSpiController

Return the AsyncSpiController, building it on first use.

SPI has a single controller per device — the chip-select line is a AsyncSpiController.configure() parameter, not a separate interface. It is cached, so repeated calls return the same object; the cache is dropped on close(). spi itself is synchronous (no I/O); await the controller’s methods to talk to the device.

Returns:

The cached or newly built AsyncSpiController.

Raises:

CapabilityError – If the device does not expose SPI.

spi_target() → AsyncSpiTarget

Return the AsyncSpiTarget interface, building it on first use.

SPI target mode runs on the same SPI hardware as spi() and takes no selector — the chip-select line this target answers on is an AsyncSpiTarget.initialize() parameter. It is cached separately from the controller interface, so repeated calls return the same object; the cache is dropped on close().

Getting the object is free; :meth:`AsyncSpiTarget.initialize` is the act that takes the role, and :meth:`AsyncSpiTarget.release` is what gives it back. Taking the SPI target role is exclusive, so while it is held a controller bring-up is refused with FW_SPI_ROLE_CONFLICT; the release clears that in-session, and reboot() — after which this handle is terminal and a fresh device must be opened — is only the fallback on firmware predating the deinit dispatch. The comment block above SpiTarget in pycosmicsdk._interfaces.spi says why at length; read it before bringing a target up on an adapter somebody else is using.

Refused here as well as at bring-up on a unit whose silicon cannot receive a chip select, because the whole interface is unusable on such a unit rather than one call of it — a caller who cannot obtain the object cannot write the rest of the program against it. That refusal is this SDK’s own: firmware applies no revision check and would answer SUCCESS, after which the adapter arms and then reports nothing ever again. Read capabilities.spi_target_capable to branch on it.

That refusal does not cost such a unit the release. The guard is about a unit’s ability to serve as a target, and spiDeinit is role-agnostic — so on a unit refused here, the peripheral’s role is handed back through AsyncSpiController.release() instead. Read that method: it exists for exactly this case.

Returns:

The cached or newly built AsyncSpiTarget.

Raises:

CapabilityError – If the device does not expose SPI, or if this hardware revision cannot act as a SPI target.

uart() → AsyncUart

Return the AsyncUart interface, building it on first use.

UART has no bus selector, so there is a single interface per device. It is cached, so repeated calls return the same object; the cache is dropped on close(). uart itself is synchronous (no I/O); await the interface’s methods to talk to the device.

Returns:

The cached or newly built AsyncUart interface.

Raises:

CapabilityError – If the device does not expose UART.

gpio() → AsyncGpio

Return the AsyncGpio interface, building it on first use.

One interface covers every GPIO pin; the pin is an argument to each call. It is cached, so repeated calls return the same object — and with it the recorded per-pin configuration. The cache is dropped on close(). gpio itself is synchronous (no I/O); await the interface’s methods to talk to the device.

Returns:

The cached or newly built AsyncGpio interface.

Raises:

CapabilityError – If the device does not expose GPIO.

subscribe(notification_type: type[N], callback: Callable[[N], Awaitable[None]], *, where: Callable[[N], bool] | None = None) → Subscription[N]

Register an async callback for notifications of the given type.

subscribe itself is synchronous (registration only). The coroutine callback returns is scheduled on the event loop running at call time via asyncio.run_coroutine_threadsafe — the dispatcher thread never runs it inline. An exception from the coroutine is logged under pycosmicsdk.dispatcher and never propagates.

Parameters:
  • notification_type – The concrete Notification subclass to match.

  • callback – Async function invoked for every matching notification.

  • where – Optional secondary predicate applied after the type check.

Returns:

A Subscription handle; unsubscribe() detaches it.

Raises:

DisconnectError – If the device is no longer OPEN.

iter_notifications(notification_type: type[N] = Notification, *, where: Callable[[N], bool] | None = None) → _NotificationStream[N]

Return an async iterator over matching notifications.

Use as an async context manager so the underlying subscription is detached on every exit path (normal, break, exception, cancellation):

async with dev.iter_notifications(UartRxNotification) as stream:
    async for n in stream:
        ...
Parameters:
  • notification_type – The Notification subclass to yield. Defaults to the base Notification (every notification).

  • where – Optional secondary predicate applied after the type check.

Returns:

A _NotificationStream async context manager / iterator.

Raises:

DisconnectError – From the iteration — not from this call — if the device is closed, rebooted or reconfigured while a consumer is parked on the stream. The stream has no other ending: it is infinite until the handle goes terminal, and it says so rather than stopping quietly, exactly as AsyncUart.receive does. Wrap the async for in except DisconnectError if a consumer task should end cleanly when the device closes.

Note

Async-only, but not because of any upstream limit. This is an async for idiom layered over subscribe(), which exists on both surfaces and is the whole of the underlying mechanism — there is no separate firmware command and nothing async-specific in L1 or L2. A blocking Device has no counterpart simply because pull-style iteration is not how the blocking surface delivers notifications; use Device.subscribe with a callback there. The same holds for AsyncUart.iter_receive.