# Data types Read-only records the SDK hands back, plus the two config objects it accepts. All of them are frozen, slotted dataclasses on the Python side, or `nanobind` value types bound from C++ ({py:obj}`~pycosmicsdk.DeviceInfo` and {py:obj}`~pycosmicsdk.I2cScanResult`). ## Device identity and capabilities {py:obj}`~pycosmicsdk.DeviceCapabilities` is captured once at `open()` from the model the firmware reports; it is a snapshot, not a live query. {py:obj}`~pycosmicsdk.DeviceInfo` is the raw identification the adapter reports about itself — note that its string fields carry the firmware's three-character tags (`"SN-634F…"`, `"HW-B"`, `"MN-Binho LLC"`), unlike {py:obj}`~pycosmicsdk.DiscoveredDevice`, whose equivalents are untagged and directly usable as `open(serial=...)`. The payload caps on {py:obj}`~pycosmicsdk.DeviceCapabilities` are **enforced**: `max_i2c_payload_bytes`, `max_i3c_payload_bytes` and `max_uart_message_bytes` are each read by a guard that raises {py:obj}`~pycosmicsdk.ValidationError` before any USB traffic, and so is SPI's 1024-byte cap, which is a constant rather than a snapshot field and so is not exposed here. :::{note} **What this section said until 2026-09-05.** It called the caps *"advisory, sourced from the datasheet rather than enforced by this SDK"*, with SPI as the one exception, and reasoned from the on-wire length field being a `uint16` and CosmicSDK declaring no length macro for I3C or UART. The premises still hold and the conclusion no longer follows: the guards read the *snapshot*, not an upstream macro, so a cap with no macro behind it is enforced exactly as one with a macro is. The old wording also contradicted the autodoc block below on this same page, whose docstring describes them as route ceilings. ::: They describe the **ordinary HID route only**, and that is what "enforced" means for the two that have a second route: over HID the cap is a hard refusal, while `I2cTransport.AUTO` and `I3cTransport.AUTO` compare a request against it and serve a bulk-capable operation above it over the bulk plane instead. UART has no bulk plane, so its cap is the whole answer; I3C's HDR-DDR is the exception in the other direction, capped at 1024 on *both* routes for bus safety. A handful of operations can move their payload over the adapter's vendor bulk endpoint instead, at a much larger ceiling that is negotiated per connection rather than fixed per model — {py:obj}`~pycosmicsdk.BulkTransportCapability` carries that, and the per-call answer is on the interfaces themselves ({py:obj}`I2cController.max_transfer_bytes ` and the two target interfaces' `max_transfer_bytes`). See [The bulk transfer plane](../guides/bulk.md). ### Capabilities you can ask about in advance Most of the snapshot is a property of the *model*. Five fields are not, and they are the ones worth knowing about, because each one is backed by a guard that refuses the call with {py:obj}`~pycosmicsdk.CapabilityError` **before any USB traffic** — so a program can either ask first and branch, or catch. Asking first is cheaper and reads better: | Ask | Guards | Why it can be `False` | |---|---|---| | `capabilities.i2c_runtime_reconfigurable` | {py:obj}`~pycosmicsdk.I2cController.configure`, {py:obj}`~pycosmicsdk.I2cController.set_pull_up` | Hardware **revision**, and an equality rather than a floor: on a Supernova whose `hw_version` is anything but `C`, a live I2C interface cannot be reconfigured at all — firmware asks `hwVersion != 'C'`, so a future Rev D would be refused exactly as Rev B is. A Pulsar is not gated at all; neither is a Supernova whose `hw_version` does not parse as a revision. `initialize` is unaffected — the bus comes up, it simply keeps the parameters that first bring-up chose. | | `capabilities.supports_i2c_pull_up(pull_up)` | {py:obj}`~pycosmicsdk.I2cController.initialize`, `configure`, `set_pull_up` | **Model**: firmware selects its pull-up resistor table per model at build time. The full accepted set is `capabilities.i2c_pull_ups`. | | `capabilities.supports_gpio_interrupt(pin)` | {py:obj}`~pycosmicsdk.Gpio.configure_interrupt` | Hardware **revision**: one pin on one revision is known not to arm — `PIN_3` on a Supernova reporting `hw_version` `B`. Everything else is attempted. The full armable set is `capabilities.interrupt_capable_gpio_pins`. | | `capabilities.spi_target_capable` | {py:obj}`SpiTarget.initialize `, `bring_up`, and {py:obj}`Device.spi_target() ` | Hardware **revision**: a rev-B Supernova's SPI level shifter has its chip-select direction pin fixed as an output, so it cannot receive an incoming chip select. **This is the one guard firmware does not also make** — `SPI_TARGET_INIT` answers `SUCCESS` and the adapter then reports nothing ever again, so the refusal is this SDK's own. It is correspondingly the one field with no firmware backstop, and therefore the one a caller who believes it wrong can override with `dataclasses.replace`. See [Target mode](../guides/target-mode.md). | | `capabilities.supports_spi_bulk()`, `capabilities.supports_i3c_bulk()` | The three protocols' `transport=` arguments with `BULK`, and both target-mode bring-ups (whose arenas have no other route) | Negotiated with the device on first use and latched per connection, and **host**-dependent as well as device-dependent — the bulk plane needs a vendor USB interface claimed on *this machine*, so the same adapter answers differently on two PCs. `capabilities.bulk` is the full {py:obj}`~pycosmicsdk.BulkTransportCapability` record — **the latch for all three protocol groups** — which reports `host_transport_open` and `device_supported` separately because they are different fixes. Note that `device_supported` is effectively hard-wired to `True` by firmware and proves nothing; `host_transport_open` is what discriminates. See [The bulk transfer plane](../guides/bulk.md). | :::{important} {py:obj}`~pycosmicsdk.CapabilityError` derives from {py:obj}`~pycosmicsdk.CosmicError`, **not** from {py:obj}`~pycosmicsdk.FirmwareError`, so `except FirmwareError` will not catch it. It also carries `StatusCode.SDK_ERROR_WRONG_REQUEST` rather than a firmware rejection code, because nothing was sent for firmware to reject — so a handler that catches it must not filter on `status_code`. See [Capability refusals arrive before the wire](errors.md). None of these is a guess. Each mirrors a condition in the adapter's firmware or its silicon, and each is arranged so that *not knowing* falls through to the device rather than into a refusal — an unrecognised model and an `hw_version` this SDK cannot read as a revision are both attempted. Two exceptions. The pull-up table enumerates what firmware accepts rather than what it rejects, and so is strict by necessity. And `spi_target_capable` is the one where "let firmware answer" is not a safety net, because firmware does not answer: it reports success and the hardware stays silent. [Refusals fail open](errors.md) is the full account, including the one place the principle deliberately does not apply. ::: ```{eval-rst} .. autoclass:: pycosmicsdk.DeviceCapabilities .. autoclass:: pycosmicsdk.BulkTransportCapability .. autoclass:: pycosmicsdk.DeviceInfo ``` ## Bring-up results {py:obj}`~pycosmicsdk.BringUpResult` is what {py:obj}`~pycosmicsdk.I2cController.bring_up`, {py:obj}`~pycosmicsdk.I3cController.bring_up`, {py:obj}`~pycosmicsdk.Uart.bring_up`, {py:obj}`~pycosmicsdk.SpiController.bring_up`, {py:obj}`I2cTarget.bring_up `, {py:obj}`SpiTarget.bring_up ` and their asyncio twins return. It exists to keep one fact from being assumed: **on return the interface is initialised and usable — anything preventing that raises — but `settings_applied` is what says whether it is running at the settings you asked for.** `settings_applied` is `False` in the usual case because the interface was already up from an earlier run and the adapter refused to reconfigure it. The interface still works, at settings that are neither yours nor reported. `already_initialized` says whether that already-up path ran at all — `True` is the normal reading on any second run of a script. `refusal` carries the exception explaining the `False`, or `None`, so `raise result.refusal` converts the report back into an exception. One case is not a refused reconfigure, and {py:obj}`~pycosmicsdk.Uart.bring_up` is the only one that has it: a `hw_handshake` asked for on a unit whose `capabilities.uart_hw_handshake_effective` is `False` comes back `settings_applied=False` even on a fresh bring-up, with `already_initialized=False`. Firmware answers `SUCCESS` and is read to force RTS/CTS off regardless, so nothing was refused on the wire — but this SDK knew the answer before the call, and the field documented as the one to consult must not affirm a setting it holds to be out of force. What that reading rests on, and what it does not, is in the [UART guide](../guides/uart.md). The record is a **report, not a promise**: `settings_applied=True` means no refusal this SDK can see, not that the device was read back and confirmed. There is deliberately no `__bool__`: a result object that is silently falsy invites the exact "proceeded anyway" mistake the record exists to prevent. The narrative version is [Bring-up is `bring_up()`](../guides/index.md). ```{eval-rst} .. autoclass:: pycosmicsdk.BringUpResult ``` ## USB configuration Accepted by {py:obj}`Device.set_usb_config ` and returned by {py:obj}`Device.get_usb_config `. Writing one is **flash-persistent** and takes effect on the next reset — read [USB configuration](../guides/lifecycle.md) before calling `set_usb_config`. The three companion enums live alongside it rather than in [Enums](enums.md) because they are only ever fields of {py:obj}`~pycosmicsdk.UsbConfig` — they have no bound C++ counterpart, but neither do the nine Python-only enums on that page, so that is not what sets them apart. They are `IntEnum`, so they compare equal to the raw firmware values they carry. ```{eval-rst} .. autoclass:: pycosmicsdk.UsbConfig .. autoclass:: pycosmicsdk.WebusbState :undoc-members: .. autoclass:: pycosmicsdk.CdcState :undoc-members: .. autoclass:: pycosmicsdk.CdcMode :undoc-members: ``` ## Voltage rails Returned by {py:obj}`AsyncDevice.get_analog_measurements `, which is **async-only** — see [Blocking and asyncio](../asyncio.md). Rails the connected model does not fit still occupy their fields with whatever the firmware reports. The four blocking multifunction readers all return the *same* field of this record — see [Analog measurements](../guides/lifecycle.md). ```{eval-rst} .. autoclass:: pycosmicsdk.AnalogMeasurements ``` ## I2C ```{eval-rst} .. autoclass:: pycosmicsdk.I2cScanResult ``` ## I2C — target mode ```{eval-rst} .. autoclass:: pycosmicsdk.I2cTargetBusEvent ``` ## SPI — target mode ```{eval-rst} .. autoclass:: pycosmicsdk.SpiTargetBusEvent ``` ## I3C ```{eval-rst} .. autoclass:: pycosmicsdk.I3cTargetInfo .. autoclass:: pycosmicsdk.I3cTargetConfiguration .. autoclass:: pycosmicsdk.I3cTargetBusEvent .. autoclass:: pycosmicsdk.I3cGetMrlResult ```