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++ (DeviceInfo and I2cScanResult).

Device identity and capabilities

DeviceCapabilities is captured once at open() from the model the firmware reports; it is a snapshot, not a live query. 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 DiscoveredDevice, whose equivalents are untagged and directly usable as open(serial=...).

The payload caps on DeviceCapabilities are enforced: max_i2c_payload_bytes, max_i3c_payload_bytes and max_uart_message_bytes are each read by a guard that raises 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 — BulkTransportCapability carries that, and the per-call answer is on the interfaces themselves (I2cController.max_transfer_bytes and the two target interfaces’ max_transfer_bytes). See The bulk transfer plane.

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 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

configure, 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)

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)

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

SpiTarget.initialize, bring_up, and 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.

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 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.

Important

CapabilityError derives from CosmicError, not from 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.

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 is the full account, including the one place the principle deliberately does not apply.

class pycosmicsdk.DeviceCapabilities(protocols: frozenset[ProtocolInterface], i2c_buses: frozenset[I2cBus], max_i2c_payload_bytes: int, max_i3c_payload_bytes: int, max_uart_message_bytes: int, hw_version: str, fw_version: str, interrupt_capable_gpio_pins: frozenset[GpioPinNumber] = frozenset({GpioPinNumber.PIN_1, GpioPinNumber.PIN_2, GpioPinNumber.PIN_3, GpioPinNumber.PIN_4, GpioPinNumber.PIN_5, GpioPinNumber.PIN_6}), i2c_runtime_reconfigurable: bool = True, bulk: BulkTransportCapability = BulkTransportCapability(queried=False, host_transport_open=False, device_supported=False, struct_version=0, operations=frozenset(), ceilings=()), i2c_pull_ups: frozenset[I2cPullUp] = frozenset({I2cPullUp.DISABLE, I2cPullUp.KOHM_10_0, I2cPullUp.KOHM_1_0, I2cPullUp.KOHM_1_5, I2cPullUp.KOHM_2_2, I2cPullUp.KOHM_3_3, I2cPullUp.KOHM_4_0, I2cPullUp.KOHM_4_7, I2cPullUp.OHM_150, I2cPullUp.OHM_220, I2cPullUp.OHM_330, I2cPullUp.OHM_470, I2cPullUp.OHM_680}), i2c_init_pull_up_effective: bool = True, uart_hw_handshake_effective: bool = True, spi_target_capable: bool = True)

Bases: object

Static snapshot of what a connected device can do.

Captured once at Device.open() from the firmware’s reported model, hardware revision and feature flags.

Most fields are a property of the model. Three are a property of the hardware revision as well — interrupt_capable_gpio_pins, i2c_runtime_reconfigurable and spi_target_capable. The first two mirror shipping firmware limits keyed on the revision rather than the model, and both surface as a flat FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE refusal on the wire. The third surfaces as nothing on the wire at all — firmware answers SUCCESS and the affected hardware then does nothing — which is exactly why this SDK is the only thing that can report it. All three exist so a caller writing portable code across revisions can branch before calling, rather than discovering the limit by catching an error; the affected methods consult the same three fields and raise CapabilityError before any request is sent. See pycosmicsdk._capabilities for the revision tables and which entries are bench-confirmed.

i2c_pull_ups is the same idea one level up, keyed on the model alone: the two adapters accept different subsets of I2cPullUp, so a value that is right on one is refused by the other. It is reported here for the same reason — so portable code can pick a value rather than discover the limit by failing on its very first I2C call.

Note

All three revision-keyed fields record limits the SDK knows about. A pin listed in interrupt_capable_gpio_pins, or an i2c_runtime_reconfigurable or spi_target_capable of True, means “no known limit” — not that this exact unit has been tested. An unknown model or revision therefore reads as unlimited and the operation is attempted, so firmware gets to answer rather than the SDK refusing something that would have worked.

spi_target_capable is the one field where “firmware gets to answer” is not a safety net, because firmware does not answer: it reports success and the hardware stays silent. That makes it the only refusal here with no firmware backstop, so a False a caller believes is wrong can be overridden — build a modified snapshot with dataclasses.replace — where overriding any other field only defers the same refusal to the device.

i2c_pull_ups is the one field that enumerates positively – it lists what works rather than what does not – because firmware’s per-model table is a closed set with no room for a value it does not name. That makes it the one field where an error in this SDK’s data would refuse a value the device accepts, so it is transcribed from the firmware table member for member; see pycosmicsdk._capabilities for the transcription and its provenance.

protocols

Protocol interfaces the device’s firmware supports.

Type:

frozenset[pycosmicsdk.enums.ProtocolInterface]

i2c_buses

I2C buses the device exposes.

Type:

frozenset[pycosmicsdk._cpp._pycosmicsdk_cpp.I2cBus]

max_i2c_payload_bytes

Largest I2C payload the ordinary HID route carries in one transfer – 1024 on both models, and not the largest I2cController.read or I2cController.write accepts. On a bulk-capable host and device those take up to 32768 bytes under I2cTransport.AUTO, which serves anything above this ceiling over the bulk plane. Ask `I2cController.max_transfer_bytes` to size a buffer: it is transport-aware and negotiates, which a snapshot cannot be – build_capabilities is a pure function of a DeviceInfo and the bulk ceiling needs a device round trip. This field is still the honest answer to its own question, and three things read it: the fallback bound when no bulk plane is reachable, the threshold AUTO compares a request against, and the ceiling forced by I2cTransport.HID.

Type:

int

max_i3c_payload_bytes

Largest I3C payload the ordinary HID route carries in one transfer – 1024 – and, as for I2C, not the largest the API accepts. I3cController.sdr_write / I3cController.sdr_read and the legacy-I2C pair reach the advertised I3C bulk ceiling (32767 on a Supernova) under I3cTransport.AUTO; ask I3cController.max_transfer_bytes. HDR-DDR is the exception in the other direction: it is capped at 1024 on both routes for bus safety, which I3cController.max_hdr_ddr_transfer_bytes reports.

Type:

int

max_uart_message_bytes

Largest UART message accepted in one send. UART has no bulk plane, so unlike the two above this is the whole answer.

Type:

int

hw_version

Hardware version string reported by the device.

Type:

str

fw_version

Firmware version string reported by the device.

Type:

str

interrupt_capable_gpio_pins

GPIO pins this unit can arm a firmware interrupt on, as Gpio.configure_interrupt does. Empty when the adapter exposes no GPIO at all. Query it with supports_gpio_interrupt.

Type:

frozenset[pycosmicsdk._cpp._pycosmicsdk_cpp.GpioPinNumber]

i2c_runtime_reconfigurable

Whether an already-initialised I2C interface accepts new parameters — that is, whether I2cController.configure and I2cController.set_pull_up work on this unit. False does not affect I2cController.initialize: the bus still comes up, it just keeps the parameters its first bring-up chose. False also when the adapter exposes no I2C bus at all.

Type:

bool

bulk

What this host and device negotiated about the bulk USB plane, as a BulkTransportCapability. The only runtime-queried and host-dependent field here, and the only one that is not filled at open(): it is negotiated lazily, so it reads as un-negotiated (queried False) until the first SpiController.write, SpiController.read, SpiController.transport, I3cController.sdr_write, I3cController.sdr_read or I3cController.transport. Query it with supports_spi_bulk and supports_i3c_bulk, or with the record’s own BulkTransportCapability.supports and BulkTransportCapability.max_payload_for – all of which refuse to answer until the negotiation has run, rather than reporting a falsy answer that cannot be told apart from “this device has no bulk plane”. It is the latch for all three protocol groups, not the SPI part of one: the I2C target arena and the I3C controller bulk path negotiate through this same object. It was called spi_bulk while SPI was the only group the negotiation read; spi_bulk is now a deprecated alias property, and dataclasses.replace needs this name.

Type:

pycosmicsdk.types.BulkTransportCapability

i2c_pull_ups

I2cPullUp selections this model accepts. Firmware picks its pull-up resistor table per model at compile time, so the two adapters accept different subsets of the enum’s thirteen members and neither accepts all of them. Empty when the adapter exposes no I2C bus at all. Query it with supports_i2c_pull_up.

Type:

frozenset[pycosmicsdk._cpp._pycosmicsdk_cpp.I2cPullUp]

i2c_init_pull_up_effective

Whether the pull_up passed to I2cController.initialize actually reaches the hardware. False on a unit whose board carries a fixed pull-up instead of the programmable potentiometer the selection addresses: the call still succeeds and the bus still comes up, but every I2cPullUp value — including DISABLE — leaves the same resistance on the bus. The SDK logs a warning rather than refusing, because nothing failed; I2cController.bring_up also carries it as the refusal on a settings_applied=False result, since unlike initialize it returns a record with somewhere to say it. Unrelated to i2c_runtime_reconfigurable, which covers the two pull-up commands that are refused rather than ignored.

Type:

bool

uart_hw_handshake_effective

Whether a non-DISABLED hw_handshake passed to Uart.initialize or Uart.configure actually reaches the hardware. False on a unit whose firmware forces RTS/CTS flow control off for this hardware revision: the call still succeeds and the UART still runs at the requested line settings, but the handshake is not applied. The SDK logs a warning rather than refusing, because nothing failed; Uart.bring_up also carries it as the refusal on a settings_applied=False result, for the reason given on the field above.

Type:

bool

spi_target_capable

Whether this unit can act as a SPI target (the controller role is unaffected and works on every revision of both models). False on a Supernova hardware revision whose SPI level shifter has its chip-select direction pin fixed as an output: the board cannot receive an incoming chip select, so it arms and then never sees a transfer. Firmware applies no revision check to SPI target bring-up and reports SUCCESS regardless, so unlike every other field here this one has no firmware refusal behind it — the SDK raises CapabilityError in firmware’s place, because the alternative a caller experiences is permanent silence. False also when the adapter exposes no SPI at all.

Type:

bool

supports(protocol: ProtocolInterface) → bool

Return True when protocol is in self.protocols.

Parameters:

protocol – The protocol interface to check for.

Returns:

True if the adapter exposes protocol, else False.

has_i2c_bus(bus: I2cBus) → bool

Return True when bus is in self.i2c_buses.

Parameters:

bus – The I2C bus to check for.

Returns:

True if the adapter exposes bus, else False.

supports_gpio_interrupt(pin: GpioPinNumber) → bool

Return True when pin is in self.interrupt_capable_gpio_pins.

Ask this before Gpio.configure_interrupt to keep code portable across hardware revisions: on a Supernova reporting hw_version == "HW-B" it is False for GpioPinNumber.PIN_3 and True for the other five pins.

Parameters:

pin – The GPIO pin to check for interrupt support.

Returns:

True if the adapter is known to support arming an interrupt on pin, else False.

supports_i2c_pull_up(pull_up: I2cPullUp) → bool

Return True when pull_up is in self.i2c_pull_ups.

Ask this before I2cController.initialize to keep code portable across models: I2cPullUp.KOHM_10_0, I2cPullUp.KOHM_4_7 and I2cPullUp.KOHM_3_3 are Supernova-only, and I2cPullUp.KOHM_4_0 is Pulsar-only. Everything from I2cPullUp.OHM_150 up to I2cPullUp.KOHM_2_2, plus I2cPullUp.DISABLE, is accepted by both.

Parameters:

pull_up – The pull-up selection to check for support.

Returns:

True if the adapter’s model accepts pull_up, else False.

supports_spi_bulk() → bool

Return True when SPI bulk transfers are usable on this host and device.

Equivalent to self.bulk.available, and named to match supports and supports_gpio_interrupt. Read bulk.host_transport_open and bulk.device_supported when False to tell a host-side driver problem from old firmware.

This name keeps its ``spi_`` deliberately, unlike the field it reads. It is a genuinely SPI-shaped question – “will my SpiController.write go over the bulk plane” – and bulk is the record that answers it for all three protocol groups. Ask supports_i3c_bulk, or bulk.supports / bulk.max_payload_for, for the other two.

No installed build can return True yet: the bulk commands live on an unmerged CosmicSDK branch, so the compiled extension carries no bulk surface. SpiController.write and SpiController.read work regardless – they fall back to the HID path.

Returns:

True if SpiController.write and SpiController.read will use the bulk plane, else False.

Raises:

CapabilityError – If the bulk transport has not been negotiated on this connection. It used to answer False there, which is indistinguishable from “this device has no bulk plane”; read bulk.queried first, or read SpiController.transport on an open device, which negotiates.

property spi_bulk: BulkTransportCapability

Deprecated alias for bulk; identical object, retired name.

The field was called spi_bulk while SPI was the only command group the negotiation looked at. It is not any more: the same record now carries the I2C target arena’s operations and the I3C controller bulk ceiling, so spi_bulk.max_payload_for(ProtocolInterface.I3C) was a sentence that argued against its own first word, and an I2C-only caller had to read a field named for a protocol they never touch.

Kept as a property so shipped reads keep working for one release. It is not a field, so dataclasses.replace(capabilities, spi_bulk=...) no longer resolves – pass bulk= instead. Nothing inside this package reads this name.

Returns:

The same BulkTransportCapability bulk carries.

Warns:

DeprecationWarning – Always. Read bulk.

supports_i3c_bulk() → bool

Return True when the device offers I3C controller bulk transfers.

This is the device’s answer, not a promise that a call will take the bulk plane, and I3C is the first capability in this package where the two come apart. The firmware implements I3C_CONTROLLER_PRIVATE_TRANSFER_BULK, dispatches it, and advertises its bit with a 32 KB ceiling on a Supernova – so this returns True on such a unit with the vendor interface claimed. Whether a transfer then takes the bulk route also depends on this build binding the six i3c_*_bulk commands, which the CosmicSDK integration branch did not and the current pin does. On a build whose extension predates them this reads True while I3cController.sdr_write keeps the 1024-byte ordinary ceiling, and that is the honest answer rather than a bug.

Ask I3cController.transport for the answer that decides what a transfer actually does; ask this to find out whether the device is the thing standing in the way. On a Pulsar it is False for a reason no firmware update changes: the part has no I3C peripheral, so no I3C group is advertised at all.

Equivalent to self.bulk.supports(BulkOperation.I3C_CONTROLLER_TRANSFER).

Returns:

True if the connected device advertises I3C controller bulk with a usable ceiling and this host holds the vendor bulk interface, else False.

Raises:

CapabilityError – If the bulk transport has not been negotiated on this connection – “the device has not been asked” is not the same answer as “the device says no”, and only one of them is about the device. I3cController.transport negotiates and reports the combined answer.

class pycosmicsdk.BulkTransportCapability(queried: bool = False, host_transport_open: bool = False, device_supported: bool = False, struct_version: int = 0, operations: frozenset[BulkOperation] = frozenset({}), ceilings: tuple[tuple[ProtocolInterface, int], ...] = ())

Bases: object

Whether this host and this device can move SPI payloads over the bulk plane.

Carried by DeviceCapabilities.bulk and consulted by SpiController.write and SpiController.read to pick a transport. The default – every field falsy, queried included – means “not negotiated yet”; the negotiation is lazy, so a caller that never touches SpiController.write, SpiController.read or SpiController.transport never sends the query.

This is the one capability field that is not a property of the device alone, and the difference is worth spelling out because it changes what the value means:

  • It is host-dependent. The bulk plane needs a vendor USB interface claimed on this machine, so the same adapter can report available on one PC and not on another – a missing WinUSB binding on Windows, a missing udev rule on Linux, or another process holding the interface.

  • It is runtime-queried, not derived from the reported DeviceInfo. Every other field comes from a model/revision table in pycosmicsdk._capabilities; this one comes from the device answering a command.

  • It is latched, not live. It is negotiated once per connection and cached, exactly as CosmicSDK caches it per handle: a device that never answers is still recorded as negotiated, so no later call pays for it again. Reconnect (close() then open()) to renegotiate.

  • It is negotiated only from the blocking surface, and this is the one real asymmetry between Device and AsyncDevice. The decode of the capability record lives in the blocking CosmicSDK::getBulkCapabilities alone – CosmicAsyncSDK’s counterpart is a bare submit with no decodable reply – so an AsyncDevice cannot run this negotiation at all, and queried stays False on a snapshot only ever touched through the asyncio surface. The async target bring-ups gate instead on whether this host claimed the adapter’s vendor interface, which is the half that carries information: firmware derives its supported flag from a group count that is never zero, so the device half is very nearly a constant. Use a blocking Device when you need the record itself.

host_transport_open and device_supported are reported separately from available on purpose: they are the difference between “install or bind a driver on this machine” and “update the adapter’s firmware”, and a caller cannot act on available == False without knowing which.

Note

No installed build of this package can report available == True yet. The bulk commands live on an unmerged CosmicSDK branch, so the compiled extension carries no bulk surface and the negotiation reports absence without any device traffic at all. The HID fallback in SpiController.write / SpiController.read is therefore the only path that runs today.

Note

An un-negotiated record answers nothing, loudly. queried False means “nobody has asked yet”, which is not an answer about this host or this device – so available, max_spi_payload_bytes, supports and max_payload_for all raise CapabilityError in that state rather than reporting False and 0. They used to report them, and that was the defect: a caller who read the snapshot before anything touched a bulk-eligible call got a permanent false negative shaped exactly like a firmware verdict (“this device has no bulk plane”), when the truth was “the negotiation has not run”. Read queried to test for the state without raising; read the raw fields below to print the record for diagnostics.

“We asked and the answer is no” is a different record – queried True with nothing advertised, which pycosmicsdk._capabilities.probe_bulk returns for every failure mode by design – and it answers plainly. So a device with no bulk plane, or a build with no bulk surface, never raises from any of this.

queried

Whether the negotiation has run on this connection. False means “not asked yet”, and every other field is meaningless – which is why the derived answers refuse rather than report it. True with device_supported False means the device was asked and either said no or said nothing – the two are indistinguishable, upstream’s own deliberate choice.

Type:

bool

host_transport_open

Whether this host claimed the adapter’s vendor bulk interface. False is a host problem: no driver bound, no permission, or another process holding it.

Type:

bool

device_supported

Whether the device answered the capability query claiming bulk support at all. False is a device problem: firmware that predates the bulk commands.

Type:

bool

struct_version

Version byte of the capability record the device sent, carried through unchanged. Upstream stores it and does not yet branch on it, so nothing here branches on it either.

Type:

int

operations

Which BulkOperation members this host and device can carry on the bulk plane. Query it with supports. Empty until the negotiation has run, and empty whenever the host holds no transport, whatever the device advertised.

Type:

frozenset[pycosmicsdk.enums.BulkOperation]

ceilings

(protocol, ceiling) pairs, one per command group the device reported with a usable ceiling, in ProtocolInterface name order. Query it with max_payload_for rather than indexing it. A tuple rather than a mapping so the dataclass stays frozen, hashable and cheap to copy; it is at most three entries.

Type:

tuple[tuple[pycosmicsdk.enums.ProtocolInterface, int], …]

Note

``operations`` and ``ceilings`` answer two different questions and neither is derivable from the other. The device advertises support per command and a payload ceiling per command group, so a group can carry a healthy ceiling while only some of its commands exist – GROUP_SPI holds the SPI controller bulk pair and the SPI target memory pair under one maxPayloadLength, and GROUP_I3C holds one command whose ceiling is the SDR figure. Deriving support from the group is a defect this package has already shipped once and fixed; deriving the ceiling from support cannot be done at all.

property available: bool

Whether SpiController.write and SpiController.read use the bulk plane.

The conjunction of host_transport_open, device_supported, the device advertising both bulk SPI controller commands, and a non-zero ceiling for the SPI command group – which is exactly supports(BulkOperation.SPI_CONTROLLER_WRITE) and supports(BulkOperation.SPI_CONTROLLER_READ), and is why this is computed rather than stored.

Both, or neither: a device advertising half the pair would leave one of the two methods silently on a different transport from the other, which is worse than putting both on HID.

A SPI-controller-shaped shortcut, and deliberately kept as one. This record is the latch for all three protocol groups, so “available” cannot mean “the bulk plane works” in general – it means “the SPI controller bulk pair is usable”, which is exactly the predicate SpiController’s transport selection and pycosmicsdk._capabilities.guard_spi_bulk need. That is narrow, not a lie. Ask supports and max_payload_for for the I2C and I3C answers; do not read this one as either. It also cannot become a module-level constant in pycosmicsdk._capabilities: that module imports this one, so the operation set has to live here.

Returns:

True when SPI controller traffic can travel on the bulk plane.

Raises:

CapabilityError – If the negotiation has not run on this connection (queried False). “Unknown” is not False; see _require_negotiated.

property max_spi_payload_bytes: int

Largest payload one bulk SPI call accepts, as the device advertised it.

0 when the device advertised no usable SPI group ceiling. Exactly max_payload_for(ProtocolInterface.SPI); see that method for what a group ceiling does and does not bound. Requests above it raise ValidationError – nothing is ever split, because chip select de-asserts between calls.

The other SPI-controller-shaped shortcut, kept for the same reason available is: it names its protocol in full, so it cannot be misread as the I2C or I3C ceiling the way an unqualified max_payload_bytes on a three-group record would be. Ask max_payload_for for those.

Returns:

The advertised SPI bulk ceiling in bytes, or 0.

Raises:

CapabilityError – If the negotiation has not run on this connection.

supports(operation: BulkOperation) → bool

Return True when this host and device can carry operation.

The per-command half of the answer, and the one a guard asks. It is the conjunction of four things, because all four have to hold before anything can be sent: this host claimed the adapter’s vendor bulk interface, the device answered the capability query, the device set the command’s bit in its group mask, and that group advertised a non-zero ceiling.

The last term is a conservatism worth knowing about. A device never sends a zero ceiling – absence of a group is how it says “no bulk here” – but the wire field is a uint16_t and the firmware’s build-time assertions compare each ceiling against the staging arena rather than against UINT16_MAX, so a doubled arena would advertise 0 uncaught. Upstream’s own gate reads a zero ceiling as “no ceiling to enforce” and would let the request through; this reads it as unsupported, which is the only reading where “available” and “the number I may send” cannot disagree.

A ``True`` here does not promise this build can call it. It is the device’s answer, and for BulkOperation.I3C_CONTROLLER_TRANSFER the device says yes while no CosmicSDK branch wraps the command – see that member’s note. I3cController.transport reports the combined answer.

Parameters:

operation – The bulk operation to ask about.

Returns:

True if operation can travel on the bulk plane on this connection, else False.

Raises:

CapabilityError – If the negotiation has not run on this connection (queried False). It used to answer False there, which is the false negative this class now refuses to produce; see _require_negotiated.

max_payload_for(protocol: ProtocolInterface) → int

Return the largest payload one bulk call may carry for protocol.

The per-group half of the answer. Mirrors upstream’s own group-keyed ceiling accessor while speaking this package’s vocabulary, and reports a real number whenever the device advertised one – even when none of that group’s operations is supported, because the number is still what the device said and zeroing it would misreport the device.

The value is clamped to this library’s own compile-time maximum for that protocol: a device may advertise less than the library accepts, never more.

Note

It is a ceiling per group and per call, not a bound a caller can apply uniformly to every command in the group. Firmware says so about its own advertisement: GROUP_SPI’s figure bounds the side that carries real data, while SPI_CONTROLLER_TRANSFER_BULK’s transmit half is separately capped at 1024 bytes; and GROUP_I3C’s figure is the SDR ceiling, with HDR-DDR capped lower inside the manager where the host cannot see it. One uint16_t per group is the wire format, which the firmware itself calls out as the reason the next revision of the command wants more than one number per group.

Parameters:

protocol – The protocol whose command group to look up. ProtocolInterface.I3C_TARGET, ProtocolInterface.UART and ProtocolInterface.GPIO have no bulk group and always answer 0.

Returns:

The ceiling in bytes, or 0 when the device advertised no usable ceiling for that group – which is also the answer when this host holds no transport.

Raises:

CapabilityError – If the negotiation has not run on this connection (queried False). This is the read that cost real bench time: it answered 0, a caller read that as “this adapter has no bulk plane”, and the adapter moved 32768 bytes in the next breath. See _require_negotiated.

class pycosmicsdk.DeviceInfo(*args, **kwargs)

Bases: object

property fw_version

(self) -> str

property hw_version

(self) -> str

property manufacturer

(self) -> str

property model

(self) -> pycosmicsdk._cpp._pycosmicsdk_cpp.DeviceModel

property product_name

(self) -> str

property serial_number

(self) -> str

Bring-up results

BringUpResult is what bring_up, bring_up, bring_up, bring_up, I2cTarget.bring_up, 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 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.

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().

class pycosmicsdk.BringUpResult(settings_applied: bool, already_initialized: bool, refusal: CosmicError | None = None)

Bases: object

Outcome of a bring_up call: the interface is up, and at whose settings.

Returned by I2cController.bring_up, SpiController.bring_up, I3cController.bring_up, Uart.bring_up and their asyncio twins. The contract every bring_up keeps is two-part, and the split is the whole reason this record exists:

  • On return, the interface is initialised and usable. A failure to reach that state raises.

  • ``settings_applied`` says whether it is running at the settings you asked for. It 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, but the settings it is running at are not the ones you asked for and are not reported: normally they are whatever the first bring-up since the interface was last released, or since the adapter was last power-cycled, chose – and where the refusal came from firmware rather than a pre-wire guard they may be a partial application of the request. Either way, do not assume them. Changing them for certain needs the interface released and re-initialised: the de-initialise is role-agnostic, so for I2C and SPI a release through the target interface clears a controller too. Where no release is wrapped – I3C has none – it needs the interface restarted and a fresh initialize, which means Device.reboot with ResetTarget.RUNTIME rather than a walk to the bench: a runtime reset is bench-confirmed to clear a latched I2C controller role, and that I3C’s is held the same way is an inference from firmware source, not a measurement – no I3C role has been tested against a reset. A power cycle also does it.

    Two bring_up methods report a refusal that is not a refused reconfigure, and both come back settings_applied=False on a fresh bring-up, with already_initialized=False:

    • Uart.bring_up, for a hw_handshake asked for on a unit whose capabilities.uart_hw_handshake_effective is False. Firmware answers SUCCESS and forces RTS/CTS off anyway.

    • I2cController.bring_up, for any pull_up asked for on a unit whose capabilities.i2c_init_pull_up_effective is False. Firmware answers SUCCESS and enables the board’s fixed network anyway. I2cPullUp.KOHM_10_0 is reported like every other value rather than exempted as UartHwHandshake.DISABLED is; _interfaces/i2c.py’s _init_pull_up_refusal argues why, and says what bench measurement would change it.

    In both, nothing was refused on the wire – but the capability table knew the answer before the call, and a field documented as the one to consult must not affirm a setting this SDK knows is not in force. The rest of each request did apply.

A caller whose claim depends on specific settings – a rig measuring a timing margin, a script that must run at 100 kHz – must branch on settings_applied rather than assume. Proceeding on settings the adapter did not accept is the failure this record exists to make impossible to miss; raise result.refusal turns the report back into an exception for callers that want no leniency at all.

settings_applied

Whether the requested settings are in force. True after a fresh initialize, and after a reconfigure the adapter accepted; False when the reconfigure was refused, or when a capability table says a requested setting is not in force regardless. It is a report, not a promise: True means no refusal this SDK can see, not that the device was read back and confirmed.

Type:

bool

already_initialized

Whether the interface was already up when bring_up was called – that is, whether initialize reported FW_INTERFACE_ALREADY_INITIALIZED and the reconfigure path ran. True is the normal reading on any second run of a script, since an interface stays initialised across a close()/reopen cycle. already_initialized and settings_applied therefore means “was already up, and successfully reconfigured”. It records which path ran, not whether the settings took, so it stays False on the two fresh-bring-up capability refusals described above.

Type:

bool

refusal

The error explaining a settings_applied=False, or None when it is True. Its status_code distinguishes the cases a caller may want to word differently: SDK_ERROR_WRONG_REQUEST from a CapabilityError means this SDK answered from its capability table rather than the wire – this adapter’s hardware revision cannot reconfigure a live interface at all, or (UART only) it forces hardware handshake off, or (I2C only) it ignores the pull-up selection at bring-up; FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE is the same refusal answered by firmware on a revision this SDK has no table for; and FW_INVALID_PARAMETER means the adapter can reconfigure but not to these values. The exception’s message already says which, and what to do about it.

Type:

pycosmicsdk.errors.CosmicError | None

USB configuration

Accepted by Device.set_usb_config and returned by Device.get_usb_config. Writing one is flash-persistent and takes effect on the next reset — read USB configuration before calling set_usb_config. The three companion enums live alongside it rather than in Enums because they are only ever fields of 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.

class pycosmicsdk.UsbConfig(webusb_enabled: WebusbState, cdc_enabled: CdcState, cdc_mode: CdcMode)

Bases: object

USB interface configuration stored in the adapter’s flash.

Returned by Device.get_usb_config and accepted by Device.set_usb_config. Mirrors upstream’s UsbConfig struct field for field.

A written configuration is saved to flash and takes effect on the next reset or power cycle; see Device.set_usb_config.

webusb_enabled

Whether the WebUSB interface is enabled. WebusbState.NO_CHANGE on a write leaves it as it is.

Type:

pycosmicsdk.types.WebusbState

cdc_enabled

Whether the USB CDC interface is enabled. CdcState.NO_CHANGE on a write leaves it as it is.

Type:

pycosmicsdk.types.CdcState

cdc_mode

Which protocol the USB CDC interface bridges. Always applied on a write — there is no no-change sentinel.

Type:

pycosmicsdk.types.CdcMode

class pycosmicsdk.WebusbState(*values)

Bases: IntEnum

Enabled state of the adapter’s WebUSB interface.

Values mirror WebusbState in CosmicSDK’s definitions.h. NO_CHANGE is a write-only sentinel: pass it to Device.set_usb_config to leave the WebUSB state untouched. A configuration read back from the device never carries it.

DISABLED = 0
ENABLED = 1
NO_CHANGE = 255
class pycosmicsdk.CdcState(*values)

Bases: IntEnum

Enabled state of the adapter’s USB CDC interface.

Values mirror CdcState in CosmicSDK’s definitions.h. NO_CHANGE is a write-only sentinel with the same meaning as WebusbState.NO_CHANGE.

DISABLED = 0
ENABLED = 1
NO_CHANGE = 255
class pycosmicsdk.CdcMode(*values)

Bases: IntEnum

Operating mode of the adapter’s USB CDC interface.

Values mirror CdcMode in CosmicSDK’s definitions.h. Unlike WebusbState and CdcState this enum has no no-change sentinel, so every set_usb_config call applies a mode; read the current configuration first and modify it if only one other field should change.

Some modes are model-specific: TERMINAL is Supernova-only, while RS485 and CANFD are Pulsar-only (each needs a transceiver the other model does not fit).

UART_PASSTHROUGH = 0
TERMINAL = 1
NOVA_COMPAT = 2
RS485 = 3
CANFD = 4

Voltage rails

Returned by AsyncDevice.get_analog_measurements, which is async-only — see Blocking and asyncio. 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.

class pycosmicsdk.AnalogMeasurements(vtarg_i2c_spi_uart_gpio_mv: int, vtarg_i3c_hv_mv: int, vtarg_i3c_lv_mv: int, vcca_mv: int, vcca_i3c_mv: int, vddio2_mv: int)

Bases: object

One snapshot of every voltage rail the adapter measures, in millivolts.

Returned by AsyncDevice.get_analog_measurements, which reads all six rails in a single firmware transaction. Mirrors upstream’s GetAnalogMeasurementsParameters (Decoder.hpp) field for field, in wire order; every field is an unsigned 16-bit millivolt count.

The three vtarg_* fields are the target-facing rails documented in docs_dev/hardware/hardware_reference.md, and they are the same values the blocking per-protocol readers return one at a time: upstream’s i2cGetBusVoltage, spiGetBusVoltage, uartGetBusVoltage and gpioGetVoltage all issue this one command and hand back vtarg_i2c_spi_uart_gpio_mv, while i3cGetBusVoltage hands back the HV/LV pair. The remaining three are internal supply rails with no per-protocol reader.

A rail a given adapter model does not fit still occupies its field; the firmware reports whatever its ADC reads there, so treat a rail the model lacks (I3C on Pulsar, for instance) as meaningless rather than as 0 V.

vtarg_i2c_spi_uart_gpio_mv

Multifunction target rail shared by I2C, SPI, UART and GPIO.

Type:

int

vtarg_i3c_hv_mv

I3C high-voltage port target rail.

Type:

int

vtarg_i3c_lv_mv

I3C low-voltage port target rail.

Type:

int

vcca_mv

VCCA supply rail.

Type:

int

vcca_i3c_mv

VCCA supply rail of the I3C section.

Type:

int

vddio2_mv

VDDIO2 supply rail.

Type:

int

I2C

class pycosmicsdk.I2cScanResult(*args, **kwargs)

Bases: object

property addresses_10bit
property addresses_7bit

I2C — target mode

class pycosmicsdk.I2cTargetBusEvent(kind: I2cTargetBusEventKind | int, target_address: int, memory_address: int, payload_length: int)

Bases: object

One bus phase observed while the adapter acts as an I2C target.

Returned by I2cTarget.wait_for_event, rebuilt from the firmware’s I2C target bus-event notification.

It carries no bytes, and that is the device’s shape rather than an omission. I3cTargetBusEvent has a payload field because the I3C notification appends the transferred bytes; the I2C one is a fixed-length metadata record, so the transferred bytes have to be read back from the arena with I2cTarget.read_memory.

The trap is sharper than “there is no payload”, and it is on the notification rather than here. I2cTargetBusEventNotification declares no payload of its own but inherits one from the notification base, and that field is not empty: the extension fills it with the entire raw notification frame (native/facade/facade_util.cpp:49 copies upstream’s Notification::payload/payloadLength, and native/notifications.hpp calls the base member “the raw-frame payload” in as many words). So a subscriber that reads payload expecting the transaction’s data gets a twelve-byte frame instead – plausible-looking bytes that are not the data and whose length has nothing to do with payload_length. This record deliberately has no payload field at all, so the mistake is not reachable from here. Measured on silicon rather than merely read out of the source: len(payload) == 12 for every event driven while payload_length ranged from 0 to 1024, so the two are definitively unrelated.

One completed transaction produces exactly one event. Firmware emits one per transfer phase and flushes them at the STOP, and an ordinary write or read is a single phase – so the count matches the SPI target’s one-per-chip-select-frame, and a loop written against the SPI shape counts correctly here. A combined read’s sub-address write is absorbed into establishing memory_address rather than being a phase of its own, so read(addr, n, subaddress=...) yields one READ_TRANSFER and no WRITE_TRANSFER.

The hazard is under-counting, and it is that nothing is delivered until the STOP. A non_stop=True transfer holds the bus with a repeated START and emits zero events for as long as it holds it, so a wait_for_event after one blocks until unrelated code closes the bus; both phases then arrive together at that STOP. I2cTarget.wait_for_event has the measurement and the full model.

kind

What the external controller did. An I2cTargetBusEventKind member for every kind this SDK knows; a plain int carrying the raw firmware value for one it does not, so a newer firmware’s event is surfaced rather than dropped.

Type:

pycosmicsdk.enums.I2cTargetBusEventKind | int

target_address

This device’s own configured target address – the address its slave hardware was armed to match, not a peer’s. Upstream calls the field “target address the transaction addressed”, which reads like the other thing. For a 10-bit target it is the reserved prefix byte rather than the 10-bit address.

Type:

int

memory_address

Byte offset into the arena the data phase started at. Unlike the SPI twin’s, this one is real: firmware fills it from the slave state’s phase start. And unlike the I3C twin’s, it is a byte offset rather than a register index – same name, same type, different unit.

Type:

int

payload_length

How many bytes moved in the data phase, which is how many to read back from memory_address. The leading register-address bytes of a write are not counted: firmware takes the figure from the slave transfer’s own transferred count once the pointer has been set, so the pair really does compose into a read_memory. Unrelated to the length of the notification’s inherited ``payload``, which is the raw frame; see above.

The SPI twin’s identically named field does not mean this. SpiTargetBusEvent.payload_length counts every byte clocked between the chip-select edges, opcode and address included, and its memory_address is a placeholder – so the two-line idiom above is I2C-only. Same names, same types, one protocol where they are usable and one where they are not.

Type:

int

Note

The bytes may have changed by the time you read them. Nothing stops the external controller writing that region again between this notification and the read; the event reports what happened, not a snapshot.

SPI — target mode

class pycosmicsdk.SpiTargetBusEvent(kind: SpiTargetBusEventKind | int, chip_select: SpiChipSelect | int, memory_address: int, payload_length: int)

Bases: object

One bus transaction observed while the adapter acts as an SPI target.

Returned by SpiTarget.wait_for_event, rebuilt from the firmware’s SPI target bus-event notification.

It carries no bytes, and that is the device’s shape rather than an omission. I3cTargetBusEvent has a payload field because the I3C notification carries the transferred bytes; the SPI one carries four pieces of metadata and nothing else, with upstream’s own comment saying plainly that the bytes are not here and have to be read out of the arena. Read them with SpiTarget.read_memory, at an offset you supply – see memory_address below, which is a firmware placeholder rather than that offset.

Nor does it give you the length, which is the sharper half and the one the field names hide. payload_length counts the whole framed transaction – command and address phases included – so read_memory(event.memory_address, event.payload_length) gets both arguments wrong: a placeholder offset and an over-long count. frame_length is the same number under an honest name, and the payload_length entry below has the per-frame overheads and why none of them can be subtracted blind. The SPI event usefully reports neither where a transaction landed nor how much it moved; both are things you know from the protocol you are emulating. The I2C twin reports both for real.

``SpiTargetBusEventNotification.payload`` is not empty either, and it is not those bytes: like the I2C event it inherits the notification base’s payload, which the extension fills with the entire raw notification frame. Reading it for the transferred data yields a twelve-byte frame whose length is unrelated to payload_length. This record carries no payload field, which is what keeps the mistake off the public surface.

A SPI target bus event is emitted only on chip-select deassert, and only when at least one byte moved, so one event is one framed transaction. The I2C target arrives at the same count by a different route – one event per transfer phase, all flushed at the STOP, and an ordinary transaction is one phase – so the counts agree and the meanings do not. What differs is this field: SpiTargetBusEvent.payload_length counts the whole framed transaction with a per-model overhead that cannot be subtracted blind, while I2cTargetBusEvent.payload_length counts data-phase bytes against a real memory_address. Only the I2C pair composes into a read_memory; see that field below. The other I2C-only behaviour is the held bus: a non_stop transfer defers its events until something issues the STOP, which has no SPI analogue.

kind

What happened on the bus. A SpiTargetBusEventKind member for every kind this SDK knows; a plain int carrying the raw firmware value for one it does not, so a newer firmware’s event is surfaced rather than dropped. That matters more here than it does for I3C: the enum has exactly one member today, so the int arm is the only thing that will make a second kind visible.

Type:

pycosmicsdk.enums.SpiTargetBusEventKind | int

chip_select

The chip-select line the transaction used. Upstream names this field address, and its own comment says it is the chip-select line – an “address” that is not one, so it is renamed here rather than copied. A SpiChipSelect member, or a plain int for a line this SDK does not name.

Type:

pycosmicsdk._cpp._pycosmicsdk_cpp.SpiChipSelect | int

memory_address

Always ``0`` on current firmware, so do not read it. It is meant to be the byte offset into the arena the transaction started at, and it is documented that way upstream, but both firmware emit sites pass a start address of zero regardless of where the transaction actually landed – the FLASH and REGISTER_MAP models track their address internally and never report it. The field is carried rather than dropped so this record matches the I2C one and so it starts working on its own if firmware ever populates it; until then, where a transaction landed is something you know from the protocol you are emulating, not something the event tells you. SpiTarget has no read_event_payload convenience for exactly this reason.

Type:

int

payload_length

The length of the whole framed transaction, not of its data phase – every byte clocked while chip select was asserted, command and address bytes included. Read frame_length, which is the same number under a name that says so. Unrelated to the length of the notification’s inherited ``payload``, which is the raw frame; see above.

Firmware counts one per byte received between the two chip-select edges and reports that count, so on a FLASH-model 0x03 + addr + N read it is N + 4 with the default three-byte address. The overhead is not constant, so there is no offset to subtract:

frame

payload_length

MEMORY, any engine

N

REGISTER_MAP, cmd + data

N + 1

FLASH READ / PAGE_PROGRAM

N + 1 + A

FLASH FAST_READ

N + 2 + A

FLASH RDSR / RDID

N + 1

FLASH WREN / WRDI

1, no data phase

A is the configured flash address width, 2 or 3. The event carries no opcode, no model and no address width, so it cannot tell you N – and neither can this SDK, which is why no property here computes one and why there is no read_event_payload. read_memory(offset, event.payload_length) over-reads by the overhead of whatever frame the controller sent.

The I2C twin is different in both fields, and the difference is real rather than a documentation accident: I2cTargetBusEvent reports the data-phase count and a genuine offset, so there the two do compose into a read_memory.

Type:

int

Note

The bytes may have changed by the time you read them. Nothing stops the external controller writing that region again between this notification and the read; the event reports what happened, not a snapshot.

property frame_length: int

payload_length under the name it should have had.

The same number, renamed rather than adjusted. It exists because payload_length is the firmware field’s own name – it is what SpiTargetBusEventNotification.payload_length is called and what a reader matches against the wire struct – so the field cannot be renamed away, but every reading of it as a data length is wrong. read_memory(offset, event.frame_length) at least reads as the mistake it is.

No arithmetic happens here, deliberately. The honest data length would be frame_length minus the overhead of the frame the controller sent, and the event does not carry which frame that was; subtracting a guessed constant is the defect that got read_event_payload removed. How many bytes a transaction moved is something you know from the protocol you are emulating.

Returns:

Bytes clocked in the transaction, command and address phases included.

I3C

class pycosmicsdk.I3cTargetInfo(static_address: int, dynamic_address: int, pid: bytes, bcr: int, dcr: int, max_read_length: int, max_write_length: int, ibi_payload_size: int)

Bases: object

One entry of the I3C controller’s target device table.

Returned by I3cController.get_device_table and init_bus, and accepted by init_bus to seed a deterministic address assignment.

When seeding, ``pid``, ``bcr`` and ``dcr`` are match keys, not values you are storing. They identify which target on the bus the entry applies to: firmware assigns the entry’s dynamic_address to the target matched by all three, instead of auto-assigning it. An entry matching no target on the bus is discarded silently – that target is auto-assigned as if the entry had never been passed, no error is raised, and the resulting record comes back built purely from discovery with max_write_length, max_read_length and ibi_payload_size all zero. Those three are the fields actually stored in the controller’s table for a matched entry, so a mistyped pid costs you the whole entry rather than reporting itself. Read the target’s PID with I3cCcc.getpid and seed from that, rather than from a literal.

static_address

7-bit static address, or 0 when the target has none.

Type:

int

dynamic_address

7-bit dynamic address assigned on the bus. When seeding, the address firmware assigns to the matched target.

Type:

int

pid

48-bit Provisional ID as 6 bytes, most significant first. When seeding, part of the match key.

Type:

bytes

bcr

Bus Characteristics Register value. When seeding, part of the match key.

Type:

int

dcr

Device Characteristics Register value. When seeding, part of the match key.

Type:

int

max_read_length

Not read from the target. Zero after a plain init_bus(), an echo of the seed otherwise; the target’s real limit comes from I3cCcc.getmrl.

Type:

int

max_write_length

Not read from the target, like max_read_length; the real limit comes from I3cCcc.getmwl.

Type:

int

ibi_payload_size

Not read from the target, like max_read_length.

Type:

int

class pycosmicsdk.I3cTargetConfiguration(is_legacy_i2c_target: bool = False, ibi_request: bool = False, cr_request: bool = False, daa_use_setdasa: bool = False, daa_use_setaasa: bool = False, daa_use_entdaa: bool = False, ibi_timestamp_enable: bool = False, pending_read_capability: bool = False)

Bases: object

Per-target behaviour flags stored in the controller’s device table.

Passed to I3cController.set_target_configuration and packed into the single configuration byte the firmware expects. The fields flagged as reserved are accepted by the firmware but have no effect yet.

is_legacy_i2c_target

Target speaks legacy I2C rather than I3C.

Type:

bool

ibi_request

Target may raise In-Band Interrupts.

Type:

bool

cr_request

Target may request the controller role. Reserved.

Type:

bool

daa_use_setdasa

Assign this target’s dynamic address with SETDASA.

Type:

bool

daa_use_setaasa

Assign this target’s dynamic address with SETAASA.

Type:

bool

daa_use_entdaa

Assign this target’s dynamic address with ENTDAA.

Type:

bool

ibi_timestamp_enable

Timestamp IBIs from this target. Reserved.

Type:

bool

pending_read_capability

Target supports pending-read. Reserved.

Type:

bool

to_byte() → int

Pack the flags into the firmware’s configuration byte.

Returns:

The configuration byte, bit 0 being is_legacy_i2c_target through bit 7 being pending_read_capability.

class pycosmicsdk.I3cTargetBusEvent(kind: I3cTargetBusEventKind | int, target_address: int, memory_address: int, payload: bytes)

Bases: object

One bus event observed while the adapter acts as an I3C target.

Returned by I3cTarget.wait_for_event, rebuilt from the firmware’s target bus-event notification. Which fields carry meaning depends on kind: only the transfer kinds populate memory_address and payload.

kind

What the controller on the bus did. An I3cTargetBusEventKind member for every kind this SDK knows; a plain int carrying the raw firmware value for one it does not, so a newer firmware’s event is surfaced rather than dropped. Compare against enum members (event.kind is I3cTargetBusEventKind.WRITE_TRANSFER) and treat anything else as unrecognised.

Type:

pycosmicsdk._cpp._pycosmicsdk_cpp.I3cTargetBusEventKind | int

target_address

For a transfer or CCC, the dynamic address of the controller that drove it; for DYN_ADDR_CHANGE, the new dynamic address this target was just assigned.

Type:

int

memory_address

Register index the transfer started at, in units of the configured memory layout’s register size. 0 for the CCC and dynamic-address kinds.

Type:

int

payload

Bytes written to, or read out of, the target memory. Empty for the CCC and dynamic-address kinds.

Type:

bytes

class pycosmicsdk.I3cGetMrlResult(max_read_length: int, ibi_payload_size: int | None)

Bases: object

Result of the GETMRL CCC.

max_read_length

Maximum read length reported by the target.

Type:

int

ibi_payload_size

Maximum IBI payload the target reported, or None when the response omitted the optional field.

Type:

int | None