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 |
|---|---|---|
|
Hardware revision, and an equality rather than a floor: on a Supernova whose |
|
|
|
Model: firmware selects its pull-up resistor table per model at build time. The full accepted set is |
|
Hardware revision: one pin on one revision is known not to arm — |
|
|
|
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 — |
|
The three protocols’ |
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. |
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:
objectStatic 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_reconfigurableandspi_target_capable. The first two mirror shipping firmware limits keyed on the revision rather than the model, and both surface as a flatFW_FEATURE_NOT_SUPPORTED_BY_HARDWARErefusal on the wire. The third surfaces as nothing on the wire at all — firmware answersSUCCESSand 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 raiseCapabilityErrorbefore any request is sent. Seepycosmicsdk._capabilitiesfor the revision tables and which entries are bench-confirmed.i2c_pull_upsis the same idea one level up, keyed on the model alone: the two adapters accept different subsets ofI2cPullUp, 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 ani2c_runtime_reconfigurableorspi_target_capableofTrue, 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_capableis 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 aFalsea caller believes is wrong can be overridden — build a modified snapshot withdataclasses.replace— where overriding any other field only defers the same refusal to the device.i2c_pull_upsis 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; seepycosmicsdk._capabilitiesfor the transcription and its provenance.- protocols
Protocol interfaces the device’s firmware supports.
- i2c_buses
I2C buses the device exposes.
- max_i2c_payload_bytes
Largest I2C payload the ordinary HID route carries in one transfer – 1024 on both models, and not the largest
I2cController.readorI2cController.writeaccepts. On a bulk-capable host and device those take up to 32768 bytes underI2cTransport.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_capabilitiesis a pure function of aDeviceInfoand 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 thresholdAUTOcompares a request against, and the ceiling forced byI2cTransport.HID.- Type:
- 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_readand the legacy-I2C pair reach the advertised I3C bulk ceiling (32767 on a Supernova) underI3cTransport.AUTO; askI3cController.max_transfer_bytes. HDR-DDR is the exception in the other direction: it is capped at 1024 on both routes for bus safety, whichI3cController.max_hdr_ddr_transfer_bytesreports.- Type:
- 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:
- interrupt_capable_gpio_pins
GPIO pins this unit can arm a firmware interrupt on, as
Gpio.configure_interruptdoes. Empty when the adapter exposes no GPIO at all. Query it withsupports_gpio_interrupt.
- i2c_runtime_reconfigurable
Whether an already-initialised I2C interface accepts new parameters — that is, whether
I2cController.configureandI2cController.set_pull_upwork on this unit.Falsedoes not affectI2cController.initialize: the bus still comes up, it just keeps the parameters its first bring-up chose.Falsealso when the adapter exposes no I2C bus at all.- Type:
- 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 atopen(): it is negotiated lazily, so it reads as un-negotiated (queriedFalse) until the firstSpiController.write,SpiController.read,SpiController.transport,I3cController.sdr_write,I3cController.sdr_readorI3cController.transport. Query it withsupports_spi_bulkandsupports_i3c_bulk, or with the record’s ownBulkTransportCapability.supportsandBulkTransportCapability.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 calledspi_bulkwhile SPI was the only group the negotiation read;spi_bulkis now a deprecated alias property, anddataclasses.replaceneeds this name.
- i2c_pull_ups
I2cPullUpselections 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 withsupports_i2c_pull_up.
- i2c_init_pull_up_effective
Whether the
pull_uppassed toI2cController.initializeactually reaches the hardware.Falseon 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 everyI2cPullUpvalue — includingDISABLE— leaves the same resistance on the bus. The SDK logs a warning rather than refusing, because nothing failed;I2cController.bring_upalso carries it as therefusalon asettings_applied=Falseresult, since unlikeinitializeit returns a record with somewhere to say it. Unrelated toi2c_runtime_reconfigurable, which covers the two pull-up commands that are refused rather than ignored.- Type:
- uart_hw_handshake_effective
Whether a non-
DISABLEDhw_handshakepassed toUart.initializeorUart.configureactually reaches the hardware.Falseon 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_upalso carries it as therefusalon asettings_applied=Falseresult, for the reason given on the field above.- Type:
- 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).
Falseon 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 reportsSUCCESSregardless, so unlike every other field here this one has no firmware refusal behind it — the SDK raisesCapabilityErrorin firmware’s place, because the alternative a caller experiences is permanent silence.Falsealso when the adapter exposes no SPI at all.- Type:
- supports(protocol: ProtocolInterface) bool
Return
Truewhenprotocolis inself.protocols.- Parameters:
protocol – The protocol interface to check for.
- Returns:
Trueif the adapter exposesprotocol, elseFalse.
- has_i2c_bus(bus: I2cBus) bool
Return
Truewhenbusis inself.i2c_buses.- Parameters:
bus – The I2C bus to check for.
- Returns:
Trueif the adapter exposesbus, elseFalse.
- supports_gpio_interrupt(pin: GpioPinNumber) bool
Return
Truewhenpinis inself.interrupt_capable_gpio_pins.Ask this before
Gpio.configure_interruptto keep code portable across hardware revisions: on a Supernova reportinghw_version == "HW-B"it isFalseforGpioPinNumber.PIN_3andTruefor the other five pins.- Parameters:
pin – The GPIO pin to check for interrupt support.
- Returns:
Trueif the adapter is known to support arming an interrupt onpin, elseFalse.
- supports_i2c_pull_up(pull_up: I2cPullUp) bool
Return
Truewhenpull_upis inself.i2c_pull_ups.Ask this before
I2cController.initializeto keep code portable across models:I2cPullUp.KOHM_10_0,I2cPullUp.KOHM_4_7andI2cPullUp.KOHM_3_3are Supernova-only, andI2cPullUp.KOHM_4_0is Pulsar-only. Everything fromI2cPullUp.OHM_150up toI2cPullUp.KOHM_2_2, plusI2cPullUp.DISABLE, is accepted by both.- Parameters:
pull_up – The pull-up selection to check for support.
- Returns:
Trueif the adapter’s model acceptspull_up, elseFalse.
- supports_spi_bulk() bool
Return
Truewhen SPI bulk transfers are usable on this host and device.Equivalent to
self.bulk.available, and named to matchsupportsandsupports_gpio_interrupt. Readbulk.host_transport_openandbulk.device_supportedwhenFalseto 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.writego over the bulk plane” – andbulkis the record that answers it for all three protocol groups. Asksupports_i3c_bulk, orbulk.supports/bulk.max_payload_for, for the other two.No installed build can return
Trueyet: the bulk commands live on an unmerged CosmicSDK branch, so the compiled extension carries no bulk surface.SpiController.writeandSpiController.readwork regardless – they fall back to the HID path.- Returns:
TrueifSpiController.writeandSpiController.readwill use the bulk plane, elseFalse.- Raises:
CapabilityError – If the bulk transport has not been negotiated on this connection. It used to answer
Falsethere, which is indistinguishable from “this device has no bulk plane”; readbulk.queriedfirst, or readSpiController.transporton an open device, which negotiates.
- property spi_bulk: BulkTransportCapability
Deprecated alias for
bulk; identical object, retired name.The field was called
spi_bulkwhile 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, sospi_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 – passbulk=instead. Nothing inside this package reads this name.- Returns:
The same
BulkTransportCapabilitybulkcarries.- Warns:
DeprecationWarning – Always. Read
bulk.
- supports_i3c_bulk() bool
Return
Truewhen 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 returnsTrueon such a unit with the vendor interface claimed. Whether a transfer then takes the bulk route also depends on this build binding the sixi3c_*_bulkcommands, which the CosmicSDK integration branch did not and the current pin does. On a build whose extension predates them this readsTruewhileI3cController.sdr_writekeeps the 1024-byte ordinary ceiling, and that is the honest answer rather than a bug.Ask
I3cController.transportfor 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 isFalsefor 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:
Trueif the connected device advertises I3C controller bulk with a usable ceiling and this host holds the vendor bulk interface, elseFalse.- 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.transportnegotiates 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:
objectWhether this host and this device can move SPI payloads over the bulk plane.
Carried by
DeviceCapabilities.bulkand consulted bySpiController.writeandSpiController.readto pick a transport. The default – every field falsy,queriedincluded – means “not negotiated yet”; the negotiation is lazy, so a caller that never touchesSpiController.write,SpiController.readorSpiController.transportnever 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
availableon 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 inpycosmicsdk._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()thenopen()) to renegotiate.It is negotiated only from the blocking surface, and this is the one real asymmetry between
DeviceandAsyncDevice. The decode of the capability record lives in the blockingCosmicSDK::getBulkCapabilitiesalone –CosmicAsyncSDK’s counterpart is a bare submit with no decodable reply – so anAsyncDevicecannot run this negotiation at all, andqueriedstaysFalseon 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 itssupportedflag from a group count that is never zero, so the device half is very nearly a constant. Use a blockingDevicewhen you need the record itself.
host_transport_openanddevice_supportedare reported separately fromavailableon 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 onavailable == Falsewithout knowing which.Note
No installed build of this package can report
available == Trueyet. 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 inSpiController.write/SpiController.readis therefore the only path that runs today.Note
An un-negotiated record answers nothing, loudly.
queriedFalsemeans “nobody has asked yet”, which is not an answer about this host or this device – soavailable,max_spi_payload_bytes,supportsandmax_payload_forall raiseCapabilityErrorin that state rather than reportingFalseand0. 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”. Readqueriedto 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 –
queriedTruewith nothing advertised, whichpycosmicsdk._capabilities.probe_bulkreturns 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.
Falsemeans “not asked yet”, and every other field is meaningless – which is why the derived answers refuse rather than report it.Truewithdevice_supportedFalsemeans the device was asked and either said no or said nothing – the two are indistinguishable, upstream’s own deliberate choice.- Type:
- host_transport_open
Whether this host claimed the adapter’s vendor bulk interface.
Falseis a host problem: no driver bound, no permission, or another process holding it.- Type:
- device_supported
Whether the device answered the capability query claiming bulk support at all.
Falseis a device problem: firmware that predates the bulk commands.- Type:
- 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:
- operations
Which
BulkOperationmembers this host and device can carry on the bulk plane. Query it withsupports. Empty until the negotiation has run, and empty whenever the host holds no transport, whatever the device advertised.
- ceilings
(protocol, ceiling)pairs, one per command group the device reported with a usable ceiling, inProtocolInterfacename order. Query it withmax_payload_forrather 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:
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_SPIholds the SPI controller bulk pair and the SPI target memory pair under onemaxPayloadLength, andGROUP_I3Cholds 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.writeandSpiController.readuse 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 exactlysupports(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 andpycosmicsdk._capabilities.guard_spi_bulkneed. That is narrow, not a lie. Asksupportsandmax_payload_forfor the I2C and I3C answers; do not read this one as either. It also cannot become a module-level constant inpycosmicsdk._capabilities: that module imports this one, so the operation set has to live here.- Returns:
Truewhen SPI controller traffic can travel on the bulk plane.- Raises:
CapabilityError – If the negotiation has not run on this connection (
queriedFalse). “Unknown” is notFalse; see_require_negotiated.
- property max_spi_payload_bytes: int
Largest payload one bulk SPI call accepts, as the device advertised it.
0when the device advertised no usable SPI group ceiling. Exactlymax_payload_for(ProtocolInterface.SPI); see that method for what a group ceiling does and does not bound. Requests above it raiseValidationError– nothing is ever split, because chip select de-asserts between calls.The other SPI-controller-shaped shortcut, kept for the same reason
availableis: it names its protocol in full, so it cannot be misread as the I2C or I3C ceiling the way an unqualifiedmax_payload_byteson a three-group record would be. Askmax_payload_forfor 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
Truewhen this host and device can carryoperation.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_tand the firmware’s build-time assertions compare each ceiling against the staging arena rather than againstUINT16_MAX, so a doubled arena would advertise0uncaught. 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_TRANSFERthe device says yes while no CosmicSDK branch wraps the command – see that member’s note.I3cController.transportreports the combined answer.- Parameters:
operation – The bulk operation to ask about.
- Returns:
Trueifoperationcan travel on the bulk plane on this connection, elseFalse.- Raises:
CapabilityError – If the negotiation has not run on this connection (
queriedFalse). It used to answerFalsethere, 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, whileSPI_CONTROLLER_TRANSFER_BULK’s transmit half is separately capped at 1024 bytes; andGROUP_I3C’s figure is the SDR ceiling, with HDR-DDR capped lower inside the manager where the host cannot see it. Oneuint16_tper 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.UARTandProtocolInterface.GPIOhave no bulk group and always answer0.- Returns:
The ceiling in bytes, or
0when 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 (
queriedFalse). This is the read that cost real bench time: it answered0, 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:
objectOutcome of a
bring_upcall: the interface is up, and at whose settings.Returned by
I2cController.bring_up,SpiController.bring_up,I3cController.bring_up,Uart.bring_upand their asyncio twins. The contract everybring_upkeeps 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
Falsein 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 freshinitialize, which meansDevice.rebootwithResetTarget.RUNTIMErather 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_upmethods report a refusal that is not a refused reconfigure, and both come backsettings_applied=Falseon a fresh bring-up, withalready_initialized=False:Uart.bring_up, for ahw_handshakeasked for on a unit whosecapabilities.uart_hw_handshake_effectiveisFalse. Firmware answersSUCCESSand forces RTS/CTS off anyway.I2cController.bring_up, for anypull_upasked for on a unit whosecapabilities.i2c_init_pull_up_effectiveisFalse. Firmware answersSUCCESSand enables the board’s fixed network anyway.I2cPullUp.KOHM_10_0is reported like every other value rather than exempted asUartHwHandshake.DISABLEDis;_interfaces/i2c.py’s_init_pull_up_refusalargues 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_appliedrather than assume. Proceeding on settings the adapter did not accept is the failure this record exists to make impossible to miss;raise result.refusalturns the report back into an exception for callers that want no leniency at all.- settings_applied
Whether the requested settings are in force.
Trueafter a freshinitialize, and after a reconfigure the adapter accepted;Falsewhen 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:Truemeans no refusal this SDK can see, not that the device was read back and confirmed.- Type:
- already_initialized
Whether the interface was already up when
bring_upwas called – that is, whetherinitializereportedFW_INTERFACE_ALREADY_INITIALIZEDand the reconfigure path ran.Trueis the normal reading on any second run of a script, since an interface stays initialised across aclose()/reopen cycle.already_initialized and settings_appliedtherefore means “was already up, and successfully reconfigured”. It records which path ran, not whether the settings took, so it staysFalseon the two fresh-bring-up capability refusals described above.- Type:
- refusal
The error explaining a
settings_applied=False, orNonewhen it isTrue. Itsstatus_codedistinguishes the cases a caller may want to word differently:SDK_ERROR_WRONG_REQUESTfrom aCapabilityErrormeans 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_HARDWAREis the same refusal answered by firmware on a revision this SDK has no table for; andFW_INVALID_PARAMETERmeans the adapter can reconfigure but not to these values. The exception’s message already says which, and what to do about it.- Type:
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:
objectUSB interface configuration stored in the adapter’s flash.
Returned by
Device.get_usb_configand accepted byDevice.set_usb_config. Mirrors upstream’sUsbConfigstruct 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_CHANGEon a write leaves it as it is.
- cdc_enabled
Whether the USB CDC interface is enabled.
CdcState.NO_CHANGEon a write leaves it as it is.
- cdc_mode
Which protocol the USB CDC interface bridges. Always applied on a write — there is no no-change sentinel.
- class pycosmicsdk.WebusbState(*values)
Bases:
IntEnumEnabled state of the adapter’s WebUSB interface.
Values mirror
WebusbStatein CosmicSDK’sdefinitions.h.NO_CHANGEis a write-only sentinel: pass it toDevice.set_usb_configto 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:
IntEnumEnabled state of the adapter’s USB CDC interface.
Values mirror
CdcStatein CosmicSDK’sdefinitions.h.NO_CHANGEis a write-only sentinel with the same meaning asWebusbState.NO_CHANGE.- DISABLED = 0
- ENABLED = 1
- NO_CHANGE = 255
- class pycosmicsdk.CdcMode(*values)
Bases:
IntEnumOperating mode of the adapter’s USB CDC interface.
Values mirror
CdcModein CosmicSDK’sdefinitions.h. UnlikeWebusbStateandCdcStatethis enum has no no-change sentinel, so everyset_usb_configcall applies a mode; read the current configuration first and modify it if only one other field should change.Some modes are model-specific:
TERMINALis Supernova-only, whileRS485andCANFDare 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:
objectOne 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’sGetAnalogMeasurementsParameters(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 indocs_dev/hardware/hardware_reference.md, and they are the same values the blocking per-protocol readers return one at a time: upstream’si2cGetBusVoltage,spiGetBusVoltage,uartGetBusVoltageandgpioGetVoltageall issue this one command and hand backvtarg_i2c_spi_uart_gpio_mv, whilei3cGetBusVoltagehands 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.
I2C
I2C — target mode
- class pycosmicsdk.I2cTargetBusEvent(kind: I2cTargetBusEventKind | int, target_address: int, memory_address: int, payload_length: int)
Bases:
objectOne 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.
I3cTargetBusEventhas apayloadfield 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 withI2cTarget.read_memory.The trap is sharper than “there is no payload”, and it is on the notification rather than here.
I2cTargetBusEventNotificationdeclares nopayloadof 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:49copies upstream’sNotification::payload/payloadLength, andnative/notifications.hppcalls the base member “the raw-framepayload” in as many words). So a subscriber that readspayloadexpecting the transaction’s data gets a twelve-byte frame instead – plausible-lookingbytesthat are not the data and whose length has nothing to do withpayload_length. This record deliberately has nopayloadfield at all, so the mistake is not reachable from here. Measured on silicon rather than merely read out of the source:len(payload) == 12for every event driven whilepayload_lengthranged 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_addressrather than being a phase of its own, soread(addr, n, subaddress=...)yields oneREAD_TRANSFERand noWRITE_TRANSFER.The hazard is under-counting, and it is that nothing is delivered until the STOP. A
non_stop=Truetransfer holds the bus with a repeated START and emits zero events for as long as it holds it, so await_for_eventafter one blocks until unrelated code closes the bus; both phases then arrive together at that STOP.I2cTarget.wait_for_eventhas the measurement and the full model.- kind
What the external controller did. An
I2cTargetBusEventKindmember for every kind this SDK knows; a plainintcarrying the raw firmware value for one it does not, so a newer firmware’s event is surfaced rather than dropped.
- 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:
- 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:
- 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 aread_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_lengthcounts every byte clocked between the chip-select edges, opcode and address included, and itsmemory_addressis 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:
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:
objectOne 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.
I3cTargetBusEventhas apayloadfield 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 withSpiTarget.read_memory, at an offset you supply – seememory_addressbelow, 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_lengthcounts the whole framed transaction – command and address phases included – soread_memory(event.memory_address, event.payload_length)gets both arguments wrong: a placeholder offset and an over-long count.frame_lengthis the same number under an honest name, and thepayload_lengthentry 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 topayload_length. This record carries nopayloadfield, 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_lengthcounts the whole framed transaction with a per-model overhead that cannot be subtracted blind, whileI2cTargetBusEvent.payload_lengthcounts data-phase bytes against a realmemory_address. Only the I2C pair composes into aread_memory; see that field below. The other I2C-only behaviour is the held bus: anon_stoptransfer defers its events until something issues the STOP, which has no SPI analogue.- kind
What happened on the bus. A
SpiTargetBusEventKindmember for every kind this SDK knows; a plainintcarrying 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 theintarm is the only thing that will make a second kind visible.
- 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. ASpiChipSelectmember, or a plainintfor a line this SDK does not name.
- 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
FLASHandREGISTER_MAPmodels 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.SpiTargethas noread_event_payloadconvenience for exactly this reason.- Type:
- 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-model0x03 + addr + Nread it isN + 4with the default three-byte address. The overhead is not constant, so there is no offset to subtract:frame
payload_lengthMEMORY, any engineNREGISTER_MAP, cmd + dataN + 1FLASHREAD / PAGE_PROGRAMN + 1 + AFLASHFAST_READN + 2 + AFLASHRDSR / RDIDN + 1FLASHWREN / WRDI1, no data phaseAis 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 noread_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:
I2cTargetBusEventreports the data-phase count and a genuine offset, so there the two do compose into aread_memory.- Type:
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_lengthunder the name it should have had.The same number, renamed rather than adjusted. It exists because
payload_lengthis the firmware field’s own name – it is whatSpiTargetBusEventNotification.payload_lengthis 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_lengthminus 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 gotread_event_payloadremoved. 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:
objectOne entry of the I3C controller’s target device table.
Returned by
I3cController.get_device_tableandinit_bus, and accepted byinit_busto 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_addressto 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 withmax_write_length,max_read_lengthandibi_payload_sizeall zero. Those three are the fields actually stored in the controller’s table for a matched entry, so a mistypedpidcosts you the whole entry rather than reporting itself. Read the target’s PID withI3cCcc.getpidand seed from that, rather than from a literal.- dynamic_address
7-bit dynamic address assigned on the bus. When seeding, the address firmware assigns to the matched target.
- Type:
- pid
48-bit Provisional ID as 6 bytes, most significant first. When seeding, part of the match key.
- Type:
- 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 fromI3cCcc.getmrl.- Type:
- max_write_length
Not read from the target, like
max_read_length; the real limit comes fromI3cCcc.getmwl.- Type:
- 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:
objectPer-target behaviour flags stored in the controller’s device table.
Passed to
I3cController.set_target_configurationand packed into the single configuration byte the firmware expects. The fields flagged as reserved are accepted by the firmware but have no effect yet.
- class pycosmicsdk.I3cTargetBusEvent(kind: I3cTargetBusEventKind | int, target_address: int, memory_address: int, payload: bytes)
Bases:
objectOne 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 onkind: only the transfer kinds populatememory_addressandpayload.- kind
What the controller on the bus did. An
I3cTargetBusEventKindmember for every kind this SDK knows; a plainintcarrying 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.
- 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:
- memory_address
Register index the transfer started at, in units of the configured memory layout’s register size.
0for the CCC and dynamic-address kinds.- Type: