# Errors Nothing in `pycosmicsdk` returns a status code. Every failure is an exception, and every exception derives from {py:obj}`~pycosmicsdk.CosmicError`, which carries a `status_code` and an optional `operation` label naming the call site. ```text Exception └── CosmicError ├── ValidationError arguments rejected before any native call ├── CapabilityError protocol the device does not have ├── DeviceNotFoundError no adapter matched the open criteria ├── UnsupportedFirmwareError firmware too old for this package to speak to ├── DisconnectError device gone mid-operation ├── AdapterNotAnsweringError handle live, adapter stopped replying ├── CosmicTimeoutError also a builtins.TimeoutError ├── ProtocolError protocol-level, not firmware state ├── CosmicSDKLibraryNotFound bundled shared library not located ├── CosmicSDKVersionError loaded library reports an incompatible version ├── CosmicInternalError a violated SDK invariant -- a bug in this SDK └── FirmwareError reported by device firmware, via a status code ├── SystemFirmwareError 0x01xx band (voltage, reset, ...) ├── I2cError 0x02xx band ├── I2cDutyCycleRefusedError set_duty_cycle's FW_INVALID_PARAMETER, with the settings kept ├── SpiError 0x03xx band — SPI target mode only, see below ├── UartError 0x04xx band ├── GpioError 0x05xx band ├── I3cError 0x06xx band └── DebugError 0x1Dxx/0x1Exx — a DebugProbe's target flash and debug session └── TargetFlashVerifyError TargetFlash.write's readback mismatch, with the address ``` The bands are upstream's own: a firmware result code carries its `GROUP_*` in the high byte (`submodules/CosmicSDK/include/definitions.h`). Generic firmware codes (`0x0001`–`0x000E`) sit *below* every band, and anything from `0x0700` up other than `0x1Exx` sits above them; both fall through to {py:obj}`~pycosmicsdk.FirmwareError` itself. `0x1Exx` are the debug probe's host-side debug codes (`DebugProbeDebugCodes.h`), raised as {py:obj}`~pycosmicsdk.DebugError` with the code's name in the message. ## Which one to catch **`CosmicError`** is the blanket catch — every example script uses it at the top level. **`FirmwareError`** is the right granularity for "the device rejected this". The per-protocol subclasses are narrower than their names suggest, because a status code lands in a protocol band only if firmware put it there: - A rejected *configuration* is normally the generic `FW_INVALID_PARAMETER`, and an uninitialised interface `FW_INTERFACE_NOT_INITIALIZED` — both below every band, so both plain {py:obj}`~pycosmicsdk.FirmwareError`. - A rejected *voltage* is in the 0x01xx system band, so {py:obj}`~pycosmicsdk.SystemFirmwareError`, whichever interface asked for it. - {py:obj}`~pycosmicsdk.UartError` covers exactly one upstream code, `FW_UART_IN_VCOM_PORT_MODE`. - {py:obj}`~pycosmicsdk.I2cError`'s band, by contrast, is well populated — address NACK, arbitration lost, SCL held low. **`ValidationError`** is raised Python-side, before any USB traffic: bad argument ranges and field widths, over-the-maximum payloads, the GPIO direction guard, the HDR-DDR length constraints, I3C target memory bounds. **`CapabilityError`** is also raised Python-side, before any USB traffic — see the section below, which is the one thing on this page most likely to catch a reader out. **`CosmicTimeoutError`** multi-inherits `TimeoutError`, so `except TimeoutError:` catches it. The `Cosmic` prefix keeps it from shadowing the built-in after a `from pycosmicsdk import *`. **`AdapterNotAnsweringError`** is this SDK refusing to send, after three consecutive timeouts on one handle with nothing answering in between. It is *not* a `TimeoutError` — see the section below for why that is deliberate. `examples/blocking_api/i2c/error_handling.py` provokes one failure per layer and prints which exception each produces. ## Capability refusals arrive before the wire {py:obj}`~pycosmicsdk.CapabilityError` means *this adapter cannot do this, and nothing was sent*. It covers two kinds of limit — a protocol the model does not have at all ({py:obj}`Device.i3c() ` on a Pulsar, an {py:obj}`~pycosmicsdk.I2cBus` the model does not carry), and a narrower limit that a *guard* checks in front of an otherwise-available call: | Refused call | Because | |---|---| | {py:obj}`~pycosmicsdk.I2cController.configure`, {py:obj}`~pycosmicsdk.I2cController.set_pull_up` | the hardware revision cannot reconfigure a live I2C interface | | {py:obj}`~pycosmicsdk.I2cController.initialize`, `configure`, `set_pull_up` | this model's firmware has no table row for the requested {py:obj}`~pycosmicsdk.I2cPullUp` | | {py:obj}`~pycosmicsdk.Gpio.configure_interrupt` | the hardware revision cannot arm an interrupt on that pin | | Any of the three protocols' `transport=` arguments with `BULK` — {py:obj}`~pycosmicsdk.SpiController.write` / `read`, {py:obj}`~pycosmicsdk.I2cController.write` / `read`, {py:obj}`~pycosmicsdk.I3cController.sdr_write` / `sdr_read` among them | the device or the host declined to negotiate the bulk transport. I2C and I3C refuse exactly as SPI does, each through its own guard: `BULK` is a demand, and `AUTO` is the argument that never raises this | | {py:obj}`SpiTarget.initialize `, `bring_up`, and {py:obj}`Device.spi_target() ` | this hardware revision cannot receive an incoming chip select — a rev-B Supernova, and only that one revision | | {py:obj}`I2cTarget.initialize ` and {py:obj}`SpiTarget.initialize `, and their `bring_up`s and asyncio twins | the target's memory arena travels only over the bulk plane and has no HID route, so an unreachable plane is refused at arming rather than two calls later. Waived with `require_arena_access=False` | | {py:obj}`~pycosmicsdk.Device.set_usb_config` | this model fits no transceiver for the requested {py:obj}`~pycosmicsdk.CdcMode` — `TERMINAL` is Supernova-only, `RS485` and `CANFD` are Pulsar-only | Two consequences, and both have bitten this repository's own examples: **It is not a `FirmwareError`.** `CapabilityError` derives straight from {py:obj}`~pycosmicsdk.CosmicError`, so a handler written as `except FirmwareError` — the natural shape for "the device rejected this" — does not catch it. Give it its own clause. **Do not filter it on `status_code`.** Because nothing reached the device, there is no firmware rejection code to carry; the guards raise `StatusCode.SDK_ERROR_WRONG_REQUEST`. A handler that catches `CapabilityError` and then re-raises unless the code is on some allow-list re-raises *every* capability refusal. That is why the pattern below puts the two on separate clauses and filters only the firmware one: ```python from pycosmicsdk import CapabilityError, FirmwareError, StatusCode try: i2c.configure(frequency_hz=400_000, pull_up=I2cPullUp.OHM_330) except CapabilityError: # Refused before the wire, so this is by definition the recoverable case: # the bus is still running exactly as initialize() left it. No status # filter -- the code is SDK_ERROR_WRONG_REQUEST, not a firmware code. settings_applied = False except FirmwareError as exc: # Here the code *is* what distinguishes a rejected config from a fault. if exc.status_code is not StatusCode.FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE: raise settings_applied = False ``` `examples/helpers.py` (`i2c_bring_up`, `i2c_set_pull_up`, and their asyncio twins) is the bench-verified version of exactly this, and every I2C example goes through it. :::{note} `StatusCode.SDK_ERROR_WRONG_REQUEST` is the SDK's generic "this request was never sent" code and it is carried by four different classes: a {py:obj}`~pycosmicsdk.ValidationError` (a bad argument), a {py:obj}`~pycosmicsdk.CapabilityError` (a model or revision limit), a {py:obj}`~pycosmicsdk.ProtocolError` (a request CosmicSDK itself rejected as malformed, and that this SDK did not pre-empt — a sub-address longer than four bytes is the live case) and {py:obj}`~pycosmicsdk.CosmicInternalError`. The class carries the meaning; the code does not. Branch on the class. Upstream also rejects an I3C PID that is not six bytes and an out-of-range target-memory address with the same code, but neither reaches it: this SDK checks both Python-side first, so they arrive as {py:obj}`~pycosmicsdk.ValidationError`. ::: Better than catching: ask first. {py:obj}`~pycosmicsdk.DeviceCapabilities` answers all five limits with no USB traffic — `capabilities.i2c_runtime_reconfigurable`, `capabilities.supports_i2c_pull_up(pull_up)`, `capabilities.supports_gpio_interrupt(pin)`, `capabilities.spi_target_capable` and `capabilities.supports_spi_bulk()` / `capabilities.supports_i3c_bulk()`. See [Capabilities you can ask about in advance](types.md). ## Refusals fail open A `CapabilityError` tells you one of three things, and it is worth knowing which before you go looking for a cable fault: **You have the wrong device.** A protocol or bus the *model* does not carry — {py:obj}`Device.i3c() ` on a Pulsar, `I2cBus.B` on a Supernova, a `CdcMode` whose transceiver this board does not fit, an {py:obj}`~pycosmicsdk.I2cPullUp` with no row in this model's firmware table. No revision of that model changes the answer and no argument avoids it; the fix is a different adapter, or a different value. **You have the wrong revision.** A limit of the *board*, not the model: reconfiguring a live I2C interface, or arming a GPIO interrupt on the one pin whose revision cannot. The same model at another revision does it fine. The message names the revision it read, so `SUPERNOVA HW-B` in the text is the thing to check. **You have the wrong expectation.** `SpiTransport.BULK` is a demand, not a preference. `SpiTransport.AUTO` asks for the best available transport and never raises this. :::{important} **An unknown answer means "attempt it", never "refuse it".** This SDK carries the model and revision tables in Python because CosmicSDK exposes no capabilities query, and a table can be out of date in two directions. Refusing an operation that would have worked is the worse failure — a user cannot work around it — so every guard is arranged so that ignorance falls through to the device: - A model firmware does not apply the limit to is not gated at all. The live-reconfigure check exists only in the Supernova firmware build, so `i2c_runtime_reconfigurable` is `True` on every Pulsar whatever its `hw_version` says, and the guard in front of {py:obj}`~pycosmicsdk.I2cController.configure` never fires there. - A {py:obj}`~pycosmicsdk.DeviceModel` this release has never been taught reaches no guard either, and the {py:obj}`~pycosmicsdk.CdcMode` check has no entry for it and passes every mode through. - An `hw_version` string that does not parse as a revision — an empty field, a firmware that starts reporting a semver, a shape nobody anticipated — is attempted rather than refused. The one gate written as an equality (`i2c_runtime_reconfigurable`, mirroring firmware's own `hwVersion != 'C'`) routes both that case and an ungated model *around* the equality rather than through it, because inverting a table is exactly where fail-open gets lost. - The GPIO interrupt table lists the pin/revision pairs known to be **broken**, so a pair absent from it is armed. That is why it holds a single cell — Supernova Rev B, `PIN_3` — rather than an enumeration of what works. The two pull-up tables are the exception, and read the other way round: they enumerate the values firmware **accepts**, per model, so a value absent from one is refused here. They can be that strict because firmware's own table is a closed per-model set — a value it does not name has no resistance to program, so there is nothing to fail open *to*. The cost of failing open is that a limit this SDK has not been taught still reaches the device and comes back as a firmware code — typically `FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE` or `FW_INVALID_PARAMETER`, as a plain {py:obj}`~pycosmicsdk.FirmwareError`. That is the deliberate trade: a loud, late, correctly-attributed failure in preference to an early refusal that might have been wrong. ::: **Whole protocols are the one thing that does not fail open, and it is worth knowing before you meet it.** `protocols` and `i2c_buses` are enumerated positively per known model, so an adapter whose model this release does not recognise reports both as empty and *every* interface accessor — {py:obj}`Device.i2c() `, {py:obj}`~pycosmicsdk.Device.spi`, {py:obj}`~pycosmicsdk.Device.uart`, {py:obj}`~pycosmicsdk.Device.gpio`, {py:obj}`~pycosmicsdk.Device.i3c` — raises `CapabilityError`. The handle still opens and {py:obj}`~pycosmicsdk.Device.info` still reports what the adapter said about itself, which is the point: you can identify an unrecognised unit and file a report, but you cannot drive it. If a supported adapter ever lands here, the model marker is what to look at first — it comes from the USB **product id**, not from a product string. :::{note} **{py:obj}`~pycosmicsdk.SpiError` has exactly one source: SPI target mode.** Both status translators map the 0x03xx firmware band to it, and all seven codes in that band are target-mode ones — `FW_SPI_ROLE_CONFLICT`, `FW_SPI_TARGET_STAGING_UNAVAILABLE`, `FW_SPI_TARGET_SURPASSED_MEMORY_SIZE`, `FW_SPI_TARGET_PAYLOAD_TRANSFER_FAILED`, `FW_SPI_TARGET_WRONG_MODEL`, `FW_SPI_TARGET_INVALID_CONFIG` and `FW_SPI_TARGET_REGION_TABLE_FULL`. So a program that never calls {py:obj}`Device.spi_target() ` will never see one, and controller-side SPI failures still arrive as {py:obj}`~pycosmicsdk.FirmwareError` (generic codes) or {py:obj}`~pycosmicsdk.SystemFirmwareError` (voltage). The [SPI guide](../guides/spi.md) has the full per-failure table. **What this note said until 2026-09-05.** It was a warning headed *"`SpiError` is unreachable today — do not rely on catching it"*, and argued that CosmicSDK's `definitions.h` declared no `FW_SPI_*` status code at all, so the band was empty upstream and nothing reached the class. That was a true statement about an older submodule pin. The pinned `definitions.h` now declares the seven codes above and `_status_maps.py` maps every one of them to `SpiError`, so the advice it gave — catch `FirmwareError` instead — is now right for controller code only. ::: :::{note} {py:obj}`~pycosmicsdk.CosmicInternalError` — a violated internal invariant, an SDK bug rather than user error — **is** re-exported at package level: ```python from pycosmicsdk import CosmicInternalError ``` It was previously the only one of the eighteen names in `pycosmicsdk.errors` that the top-level package did not re-export, which for a raisable class was an oversight rather than a decision. It is now in `pycosmicsdk.__all__`, so `from pycosmicsdk import *` brings it in and it needs no import from the submodule. It has one raise site — transaction-id exhaustion in the async engine, which can only happen if the SDK leaks pending entries — so an application that catches {py:obj}`~pycosmicsdk.CosmicError` already covers it, and one that catches it specifically is reporting a bug in this SDK rather than handling a device condition. ::: ## `AdapterNotAnsweringError` means this SDK stopped sending {py:obj}`~pycosmicsdk.AdapterNotAnsweringError` is the only class on this page that is about the SDK's own conduct rather than about your arguments, your device's answer, or a message that did not parse. It says: **three commands in a row on this handle timed out with no reply in between, so nothing more will be sent on it.** No command was issued for the call that raised it. It exists because continuing is not neutral. Backlog ticket 72 records a bench adapter driven through several handle cycles of timed-out commands, each one absorbed and followed by another, which ended up enumerating over USB-HID while refusing every {py:obj}`~pycosmicsdk.Device.open` for more than 270 seconds — a state a physical unplug/replug was the only way out of, because no handle could be opened to send anything on, a reset included. On the firmware side the queues that back up are a handful of entries deep and every enqueue waits forever rather than failing, so a run of unanswered commands is what walks a device into it. **Three, not one.** A single timeout is a normal, documented thing on real hardware and its shape is untouched here: it is still a {py:obj}`~pycosmicsdk.CosmicTimeoutError`, and so are the second and third. The guard changes nothing about a command it issued — only the command *after* the third refusal-free timeout is declined. Anything that answers, **including a firmware rejection**, resets the count: an empty I2C bus reporting `FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED` in five milliseconds is a healthy adapter. **Both surfaces, one rule.** {py:obj}`~pycosmicsdk.Device` and {py:obj}`~pycosmicsdk.AsyncDevice` count identically — same limit of three, same definition of an answer, same refusal — because they share the count. An `await` reaches a wedged adapter exactly as a call does. Two asyncio details follow from commands being able to overlap there: concurrent awaits that all time out each count, so a `gather` of four unanswered commands trips the guard as surely as four sequential ones; and a command you cancel yourself counts for nothing in either direction, because a cancellation says nothing about whether the adapter would have replied. **What is exempt.** Operations that cross no wire — {py:obj}`Device.get_version ` and {py:obj}`Device.subscribe `, with their async twins — are neither refused on a tripped handle nor treated as answers on a live one, so a diagnostic loop cannot silently disarm the guard and a post-mortem can still read the handle, and start watching it, after it trips. Registering a subscription installs a host-side callback and sends nothing. Everything that *can* reach the wire is refused once the count is reached, including calls that would have sent nothing this time: the guard is about the handle, not the command. **Only an issued command counts, in either direction.** A call that takes the handle and sends nothing is not an answer, and does not reset the run. That covers the calls a poll loop is built out of — {py:obj}`I2cTarget.drain_events `, {py:obj}`wait_for_event `, {py:obj}`Uart.receive `, the `max_transfer_bytes` and `transport` reads — none of which reach the adapter at all. Interleaving one between every command therefore has no effect on the count, which is what makes the guard reach the callers most likely to need it. Their own timeouts are not evidence either: {py:obj}`Uart.receive ` timing out means the peer sent nothing, not that the adapter stopped answering. This is separate from the exemption above, which is about the *refusal*: once the count is reached these calls are declined like any other. **Catching it.** It derives from {py:obj}`~pycosmicsdk.CosmicError` and deliberately from neither {py:obj}`~pycosmicsdk.CosmicTimeoutError` nor {py:obj}`~pycosmicsdk.FirmwareError`. `except TimeoutError:` will *not* catch it, which is the point: retrying on a timeout is the behaviour it exists to interrupt, and a handler that swallowed it as one more timeout would keep the loop running. **Recovering.** The count is per handle. A new {py:obj}`~pycosmicsdk.Device.open` or {py:obj}`~pycosmicsdk.AsyncDevice.open` starts a fresh one, and that the count is not keyed to the adapter's serial is deliberate: such a latch would outlive the physical replug that fixes the worst case. Whether the adapter answers on that new handle is a property of the adapter. **If it does answer on the new handle, the SDK has one more lever**, and this page said until 2026-09-05 that it had none: {py:obj}`~pycosmicsdk.Device.reboot` with {py:obj}`~pycosmicsdk.ResetTarget.RUNTIME`. A runtime reset clears latched firmware interface state — measured on a Pulsar, it cleared a persistent I2C controller role on both buses — and the adapter was openable again 0.825 s later. That is **one observation on one adapter**, so poll {py:obj}`~pycosmicsdk.list_devices` rather than sleeping a fixed interval, and note that it does **not** clear the bus VTARG setpoint. **A replug is what remains for the case where the reset is not dispatched at all.** Ticket 72's state is exactly that: a stranded protocol manager blocks the USB receive decoder, after which no command of any group is decoded, `SYS` included — so the reset request never reaches the code that would execute it. Do not read "a reset clears latched state" as "a reset recovers a wedge"; that has not been tested, and the one relevant observation says the opposite for a device in that state. :::{warning} This guard cannot un-wedge a device, and it is not a proof against wedging one. It stops this SDK adding to a run of unanswered commands; it arrives too late for a device that has already stopped answering, and firmware has at least one path to a mute HID interface that a single abandoned transfer can reach, which no host-side count can prevent. Treat it as a diagnosis worth acting on, not as a guarantee. ::: ## `ProtocolError` means the answer did not parse {py:obj}`~pycosmicsdk.ProtocolError` is the one class on this page that is about *neither* your arguments *nor* a firmware rejection. It says: the SDK and the device did not agree on the shape of a message. Nothing in it is a firmware status code, so there is no `FW_*` value to branch on and no bus condition to retry through. Both status translators map exactly three SDK-level codes to it — `SDK_ERROR_WRONG_REQUEST`, `SDK_ERROR_WRONG_RESPONSE` and `SDK_ERROR_UNEXPECTED_RESPONSE` — which is two distinct situations: **The request was never formed.** CosmicSDK refused to build the frame, so nothing was sent. The live case is a `subaddress` longer than four bytes on any I2C or I3C `read`/`write`; each of those methods documents {py:obj}`~pycosmicsdk.ProtocolError` in its `Raises:` block for exactly that. **The response could not be decoded.** The adapter answered, and the reply was too short to carry the fields it should have, or carried a value this release does not name. {py:obj}`~pycosmicsdk.Device.get_usb_config` raises it for a {py:obj}`~pycosmicsdk.CdcMode` newer than this SDK; {py:obj}`~pycosmicsdk.AsyncDevice.get_analog_measurements` raises it for a reply that does not carry all six rails; the GPIO logic-level read, the CCC decoders and the length-prefixed protocol reads each have the same bounds check in front of them, because upstream reads several of these blocks by `reinterpret_cast` with no length check at all. :::{important} **Firmware that predates a command reports itself as a {py:obj}`~pycosmicsdk.ProtocolError`, not as an unsupported-command {py:obj}`~pycosmicsdk.FirmwareError`.** A firmware error response carries the six-byte generic header and nothing else, so from the decoder's point of view it is simply a reply too short to parse. This is documented behaviour on {py:obj}`~pycosmicsdk.Device.get_usb_config` and {py:obj}`~pycosmicsdk.AsyncDevice.get_analog_measurements`, whose docstrings both say so — a `ProtocolError` from either on an older adapter most likely means *update the firmware*, not *retry*. ::: So a handler for it does not recover; it reports. What is worth printing alongside is which versions were in play, since a mismatch between them is the usual cause: ```python import pycosmicsdk from pycosmicsdk import Device, ProtocolError with Device.open() as dev: try: config = dev.get_usb_config() except ProtocolError as exc: print(f"could not read the USB configuration: {exc}") print(f" pycosmicsdk : {pycosmicsdk.__version__}") print(f" CosmicSDK library: {dev.get_version()}") print(f" adapter firmware : {dev.info.fw_version}") else: print(config) ``` ## The two loader errors nothing raises yet {py:obj}`~pycosmicsdk.CosmicSDKLibraryNotFound` and {py:obj}`~pycosmicsdk.CosmicSDKVersionError` are in the hierarchy above and in `pycosmicsdk.__all__`, and **no code path in this SDK raises either of them**. They are wired for the same reason {py:obj}`~pycosmicsdk.SpiError` is: the class should already exist if the condition ever becomes reachable. The condition is not reachable today because there is nothing to load. The native library is **statically linked into the compiled extension**, so there is no shared library to locate on disk (`Device.open()` takes no `lib_path`, and there is no `COSMICSDK_LIB_PATH` environment variable) and no separately installed version to check against. Both names are carried over from a superseded `ctypes` design in which the library *was* found and version-checked at run time. What you can actually meet at import time is Python's own `ImportError`, from `import pycosmicsdk` failing to load the extension module — a broken or mismatched wheel, or a source checkout that was never built. That is not a {py:obj}`~pycosmicsdk.CosmicError` and no `except CosmicError` will see it. Do not write a handler for these two expecting to catch a bad install. ## Reference ```{eval-rst} .. autoclass:: pycosmicsdk.CosmicError .. autoclass:: pycosmicsdk.ValidationError .. autoclass:: pycosmicsdk.CapabilityError .. autoclass:: pycosmicsdk.DeviceNotFoundError .. autoclass:: pycosmicsdk.UnsupportedFirmwareError .. autoclass:: pycosmicsdk.DisconnectError .. autoclass:: pycosmicsdk.AdapterNotAnsweringError .. autoclass:: pycosmicsdk.CosmicTimeoutError .. autoclass:: pycosmicsdk.ProtocolError .. autoclass:: pycosmicsdk.CosmicSDKLibraryNotFound .. autoclass:: pycosmicsdk.CosmicSDKVersionError .. autoclass:: pycosmicsdk.CosmicInternalError .. autoclass:: pycosmicsdk.FirmwareError .. autoclass:: pycosmicsdk.I2cError .. autoclass:: pycosmicsdk.I2cDutyCycleRefusedError .. autoclass:: pycosmicsdk.UartError .. autoclass:: pycosmicsdk.I3cError .. autoclass:: pycosmicsdk.SpiError .. autoclass:: pycosmicsdk.GpioError .. autoclass:: pycosmicsdk.SystemFirmwareError .. autoclass:: pycosmicsdk.DebugError .. autoclass:: pycosmicsdk.TargetFlashVerifyError ```