Protocol interfaces

Nine interface classes, each with an Async twin — eighteen classes in all. None of them is constructed directly. Eight come from an accessor on Device or AsyncDevice, which caches it per device (and, for I2C, per bus — the controller and the target for one bus are cached under distinct keys, so they are two objects rather than one). The ninth, I3cCcc, is a sub-interface reached through the I3cController.ccc property rather than from the device.

Three of the accessors are themselves capability-guarded, and refusing at the accessor is deliberate: a caller who cannot obtain the object cannot write the rest of the program against it. Device.i3c() refuses on a Pulsar, and Device.spi_target() refuses on a hardware revision that cannot receive a chip select.

Accessor

Blocking

asyncio

dev.i2c(bus=...)

I2cController

AsyncI2cController

dev.i2c_target(bus=...)

I2cTarget

AsyncI2cTarget

dev.i3c()

I3cController

AsyncI3cController

dev.i3c().ccc

I3cCcc

AsyncI3cCcc

dev.i3c_target()

I3cTarget

AsyncI3cTarget

dev.uart()

Uart

AsyncUart

dev.spi()

SpiController

AsyncSpiController

dev.spi_target()

SpiTarget

AsyncSpiTarget

dev.gpio()

Gpio

AsyncGpio

I2cTarget is reached per bus by design — i2cTargetInit takes a bus id as its first parameter, which is forced by the upstream surface rather than chosen. SpiTarget and I3cTarget are not, because their peripherals are not.

The narrative counterparts are the protocol guides, and the two roles that share the bulk plane have their own page: Target mode.

I2C

class pycosmicsdk.I2cController(*, device: Device, bus: I2cBus)

Bases: object

Blocking controller for a single I2C bus of an open device.

Obtain one from Device.i2c; the constructor is package-private. Instances are cached per bus on the device, so repeated dev.i2c(bus=...) calls return the same controller.

property bus: I2cBus

The I2C bus this controller drives.

property max_transfer_bytes: int

Largest payload one write or read can carry on this connection.

1024 – the ordinary HID route’s ceiling – unless this host and device can both carry the I2C controller bulk pair, in which case it is the ceiling the device advertised for its I2C command group (32768 on a Supernova, 16384 on a Pulsar).

Reading it runs the bulk negotiation if it has not run yet, which is at most one device query per connection.

Returns:

The per-call ceiling in bytes.

Raises:

DisconnectError – If the device is no longer open.

initialize(frequency_hz: int, pull_up: I2cPullUp) → I2cController

Bring the bus up as a controller and return self for chaining.

Not idempotent: an already-initialised bus reports FW_INTERFACE_ALREADY_INITIALIZED. The bus stays initialised across reconnects – the role lives in firmware, so it outlives this handle and this process – and a re-run hits this unless the bus is released or the device is reset first. Prefer bring_up, which absorbs the status and reports whether the settings took, and use configure to change parameters of an already-initialised bus.

Where a genuine reset of unknown state is wanted, release the bus rather than rebooting: i2cDeinit is role-agnostic and per bus, so I2cTarget.release clears a controller on this bus too, even though it is spelled through the target factory – dev.i2c_target(bus=...).release(). The factories issue no commands, so reaching for that handle costs nothing, and FW_INTERFACE_NOT_INITIALIZED from the release just means the bus was already idle, which is the state the caller wanted.

Parameters:
  • frequency_hz – SCL clock in Hz (e.g. 400_000 for 400 kHz). Both supported adapters accept up to 1 MHz; an unsupported rate is rejected by the firmware as FW_INVALID_PARAMETER and surfaced as FirmwareError. Restores the default timing: on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). set_duty_cycle makes it exact.

  • pull_up – On-board pull-up resistor setting. Which members are accepted depends on the model, not on all thirteen being universal: firmware carries a different pull-up table per adapter, so ask dev.capabilities.i2c_pull_ups (or DeviceCapabilities.supports_i2c_pull_up) when the code has to run on both.

Returns:

This controller, to allow chaining.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the connected model’s firmware has no pull-up table row for pull_up – I2cPullUp.KOHM_3_3, I2cPullUp.KOHM_4_7 and I2cPullUp.KOHM_10_0 are Supernova-only, I2cPullUp.KOHM_4_0 is Pulsar-only. No facade call is made, which is the point: firmware validates the pull-up before bringing the bus up, so the round trip this replaces would have failed with a bare FW_INVALID_PARAMETER and left the bus down.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), or the firmware rejects the configuration (FW_INVALID_PARAMETER). Upstream i2cControllerInit reports no 0x02xx code, so bring-up never surfaces as I2cError.

bring_up(frequency_hz: int, pull_up: I2cPullUp) → BringUpResult

Bring the bus up as a controller, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring an I2C bus up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and an I2C interface stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live interface with configure instead, and reports a refused reconfigure through the return value rather than raising.

Refusal is not hypothetical and not one-shaped: a Supernova earlier than revision C cannot reconfigure a live I2C interface at all and is refused by configure’s capability guard before anything reaches the device, while a Pulsar can reconfigure but rejects some pull-up values. Both leave the bus up and usable, so both are reported. A pull-up this model has no firmware table row for is a different thing entirely – a bad argument for this adapter, which no fallback repairs – and its CapabilityError propagates from initialize untouched.

One reported refusal is not a refused reconfigure, and it is the only one that can arrive with already_initialized=False: a pull_up asked for on a unit whose capabilities.i2c_init_pull_up_effective is False comes back settings_applied=False even on a fresh bring-up. Firmware answers SUCCESS and enables the board’s fixed network regardless, so 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 bus is up and frequency_hz did apply. Every pull-up value is reported on such a unit, I2cPullUp.KOHM_10_0 included; _init_pull_up_refusal argues why, and says what would settle it.

Parameters:
  • frequency_hz – SCL clock in Hz requested for the bus (e.g. 400_000 for 400 kHz). With the default timing, on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). set_duty_cycle makes it exact.

  • pull_up – On-board pull-up resistor setting requested for the bus. Checked against dev.capabilities.i2c_pull_ups before anything is sent, exactly as initialize checks it.

Returns:

A BringUpResult. On return the bus is up and usable; consult settings_applied before relying on frequency_hz or pull_up actually being in force, and refusal for why they are not. It is a report, not a promise: nothing here guarantees a setting the device accepted is in force, only that a refusal this SDK can see is not reported as a success.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the connected model’s firmware has no pull-up table row for pull_up. A refusal to reconfigure is reported in the result instead; this one is a bad argument, so it propagates.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On any firmware failure other than the two the fallback absorbs – an unsupported bus id (FW_BUS_NOT_SUPPORTED), for instance, or a frequency initialize itself rejects.

  • I2cError – If the pull-up resistors could not be applied during the reconfigure (FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

configure(frequency_hz: int, pull_up: I2cPullUp) → None

Reconfigure an already-initialised bus at runtime.

The bus must have been initialised with initialize; an uninitialised bus reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError. The interface stays up – no re-initialize is needed afterwards – but the peripheral itself does not: the firmware de-initialises the I2C block, applies the new baud rate, re-initialises it, then sets the pull-up. This is therefore the only lever on the public surface that resets the I2C peripheral (and so releases the bus lines) without restarting the firmware.

Not every adapter can do this at all, and it depends on the hardware revision rather than the model. Ask dev.capabilities.i2c_runtime_reconfigurable before calling: on a Supernova whose revision is anything but C it is False, and this method raises CapabilityError before anything reaches the device. initialize is unaffected, so on such a unit the bus still comes up – it simply keeps the parameters that first bring-up chose, and the peripheral-reset lever is not reachable there.

Parameters:
  • frequency_hz – New SCL clock in Hz. Both supported adapters accept up to 1 MHz; an unsupported rate is rejected by the firmware as FW_INVALID_PARAMETER and surfaced as FirmwareError. With the default timing, on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). A ratio set with set_duty_cycle is kept, and makes the rate exact.

  • pull_up – New on-board pull-up resistor setting.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the adapter’s hardware revision cannot reconfigure a live I2C interface (a Supernova whose revision is anything but C), or if the connected model’s firmware has no pull-up table row for pull_up. No facade call is made in either case, so this replaces the firmware round trip rather than following it.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus has not been initialised (status_code FW_INTERFACE_NOT_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), the hardware lacks the feature (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE – pre-empted for every Supernova revision but C, so reachable only on hardware whose gate this SDK does not model), or the firmware rejects a parameter (FW_INVALID_PARAMETER). None of these is a 0x02xx code, so none surfaces as I2cError.

  • I2cError – If the pull-up resistors could not be applied (status_code FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

set_duty_cycle(high_pct: int) → I2cDutyCycle

Set the share of each SCL period the clock is held high.

Any ratio, 50 included, also moves the bus to an exact clock: 550 kHz runs at 549.8, not the default timing’s 600. I2C_DUTY_CYCLE_DEFAULT and initialize return to the default timing.

The frequency from initialize or configure stays; only the high:low split changes, and the ratio survives later configure calls. With a ratio the frequency is never above the request and at most about 0.3 % under it.

Values are not clamped to the I2C specification, so out-of-spec timing is available for margin testing: 30 at 100 kHz gives a tHIGH of 3.0 us, under Standard-mode’s 4.0 us.

Parameters:

high_pct – SCL high time in percent of the period: 20 to 80 in steps of 10, or I2C_DUTY_CYCLE_DEFAULT (0) for the default timing. The firmware decides what is valid.

Returns:

The settings in effect after the call.

Raises:
  • ValidationError – If high_pct does not fit a uint8. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • I2cDutyCycleRefusedError – If firmware refuses the value (FW_INVALID_PARAMETER). The setting in effect is kept, and the error’s frequency_hz and high_pct say what it is.

  • FirmwareError – If the bus is not an initialised controller (FW_INTERFACE_NOT_INITIALIZED), or the firmware predates the command (FW_UNSUPPORTED_COMMAND, 4.5.1 and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the settings (SDK_ERROR_WRONG_RESPONSE).

set_pull_up(pull_up: I2cPullUp) → None

Change the on-board pull-up resistors for this bus.

Gated by the same hardware-revision limit as configure, so ask dev.capabilities.i2c_runtime_reconfigurable first: on a Supernova whose revision is anything but C it is False, and this method raises CapabilityError before anything reaches the device – the bus keeps the pull-ups initialize selected. pull_up is additionally checked against dev.capabilities.i2c_pull_ups, the per-model set firmware will accept.

Parameters:

pull_up – New pull-up resistor setting.

Raises:
  • CapabilityError – If the adapter’s hardware revision cannot reconfigure a live I2C interface (a Supernova whose revision is anything but C), or if the connected model’s firmware has no pull-up table row for pull_up. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus has not been initialised (FW_INTERFACE_NOT_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), the hardware lacks the feature (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE – pre-empted for every Supernova revision but C, so reachable only on hardware whose gate this SDK does not model), or the value is rejected (FW_INVALID_PARAMETER).

  • I2cError – If the pull-up resistors could not be applied (status_code FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware and surfaced as SystemFirmwareError. Setting 0 and then a voltage again power-cycles a DUT the adapter powers, the only way to recover a peripheral hung holding SDA low. That works only if nothing else drives the net: on a multi-adapter bench hand every other source off with use_external_voltage first, then read the rail back. The rail is shared by I2C, SPI, UART and GPIO.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as I2cError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

use_external_voltage(*, persist: bool = False) → None

Hand the I2C rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with SPI, UART and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead. Read back what it measured with get_bus_voltage.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:
get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

This is not an I2C-specific measurement. The adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and this reads that single rail, so the reading reflects whatever last set or is driving it — pair this with use_external_voltage.

SpiController.get_bus_voltage, Uart.get_bus_voltage and Gpio.get_voltage return the very same number. Upstream implements all four by issuing one getAnalogMeasurements command and returning its vmeasVtargI2cSpiUartGpio_mV field (CosmicSDK.cpp); the four names exist to keep each protocol’s surface self-contained, not because four rails are measured. I3cController.get_bus_voltage is the genuine exception: I3C has its own HV and LV rails and reports both.

Returns:

The measured voltage in millivolts.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x02xx code, so this never surfaces as I2cError.

write(address: int, data: bytes, *, subaddress: bytes = b'', non_stop: bool = False, transport: I2cTransport = I2cTransport.AUTO) → None

Write data to a 7-bit target address.

Parameters:
  • address – 7-bit target address (0x00–0x7F).

  • data – Payload bytes. Must not exceed the active transport’s ceiling – 1024 bytes on the ordinary HID route, or the device’s advertised I2C bulk ceiling. max_transfer_bytes reports which applies; a larger value raises ValidationError and is never split across two calls.

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError. It travels inside the request on either transport.

  • non_stop – When True, hold the bus with a repeated START instead of issuing a STOP after the transfer.

  • transport – Which host transport to use. I2cTransport.AUTO, the default, keeps the ordinary route for anything that fits its ceiling and reaches for the bulk plane only above it – see I2cTransport for why that is narrower than SpiTransport.AUTO.

Raises:
  • ValidationError – If address is not a 7-bit value, or data exceeds the active transport’s ceiling.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • CapabilityError – If transport is I2cTransport.BULK and the bulk transport cannot serve it – no vendor interface claimed here, firmware that does not advertise the command, or a build of this package with no I2C bulk binding. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure (e.g. address NACK).

Note

The bulk route is blocking-only. AsyncI2cController.write keeps the 1024-byte ceiling and takes no transport argument – see AsyncI2cController.

read(address: int, length: int, *, subaddress: bytes = b'', non_stop: bool = False, transport: I2cTransport = I2cTransport.AUTO) → bytes

Read length bytes from a 7-bit target address.

Parameters:
  • address – 7-bit target address (0x00–0x7F).

  • length – Number of bytes to read. Must not exceed the active transport’s ceiling; max_transfer_bytes reports which applies. A larger value raises ValidationError and the read is never split.

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • non_stop – When True, hold the bus with a repeated START instead of issuing a STOP after the transfer. Carried on both transports – the bulk read takes it as a parameter exactly as the ordinary one does.

  • transport – Which host transport to use. I2cTransport.AUTO, the default, keeps the ordinary route for anything that fits its ceiling and reaches for the bulk plane only above it.

Returns:

The bytes read from the target. May be shorter than ``length`` on a NACKed or short transfer: the count is the one the device announced, not the one that was requested, and upstream does not clamp the two together.

Raises:
  • ValidationError – If address is not a 7-bit value, or length is invalid or exceeds the active transport’s ceiling.

  • ProtocolError – If subaddress is longer than 4 bytes, or – on the bulk route – if the reply was too short to carry its own announced length.

  • CapabilityError – If transport is I2cTransport.BULK and the bulk transport cannot serve it. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure (e.g. address NACK).

Note

The bulk route is blocking-only, and for a second reason on top of the write’s: the reply only announces the length, and the data phase is then collected from the vendor IN endpoint by a call that blocks until it arrives. AsyncI2cController.read therefore keeps the 1024-byte ceiling – see AsyncI2cController.

scan(*, include_10bit: bool = False) → I2cScanResult

Scan the bus and return the addresses that acknowledged.

Parameters:

include_10bit – When True, also probe 10-bit addresses.

Returns:

An I2cScanResult with the discovered 7-bit (and optionally 10-bit) addresses. Empty when nobody answered.

Raises:
class pycosmicsdk.AsyncI2cController(*, device: AsyncDevice, bus: I2cBus)

Bases: object

Asyncio I2C controller for a single I2C bus of an open device.

The asyncio twin of I2cController, returned by AsyncDevice.i2c; the constructor is package-private. Instances are cached per bus on the device, so repeated dev.i2c(bus=...) calls return the same controller. Argument validation matches the blocking controller; commands are awaited and correlated through the device’s engine rather than serialized under a lock.

Note

The rail surface matches I2cController in full, get_bus_voltage included. CosmicAsyncSDK declares no i2cGetBusVoltage, but it does declare getAnalogMeasurements, which carries the shared I2C/SPI/UART/GPIO rail — and that is the same command upstream’s blocking i2cGetBusVoltage issues internally. So the async reader is built the same way as the blocking one, and reports the same value.

Note

The bulk plane is deliberately absent here. write and read carry no transport argument and keep the 1024-byte HID ceiling, where their blocking twins can reach the device’s advertised I2C bulk ceiling. That is a decision, not an oversight, and it is the same one AsyncSpiController records for the SPI bulk pair: upstream’s async bulk write calls sendPayload inside the submit, so what looks like a non-blocking submit blocks the calling thread for the whole transfer, and the async bulk read cannot be expressed as one submit at all – the reply only announces the length, and a second, blocking call collects the data phase. On the asyncio path either one stalls the event loop thread.

Use I2cController.write and I2cController.read for a payload above 1024 bytes – from a thread, or through loop.run_in_executor if the surrounding code is async. docs_dev/designs/async-bulk-transfer-shape.md has the analysis.

property bus: I2cBus

The I2C bus this controller drives.

async initialize(frequency_hz: int, pull_up: I2cPullUp) → AsyncI2cController

Bring the bus up as a controller and return self for chaining.

Not idempotent: an already-initialised bus reports FW_INTERFACE_ALREADY_INITIALIZED. The bus stays initialised across reconnects – the role lives in firmware, so it outlives this handle and this process – and a re-run hits this unless the bus is released or the device is reset first. Prefer bring_up, which absorbs the status and reports whether the settings took, and use configure to change parameters of an already-initialised bus.

Where a genuine reset of unknown state is wanted, release the bus rather than rebooting: i2cDeinit is role-agnostic and per bus, so AsyncI2cTarget.release clears a controller on this bus too, even though it is spelled through the target factory – dev.i2c_target(bus=...).release(). The factories issue no commands, so reaching for that handle costs nothing, and FW_INTERFACE_NOT_INITIALIZED from the release just means the bus was already idle, which is the state the caller wanted.

Parameters:
  • frequency_hz – SCL clock in Hz (e.g. 400_000 for 400 kHz). Both supported adapters accept up to 1 MHz; an unsupported rate is rejected by the firmware as FW_INVALID_PARAMETER and surfaced as FirmwareError. Restores the default timing: on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). set_duty_cycle makes it exact.

  • pull_up – On-board pull-up resistor setting. Which members are accepted depends on the model, not on all thirteen being universal: firmware carries a different pull-up table per adapter, so ask dev.capabilities.i2c_pull_ups (or DeviceCapabilities.supports_i2c_pull_up) when the code has to run on both.

Returns:

This controller, to allow chaining.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the connected model’s firmware has no pull-up table row for pull_up – I2cPullUp.KOHM_3_3, I2cPullUp.KOHM_4_7 and I2cPullUp.KOHM_10_0 are Supernova-only, I2cPullUp.KOHM_4_0 is Pulsar-only. No facade call is made, which is the point: firmware validates the pull-up before bringing the bus up, so the round trip this replaces would have failed with a bare FW_INVALID_PARAMETER and left the bus down.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), or the firmware rejects the configuration (FW_INVALID_PARAMETER). Upstream i2cControllerInit reports no 0x02xx code, so bring-up never surfaces as I2cError.

async bring_up(frequency_hz: int, pull_up: I2cPullUp) → BringUpResult

Bring the bus up as a controller, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring an I2C bus up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and an I2C interface stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live interface with configure instead, and reports a refused reconfigure through the return value rather than raising.

Refusal is not hypothetical and not one-shaped: a Supernova earlier than revision C cannot reconfigure a live I2C interface at all and is refused by configure’s capability guard before anything reaches the device, while a Pulsar can reconfigure but rejects some pull-up values. Both leave the bus up and usable, so both are reported. A pull-up this model has no firmware table row for is a different thing entirely – a bad argument for this adapter, which no fallback repairs – and its CapabilityError propagates from initialize untouched.

One reported refusal is not a refused reconfigure, and it is the only one that can arrive with already_initialized=False: a pull_up asked for on a unit whose capabilities.i2c_init_pull_up_effective is False comes back settings_applied=False even on a fresh bring-up. Firmware answers SUCCESS and enables the board’s fixed network regardless, so 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 bus is up and frequency_hz did apply. Every pull-up value is reported on such a unit, I2cPullUp.KOHM_10_0 included; _init_pull_up_refusal argues why, and says what would settle it.

Parameters:
  • frequency_hz – SCL clock in Hz requested for the bus (e.g. 400_000 for 400 kHz). With the default timing, on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). set_duty_cycle makes it exact.

  • pull_up – On-board pull-up resistor setting requested for the bus. Checked against dev.capabilities.i2c_pull_ups before anything is sent, exactly as initialize checks it.

Returns:

A BringUpResult. On return the bus is up and usable; consult settings_applied before relying on frequency_hz or pull_up actually being in force, and refusal for why they are not. It is a report, not a promise: nothing here guarantees a setting the device accepted is in force, only that a refusal this SDK can see is not reported as a success.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the connected model’s firmware has no pull-up table row for pull_up. A refusal to reconfigure is reported in the result instead; this one is a bad argument, so it propagates.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On any firmware failure other than the two the fallback absorbs – an unsupported bus id (FW_BUS_NOT_SUPPORTED), for instance, or a frequency initialize itself rejects.

  • I2cError – If the pull-up resistors could not be applied during the reconfigure (FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

async configure(frequency_hz: int, pull_up: I2cPullUp) → None

Reconfigure an already-initialised bus at runtime.

The bus must have been initialised with initialize; an uninitialised bus reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError. The interface stays up – no re-initialize is needed afterwards – but the peripheral itself does not: the firmware de-initialises the I2C block, applies the new baud rate, re-initialises it, then sets the pull-up. This is therefore the only lever on the public surface that resets the I2C peripheral (and so releases the bus lines) without restarting the firmware.

Not every adapter can do this at all, and it depends on the hardware revision rather than the model. Ask dev.capabilities.i2c_runtime_reconfigurable before calling: on a Supernova whose revision is anything but C it is False, and this method raises CapabilityError before anything reaches the device. initialize is unaffected, so on such a unit the bus still comes up – it simply keeps the parameters that first bring-up chose, and the peripheral-reset lever is not reachable there.

Parameters:
  • frequency_hz – New SCL clock in Hz. Both supported adapters accept up to 1 MHz; an unsupported rate is rejected by the firmware as FW_INVALID_PARAMETER and surfaced as FirmwareError. With the default timing, on firmware 4.6.2 it runs at or below the request (550 kHz runs at 500 kHz), earlier firmware may run above it (550 kHz runs at 600 kHz). A ratio set with set_duty_cycle is kept, and makes the rate exact.

  • pull_up – New on-board pull-up resistor setting.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • CapabilityError – If the adapter’s hardware revision cannot reconfigure a live I2C interface (a Supernova whose revision is anything but C), or if the connected model’s firmware has no pull-up table row for pull_up. No facade call is made in either case, so this replaces the firmware round trip rather than following it.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus has not been initialised (status_code FW_INTERFACE_NOT_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), the hardware lacks the feature (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE – pre-empted for every Supernova revision but C, so reachable only on hardware whose gate this SDK does not model), or the firmware rejects a parameter (FW_INVALID_PARAMETER). None of these is a 0x02xx code, so none surfaces as I2cError.

  • I2cError – If the pull-up resistors could not be applied (status_code FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

async set_duty_cycle(high_pct: int) → I2cDutyCycle

Set the share of each SCL period the clock is held high.

Any ratio, 50 included, also moves the bus to an exact clock: 550 kHz runs at 549.8, not the default timing’s 600. I2C_DUTY_CYCLE_DEFAULT and initialize return to the default timing.

The frequency from initialize or configure stays; only the high:low split changes, and the ratio survives later configure calls. With a ratio the frequency is never above the request and at most about 0.3 % under it.

Values are not clamped to the I2C specification, so out-of-spec timing is available for margin testing: 30 at 100 kHz gives a tHIGH of 3.0 us, under Standard-mode’s 4.0 us.

Parameters:

high_pct – SCL high time in percent of the period: 20 to 80 in steps of 10, or I2C_DUTY_CYCLE_DEFAULT (0) for the default timing. The firmware decides what is valid.

Returns:

The settings in effect after the call.

Raises:
  • ValidationError – If high_pct does not fit a uint8. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • I2cDutyCycleRefusedError – If firmware refuses the value (FW_INVALID_PARAMETER). The setting in effect is kept, and the error’s frequency_hz and high_pct say what it is.

  • FirmwareError – If the bus is not an initialised controller (FW_INTERFACE_NOT_INITIALIZED), or the firmware predates the command (FW_UNSUPPORTED_COMMAND, 4.5.1 and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the settings (SDK_ERROR_WRONG_RESPONSE).

async set_pull_up(pull_up: I2cPullUp) → None

Change the on-board pull-up resistors for this bus.

Gated by the same hardware-revision limit as configure, so ask dev.capabilities.i2c_runtime_reconfigurable first: on a Supernova whose revision is anything but C it is False, and this method raises CapabilityError before anything reaches the device – the bus keeps the pull-ups initialize selected. pull_up is additionally checked against dev.capabilities.i2c_pull_ups, the per-model set firmware will accept.

Parameters:

pull_up – New pull-up resistor setting.

Raises:
  • CapabilityError – If the adapter’s hardware revision cannot reconfigure a live I2C interface (a Supernova whose revision is anything but C), or if the connected model’s firmware has no pull-up table row for pull_up. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the bus has not been initialised (FW_INTERFACE_NOT_INITIALIZED), the bus id is unsupported (FW_BUS_NOT_SUPPORTED), the hardware lacks the feature (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE – pre-empted for every Supernova revision but C, so reachable only on hardware whose gate this SDK does not model), or the value is rejected (FW_INVALID_PARAMETER).

  • I2cError – If the pull-up resistors could not be applied (status_code FW_I2C_PULLUP_RESISTOR_SETTING_FAILURE).

async set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware and surfaced as SystemFirmwareError. Setting 0 and then a voltage again power-cycles a DUT the adapter powers, the only way to recover a peripheral hung holding SDA low. That works only if nothing else drives the net: on a multi-adapter bench hand every other source off with use_external_voltage first, then read the rail back. The rail is shared by I2C, SPI, UART and GPIO.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as I2cError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

async use_external_voltage(*, persist: bool = False) → None

Hand the I2C rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with SPI, UART and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:

Note

Read back what the adapter measured with get_bus_voltage.

async get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

The asyncio twin of I2cController.get_bus_voltage, with the same return type and the same meaning. It is not an I2C-specific measurement: the adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and AsyncSpiController.get_bus_voltage, AsyncUart.get_bus_voltage and AsyncGpio.get_voltage return the very same number. Pair this with use_external_voltage.

Implemented over getAnalogMeasurements, which reads every rail in one transaction; this keeps the shared-rail field and discards the rest, exactly as upstream’s blocking i2cGetBusVoltage does. Use AsyncDevice.get_analog_measurements to get all six rails at once instead of one field per call.

Returns:

The measured voltage in millivolts.

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

  • ProtocolError – If the response was too short to carry the measurements, which is also how firmware that does not implement the command reports itself.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x02xx code, so this never surfaces as I2cError.

async write(address: int, data: bytes, *, subaddress: bytes = b'', non_stop: bool = False) → None

Write data to a 7-bit target address.

Parameters:
  • address – 7-bit target address (0x00–0x7F).

  • data – Payload bytes. Must not exceed the device’s maximum I2C payload (AsyncDevice.capabilities.max_i2c_payload_bytes); a larger value raises ValidationError.

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • non_stop – When True, hold the bus with a repeated START instead of issuing a STOP after the transfer.

Raises:
  • ValidationError – If address is not a 7-bit value, or data exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure (e.g. address NACK).

async read(address: int, length: int, *, subaddress: bytes = b'', non_stop: bool = False) → bytes

Read length bytes from a 7-bit target address.

Parameters:
  • address – 7-bit target address (0x00–0x7F).

  • length – Number of bytes to read. Must not exceed the device’s maximum I2C payload (AsyncDevice.capabilities.max_i2c_payload_bytes); a larger value raises ValidationError.

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • non_stop – When True, hold the bus with a repeated START instead of issuing a STOP after the transfer.

Returns:

The bytes read from the target.

Raises:
  • ValidationError – If address is not a 7-bit value, or length is invalid or exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure (e.g. address NACK).

async scan(*, include_10bit: bool = False) → I2cScanResult

Scan the bus and return the addresses that acknowledged.

Parameters:

include_10bit – When True, also probe 10-bit addresses.

Returns:

An I2cScanResult with the discovered 7-bit (and optionally 10-bit) addresses. Empty when nobody answered.

Raises:

I2C — target mode

class pycosmicsdk.I2cTarget(*, device: Device, bus: I2cBus)

Bases: object

Blocking I2C target-mode interface for one I2C bus of an open device.

Obtain one from Device.i2c_target; the constructor is package-private. Instances are cached per bus on the device, so repeated dev.i2c_target(bus=...) calls return the same interface.

In target mode the adapter answers an external controller instead of driving one, serving reads and writes out of an arena drawn from the shared bulk-staging pool – 32 KB on a Supernova, 16 KB on a Pulsar, reported by initialize and readable from arena_size. Preload it with write_memory and read back what the controller wrote with read_memory. Which of three device kinds the arena presents on the bus is the model argument’s job: a flat memory, a 24Cxx-style EEPROM, or a register map, each configured by its own follow-up method.

The accessor is per bus because i2cTargetInit takes busId as its first parameter – forced by the surface, not chosen. Note that getting this object is free and `initialize` is the act that takes the role; release is what gives it back, and a caller who is not sure should read the comment block above this class before calling either.

Three ways this differs from I3cTarget, each of which will bite a reader who assumes the two are the same shape:

  • ``write_memory``’s ``address`` is a byte offset, where I3cTarget’s is a register index the firmware multiplies by the layout’s register size. Same name, same type, different unit.

  • ``address_width`` scales nothing here. It is upstream’s memoryLayout, and it selects how many address bytes the external controller sends on the bus: 8-bit confines the bus-visible window to arena bytes 0..255, 16-bit exposes the whole arena. The name was changed deliberately, because memory_layout would collide in a reader’s head with the I3C concept that does scale.

  • The arena is bulk-only, so initialize can refuse on a host that I3cTarget.initialize would have served – see item 3 of the comment block above.

Note

Failures here surface as I2cError, not the bare FirmwareError the I3C target’s docstrings have to promise: every target-mode status firmware reports sits in the 0x02xx band both translators map to I2cError. The exceptions are FW_INTERFACE_ALREADY_INITIALIZED (0x0007, generic, so FirmwareError) and SDK_ERROR_BULK_UNAVAILABLE, which this interface pre-empts with a CapabilityError before anything is sent.

Note

One target at a time, and one role per bus – released explicitly. The arena is drawn from the device’s shared bulk-staging pool and is held while a target is armed, so an I2C target and a SPI target cannot both be up: the second answers FW_SPI_TARGET_STAGING_UNAVAILABLE. And on a single adapter an I2C target and an ordinary I2C controller cannot both be up in either order: initialize refuses while any role is up on either bus, and once it holds the role I2cController.initialize refuses in turn. Both are firmware behaviour, and both are cleared by release rather than by a power cycle – so either pairing works sequentially in one session. Mind the asymmetry when recovering: the target guard is per device while release is per bus, so freeing a two-bus adapter for a target bring-up takes a release on each. The comment block above this class has the citations, and says which part of the ordering is a reading of the guard sites rather than a bench result.

Note

I2cTargetBusEventNotification – subscribe to it through Device.subscribe – declares metadata only: event, target_address, memory_address and payload_length. Firmware appends the transferred bytes only to the I3C bus event, so they are not in this one; use payload_length to learn how much moved and read_memory to fetch them, accepting that the arena is live, so the region may already have been overwritten by the time the read lands.

Do not read the inherited ``payload``. It exists, typed bytes, and it is neither empty nor the transferred data: it is the entire raw notification frame, twelve bytes for every well-formed event, and its length is unrelated to payload_length. The I3C twin’s payload genuinely is the transferred data, which is exactly why this one misleads. The comment block above this class has the citations, and I2cTargetBusEvent – what wait_for_event returns – deliberately has no payload field so the mistake is unreachable from it.

property bus: I2cBus

The I2C bus this target answers on.

property arena_size: int | None

Capacity of this target’s arena in bytes, as the device reported it.

None until this object has brought the interface up, which is the same fail-open posture I3cTarget.write_memory takes: an adapter another process left in target mode has an arena whose size this object cannot know, and write_memory then bounds the call on the transport ceiling alone rather than refusing.

One consequence worth knowing: a bare initialize that raises FW_INTERFACE_ALREADY_INITIALIZED leaves this unset, because upstream leaves memorySize at 0 on that path. bring_up is the only single call that always populates it, which is one more reason it is the recommended entry point.

property max_transfer_bytes: int

Largest payload one write_memory or read_memory call may carry.

min(arena_size, advertised ceiling) – the pair, never upstream’s compile-time I2C_BULK_MAX_PAYLOAD, because the ceiling differs between products and a host that assumes one product’s number gets its oversized requests refused on another. 0 when the arena is unreachable on this connection, since no size is honest then.

Reading it runs the bulk negotiation if it has not run yet, which is at most one device query per interface.

Returns:

The per-call ceiling in bytes, or 0.

Raises:

DisconnectError – If the device is no longer open.

initialize(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel, require_arena_access: bool = True) → I2cTarget

Arm the adapter as an I2C target on this bus and return self.

The faithful primitive. Not idempotent: an adapter already in I2C target mode reports FW_INTERFACE_ALREADY_INITIALIZED, and an I2C interface stays initialised across a close()/reopen cycle, so a re-run hits that. Prefer bring_up unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED; use configure to change parameters of a target that is already up.

This takes the device’s I2C role, and release is what gives it back. The refusal a controller gets meanwhile is FW_I2C_ROLE_CONFLICT, and for the target direction the exclusivity is per device, not per bus: a controller up on either bus refuses this call, and clearing that takes a release on the bus holding it. The comment block above this class has the detail.

Bus-event capture is armed before the role switch, so a transaction driven the instant the target comes up is queued rather than lost, and a switch that then fails leaves nothing installed behind it: the arm is undone unless the failure is one that may still have left a target servicing the bus. _role_may_be_live names those two and says why.

Parameters:
  • address – Address the target answers to – 0x08-0x77 in 7-bit mode (the ends are the I2C reserved blocks), or 0x000-0x3FF in 10-bit mode.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends (upstream’s memoryLayout). 8-bit confines the bus-visible window to arena bytes 0..255; 16-bit exposes the whole arena. It does not scale write_memory’s address.

  • model – Which device kind the arena presents on the bus. Configure it afterwards with configure_eeprom or add_register_region; the flat-memory model needs no follow-up.

  • require_arena_access – When True (the default), refuse before arming if this host and device cannot carry arena traffic. Pass False to arm a target the host will never seed or read – a real but narrow case, and the reason the check is a default rather than a hard rule.

Returns:

This interface, to allow chaining.

Raises:
  • ValidationError – If address is outside the range its mode allows, or does not fit a uint16. No facade call is made.

  • CapabilityError – If require_arena_access is set and the vendor bulk transport cannot carry arena traffic on this connection, or if this build carries no I2C target-mode surface. The role is not taken in either case.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the adapter is already an I2C target (FW_INTERFACE_ALREADY_INITIALIZED).

  • I2cError – If the other I2C role owns the peripheral (FW_I2C_ROLE_CONFLICT) or the arena could not be drawn from the shared staging pool (FW_I2C_TARGET_STAGING_UNAVAILABLE).

configure(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel) → None

Reconfigure a live I2C target in place, keeping the arena contents.

The idempotent way to re-run configuration, in upstream’s own words, and the reason bring_up has a working fallback where the I3C target has none. It re-arms the target with a new address, width or model without discarding what is in the arena – but it does reset the register-map region table and the address pointer, so every add_register_region call has to be re-made afterwards.

Parameters:
  • address – New address the target answers to.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends. See initialize.

  • model – Which device kind the arena presents on the bus.

Raises:
  • ValidationError – If address is outside the range its mode allows. No facade call is made.

  • CapabilityError – If this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure – notably when no I2C target is up on this bus. Deliberately the base class and not I2cError: i2cTargetSetParameters documents no failure beyond the two transport errors, so nothing is known to land in the 0x02xx band here. That absence is also why bring_up’s reconfigure arm absorbs nothing.

bring_up(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel, require_arena_access: bool = True) → BringUpResult

Arm the target re-runnably, and report whether it was already up.

The composite over initialize and configure, and the recommended way to bring an I2C target up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

It is also the only single call that always leaves arena_size populated: upstream leaves memorySize at 0 on the already-initialised path, and the reconfigure this falls back to reports it.

Unlike the four controller composites this has no refused-status filter, because i2cTargetSetParameters documents no refusal status to put in one. settings_applied is therefore always True while already_initialized varies – and the varying field is the one a re-runnable script cares about. The comment block above this class argues it at length; do not add a filter out of symmetry with I2cController.bring_up.

FW_I2C_ROLE_CONFLICT is never absorbed. A result claiming the target is up while the adapter drives the bus as a controller would be a lie no caller can detect, so it propagates as I2cError.

Parameters:
  • address – Address the target answers to.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends. See initialize.

  • model – Which device kind the arena presents on the bus.

  • require_arena_access – Passed through to initialize.

Returns:

A BringUpResult. On return the target is armed; already_initialized says whether this call armed it or found it armed already.

Raises:
  • ValidationError – If address is outside the range its mode allows.

  • CapabilityError – If the arena is unreachable and require_arena_access is set, or if this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On any firmware failure other than the one the fallback absorbs.

  • I2cError – If the other I2C role owns the peripheral (FW_I2C_ROLE_CONFLICT) or the staging arena was unavailable (FW_I2C_TARGET_STAGING_UNAVAILABLE). Neither is absorbed: the first would misreport the bus role and the second leaves no arena to reconfigure.

release() → None

Release this bus’s I2C role, and the arena with it.

i2cDeinit in upstream’s terms, and the call that makes a role conflict recoverable. After it the bus holds no role, so the other role initialises normally – an in-session release, with no reboot and no fresh Device.

It releases whichever role the bus holds, not specifically a target one. Firmware dispatches on its own per-bus role variable: a bus up as a controller is torn down by this same call. The method lives here because this is where the need arises, and because it is the escape from the per-device guard described below – not because the role has to be a target’s.

The exclusivity is per device but the release is per bus, which is the asymmetry to plan around. i2cTargetInit refuses while any role is up on either bus, while i2cDeinit clears one bus. So freeing a two-bus adapter enough for a target bring-up means releasing both buses, each through its own interface:

dev.i2c_target(bus=I2cBus.A).release()
dev.i2c_target(bus=I2cBus.B).release()

A controller bring-up needs less: it tests only its own bus, so releasing that one bus is enough. The comment block above this class has the firmware citations, and flags which part of the ordering is a reading of the guard sites rather than a bench result.

What this object forgets. arena_size and the remembered model go back to None, because the arena they describe is gone – firmware’s teardown hands the shared bulk-staging pool back, which is what lets the other protocol enter target mode. They are cleared even when the call fails, since a refusal or a timeout leaves the arena’s fate unknown and None is this class’s word for unknown. max_transfer_bytes therefore falls back to the advertised ceiling, exactly as it does before a bring-up.

Event capture is torn down too – the subscription is cancelled and any thread parked in wait_for_event is woken with DisconnectError, following Uart.deinit – but only when the release succeeded. A release that failed may have left a live target whose events are still the only record that a transaction happened, and those must not be dropped.

What it does not forget is the bulk negotiation. That latch records whether this host holds the adapter’s vendor interface and what the device advertises per group; neither changes when a role is released, and the latch is shared with I2cController.

Note

Nothing is absorbed. In particular a bus holding no role answers FW_INTERFACE_NOT_INITIALIZED rather than succeeding quietly, matching Uart.deinit; a caller who wants an idempotent release catches that one code.

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

  • FirmwareError – If the bus held no role (FW_INTERFACE_NOT_INITIALIZED), the device has no such bus (FW_BUS_NOT_SUPPORTED), or the firmware predates the command (FW_UNSUPPORTED_COMMAND). All three are generic codes below every protocol band, so none surfaces as I2cError. The last is the one a caller cannot pre-empt: firmware’s version string is a compile-time constant, so a build that dispatches the command and one that does not both report the same version.

configure_eeprom(*, total_size: int, page_size: int, write_cycle_us: int, erase_cycle_us: int, busy_behavior: I2cTargetEepromBusyBehavior, erased_value: int = 0xFF, write_protect: tuple[int, int] | None = None) → None

Configure the EEPROM emulation model: page wrap, busy timing, write protect.

Valid only while the active model is EEPROM. When this object chose the model, a wrong one is refused here before anything is sent; when the model is unknown – an adapter another process left in target mode – the command is sent and firmware answers FW_I2C_TARGET_WRONG_MODEL.

The timing arguments keep upstream’s microseconds and say so in their names, deliberately breaking with wait_for_event(timeout=...)’s float seconds: these are the emulated part’s datasheet numbers, so a user copying “5 ms write cycle” from a 24Cxx datasheet types 5000.

Parameters:
  • total_size – Addressable size in bytes; must not exceed the arena. Reads and writes wrap here.

  • page_size – Page-write wrap granularity in bytes. Must be a power of two.

  • write_cycle_us – Program time in microseconds. A data write is busy for erase_cycle_us + write_cycle_us.

  • erase_cycle_us – Erase time in microseconds. An explicit erase is busy for this alone.

  • busy_behavior – Whether the target NACKs its address until done (ack-polling) or holds SCL low.

  • erased_value – Value of an erased byte, typically 0xFF. Also the fill value a write-only or reserved register region reads back.

  • write_protect – (start, end) with end exclusive, or None for no write protection. None sends the zero-length span upstream documents as disabling it, rather than a sentinel.

Raises:
  • ValidationError – If a numeric argument does not fit its field, if page_size is not a power of two, if the write-protect span is inverted, or if this object knows the active model is not EEPROM. No facade call is made.

  • CapabilityError – If this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • I2cError – If the active model is not EEPROM (FW_I2C_TARGET_WRONG_MODEL) or firmware rejects the configuration (FW_I2C_TARGET_INVALID_CONFIG).

erase(*, scope: I2cTargetEraseScope, address: int = 0) → None

Fill a page or the whole arena with the configured erased value.

Valid only in the EEPROM model, and gated the same way configure_eeprom is. The target enters its busy window for erase_cycle_us afterwards, so an external controller sees the behaviour busy_behavior selected.

Parameters:
  • scope – Erase one page or the whole arena.

  • address – For page scope, any address inside the page to erase. Ignored for chip scope.

Raises:
  • ValidationError – If address does not fit a uint16, or if this object knows the active model is not EEPROM. No facade call is made.

  • CapabilityError – If this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • I2cError – If the active model is not EEPROM (FW_I2C_TARGET_WRONG_MODEL).

add_register_region(*, start: int, end: int, access: I2cTargetRegisterAccess, width: int, auto_increment: bool = True) → None

Add one region descriptor to the REGISTER_MAP model.

“Add” rather than “set”, because upstream adds one descriptor per call, up to sixteen, and the table is cleared only by configure. Bytes outside every defined region default to read/write. The register values themselves live in the arena, so preload their defaults with write_memory.

Parameters:
  • start – Region start address, inclusive.

  • end – Region end address, exclusive. Must exceed start and must not exceed the arena.

  • access – Access policy for the span.

  • width – Register width in bytes: 1 or 2.

  • auto_increment – When True (the default) the register pointer advances through the region as the controller clocks bytes. False pins it, and a pinned region serves the region’s width bytes per access rather than one – so a 16-bit-wide pinned region still returns two bytes before wrapping to itself. Exposed as a boolean because upstream’s flags byte reserves every other bit and asks callers to send zero.

Raises:
  • ValidationError – If the span is empty or inverted, if width is neither 1 nor 2, or if this object knows the active model is not REGISTER_MAP. No facade call is made.

  • CapabilityError – If this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • I2cError – If the active model is not REGISTER_MAP (FW_I2C_TARGET_WRONG_MODEL), the sixteen-region limit is reached (FW_I2C_TARGET_REGION_TABLE_FULL), or the descriptor is rejected (FW_I2C_TARGET_INVALID_CONFIG).

write_memory(address: int, data: bytes) → None

Preload data into the target’s arena over the vendor bulk endpoint.

Seeds the bytes an external controller will read back. ``address`` is a byte offset into the arena, not a register index – the trap this interface shares a signature with and not a unit: I3cTarget scales its address by the register size and this does not.

The whole arena moves in one call on both products, because each model’s advertised ceiling equals its arena. Nothing is ever split, and the refusal says so – but unlike an SPI controller transfer you may safely split this yourself by address, because an arena write is a host-to-adapter memory write that generates no bus traffic at all.

Note

Once this returns, the arena is guaranteed bus-ready. Firmware defers its reply until the target’s DMA descriptor chain has re-armed, precisely so a controller cannot clock stale bytes after a preload, so no caller-side settle delay is ever necessary – needing one indicates a firmware regression rather than something to work around here.

Note

Blocking-only. There is deliberately no AsyncI2cTarget.write_memory – see AsyncI2cTarget.

Parameters:
  • address – Byte offset into the arena to start writing at.

  • data – Payload bytes. Must not be empty, must fit the arena from address onwards, and must not exceed max_transfer_bytes.

Raises:
  • ValidationError – If data is empty, exceeds the negotiated ceiling, or would run past the end of the arena. No facade call is made.

  • CapabilityError – If the vendor bulk transport cannot carry arena traffic on this connection – there is no HID fallback – or if this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure, notably FW_I2C_TARGET_SURPASSED_MEMORY_SIZE when the arena is smaller than this object believes.

read_memory(address: int, length: int) → bytes

Read length bytes back out of the target’s arena.

Reads what an external controller has written. address is a byte offset, exactly as in write_memory.

Note

Nothing stops the external controller overwriting the region between a bus event and this read: the arena is live while the target is armed. A read reports what the arena holds now, which is not necessarily what any one transaction put there.

Note

Blocking-only. There is deliberately no AsyncI2cTarget.read_memory – see AsyncI2cTarget.

Parameters:
  • address – Byte offset into the arena to start reading from.

  • length – Number of bytes to read. Must be at least 1, must fit the arena from address onwards, and must not exceed max_transfer_bytes.

Returns:

The bytes read out of the arena.

Raises:
  • ValidationError – If length is zero, exceeds the negotiated ceiling, or would run past the end of the arena. No facade call is made.

  • CapabilityError – If the vendor bulk transport cannot carry arena traffic on this connection, or if this build carries no I2C target-mode surface.

  • DisconnectError – If the device is no longer open.

  • I2cError – On a firmware-reported failure, notably FW_I2C_TARGET_SURPASSED_MEMORY_SIZE.

wait_for_event(*, timeout: float | None = None) → I2cTargetBusEvent

Return the oldest queued bus event, waiting for one if none is queued.

The pull path for what an external controller does to this target. Events are queued as the firmware reports them and each call pops exactly one, oldest first. For push delivery subscribe to I2cTargetBusEventNotification via Device.subscribe; for the whole queue at once use drain_events.

One completed transaction produces exactly one event: firmware emits one per transfer phase and flushes at the STOP, and an ordinary write or read is a single phase. An address-only transaction produces none, and nothing arrives while a non_stop=True transfer holds the bus, so timeout=None after such a write blocks until unrelated code issues the STOP. The event carries no bytes; read them from the arena, which costs a bulk round trip and races the controller:

event = target.wait_for_event(timeout=1.0)
data = target.read_memory(event.memory_address, event.payload_length)

There is deliberately no read_event_payload convenience: it would hide that round trip and its race behind what reads like a field access.

Note

Safe to call concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait when nothing is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:
drain_events() → list[I2cTargetBusEvent]

Return every queued bus event at once, oldest first, without waiting.

The batch counterpart of wait_for_event, and the reason not to write the loop it replaces:

while True:                              # don't
    try:
        handle(target.wait_for_event(timeout=0))
    except CosmicTimeoutError:
        break

for event in target.drain_events():      # do
    handle(event)

Order is preserved. One completed transaction is exactly one event (see wait_for_event), so the sequence is a sequence of transactions. Order still carries meaning in the one case where a controller-level operation does yield two: a non_stop transfer defers both its phases to whatever eventually issues the STOP, and they arrive together, in phase order. Either way a burst serviced between two calls comes back whole.

It also arms event capture, which is the point of having it. Capture is installed lazily: initialize arms before the role switch, so a script that brings its own target up never has a gap. A script that attaches to a target another process armed has no such moment, and until something subscribes the device has installed no notification bridge at all – so a transaction driven before the first wait_for_event is not queued, not counted, and not reported. Calling this first is the fix, and it is worth calling for its own sake anyway: it establishes a known-empty baseline, so the events that follow are attributable to what you did next:

target = dev.i2c_target(bus=I2cBus.A)   # already up elsewhere
target.drain_events()                   # arm, and discard history
drive_the_bus()
for event in target.drain_events():
    ...

There is deliberately no ``arm_events()``. An arming call with no observable result fails the same silent way the missing arm does – nothing tells you that you forgot it – so every arm on this surface is a side effect of a call that does something a caller can see. release is the mirror: it tears capture down on success and does not re-arm, because no bus event can arrive for a role the device no longer holds. After a release, this method (or the next initialize) arms a fresh queue.

Note

Safe to call concurrently, and alongside wait_for_event, with the same competing-consumer semantics: each event is delivered once, to exactly one caller.

Returns:

The queued events, oldest first; an empty list when nothing is queued. Never raises CosmicTimeoutError – an empty bus is an empty list, not an exception.

Raises:

DisconnectError – If the device is not open, or if it closed or the role was released while events were queued and none had been drained yet. Whatever had been drained is returned instead of being lost to the exception, since a bus event is the only record that a transaction happened; the next call raises.

class pycosmicsdk.AsyncI2cTarget(*, device: AsyncDevice, bus: I2cBus)

Bases: object

Asyncio I2C target-mode interface for one I2C bus of an open device.

The asyncio twin of I2cTarget, returned by AsyncDevice.i2c_target; the constructor is package-private. Argument validation matches the blocking interface; commands are awaited and correlated through the device’s engine rather than serialized under a lock.

Note

The arena is deliberately missing here. There is no write_memory and no read_memory, and that is a decision rather than an omission. Upstream’s async write calls sendPayload inside the submit, with a 2000 ms budget, so what looks like a non-blocking submit blocks the calling thread for the whole transfer – roughly 25 ms for a 16 KB preload on upstream’s own throughput estimate. The read is worse: it cannot be expressed as one submit at all, because the reply only announces the length and the payload is then collected by a second, blocking call. On the asyncio path either one stalls the event loop thread, which is not something a library advertising an asyncio API may do quietly.

So use I2cTarget.write_memory and I2cTarget.read_memory – from a thread, or through loop.run_in_executor if the surrounding code is async. AsyncSpiController carries the same asymmetry for the SPI bulk pair and for the same reason, and docs_dev/designs/async-bulk-transfer-shape.md has the analysis.

Everything that is a genuine non-blocking submit is here: arming, reconfiguring, the bring-up composite, and all three emulation-model commands. arena_size is here too, decoded off the init response packet – upstream’s async wrapper drops the value, but it is on the wire, and an async surface whose arena size were permanently None would be strictly worse at the one number a caller needs in order to size a preload.

property bus: I2cBus

The I2C bus this target answers on.

property arena_size: int | None

Capacity of this target’s arena in bytes, as the device reported it.

None until this object has armed or reconfigured the interface, matching I2cTarget.arena_size. Decoded from the init response’s {uint8 busId; uint32 memorySize} parameters, because CosmicAsyncSDK has no out-parameter to carry it.

async initialize(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel, require_arena_access: bool = True) → AsyncI2cTarget

Arm the adapter as an I2C target on this bus and return self.

The asyncio twin of I2cTarget.initialize, with the same guards, the same non-idempotence and the same warning: this takes the device’s I2C role and this SDK cannot give it back. See the comment block above I2cTarget.

Bus-event capture is armed before the role switch and unwound if that switch then fails, exactly as on the blocking twin; _role_may_be_live names the two failures that keep it.

Parameters:
  • address – Address the target answers to – 0x08-0x77 in 7-bit mode, 0x000-0x3FF in 10-bit mode.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends. It does not scale an arena offset.

  • model – Which device kind the arena presents on the bus.

  • require_arena_access – When True (the default), refuse before arming if this host holds no vendor bulk transport – the only half of that question the async facade can ask.

Returns:

This interface, to allow chaining.

Raises:
  • ValidationError – If address is outside the range its mode allows. No submit is made.

  • CapabilityError – If require_arena_access is set and this host holds no vendor bulk transport. The role is not taken.

  • DisconnectError – If the device is no longer open.

  • ProtocolError – If the response is too short to carry the arena size.

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

  • FirmwareError – If the adapter is already an I2C target (FW_INTERFACE_ALREADY_INITIALIZED).

  • I2cError – If the other I2C role owns the peripheral (FW_I2C_ROLE_CONFLICT) or the arena could not be drawn from the shared staging pool (FW_I2C_TARGET_STAGING_UNAVAILABLE).

async configure(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel) → None

Reconfigure a live I2C target in place, keeping the arena contents.

The asyncio twin of I2cTarget.configure. Idempotent, and it resets the register-map region table and the address pointer, so every add_register_region call has to be re-made afterwards.

Parameters:
  • address – New address the target answers to.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends.

  • model – Which device kind the arena presents on the bus.

Raises:
  • ValidationError – If address is outside the range its mode allows. No submit is made.

  • DisconnectError – If the device is no longer open.

  • ProtocolError – If the response is too short to carry the arena size.

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

  • FirmwareError – On a firmware-reported failure – notably when no I2C target is up on this bus. The base class and not I2cError, for the reason I2cTarget.configure gives: i2cTargetSetParameters documents no failure beyond the two transport errors.

async bring_up(*, address: int, address_mode: I2cTargetAddressMode, address_width: I2cTargetMemoryLayout, model: I2cTargetModel, require_arena_access: bool = True) → BringUpResult

Arm the target re-runnably, and report whether it was already up.

The asyncio twin of I2cTarget.bring_up, with the same live fallback arm, the same absent refused-status filter, and the same rule that FW_I2C_ROLE_CONFLICT is never absorbed.

Parameters:
  • address – Address the target answers to.

  • address_mode – 7-bit or 10-bit addressing.

  • address_width – How many address bytes the external controller sends.

  • model – Which device kind the arena presents on the bus.

  • require_arena_access – Passed through to initialize.

Returns:

A BringUpResult. settings_applied is always True; already_initialized says whether this call armed the target or found it armed already.

Raises:
  • ValidationError – If address is outside the range its mode allows.

  • CapabilityError – If this host holds no vendor bulk transport and require_arena_access is set.

  • DisconnectError – If the device is no longer open.

  • ProtocolError – If a response is too short to carry the arena size.

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

  • FirmwareError – On any firmware failure other than the one the fallback absorbs.

  • I2cError – If the other I2C role owns the peripheral (FW_I2C_ROLE_CONFLICT) or the staging arena was unavailable (FW_I2C_TARGET_STAGING_UNAVAILABLE).

async release() → None

Release this bus’s I2C role, and the arena with it.

The asyncio twin of I2cTarget.release, with the same semantics: the command releases whichever role the bus holds, arena_size and the remembered model go back to None on every path, event capture is cancelled only on success, and the shared bulk negotiation is left alone. Read that method for the per-device-guard-versus-per-bus-release asymmetry – the reason freeing a two-bus adapter for a target bring-up takes two calls.

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

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

  • FirmwareError – If the bus held no role (FW_INTERFACE_NOT_INITIALIZED), the device has no such bus (FW_BUS_NOT_SUPPORTED), or the firmware predates the command (FW_UNSUPPORTED_COMMAND). None is absorbed, and all three are generic codes rather than I2cError.

async configure_eeprom(*, total_size: int, page_size: int, write_cycle_us: int, erase_cycle_us: int, busy_behavior: I2cTargetEepromBusyBehavior, erased_value: int = 0xFF, write_protect: tuple[int, int] | None = None) → None

Configure the EEPROM emulation model.

The asyncio twin of I2cTarget.configure_eeprom, with the same model pre-guard and the same fail-open behaviour when the model is unknown.

Parameters:
  • total_size – Addressable size in bytes; must not exceed the arena.

  • page_size – Page-write wrap granularity. Must be a power of two.

  • write_cycle_us – Program time in microseconds.

  • erase_cycle_us – Erase time in microseconds.

  • busy_behavior – NACK the address until done, or hold SCL low.

  • erased_value – Value of an erased byte, typically 0xFF.

  • write_protect – (start, end) with end exclusive, or None.

Raises:
  • ValidationError – If a numeric argument does not fit its field, if page_size is not a power of two, if the write-protect span is inverted, or if this object knows the active model is not EEPROM. No submit is made.

  • DisconnectError – If the device is no longer open.

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

  • I2cError – If the active model is not EEPROM (FW_I2C_TARGET_WRONG_MODEL) or firmware rejects the configuration (FW_I2C_TARGET_INVALID_CONFIG).

async erase(*, scope: I2cTargetEraseScope, address: int = 0) → None

Fill a page or the whole arena with the configured erased value.

The asyncio twin of I2cTarget.erase.

Note

The submit returns as soon as firmware accepts the command, but a chip-scope erase holds the emulated part busy for erase_cycle_us, during which an external controller sees whatever busy_behavior selected. A long erase cycle is therefore visible to the bus rather than to this coroutine.

Parameters:
  • scope – Erase one page or the whole arena.

  • address – For page scope, any address inside the page to erase.

Raises:
  • ValidationError – If address does not fit a uint16, or if this object knows the active model is not EEPROM. No submit is made.

  • DisconnectError – If the device is no longer open.

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

  • I2cError – If the active model is not EEPROM (FW_I2C_TARGET_WRONG_MODEL).

async add_register_region(*, start: int, end: int, access: I2cTargetRegisterAccess, width: int, auto_increment: bool = True) → None

Add one region descriptor to the REGISTER_MAP model.

The asyncio twin of I2cTarget.add_register_region. One descriptor per call, up to sixteen, and configure is the only thing that clears the table.

Parameters:
  • start – Region start address, inclusive.

  • end – Region end address, exclusive.

  • access – Access policy for the span.

  • width – Register width in bytes: 1 or 2.

  • auto_increment – When True the register pointer advances through the region; False pins it, and a pinned region still serves width bytes per access.

Raises:
  • ValidationError – If the span is empty or inverted, if width is neither 1 nor 2, or if this object knows the active model is not REGISTER_MAP. No submit is made.

  • DisconnectError – If the device is no longer open.

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

  • I2cError – If the active model is not REGISTER_MAP (FW_I2C_TARGET_WRONG_MODEL), the sixteen-region limit is reached (FW_I2C_TARGET_REGION_TABLE_FULL), or the descriptor is rejected (FW_I2C_TARGET_INVALID_CONFIG).

async wait_for_event(*, timeout: float | None = None) → I2cTargetBusEvent

Return the oldest queued bus event, awaiting one if none is queued.

The asyncio twin of I2cTarget.wait_for_event, and the half of the target surface the async side is genuinely good at: observation costs no payload transfer, so nothing here blocks the event loop. Events are queued as the firmware reports them and each call pops exactly one, oldest first; the queue is never trimmed. For push delivery, subscribe to I2cTargetBusEventNotification via AsyncDevice.subscribe, or iterate AsyncDevice.iter_notifications, whose async for body runs on the event loop rather than on the dispatcher thread.

For the whole queue at once, await `drain_events` rather than looping this with timeout=0 and catching CosmicTimeoutError; it is also the arm point for a target this process did not initialize.

The bytes are not here, and this class cannot fetch them. The event carries metadata only by the device’s design – and the notification’s inherited payload is the raw frame rather than the data, which is why I2cTargetBusEvent carries no such field. This interface has no arena at all either: see the class docstring for why read_memory is deliberately absent from the asyncio surface. So an async target reports that a phase completed, at which arena offset, and how many bytes it moved; reading the bytes back is a blocking I2cTarget’s job. memory_address is real here, unlike the SPI twin’s, so it is the offset to hand that blocking handle.

Note

One completed transaction produces exactly one event: firmware emits one per transfer phase and flushes them at the STOP, and an ordinary transaction is one phase. The trap is a non_stop transfer: it emits nothing at all while it holds the bus, so an await wait_for_event() after one hangs until unrelated code issues the STOP. I2cTarget.wait_for_event has the measurement.

Note

Safe to await concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait when nothing is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:
async drain_events() → list[I2cTargetBusEvent]

Return every queued bus event at once, oldest first, without awaiting.

The asyncio twin of I2cTarget.drain_events, with the same contract: it replaces a wait_for_event(timeout=0) loop that has to catch CosmicTimeoutError to terminate, it preserves order – one completed transaction is one event, so ordinarily the sequence is a sequence of transactions, and the exception is a non_stop transfer’s two phases arriving together at the STOP that releases them – and it arms event capture, which is the arm point a script attaching to a target another process brought up otherwise does not have. I2cTarget.drain_events argues why there is no arm_events() and why release does not re-arm.

It is a coroutine although nothing in it awaits: arming captures the running loop, so this must be called from one, and a sync method that silently required a loop would be the worse of the two shapes.

Note

Safe to await concurrently, and alongside wait_for_event, with the same competing-consumer semantics: each event is delivered once, to exactly one caller.

Returns:

The queued events, oldest first; an empty list when nothing is queued. Never raises CosmicTimeoutError.

Raises:

DisconnectError – If the device is not open, or if it closed or the role was released while events were queued and none had been drained yet; whatever had been drained is returned rather than lost, and the next call raises.

I3C — controller

class pycosmicsdk.I3cController(*, device: Device)

Bases: object

Blocking I3C controller for an open device.

Obtain one from Device.i3c; the constructor is package-private. The interface is cached on the device, so repeated dev.i3c() calls return the same controller.

property ccc: I3cCcc

The common-command-code sub-interface, built on first access.

initialize(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → I3cController

Bring the bus up as an I3C controller and return self for chaining.

Not idempotent: an already-initialised bus reports FW_INTERFACE_ALREADY_INITIALIZED. The bus stays initialised across reconnects, so a re-run hits this unless the device is reset first. Use configure to change parameters of an already-initialised bus.

Parameters:
  • push_pull_rate – Push-pull clock rate for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate used for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate used for legacy I2C targets on the bus.

  • drive_strength – Output driver strength of the push-pull stage.

Returns:

This controller, to allow chaining.

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

  • FirmwareError – If the bus is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED) or the firmware rejects the configuration (FW_INVALID_PARAMETER). Those are the only two codes upstream i3cControllerInit reports and neither is in the 0x06xx band, so bring-up never surfaces as I3cError.

bring_up(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → BringUpResult

Bring the bus up as an I3C controller, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring an I3C bus up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the bus stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live bus with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Three I3C-specific details are worth knowing:

  • configure reaches every parameter initialize takes, so the fallback path requests exactly what the fresh path would have.

  • No capability guard gates I3C reconfiguration today, so in practice the fallback succeeds and settings_applied is False only if firmware rejects a rate it accepted at first bring-up. The CapabilityError arm below is not dead weight: it is what keeps this contract protocol-independent without an edit here if a guard lands.

  • Bringing the controller up is not the same as enumerating the bus. init_bus still has to run afterwards, exactly as it does after a bare initialize, and this method neither calls it nor reports on it.

A refusal raised by initialize itself is a different thing from a refused reconfigure and propagates untouched, whatever its type: nothing reached the bus, so the interface is not up, and the first half of bring_up’s contract – on return the interface is usable – cannot be kept by a return value. Only the already-up path can report.

Parameters:
  • push_pull_rate – Push-pull clock rate requested for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate requested for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate requested for legacy I2C targets on the bus.

  • drive_strength – Output driver strength requested for the push-pull stage.

Returns:

A BringUpResult. On return the bus is up and usable; consult settings_applied before relying on the requested rates actually being in force.

Raises:
configure(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → None

Reconfigure an already-initialised bus at runtime.

Updates controller parameters without tearing the bus down. The bus must have been initialised with initialize; an uninitialised bus reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • push_pull_rate – Push-pull clock rate for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate used for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate used for legacy I2C targets on the bus.

  • drive_strength – Output driver strength of the push-pull stage.

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

  • FirmwareError – If the bus has not been initialised (status_code FW_INTERFACE_NOT_INITIALIZED) or the firmware rejects the configuration (FW_INVALID_COMMAND, FW_INVALID_PARAMETER). None of the three is in the 0x06xx band, so this never surfaces as I3cError.

enable_i2c_clock_stretching(frequency_hz: int = 100_000) → I2cClockStretching

Let legacy I2C transfers wait for a target that stretches SCL.

The I3C engine keeps clocking while a target holds SCL low, so a target that stretches the clock (SMBus and MCTP devices, for example) loses bits. With stretching on, legacy_i2c_write and legacy_i2c_read are clocked in software and wait for the target. initialize and configure turn it off.

Example

With i3c = dev.i3c() and rates holding the three bus rates:

i3c.bring_up(**rates)
i3c.enable_i2c_clock_stretching(100_000)
i3c.legacy_i2c_write(0x50, bytes([0x00, 0x01]))
Parameters:

frequency_hz – SCL rate in Hz, 10 kHz to 1 MHz; the firmware refuses anything else. The rate it runs at is at or below the request, and the ceiling depends on the wiring: 746 kHz on one bench, 385 kHz on another. Read it from the return value.

Returns:

enabled true and the actual SCL rate.

Return type:

The state in effect

Raises:
  • ValidationError – If frequency_hz does not fit a uint32. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If frequency_hz is out of range (FW_INVALID_PARAMETER), the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED), or the firmware is too old for this feature (FW_UNSUPPORTED_COMMAND, 4.5.x and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the state.

Note

While on, a stretch longer than the SMBus 35 ms timeout ends the legacy transfer with FW_BUS_TIMEOUT, and SDR/HDR/CCC calls, init_bus and reset_bus raise I3cError (FW_I3C_I2C_CLOCK_STRETCHING_ACTIVE) until disable_i2c_clock_stretching.

disable_i2c_clock_stretching() → I2cClockStretching

Return legacy I2C transfers to the I3C engine, which cannot wait for a stretched clock.

Also what makes SDR, HDR and CCC calls work again after enable_i2c_clock_stretching.

Returns:

enabled false and frequency_hz 0.

Return type:

The state in effect

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

  • FirmwareError – If the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED) or the firmware is too old for this feature (FW_UNSUPPORTED_COMMAND, 4.5.x and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the state.

hold_scl_low(duration_us: int) → int

Hold SCL low for duration_us, then release it (SMBus interface reset, T2wrst).

SDA is released for the whole hold. Works with I2C clock stretching on or off; the mode is left as it was. SCL has no on-board pull-up: an external one is needed for SCL to rise after the release. The call waits out the hold: its response timeout is extended by duration_us plus 136 ms.

Example

With i3c = dev.i3c() brought up as a controller:

held_us = i3c.hold_scl_low(35_000)
Parameters:

duration_us – Hold time in microseconds, 1 to 1 000 000; the firmware refuses anything else.

Returns:

How long SCL was held, in microseconds, as the firmware measured it.

Raises:
  • ValidationError – If duration_us does not fit a uint32. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If duration_us is 0 or above 1 s (FW_INVALID_PARAMETER), the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED), SCL was still low 35 ms after the release – a target holds it or SCL has no pull-up (FW_BUS_TIMEOUT), or the firmware predates the command (FW_UNSUPPORTED_COMMAND).

  • ProtocolError – If a SUCCESS reply is too short to carry the hold time.

set_sda_pull_up(mode: I3cSdaPullUp) → I3cSdaPullUp

Set the 1 kOhm pull-up on the I3C port’s SDA line.

AUTO (default, and every earlier firmware’s behaviour) lets the I3C engine drive it; OFF and ON disconnect or connect it while the I2C clock-stretching mode is on (the I3C engine drives it otherwise). SCL has no on-board pull-up. The setting survives I3C re-inits until changed or the adapter resets.

Example

With i3c = dev.i3c():

i3c.set_sda_pull_up(I3cSdaPullUp.ON)
Parameters:

mode – The setting to apply.

Returns:

The stored setting after the call. It governs the pull-up only while clock stretching is on; with it off the I3C engine drives the pull-up whatever this returns.

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

  • FirmwareError – If the firmware refuses the value (FW_INVALID_PARAMETER) or predates the command (FW_UNSUPPORTED_COMMAND).

  • ProtocolError – If a SUCCESS reply is too short to carry the setting.

set_voltage(voltage_mv: int) → None

Set the target voltage of the I3C bus.

The voltage alone selects the port: 800-1199 mV drives the low-voltage connector, 1200-3300 mV the high-voltage one. Unlike I2C and UART, the firmware offers no way to persist the setting.

Parameters:

voltage_mv – Target voltage in millivolts: 0 switches VTARG off and powers down the devices on the bus, otherwise 800-3300 mV.

Raises:
  • ValidationError – If voltage_mv is neither 0 nor within 800-3300 mV.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as I3cError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

use_external_voltage(*, persist: bool = False) → None

Hand the I3C rail over to an externally supplied voltage.

Tells the adapter that the I3C bus voltage comes from outside, so it stops driving VTARG itself and measures the supplied rail instead. Exactly one of the two I3C ports must be powered when this runs: with neither powered the firmware reports FW_I3C_PORTS_NOT_POWERED, with both FW_BOTH_I3C_PORTS_POWERED. Read back what it measured with get_bus_voltage. The HV reading saturates at ~3299 mV, so an HV supply above 3.3 V is accepted, not refused: keep it at or below 3.3 V.

Parameters:

persist – Ignored by the firmware: the I3C rails come from DACs that store nothing, so the switch lasts for this session only.

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

  • SystemFirmwareError – If the firmware rejects the switch. Firmware classifies in a fixed order and reports only the first match: FW_BOTH_I3C_PORTS_POWERED (0x0105), then FW_I3C_PORTS_NOT_POWERED (0x0104), then FW_VOLTAGE_OUT_OF_RANGE_ON_I3C_HV (0x0107), then FW_VOLTAGE_OUT_OF_RANGE_ON_I3C_LV (0x0106) — so a board with both ports powered never reports an out-of-range code. 0x0107 means the HV port read below 1200 mV: the upper-bound half of firmware’s HV test reads the low-voltage rail by mistake (sys_mgr.c:836), so an HV port driven above 3300 mV is accepted as valid rather than reported. All four are 0x01xx system-band codes, so they do not surface as I3cError.

get_bus_voltage() → tuple[int, int]

Read the externally supplied voltage measured on each I3C rail.

The I3C interface has two independent rails, and this reports the VTARG measurement for both: high-voltage (1200-3300 mV) and low-voltage (800-1199 mV). Only the port that is actually powered reads back a meaningful value, so pair this with use_external_voltage.

Unlike the four multifunction readers — I2cController.get_bus_voltage, SpiController.get_bus_voltage, Uart.get_bus_voltage and Gpio.get_voltage, which are four names for one shared-rail reading — this reports rails that are genuinely I3C’s own. Upstream reads all of them from the same getAnalogMeasurements command but takes its vmeasVtargI3cHV_mV and vmeasVtargI3cLV_mV fields rather than the shared vmeasVtargI2cSpiUartGpio_mV one (CosmicSDK.cpp).

Returns:

(hv_mv, lv_mv) — the high- and low-voltage rail measurements in millivolts.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x06xx code, so this never surfaces as I3cError.

init_bus(*, targets: Sequence[I3cTargetInfo] | None = None) → list[I3cTargetInfo]

Run dynamic address assignment and return the resulting device table.

Issues RSTDAA followed by ENTDAA. Passing targets pre-seeds the controller’s table so known devices keep a deterministic dynamic address; anything else on the bus is assigned by the firmware.

Parameters:

targets – Known targets to seed the table with, or None for pure discovery.

Returns:

Every target the controller now has in its device table.

Raises:
reset_bus() → None

Issue RSTDAA and clear the controller’s device table.

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

  • FirmwareError – On a firmware-reported failure. Upstream i3cResetBus reports only FW_BUS_TIMEOUT, FW_INTERFACE_NOT_INITIALIZED and FW_INVALID_COMMAND, none in the 0x06xx band, so this never surfaces as I3cError.

get_device_table() → list[I3cTargetInfo]

Return the controller’s cached table of known targets.

A pure query of controller state; no traffic is put on the bus.

Returns:

Every target currently in the controller’s device table.

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

  • FirmwareError – On a firmware-reported failure. A controller-state query reports no 0x06xx code, so this never surfaces as I3cError.

set_target_configuration(address: int, configuration: I3cTargetConfiguration) → None

Update the stored configuration byte of a known target.

The target must already be in the controller’s device table, so run init_bus or get_device_table first.

Parameters:
  • address – Dynamic address of the target to reconfigure (0x00-0x7F).

  • configuration – Behaviour flags to store for the target; packed into the firmware’s configuration byte.

Raises:
property max_transfer_bytes: int

Largest payload one SDR or legacy-I2C transfer can carry, on this connection.

1024 – capabilities.max_i3c_payload_bytes, the ordinary route’s ceiling – unless this build, this host and this device can all serve I3C controller bulk, in which case it is the ceiling the device advertised for its I3C command group, clamped to I3C_BULK_MAX_PAYLOAD_BYTES.

On a Supernova that is 32767, and the odd number is the answer rather than a rounding error: the I3C peripheral’s frame-length field is 15 bits, so 0x7FFF is the longest payload one frame can describe – one byte short of the 32768-byte staging arena the device advertises and that I2C and SPI bulk transfers do reach in full. Nobody guesses that number, which is the reason this property exists. On a Pulsar it is always 1024: the part has no I3C peripheral and advertises no I3C group, so no firmware update moves it.

It is exactly the ceiling the transfer methods enforce, taken from the same _select_transport call they make, so this and their refusals cannot disagree.

Note

Two things this one number does not cover, both of them real.

  • A bulk *write* shares its frame with the register sub-address, so its usable payload is max_transfer_bytes - len(subaddress) while a read takes no deduction at all. The deduction is bulk-only – on the ordinary route the sub-address rides outside the payload – and it cannot be folded in here, because a property takes no arguments and the sub-address is per call. Read transport to find out which route is in force; sdr_write states the arithmetic, and its refusal repeats it.

  • HDR-DDR is capped lower, on *both* routes, and the advertised group ceiling does not apply to it at all. Sending it this number would not cost a refused transfer, it would cost the caller their bus until re-enumeration – which is why that limit gets its own accessor, max_hdr_ddr_transfer_bytes, rather than a sentence here. A single number per controller genuinely cannot describe SDR and DDR both; these are the two numbers.

Reading it runs the bulk negotiation if it has not run yet, which is at most one device query per connection.

Returns:

The per-call ceiling in bytes for sdr_write (before any sub-address deduction), sdr_read, legacy_i2c_write and legacy_i2c_read.

Raises:

DisconnectError – If the device is no longer open.

property max_hdr_ddr_transfer_bytes: int

Largest payload one hdr_ddr_write or hdr_ddr_read may carry.

I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024), on either transport and on every connection – so unlike max_transfer_bytes this needs no negotiation, reads no capability, and produces no device traffic.

It is a separate property because the number is separate in kind, not just in value. The cap is bus safety rather than buffer size: upstream measures DDR reliable to 1024 bytes and finds that at 2048 and above the target stops answering its dynamic address and stays that way until re-enumeration. So the advertised I3C group ceiling – 32767 on a Supernova, which max_transfer_bytes reports and SDR really does reach – does not apply here at all, and a caller who sized a DDR transfer from it would lose their bus rather than get an error. The capability reply has one uint16_t per group and nowhere to carry this second number, which is precisely why the SDK has to.

The practical consequence, stated because a caller will expect otherwise: HDR-DDR gains nothing from the bulk route today. The ordinary route already reaches 1024. The bulk entry points exist so the route covers every mode and lifting the cap later is not a new API.

Returns:

The per-call ceiling in bytes for the HDR-DDR pair. A constant.

property transport: I3cTransport

Which transport the six private-transfer methods use on this connection.

The answer to “did my transfer go over the fast path”, askable before the transfer rather than after it. It is a stable property of the connection, not a record of the last call: the negotiation is latched, and transport selection is otherwise a pure function of that latch, this build’s bindings and the transport= argument.

Reading it runs the negotiation if it has not run yet, which is the only device traffic this property can cause and happens at most once per connection.

It answers I3cTransport.HID for four independent reasons, and they need four different responses: this build binds no I3C bulk command, this host never claimed the adapter’s vendor USB interface, this device’s firmware advertises no I3C bulk, or the adapter is a Pulsar and has no I3C hardware at all. The second is the common one in practice. Note that HID here is not inconsistent with capabilities.supports_i3c_bulk() being True: that reports the device’s answer, and on a build whose extension predates the six bindings the two legitimately disagree. Read guard_i3c_bulk’s message, or capabilities.bulk, to tell them apart.

Note

BULK here does not mean every mode got faster. HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD_BYTES on both routes and gains nothing; legacy I2C is bounded by its own bus rate. SDR is the mode with a real gain.

Returns:

I3cTransport.BULK or I3cTransport.HID – never I3cTransport.AUTO.

Raises:

DisconnectError – If the device is no longer open.

legacy_i2c_write(address: int, data: bytes, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.NONE, transport: I3cTransport = I3cTransport.AUTO) → None

Write data to a legacy I2C target on the I3C bus.

This is how a script reaches an I2C or SMBus device through the Supernova’s I3C port, alongside I3C targets and without moving to the I2C port. The rate is i2c_open_drain_rate from bring_up.

A target that stretches SCL needs enable_i2c_clock_stretching first: the I3C engine itself cannot wait for a stretched clock.

Note

The bulk route works here and buys less than it does for SDR. A legacy I2C target on an I3C bus is bounded by its own open-drain bus rate long before the transport matters, so a larger single call saves USB round trips rather than bus time. See §”Transport negotiation”.

Parameters:
  • address – 7-bit target address (0x00-0x7F).

  • data – Payload bytes. Must not exceed the active transport’s ceiling – Device.capabilities.max_i3c_payload_bytes on the ordinary route, or the advertised I3C bulk ceiling minus len(subaddress) on the bulk one, since the sub-address shares the frame. A larger value raises ValidationError; it is never split.

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame.

  • transport – Which host transport carries the payload. I3cTransport.AUTO, the default, takes the bulk plane when this build, this host and this device can all serve it and the ordinary route otherwise. I3cTransport.BULK demands it and raises CapabilityError when it is unavailable.

Raises:
legacy_i2c_read(address: int, length: int, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.NONE, transport: I3cTransport = I3cTransport.AUTO) → bytes

Read length bytes from a legacy I2C target on the I3C bus.

A target that stretches SCL needs enable_i2c_clock_stretching first: the I3C engine itself cannot wait for a stretched clock.

Note

The bulk route works here and buys less than it does for SDR, for the reason legacy_i2c_write gives.

Parameters:
  • address – 7-bit target address (0x00-0x7F).

  • length – Number of bytes to read. Must not exceed the active transport’s ceiling – Device.capabilities.max_i3c_payload_bytes on the ordinary route, or the advertised I3C bulk ceiling on the bulk one. Unlike a write, a read takes no deduction for subaddress. A larger value raises ValidationError; the read is never split.

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame.

  • transport – Which host transport carries the payload. I3cTransport.AUTO, the default, takes the bulk plane when this build, this host and this device can all serve it and the ordinary route otherwise. I3cTransport.BULK demands it and raises CapabilityError when it is unavailable.

Returns:

The bytes read from the target. On the bulk route the buffer is sized to the length the device announced, which may be shorter than length; treat a short result as a failed transfer rather than retrying, since the stream has already moved on.

Raises:
sdr_write(address: int, data: bytes, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.START_WITH_7E, transport: I3cTransport = I3cTransport.AUTO) → None

Write data to an I3C target in SDR mode.

Note

SDR is the mode the bulk route is worth taking for. The ordinary route carries its bytes inside the request and so cannot exceed Device.capabilities.max_i3c_payload_bytes (1024), while the bulk route reaches whatever the device advertises for the I3C command group – 32767 bytes on a Supernova. See §”Transport negotiation” for why the other two modes gain less or nothing.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • data – Payload bytes. Must not exceed the active transport’s ceiling – Device.capabilities.max_i3c_payload_bytes on the ordinary route, or the advertised I3C bulk ceiling minus len(subaddress) on the bulk one, since the register sub-address shares the frame with the payload on a write. A larger value raises ValidationError; it is never split.

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame. The default opens the frame with the broadcast address, matching the firmware default.

  • transport – Which host transport carries the payload. I3cTransport.AUTO, the default, takes the bulk plane when this build, this host and this device can all serve it and the ordinary route otherwise. I3cTransport.BULK demands it and raises CapabilityError when it is unavailable.

Raises:
  • ValidationError – If address is not a 7-bit value, or data exceeds the active transport’s ceiling.

  • CapabilityError – If transport is I3cTransport.BULK and the bulk transport is unavailable. The message names which of the four gaps applies – this build’s bindings, this host’s driver, this device’s firmware, or a model with no I3C hardware. No facade call is made.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. address NACK).

Note

Blocking-only. AsyncI3cController.sdr_write takes no transport argument and always uses the ordinary route – see AsyncI3cController.

sdr_read(address: int, length: int, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.START_WITH_7E, transport: I3cTransport = I3cTransport.AUTO) → bytes

Read length bytes from an I3C target in SDR mode.

Note

SDR is the mode the bulk route is worth taking for, on the read side as on the write side – and a read takes no deduction for its sub-address, so it reaches the full advertised ceiling.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • length – Number of bytes to read. Must not exceed the active transport’s ceiling – Device.capabilities.max_i3c_payload_bytes on the ordinary route, or the advertised I3C bulk ceiling on the bulk one. A larger value raises ValidationError; the read is never split, because continuing one requires re-issuing the target’s own register address and this SDK does not know that encoding.

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame. The default opens the frame with the broadcast address, matching the firmware default.

  • transport – Which host transport carries the payload. I3cTransport.AUTO, the default, takes the bulk plane when this build, this host and this device can all serve it and the ordinary route otherwise. I3cTransport.BULK demands it and raises CapabilityError when it is unavailable.

Returns:

The bytes read from the target. On the bulk route the buffer is sized to the length the device announced, which may be shorter than length; treat a short result as a failed transfer rather than retrying, since the bulk stream carries no framing to resynchronise against.

Raises:

Note

Blocking-only, like sdr_write.

hdr_ddr_write(address: int, command: int, data: bytes, *, flags: I3cTransferFlags = I3cTransferFlags.NONE, transport: I3cTransport = I3cTransport.AUTO) → None

Write data to an I3C target in HDR-DDR mode.

The bus enters HDR-DDR automatically and leaves it again with an HDR exit pattern unless flags carries SKIP_EXIT_PATTERN.

Warning

HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on both transports, and the cap protects the bus rather than a buffer: above roughly 2048 bytes the target stops answering its dynamic address until re-enumeration. The bulk ceiling the device advertises for the I3C group does not apply here, so transport changes where the bytes travel and nothing else.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • command – HDR-DDR write command code, 0x00-0xFF. Bit 7 is the I3C direction convention (clear for a write); the adapter does not enforce it, so the whole byte is accepted.

  • data – Payload bytes; the length must be a multiple of two, because DDR moves 16-bit words and an odd-length frame cannot be expressed. Must not exceed I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on either transport.

  • flags – Transfer flags for the frame. Only NONE and SKIP_EXIT_PATTERN are meaningful in HDR-DDR.

  • transport – Which host transport carries the payload. Accepted for symmetry with the other two modes and with no effect on the ceiling, per the warning above. I3cTransport.BULK still raises CapabilityError when the bulk transport is unavailable, which makes it usable as a benchmark control.

Raises:
hdr_ddr_read(address: int, command: int, length: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE, transport: I3cTransport = I3cTransport.AUTO) → bytes

Read length bytes from an I3C target in HDR-DDR mode.

The bus enters HDR-DDR automatically and leaves it again with an HDR exit pattern unless flags carries SKIP_EXIT_PATTERN.

Warning

HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on both transports, and the cap protects the bus rather than a buffer: above roughly 2048 bytes the target stops answering its dynamic address until re-enumeration. The bulk ceiling the device advertises for the I3C group does not apply here, so transport changes where the bytes travel and nothing else.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • command – HDR-DDR read command code, 0x80-0xFF (bit 7 set marks a read). The bound is this SDK’s, not the device’s.

  • length – Number of bytes to read; must be a multiple of two, because DDR moves 16-bit words. Must not exceed I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on either transport.

  • flags – Transfer flags for the frame. Only NONE and SKIP_EXIT_PATTERN are meaningful in HDR-DDR.

  • transport – Which host transport carries the payload. Accepted for symmetry with the other two modes and with no effect on the ceiling, per the warning above. I3cTransport.BULK still raises CapabilityError when the bulk transport is unavailable, which makes it usable as a benchmark control.

Returns:

The bytes read from the target. On the bulk route the buffer is sized to the length the device announced, which may be shorter than length.

Raises:
target_reset_pattern() → None

Emit the I3C target reset pattern on the bus.

Raises:
hdr_exit_pattern() → None

Emit the HDR exit pattern to return the bus to SDR mode.

Raises:
class pycosmicsdk.AsyncI3cController(*, device: AsyncDevice)

Bases: object

Asyncio I3C controller for an open device.

Obtain one from AsyncDevice.i3c; the constructor is package-private. The interface is cached on the device, so repeated dev.i3c() calls return the same controller.

Note

The rail surface matches I3cController in full, get_bus_voltage included. CosmicAsyncSDK declares none of the per-protocol voltage readers, but it does declare getAnalogMeasurements, whose response carries the I3C HV and LV rails alongside the shared I2C/SPI/UART/GPIO one — and that is the same command upstream’s blocking i3cGetBusVoltage issues internally. So the async reader is built the same way as the blocking one, and reports the same pair.

property ccc: AsyncI3cCcc

The common-command-code sub-interface, built on first access.

property max_hdr_ddr_transfer_bytes: int

Largest payload one hdr_ddr_write or hdr_ddr_read may carry.

The asyncio twin of I3cController.max_hdr_ddr_transfer_bytes, which argues the number at length: I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024), a bus-safety cap rather than a buffer size, because above it the target stops answering its dynamic address until re-enumeration.

It is here although `transport` and `max_transfer_bytes` are not, and the difference is real rather than an inconsistency. Those two describe the bulk route, which this surface cannot take, so on the async side they would be answering about a road that is not there. This one describes neither route: it needs no negotiation, reads no capability and produces no device traffic, and it answers 1024 on the bulk plane too. Leaving it off cost the async caller the one number whose whole purpose is to stop them wedging their bus, and left hard-coding 1024 as the only way to get it – which is exactly what the property exists to prevent. guard_max_length still refuses an over-long DDR transfer either way; being refused is not the same as being able to ask.

Returns:

The per-call ceiling in bytes for the HDR-DDR pair. A constant.

async initialize(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → AsyncI3cController

Bring the bus up as an I3C controller and return self for chaining.

Not idempotent: an already-initialised bus reports FW_INTERFACE_ALREADY_INITIALIZED. The bus stays initialised across reconnects, so a re-run hits this unless the device is reset first. Use configure to change parameters of an already-initialised bus.

Parameters:
  • push_pull_rate – Push-pull clock rate for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate used for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate used for legacy I2C targets on the bus.

  • drive_strength – Output driver strength of the push-pull stage.

Returns:

This controller, to allow chaining.

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

  • FirmwareError – If the bus is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED) or the firmware rejects the configuration (FW_INVALID_PARAMETER). Those are the only two codes upstream i3cControllerInit reports and neither is in the 0x06xx band, so bring-up never surfaces as I3cError.

async bring_up(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → BringUpResult

Bring the bus up as an I3C controller, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring an I3C bus up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the bus stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live bus with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Three I3C-specific details are worth knowing:

  • configure reaches every parameter initialize takes, so the fallback path requests exactly what the fresh path would have.

  • No capability guard gates I3C reconfiguration today, so in practice the fallback succeeds and settings_applied is False only if firmware rejects a rate it accepted at first bring-up. The CapabilityError arm below is not dead weight: it is what keeps this contract protocol-independent without an edit here if a guard lands.

  • Bringing the controller up is not the same as enumerating the bus. init_bus still has to run afterwards, exactly as it does after a bare initialize, and this method neither calls it nor reports on it.

A refusal raised by initialize itself is a different thing from a refused reconfigure and propagates untouched, whatever its type: nothing reached the bus, so the interface is not up, and the first half of bring_up’s contract – on return the interface is usable – cannot be kept by a return value. Only the already-up path can report.

Parameters:
  • push_pull_rate – Push-pull clock rate requested for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate requested for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate requested for legacy I2C targets on the bus.

  • drive_strength – Output driver strength requested for the push-pull stage.

Returns:

A BringUpResult. On return the bus is up and usable; consult settings_applied before relying on the requested rates actually being in force.

Raises:
async configure(*, push_pull_rate: I3cPushPullRate, open_drain_rate: I3cOpenDrainRate, i2c_open_drain_rate: I2cOpenDrainRate, drive_strength: I3cDriveStrength = I3cDriveStrength.FAST) → None

Reconfigure an already-initialised bus at runtime.

Updates controller parameters without tearing the bus down. The bus must have been initialised with initialize; an uninitialised bus reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • push_pull_rate – Push-pull clock rate for I3C SDR traffic.

  • open_drain_rate – Open-drain clock rate used for broadcast and CCC frames.

  • i2c_open_drain_rate – Open-drain clock rate used for legacy I2C targets on the bus.

  • drive_strength – Output driver strength of the push-pull stage.

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

  • FirmwareError – If the bus has not been initialised (status_code FW_INTERFACE_NOT_INITIALIZED) or the firmware rejects the configuration (FW_INVALID_COMMAND, FW_INVALID_PARAMETER). None of the three is in the 0x06xx band, so this never surfaces as I3cError.

async enable_i2c_clock_stretching(frequency_hz: int = 100_000) → I2cClockStretching

Let legacy I2C transfers wait for a target that stretches SCL.

The I3C engine keeps clocking while a target holds SCL low, so a target that stretches the clock (SMBus and MCTP devices, for example) loses bits. With stretching on, legacy_i2c_write and legacy_i2c_read are clocked in software and wait for the target. initialize and configure turn it off.

Example

With i3c = dev.i3c() and rates holding the three bus rates:

await i3c.bring_up(**rates)
await i3c.enable_i2c_clock_stretching(100_000)
await i3c.legacy_i2c_write(0x50, bytes([0x00, 0x01]))
Parameters:

frequency_hz – SCL rate in Hz, 10 kHz to 1 MHz; the firmware refuses anything else. The rate it runs at is at or below the request, and the ceiling depends on the wiring: 746 kHz on one bench, 385 kHz on another. Read it from the return value.

Returns:

enabled true and the actual SCL rate.

Return type:

The state in effect

Raises:
  • ValidationError – If frequency_hz does not fit a uint32. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If frequency_hz is out of range (FW_INVALID_PARAMETER), the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED), or the firmware is too old for this feature (FW_UNSUPPORTED_COMMAND, 4.5.x and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the state.

Note

While on, a stretch longer than the SMBus 35 ms timeout ends the legacy transfer with FW_BUS_TIMEOUT, and SDR/HDR/CCC calls, init_bus and reset_bus raise I3cError (FW_I3C_I2C_CLOCK_STRETCHING_ACTIVE) until disable_i2c_clock_stretching.

async disable_i2c_clock_stretching() → I2cClockStretching

Return legacy I2C transfers to the I3C engine, which cannot wait for a stretched clock.

Also what makes SDR, HDR and CCC calls work again after enable_i2c_clock_stretching.

Returns:

enabled false and frequency_hz 0.

Return type:

The state in effect

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

  • FirmwareError – If the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED) or the firmware is too old for this feature (FW_UNSUPPORTED_COMMAND, 4.5.x and older).

  • ProtocolError – If a SUCCESS reply is too short to carry the state.

async hold_scl_low(duration_us: int) → int

Hold SCL low for duration_us, then release it (SMBus interface reset, T2wrst).

SDA is released for the whole hold. Works with I2C clock stretching on or off; the mode is left as it was. SCL has no on-board pull-up: an external one is needed for SCL to rise after the release. The call waits out the hold: its response timeout is extended by duration_us plus 136 ms.

Example

With i3c = dev.i3c() brought up as a controller:

held_us = await i3c.hold_scl_low(35_000)
Parameters:

duration_us – Hold time in microseconds, 1 to 1 000 000; the firmware refuses anything else.

Returns:

How long SCL was held, in microseconds, as the firmware measured it.

Raises:
  • ValidationError – If duration_us does not fit a uint32. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If duration_us is 0 or above 1 s (FW_INVALID_PARAMETER), the controller is not initialised (FW_INTERFACE_NOT_INITIALIZED), SCL was still low 35 ms after the release – a target holds it or SCL has no pull-up (FW_BUS_TIMEOUT), or the firmware predates the command (FW_UNSUPPORTED_COMMAND).

  • ProtocolError – If a SUCCESS reply is too short to carry the hold time.

async set_sda_pull_up(mode: I3cSdaPullUp) → I3cSdaPullUp

Set the 1 kOhm pull-up on the I3C port’s SDA line.

AUTO (default, and every earlier firmware’s behaviour) lets the I3C engine drive it; OFF and ON disconnect or connect it while the I2C clock-stretching mode is on (the I3C engine drives it otherwise). SCL has no on-board pull-up. The setting survives I3C re-inits until changed or the adapter resets.

Example

With i3c = dev.i3c():

await i3c.set_sda_pull_up(I3cSdaPullUp.ON)
Parameters:

mode – The setting to apply.

Returns:

The stored setting after the call. It governs the pull-up only while clock stretching is on; with it off the I3C engine drives the pull-up whatever this returns.

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

  • FirmwareError – If the firmware refuses the value (FW_INVALID_PARAMETER) or predates the command (FW_UNSUPPORTED_COMMAND).

  • ProtocolError – If a SUCCESS reply is too short to carry the setting.

async set_voltage(voltage_mv: int) → None

Set the target voltage of the I3C bus.

The voltage alone selects the port: 800-1199 mV drives the low-voltage connector, 1200-3300 mV the high-voltage one. Unlike I2C and UART, the firmware offers no way to persist the setting.

Parameters:

voltage_mv – Target voltage in millivolts: 0 switches VTARG off and powers down the devices on the bus, otherwise 800-3300 mV.

Raises:
  • ValidationError – If voltage_mv is neither 0 nor within 800-3300 mV.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as I3cError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

async use_external_voltage(*, persist: bool = False) → None

Hand the I3C rail over to an externally supplied voltage.

Tells the adapter that the I3C bus voltage comes from outside, so it stops driving VTARG itself and measures the supplied rail instead. Exactly one of the two I3C ports must be powered when this runs: with neither powered the firmware reports FW_I3C_PORTS_NOT_POWERED, with both FW_BOTH_I3C_PORTS_POWERED. The HV reading saturates at ~3299 mV, so an HV supply above 3.3 V is accepted, not refused: keep it at or below 3.3 V.

Parameters:

persist – Ignored by the firmware: the I3C rails come from DACs that store nothing, so the switch lasts for this session only.

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

  • SystemFirmwareError – If the firmware rejects the switch. Firmware classifies in a fixed order and reports only the first match: FW_BOTH_I3C_PORTS_POWERED (0x0105), then FW_I3C_PORTS_NOT_POWERED (0x0104), then FW_VOLTAGE_OUT_OF_RANGE_ON_I3C_HV (0x0107), then FW_VOLTAGE_OUT_OF_RANGE_ON_I3C_LV (0x0106) — so a board with both ports powered never reports an out-of-range code. 0x0107 means the HV port read below 1200 mV: the upper-bound half of firmware’s HV test reads the low-voltage rail by mistake (sys_mgr.c:836), so an HV port driven above 3300 mV is accepted as valid rather than reported. All four are 0x01xx system-band codes, so they do not surface as I3cError.

Note

Read back what the adapter measured with get_bus_voltage.

async get_bus_voltage() → tuple[int, int]

Read the externally supplied voltage measured on each I3C rail.

The asyncio twin of I3cController.get_bus_voltage, with the same return type and the same meaning. The I3C interface has two independent rails, and this reports the VTARG measurement for both: high-voltage (1200-3300 mV) and low-voltage (800-1199 mV). Only the port that is actually powered reads back a meaningful value, so pair this with use_external_voltage.

Implemented over getAnalogMeasurements, which reads every rail in one transaction; this keeps the two I3C fields and discards the rest, exactly as upstream’s blocking i3cGetBusVoltage does. Use AsyncDevice.get_analog_measurements to get all six rails at once instead of a pair per call.

Returns:

(hv_mv, lv_mv) — the high- and low-voltage rail measurements in millivolts.

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

  • ProtocolError – If the response was too short to carry the measurements, which is also how firmware that does not implement the command reports itself.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x06xx code, so this never surfaces as I3cError.

async init_bus(*, targets: Sequence[I3cTargetInfo] | None = None) → list[I3cTargetInfo]

Run dynamic address assignment and return the resulting device table.

Issues RSTDAA followed by ENTDAA. Passing targets pre-seeds the controller’s table so known devices keep a deterministic dynamic address; anything else on the bus is assigned by the firmware.

Parameters:

targets – Known targets to seed the table with, or None for pure discovery.

Returns:

Every target the controller now has in its device table.

Raises:
async reset_bus() → None

Issue RSTDAA and clear the controller’s device table.

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

  • FirmwareError – On a firmware-reported failure. Upstream i3cResetBus reports only FW_BUS_TIMEOUT, FW_INTERFACE_NOT_INITIALIZED and FW_INVALID_COMMAND, none in the 0x06xx band, so this never surfaces as I3cError.

async get_device_table() → list[I3cTargetInfo]

Return the controller’s cached table of known targets.

A pure query of controller state; no traffic is put on the bus.

Returns:

Every target currently in the controller’s device table.

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

  • FirmwareError – On a firmware-reported failure. A controller-state query reports no 0x06xx code, so this never surfaces as I3cError.

async set_target_configuration(address: int, configuration: I3cTargetConfiguration) → None

Update the stored configuration byte of a known target.

The target must already be in the controller’s device table, so run init_bus or get_device_table first.

Parameters:
  • address – Dynamic address of the target to reconfigure (0x00-0x7F).

  • configuration – Behaviour flags to store for the target; packed into the firmware’s configuration byte.

Raises:
async legacy_i2c_write(address: int, data: bytes, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Write data to a legacy I2C target on the I3C bus.

A target that stretches SCL needs enable_i2c_clock_stretching first: the I3C engine itself cannot wait for a stretched clock.

Parameters:
  • address – 7-bit target address (0x00-0x7F).

  • data – Payload bytes. Must not exceed the device’s maximum I3C payload (AsyncDevice.capabilities.max_i3c_payload_bytes).

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame.

Raises:
  • ValidationError – If address is not a 7-bit value, or data exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. address NACK).

async legacy_i2c_read(address: int, length: int, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read length bytes from a legacy I2C target on the I3C bus.

A target that stretches SCL needs enable_i2c_clock_stretching first: the I3C engine itself cannot wait for a stretched clock.

Parameters:
  • address – 7-bit target address (0x00-0x7F).

  • length – Number of bytes to read. Must not exceed the device’s maximum I3C payload (AsyncDevice.capabilities.max_i3c_payload_bytes).

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame.

Returns:

The bytes read from the target.

Raises:
  • ValidationError – If address or length is out of range, or length exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. address NACK).

async sdr_write(address: int, data: bytes, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.START_WITH_7E) → None

Write data to an I3C target in SDR mode.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • data – Payload bytes. Must not exceed the device’s maximum I3C payload (AsyncDevice.capabilities.max_i3c_payload_bytes).

  • subaddress – Optional register/sub-address sent before data. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame. The default opens the frame with the broadcast address, matching the firmware default.

Raises:
  • ValidationError – If address is not a 7-bit value, or data exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. address NACK).

async sdr_read(address: int, length: int, *, subaddress: bytes = b'', flags: I3cTransferFlags = I3cTransferFlags.START_WITH_7E) → bytes

Read length bytes from an I3C target in SDR mode.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • length – Number of bytes to read. Must not exceed the device’s maximum I3C payload (AsyncDevice.capabilities.max_i3c_payload_bytes).

  • subaddress – Optional register/sub-address sent before the read. The host adapter accepts at most 4 bytes; a longer value raises ProtocolError.

  • flags – Transfer flags for the frame. The default opens the frame with the broadcast address, matching the firmware default.

Returns:

The bytes read from the target.

Raises:
  • ValidationError – If address or length is out of range, or length exceeds the device’s maximum payload.

  • ProtocolError – If subaddress is longer than 4 bytes.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. address NACK).

async hdr_ddr_write(address: int, command: int, data: bytes, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Write data to an I3C target in HDR-DDR mode.

The bus enters HDR-DDR automatically and leaves it again with an HDR exit pattern unless flags carries SKIP_EXIT_PATTERN.

Warning

HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on both transports, and the cap protects the bus rather than a buffer: above roughly 2048 bytes the target stops answering its dynamic address until re-enumeration. The bulk ceiling the device advertises for the I3C group does not apply here, so transport changes where the bytes travel and nothing else.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • command – HDR-DDR write command code, 0x00-0xFF. Bit 7 is the I3C direction convention (clear for a write); the adapter does not enforce it, so the whole byte is accepted.

  • data – Payload bytes; the length must be a multiple of two, because DDR moves 16-bit words and an odd-length frame cannot be expressed. Must not exceed I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024).

  • flags – Transfer flags for the frame. Only NONE and SKIP_EXIT_PATTERN are meaningful in HDR-DDR.

Raises:
  • ValidationError – If address or command is out of range, data has an odd length, or data exceeds I3C_HDR_DDR_MAX_PAYLOAD_BYTES.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure.

async hdr_ddr_read(address: int, command: int, length: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read length bytes from an I3C target in HDR-DDR mode.

The bus enters HDR-DDR automatically and leaves it again with an HDR exit pattern unless flags carries SKIP_EXIT_PATTERN.

Warning

HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024) on both transports, and the cap protects the bus rather than a buffer: above roughly 2048 bytes the target stops answering its dynamic address until re-enumeration. The bulk ceiling the device advertises for the I3C group does not apply here, so transport changes where the bytes travel and nothing else.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • command – HDR-DDR read command code, 0x80-0xFF (bit 7 set marks a read). The bound is this SDK’s, not the device’s.

  • length – Number of bytes to read; must be a multiple of two, because DDR moves 16-bit words. Must not exceed I3C_HDR_DDR_MAX_PAYLOAD_BYTES (1024).

  • flags – Transfer flags for the frame. Only NONE and SKIP_EXIT_PATTERN are meaningful in HDR-DDR.

Returns:

The bytes read from the target.

Raises:
  • ValidationError – If address, command or length is out of range, length is odd, or length exceeds I3C_HDR_DDR_MAX_PAYLOAD_BYTES.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure.

async target_reset_pattern() → None

Emit the I3C target reset pattern on the bus.

Raises:
async hdr_exit_pattern() → None

Emit the HDR exit pattern to return the bus to SDR mode.

Raises:

I3C — common command codes

class pycosmicsdk.I3cCcc(*, device: Device)

Bases: object

Blocking common-command-code interface of an I3C controller.

Obtain one from I3cController.ccc; the constructor is package-private. Every method issues one common command code and shares the owning controller’s locking and lifecycle guarantees.

Note

There is deliberately no transfer here, and unlike the rail readers that absence is forced by upstream rather than an oversight. AsyncI3cCcc.transfer exposes CosmicAsyncSDK::i3cCccTransfer, the raw by-opcode primitive; CosmicSDK declares no generic CCC entry point at all — only the ~40 named per-opcode wrappers, each of which reaches i3cCccTransfer through the private asyncSDK member (CosmicSDK.hpp, private: section). So there is no blocking route to an opcode the SDK does not name, and this interface offers the named wrappers alone. Reaching an unnamed or vendor-defined CCC requires an AsyncDevice.

rstdaa(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTDAA to clear every assigned dynamic address.

Table entries survive: only their dynamic-address fields are reset, unlike I3cController.reset_bus, which drops the table entirely.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entdaa(*, targets: Sequence[I3cTargetInfo] | None = None) → None

Broadcast ENTDAA to assign a dynamic address to every target on the bus.

Alone among the common command codes, ENTDAA carries no transfer flags. The controller’s device table holds the outcome; read it back with I3cController.get_device_table.

Parameters:

targets – Known targets matched on PID, BCR and DCR so each keeps its requested dynamic address; anything unmatched is assigned by the firmware.

Raises:
setdasa(*, static: int, dynamic: int, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Assign a dynamic address to a target that has a static address.

Parameters:
  • static – Static address the target answers to today.

  • dynamic – Dynamic address to assign.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setaasa(static_addresses: Sequence[int], *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETAASA so targets adopt their static address as dynamic.

Parameters:
  • static_addresses – Static addresses expected to answer; an entry is added to the device table for each responder.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setnewda(*, old: int, new: int, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Move a target from one dynamic address to another.

Parameters:
  • old – Dynamic address the target holds today.

  • new – Dynamic address to move it to.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
getpid(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s 48-bit provisional ID.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The PID bytes, most significant first — six for a target that answers in full. A target that returns fewer is reported as it answered, without raising, so check len() before unpacking.

Raises:
getbcr(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s bus characteristics register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The BCR byte.

Raises:
getdcr(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s device characteristics register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The DCR byte.

Raises:
getstatus(address: int, *, def_byte: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s status register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Optional defining byte selecting the format-2 variant of the CCC; None sends the format-1 variant.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The status bytes reported by the target.

Raises:
getcaps(address: int, *, def_byte: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s capability register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Optional defining byte selecting the format-2 variant of the CCC; None sends the format-1 variant.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The capability bytes reported by the target.

Raises:
getmxds(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s maximum sustained data-rate descriptor.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The MXDS bytes reported by the target.

Raises:
getxtime(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s exchange-timing support information.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The four XTIME bytes reported by the target.

Raises:
rstact_broadcast(def_byte: I3cTargetResetDefByte, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTACT to arm a target-reset action on every target.

The matching target reset pattern is emitted in the same frame.

Parameters:
  • def_byte – Reset action to arm.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
rstgrpa_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTGRPA to clear every assigned group address.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas0_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS0 to put every target in activity state 0.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas1_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS1 to put every target in activity state 1.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas2_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS2 to put every target in activity state 2.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas3_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS3 to put every target in activity state 3.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas3_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 3.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas2_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 2.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas1_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 1.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
entas0_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 0.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
rstgrpa_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Clear the group address of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
rstact_direct(address: int, def_byte: I3cTargetResetDefByte, direction: I3cTransferDirection, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int | None

Arm a target-reset action on one target, optionally reading it back.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Reset action to arm or value to read.

  • direction – WRITE to arm the action, READ to read the value back.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The byte returned by a READ, or None for a WRITE.

Raises:
setgrpa_direct(address: int, grpa: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Assign a group address to one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • grpa – Group address to assign (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
enec_broadcast(events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENEC to enable bus events on every target.

Parameters:
  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
enec_direct(address: int, events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Enable bus events on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
disec_broadcast(events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast DISEC to disable bus events on every target.

Parameters:
  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
disec_direct(address: int, events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Disable bus events on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
getmwl(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s maximum write length.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The maximum number of bytes the target accepts in one write.

Raises:
setmwl_broadcast(mwl: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETMWL to set the maximum write length on every target.

Parameters:
  • mwl – Maximum write length in bytes.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setmwl_direct(address: int, mwl: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Set the maximum write length of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • mwl – Maximum write length in bytes.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
getmrl(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → I3cGetMrlResult

Read a target’s maximum read length and optional IBI payload size.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The maximum read length, plus the IBI payload size when the target reported one.

Raises:
setmrl_broadcast(mrl: int, *, ibi_length: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETMRL to set the maximum read length on every target.

Parameters:
  • mrl – Maximum read length in bytes.

  • ibi_length – IBI payload size to announce alongside the length; None omits the optional field.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setmrl_direct(address: int, mrl: int, *, ibi_length: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Set the maximum read length of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • mrl – Maximum read length in bytes.

  • ibi_length – IBI payload size to announce alongside the length; None omits the optional field.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setxtime_broadcast(sub_command: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETXTIME to configure exchange timing on every target.

Parameters:
  • sub_command – SETXTIME sub-command byte.

  • data – Optional payload following the sub-command byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setxtime_direct(address: int, sub_command: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Configure exchange timing on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • sub_command – SETXTIME sub-command byte.

  • data – Optional payload following the sub-command byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
setbuscon_broadcast(context: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETBUSCON to announce the bus context to every target.

Parameters:
  • context – Higher-level protocol context byte.

  • data – Optional payload following the context byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
class pycosmicsdk.AsyncI3cCcc(*, device: AsyncDevice)

Bases: object

Asyncio common-command-code interface of an I3C controller.

Obtain one from AsyncI3cController.ccc; the constructor is package-private. Every method issues one common command code and shares the owning controller’s locking and lifecycle guarantees.

transfer is the raw primitive the named methods are wrappers around, and the only route to an opcode the SDK does not name.

Note

transfer has no blocking twin, and that is forced by upstream. i3cCccTransfer is declared on CosmicAsyncSDK only; CosmicSDK exposes no generic CCC entry point, and the ~40 named wrappers it does expose reach i3cCccTransfer through its private asyncSDK member (CosmicSDK.hpp, private: section), which the blocking facade cannot call. So I3cCcc offers the named wrappers alone, and an unnamed or vendor-defined CCC needs this async surface.

async transfer(ccc: int, address: int, direction: I3cTransferDirection, *, data: bytes = b'', length: int = 0, def_byte: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Issue one common command code by raw opcode.

The escape hatch under every other method on this interface: each of those wraps one specific opcode, so this is the only route to a vendor-defined or MIPI-reserved CCC the SDK does not name. Prefer the named wrapper where one exists — it fills in the payload length, the direction and the defining byte for you.

ccc is a plain integer rather than an enum on purpose. Upstream’s CCC table is dense: all 256 opcodes carry an enumerator, and the reserved and vendor ranges get placeholder names that merely restate their own value (B_VENCCC_62 is 0x62). Writing 0x62 is how a vendor datasheet spells it, and it is what the wire carries.

Two arguments upstream exposes are not offered here. The transfer mode is fixed to SDR, which is the only mode a CCC is defined in. The CCC-type nibble is derived from def_byte, so it cannot contradict the byte actually sent.

Parameters:
  • ccc – CCC opcode (0x00-0xFF). Broadcast opcodes are 0x00-0x7F and are addressed to 0x7E; direct opcodes are 0x80-0xFF and are addressed to one target.

  • address – Address the frame is directed at: 0x7E for a broadcast CCC, the target’s 7-bit dynamic address for a direct one.

  • direction – WRITE sends data; READ requests length bytes back.

  • data – Payload for a WRITE, at most 255 bytes. Must be empty on a READ.

  • length – Bytes to request on a READ, at most 255. Must be 0 on a WRITE, whose payload length is taken from data.

  • def_byte – Defining byte for a CCC that takes one. None sends the frame without one, which is what every CCC the SDK names does bar the format-2 GETCAPS/GETSTATUS variants.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The data field of the CCC response – the bytes the target returned on a READ, and empty on a WRITE.

Raises:
  • ValidationError – If ccc, address, def_byte or length is out of range, if data exceeds 255 bytes, or if data and length contradict direction.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure (e.g. the target NACKed the opcode).

async rstdaa(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTDAA to clear every assigned dynamic address.

Table entries survive: only their dynamic-address fields are reset, unlike I3cController.reset_bus, which drops the table entirely.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entdaa(*, targets: Sequence[I3cTargetInfo] | None = None) → None

Broadcast ENTDAA to assign a dynamic address to every target on the bus.

Alone among the common command codes, ENTDAA carries no transfer flags. The controller’s device table holds the outcome; read it back with I3cController.get_device_table.

Parameters:

targets – Known targets matched on PID, BCR and DCR so each keeps its requested dynamic address; anything unmatched is assigned by the firmware.

Raises:
async setdasa(*, static: int, dynamic: int, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Assign a dynamic address to a target that has a static address.

Parameters:
  • static – Static address the target answers to today.

  • dynamic – Dynamic address to assign.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setaasa(static_addresses: Sequence[int], *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETAASA so targets adopt their static address as dynamic.

Parameters:
  • static_addresses – Static addresses expected to answer; an entry is added to the device table for each responder.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setnewda(*, old: int, new: int, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Move a target from one dynamic address to another.

Parameters:
  • old – Dynamic address the target holds today.

  • new – Dynamic address to move it to.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async getpid(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s 48-bit provisional ID.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The PID bytes, most significant first — six for a target that answers in full. A target that returns fewer is reported as it answered, without raising, so check len() before unpacking.

Raises:
async getbcr(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s bus characteristics register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The BCR byte.

Raises:
async getdcr(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s device characteristics register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The DCR byte.

Raises:
async getstatus(address: int, *, def_byte: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s status register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Optional defining byte selecting the format-2 variant of the CCC; None sends the format-1 variant.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The status bytes reported by the target.

Raises:
async getcaps(address: int, *, def_byte: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s capability register.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Optional defining byte selecting the format-2 variant of the CCC; None sends the format-1 variant.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The capability bytes reported by the target.

Raises:
async getmxds(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s maximum sustained data-rate descriptor.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The MXDS bytes reported by the target.

Raises:
async getxtime(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → bytes

Read a target’s exchange-timing support information.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The four XTIME bytes reported by the target.

Raises:
async rstact_broadcast(def_byte: I3cTargetResetDefByte, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTACT to arm a target-reset action on every target.

The matching target reset pattern is emitted in the same frame.

Parameters:
  • def_byte – Reset action to arm.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async rstgrpa_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast RSTGRPA to clear every assigned group address.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas0_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS0 to put every target in activity state 0.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas1_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS1 to put every target in activity state 1.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas2_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS2 to put every target in activity state 2.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas3_broadcast(*, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENTAS3 to put every target in activity state 3.

Parameters:

flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas3_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 3.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas2_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 2.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas1_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 1.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async entas0_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Put one target in activity state 0.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async rstgrpa_direct(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Clear the group address of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async rstact_direct(address: int, def_byte: I3cTargetResetDefByte, direction: I3cTransferDirection, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int | None

Arm a target-reset action on one target, optionally reading it back.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • def_byte – Reset action to arm or value to read.

  • direction – WRITE to arm the action, READ to read the value back.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The byte returned by a READ, or None for a WRITE.

Raises:
async setgrpa_direct(address: int, grpa: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Assign a group address to one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • grpa – Group address to assign (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async enec_broadcast(events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast ENEC to enable bus events on every target.

Parameters:
  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async enec_direct(address: int, events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Enable bus events on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async disec_broadcast(events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast DISEC to disable bus events on every target.

Parameters:
  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async disec_direct(address: int, events: I3cEvent, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Disable bus events on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • events – Bit mask of the bus events to act on; members may be combined with |.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async getmwl(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → int

Read a target’s maximum write length.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The maximum number of bytes the target accepts in one write.

Raises:
  • ValidationError – If address is not a 7-bit value.

  • ProtocolError – If the target’s data field carries fewer than the two bytes MWL needs; slicing a short frame would report 1 where the target meant 256.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure.

async setmwl_broadcast(mwl: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETMWL to set the maximum write length on every target.

Parameters:
  • mwl – Maximum write length in bytes.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setmwl_direct(address: int, mwl: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Set the maximum write length of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • mwl – Maximum write length in bytes.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async getmrl(address: int, *, flags: I3cTransferFlags = I3cTransferFlags.NONE) → I3cGetMrlResult

Read a target’s maximum read length and optional IBI payload size.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Returns:

The maximum read length, plus the IBI payload size when the target reported one.

Raises:
async setmrl_broadcast(mrl: int, *, ibi_length: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETMRL to set the maximum read length on every target.

Parameters:
  • mrl – Maximum read length in bytes.

  • ibi_length – IBI payload size to announce alongside the length; None omits the optional field.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setmrl_direct(address: int, mrl: int, *, ibi_length: int | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Set the maximum read length of one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • mrl – Maximum read length in bytes.

  • ibi_length – IBI payload size to announce alongside the length; None omits the optional field.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setxtime_broadcast(sub_command: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETXTIME to configure exchange timing on every target.

Parameters:
  • sub_command – SETXTIME sub-command byte.

  • data – Optional payload following the sub-command byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setxtime_direct(address: int, sub_command: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Configure exchange timing on one target.

Parameters:
  • address – Dynamic address of the target (0x00-0x7F).

  • sub_command – SETXTIME sub-command byte.

  • data – Optional payload following the sub-command byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:
async setbuscon_broadcast(context: int, *, data: bytes | None = None, flags: I3cTransferFlags = I3cTransferFlags.NONE) → None

Broadcast SETBUSCON to announce the bus context to every target.

Parameters:
  • context – Higher-level protocol context byte.

  • data – Optional payload following the context byte.

  • flags – Transfer flags for the frame. Only NONE and NONSTOP are valid on a CCC transfer; START_WITH_7E has no effect.

Raises:

I3C — target mode

class pycosmicsdk.I3cTarget(*, device: Device)

Bases: object

Blocking I3C target-mode interface for an open device.

Obtain one from Device.i3c_target; the constructor is package-private. The interface is cached on the device, so repeated dev.i3c_target() calls return the same instance. In target mode the adapter answers an external controller out of a 1024-byte register arena, and reports every transfer, CCC and dynamic-address change it services as a bus event.

initialize(*, memory_layout: I3cTargetMemoryLayout, pid: bytes, bcr: int, dcr: int, static_address: int, max_write_length: int = 1024, max_read_length: int = 1024) → I3cTarget

Switch the adapter into I3C target mode and return self for chaining.

Takes effect whatever the current bus role: an adapter already acting as a controller is re-initialised as a target. Event capture is armed before the switch, so a transfer the controller drives the instant target mode comes up is queued rather than lost – unconditionally, so a caller who reads events by subscription rather than through wait_for_event should call stop_event_capture afterwards instead of leaving an unread queue to grow. Use configure to change these parameters once target mode is up.

Unlike every controller ``initialize`` in this SDK, this one is idempotent: it re-initialises the peripheral unconditionally and never reports FW_INTERFACE_ALREADY_INITIALIZED. Call it on every run – there is no bring_up composite for target mode and no need for one; the comment block above I3cTarget says why at length.

Parameters:
  • memory_layout – How the 1024-byte register arena is grouped into registers.

  • pid – 48-bit Provisional ID as exactly 6 bytes, most significant first.

  • bcr – Bus Characteristics Register value. Bits 7-6 encode the device role, bit 1 the IBI-request capability.

  • dcr – Device Characteristics Register value, advertising the kind of device this target presents itself as.

  • static_address – 7-bit I2C static address, or 0x00 for none.

  • max_write_length – Largest write the target accepts, answered to GETMWL (0-1024).

  • max_read_length – Largest read the target allows, answered to GETMRL (0-1024).

Returns:

This interface, to allow chaining.

Raises:
  • ValidationError – If pid is not 6 bytes, or a numeric argument is out of range.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the configuration. Upstream i3cTargetInit reports only FW_UNSUPPORTED_COMMAND, which is not in the 0x06xx band, so this never surfaces as I3cError.

configure(*, memory_layout: I3cTargetMemoryLayout, pid: bytes, bcr: int, dcr: int, static_address: int, max_write_length: int = 1024, max_read_length: int = 1024) → None

Reapply the target parameters of an adapter already in target mode.

The adapter must have been switched to the target role with initialize first: one still acting as a controller reports FW_INVALID_COMMAND and one with no I3C role yet FW_INTERFACE_NOT_INITIALIZED, both surfacing as I3cError.

Parameters:
  • memory_layout – How the 1024-byte register arena is grouped into registers.

  • pid – 48-bit Provisional ID as exactly 6 bytes, most significant first.

  • bcr – Bus Characteristics Register value. Bits 7-6 encode the device role, bit 1 the IBI-request capability.

  • dcr – Device Characteristics Register value, advertising the kind of device this target presents itself as.

  • static_address – 7-bit I2C static address, or 0x00 for none.

  • max_write_length – Largest write the target accepts, answered to GETMWL (0-1024).

  • max_read_length – Largest read the target allows, answered to GETMRL (0-1024).

Raises:
  • ValidationError – If pid is not 6 bytes, or a numeric argument is out of range.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the configuration, notably when the adapter is not in target mode. Upstream i3cTargetSetParameters reports only FW_INTERFACE_NOT_INITIALIZED, FW_INVALID_COMMAND and FW_UNSUPPORTED_COMMAND, none in the 0x06xx band, so this never surfaces as I3cError.

write_memory(address: int, data: bytes) → None

Write data into the target’s register memory from the host side.

Seeds the bytes an external controller will read back. address is a register index, not a byte offset: the firmware multiplies it by the register size of the configured memory layout (1, 2 or 4 bytes) to get a byte offset into the 1024-byte arena, so the usable range is 0-1023 for a 1-byte layout, 0-511 for 2 bytes and 0-255 for 4 bytes. Until initialize has picked a layout, the widest range is assumed.

Parameters:
  • address – Register index to start writing at.

  • data – Payload bytes. Must fit the target memory from address onwards.

Raises:
  • ValidationError – If address is not a valid register index for the configured layout, or data would run past the end of the target memory.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure, notably when the adapter is not in target mode.

read_memory(address: int, length: int) → bytes

Read length bytes back out of the target’s register memory.

Reads what an external controller has written. address is a register index scaled by the configured layout’s register size, exactly as in write_memory.

Parameters:
  • address – Register index to start reading at.

  • length – Number of bytes to read. Must fit the target memory from address onwards.

Returns:

The bytes read out of the target memory.

Raises:
  • ValidationError – If address is not a valid register index for the configured layout, or length would run past the end of the target memory.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure, notably when the adapter is not in target mode.

wait_for_event(*, timeout: float | None = None) → I3cTargetBusEvent

Return the oldest queued bus event, waiting for one if the queue is empty.

The pull path for what the external controller does to this target. Events are queued as the firmware reports them and each call pops exactly one, oldest first; the queue is never trimmed, so a burst serviced between two calls is delivered in full. For push delivery, subscribe to I3cTargetBusEventNotification via Device.subscribe – and then call stop_event_capture, because initialize arms this queue whether or not anything ever reads it.

Note

Safe to call concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait when no event is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:
stop_event_capture() → None

Stop queueing bus events for wait_for_event, and drop the queue.

This does not release the target role, and cannot. There is no i3cTargetDeinit upstream – the comment block above I3cTarget says why target mode has no release and no bring_up – so after this call the adapter still answers the external controller and firmware still reports every transfer it services. What stops is this SDK queueing those reports. A Device.subscribe subscription of the caller’s own is untouched, because it is a different subscription.

It exists because the queue is the one part of target-mode teardown that needs no firmware command, and because initialize arms capture unconditionally – before the role switch, so that a transfer driven the instant target mode comes up is not lost. A caller who then takes the push route (wait_for_event recommends it) never reads what that arming built, and the queue is unbounded, so a second unread copy of every event grows for the life of the device with its payload attached. This is the call that ends it.

Why the queue stays unbounded rather than gaining UART’s drop-oldest cap. Uart’s QUEUE_MAXSIZE bounds a byte stream that arrives whether anyone asked for it or not, where the loss of an old chunk is a lesser harm than unbounded growth. A bus event is a discrete record of a transaction this target already serviced, and it is the only record – dropping one silently would put a caller’s event loop out of step with the bus it is meant to be following, which is the failure I2cTarget and SpiTarget both refuse for the same reason. Bounding it here and nowhere else would also make three sibling roles disagree about what an event queue is. So the cap is this method instead: growth a caller can end, rather than losses a caller cannot see.

Capture is lazy, so this is not a one-way door: a later wait_for_event builds a fresh queue and resumes. Idempotent – a target whose capture was never armed has nothing to tear down.

The device need not still be open. Nothing is issued, the work is an unsubscribe – already idempotent, and already safe after a close – and refusing here would fail the one call a finally block has, in the one state it most wants it. That is the same shape Subscription.unsubscribe has and the opposite of write_memory’s.

One consequence to know before calling it: a thread parked in wait_for_event(timeout=None) is woken with DisconnectError on a device that is still open. I2cTarget._release_events argues that choice at length – it is _wake_closed’s sentinel, and inventing a second “capture stopped” sentinel for one caller buys less than it costs. Stopping capture with a waiter parked is a race the caller created; leaving that waiter parked on a queue nothing will refill is the only alternative.

class pycosmicsdk.AsyncI3cTarget(*, device: AsyncDevice)

Bases: object

Asyncio I3C target-mode interface for an open device.

The asyncio twin of I3cTarget, returned by AsyncDevice.i3c_target; the constructor is package-private. The interface is cached on the device, so repeated dev.i3c_target() calls return the same instance. Argument validation matches the blocking interface; commands are awaited and correlated through the device’s engine rather than serialized under a lock.

async initialize(*, memory_layout: I3cTargetMemoryLayout, pid: bytes, bcr: int, dcr: int, static_address: int, max_write_length: int = 1024, max_read_length: int = 1024) → AsyncI3cTarget

Switch the adapter into I3C target mode and return self for chaining.

Takes effect whatever the current bus role: an adapter already acting as a controller is re-initialised as a target. Event capture is armed before the switch, so a transfer the controller drives the instant target mode comes up is queued rather than lost – unconditionally, so a caller who reads events by subscription rather than through wait_for_event should call stop_event_capture afterwards instead of leaving an unread queue to grow. Use configure to change these parameters once target mode is up.

Unlike every controller ``initialize`` in this SDK, this one is idempotent: it re-initialises the peripheral unconditionally and never reports FW_INTERFACE_ALREADY_INITIALIZED. Call it on every run – there is no bring_up composite for target mode and no need for one; the comment block above I3cTarget says why at length.

Parameters:
  • memory_layout – How the 1024-byte register arena is grouped into registers.

  • pid – 48-bit Provisional ID as exactly 6 bytes, most significant first.

  • bcr – Bus Characteristics Register value. Bits 7-6 encode the device role, bit 1 the IBI-request capability.

  • dcr – Device Characteristics Register value, advertising the kind of device this target presents itself as.

  • static_address – 7-bit I2C static address, or 0x00 for none.

  • max_write_length – Largest write the target accepts, answered to GETMWL (0-1024).

  • max_read_length – Largest read the target allows, answered to GETMRL (0-1024).

Returns:

This interface, to allow chaining.

Raises:
  • ValidationError – If pid is not 6 bytes, or a numeric argument is out of range.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the configuration. Upstream i3cTargetInit reports only FW_UNSUPPORTED_COMMAND, which is not in the 0x06xx band, so this never surfaces as I3cError.

async configure(*, memory_layout: I3cTargetMemoryLayout, pid: bytes, bcr: int, dcr: int, static_address: int, max_write_length: int = 1024, max_read_length: int = 1024) → None

Reapply the target parameters of an adapter already in target mode.

The adapter must have been switched to the target role with initialize first: one still acting as a controller reports FW_INVALID_COMMAND and one with no I3C role yet FW_INTERFACE_NOT_INITIALIZED, both surfacing as I3cError.

Parameters:
  • memory_layout – How the 1024-byte register arena is grouped into registers.

  • pid – 48-bit Provisional ID as exactly 6 bytes, most significant first.

  • bcr – Bus Characteristics Register value. Bits 7-6 encode the device role, bit 1 the IBI-request capability.

  • dcr – Device Characteristics Register value, advertising the kind of device this target presents itself as.

  • static_address – 7-bit I2C static address, or 0x00 for none.

  • max_write_length – Largest write the target accepts, answered to GETMWL (0-1024).

  • max_read_length – Largest read the target allows, answered to GETMRL (0-1024).

Raises:
  • ValidationError – If pid is not 6 bytes, or a numeric argument is out of range.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the configuration, notably when the adapter is not in target mode. Upstream i3cTargetSetParameters reports only FW_INTERFACE_NOT_INITIALIZED, FW_INVALID_COMMAND and FW_UNSUPPORTED_COMMAND, none in the 0x06xx band, so this never surfaces as I3cError.

async write_memory(address: int, data: bytes) → None

Write data into the target’s register memory from the host side.

Seeds the bytes an external controller will read back. address is a register index, not a byte offset: the firmware multiplies it by the register size of the configured memory layout (1, 2 or 4 bytes) to get a byte offset into the 1024-byte arena, so the usable range is 0-1023 for a 1-byte layout, 0-511 for 2 bytes and 0-255 for 4 bytes. Until initialize has picked a layout, the widest range is assumed.

Parameters:
  • address – Register index to start writing at.

  • data – Payload bytes. Must fit the target memory from address onwards.

Raises:
  • ValidationError – If address is not a valid register index for the configured layout, or data would run past the end of the target memory.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure, notably when the adapter is not in target mode.

async read_memory(address: int, length: int) → bytes

Read length bytes back out of the target’s register memory.

Reads what an external controller has written. address is a register index scaled by the configured layout’s register size, exactly as in write_memory.

Parameters:
  • address – Register index to start reading at.

  • length – Number of bytes to read. Must fit the target memory from address onwards.

Returns:

The bytes read out of the target memory.

Raises:
  • ValidationError – If address is not a valid register index for the configured layout, or length would run past the end of the target memory.

  • DisconnectError – If the device is no longer open.

  • I3cError – On a firmware-reported failure, notably when the adapter is not in target mode.

async wait_for_event(*, timeout: float | None = None) → I3cTargetBusEvent

Return the oldest queued bus event, waiting for one if the queue is empty.

The pull path for what the external controller does to this target. Events are queued as the firmware reports them and each call pops exactly one, oldest first; the queue is never trimmed, so a burst serviced between two calls is delivered in full. For push delivery, subscribe to I3cTargetBusEventNotification via AsyncDevice.subscribe – and then await stop_event_capture, because initialize arms this queue whether or not anything ever reads it.

Note

Safe to await concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait when no event is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:
async stop_event_capture() → None

Stop queueing bus events for wait_for_event, and drop the queue.

The asyncio twin of I3cTarget.stop_event_capture, with the same contract and the same three arguments behind it: it tears down only the event queue and never the target role, because there is no i3cTargetDeinit for it to call; it exists because initialize arms capture whether or not anything reads it, and the queue is unbounded; and the queue stays unbounded on purpose, with this method as the bound rather than a drop-oldest cap that would lose the only record of a transaction the target already serviced.

It is a coroutine although nothing in it awaits, for AsyncI2cTarget.drain_events’s reason turned around: the sentinel goes onto an asyncio.Queue with put_nowait, which is only safe from the loop that owns it, and a sync method that silently required a loop would be the worse of the two shapes.

A coroutine parked in wait_for_event(timeout=None) is woken with DisconnectError on a device that is still open; _release_events says why that sentinel rather than a second one invented here.

UART

class pycosmicsdk.Uart(*, device: Device)

Bases: object

Blocking UART interface for an open device.

Obtain one from Device.uart; the constructor is package-private. The interface is cached on the device, so repeated dev.uart() calls return the same instance.

initialize(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE, send_only_mode: bool = False) → Uart

Bring the UART up and return self for chaining.

Not idempotent: a re-run on an already-up UART raises FirmwareError with FW_INTERFACE_ALREADY_INITIALIZED. The UART stays initialised across reconnects, so use configure to change parameters instead.

Reception is armed here by default: the RX queue and its subscription are installed before bring-up, so data the peer emits the instant the UART comes up is captured, not dropped. Pass send_only_mode=True to skip it for pure transmitters.

send_only_mode selects the RX path in both directions, which is the only way the flag can mean anything on a re-run: on a UART whose reception is already armed – the normal state on the second run of a script, since the flag used to be write-only – True tears the subscription and queue down again rather than leaving them running behind a flag that says otherwise. It is purely host-side either way: no firmware parameter is behind it, and the device keeps receiving.

Parameters:
  • baudrate – Line speed.

  • hw_handshake – RTS/CTS hardware flow control.

  • parity – Parity bit mode.

  • data_size – Bits per character.

  • stop_bit – Number of stop bits.

  • send_only_mode – When True, do not arm reception at bring-up — no RX queue or dispatcher thread, for write-only traffic, and release them if an earlier bring-up armed them. A later receive/query still works (armed lazily on first use) but may have missed data that arrived before that call; a caller parked in one when reception is released is woken with DisconnectError, exactly as deinit wakes it.

Returns:

This interface, to allow chaining.

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

  • FirmwareError – If the UART is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), or the firmware rejects the configuration (FW_INVALID_PARAMETER). Neither code sits in the 0x04xx UART band, so neither surfaces as UartError.

  • UartError – If the port is currently held by the USB CDC (status_code FW_UART_IN_VCOM_PORT_MODE). This is the only firmware code in the UART band, and the only UART call upstream documents it for.

bring_up(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE, send_only_mode: bool = False) → BringUpResult

Bring the UART up, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring the UART up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the UART stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live UART with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Two UART-specific details are worth knowing:

  • send_only_mode is a this-SDK setting, not a firmware one – configure does not take it, because it selects whether the RX queue and its dispatcher subscription are armed. initialize arms or releases them before it talks to the device, so the choice is honoured whichever path runs, in both directions, and is never the thing settings_applied is reporting about.

  • Unlike I2C, no capability guard gates UART reconfiguration and the UART does expose deinit, so in practice the fallback here succeeds. settings_applied is False in two cases: firmware rejected a line parameter it accepted at first bring-up, or hw_handshake was requested on a unit whose capabilities.uart_hw_handshake_effective is False – the one UART setting whose refusal is knowable before the call, and which initialize can only warn about because it returns nothing. Either way the UART is up, the other line parameters did apply, and refusal says which case it was. The two are independent and can both hold on one call; refusal then names both, keeping the reconfigure refusal’s own class and status_code so a caller branching on either still matches. See refused_with.

Parameters:
  • baudrate – Line speed requested for the UART.

  • hw_handshake – RTS/CTS hardware flow control.

  • parity – Parity bit mode.

  • data_size – Bits per character.

  • stop_bit – Number of stop bits.

  • send_only_mode – When True, do not arm reception, for write-only traffic. Honoured whichever path runs; see above.

Returns:

A BringUpResult. On return the UART is up and usable; consult settings_applied before relying on the requested line parameters actually being in force, and refusal for why they are not. It is a report, not a promise: nothing here guarantees a setting the device accepted is in force, only that a refusal this SDK can see is not reported as a success.

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

  • FirmwareError – On any firmware failure other than the two the fallback absorbs.

  • UartError – If the port is currently held by the USB CDC (status_code FW_UART_IN_VCOM_PORT_MODE), which only initialize reports.

deinit() → None

Release the UART peripheral on the firmware.

After this call the UART is uninitialized on the firmware side; any subsequent UART call will return FW_INTERFACE_NOT_INITIALIZED until initialize is called again. Also cancels the internal RX subscription (if one exists) so no stale notifications fire after teardown.

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

  • FirmwareError – If the UART was not initialized (status_code FW_INTERFACE_NOT_INITIALIZED, 0x0006). A generic firmware code, not a 0x04xx one, so it does not surface as UartError.

configure(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE) → None

Reconfigure an already-initialised UART at runtime.

Updates the line parameters without tearing the UART down. It must have been brought up first with initialize; calling this on an uninitialised UART comes back as FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • baudrate – New line speed.

  • hw_handshake – New RTS/CTS hardware flow control setting.

  • parity – New parity bit mode.

  • data_size – New bits per character.

  • stop_bit – New number of stop bits.

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

  • FirmwareError – If the firmware rejects the request — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER when a line parameter is unsupported. Both are generic codes below the 0x04xx UART band, so neither surfaces as UartError.

set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the UART bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. An out-of-range value is rejected by the firmware and surfaced as SystemFirmwareError.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as UartError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

use_external_voltage(*, persist: bool = False) → None

Hand the UART rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, SPI and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead. Read back what it measured with get_bus_voltage.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:
get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

This is not a UART-specific measurement. The adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and this reads that single rail, so the reading reflects whatever last set or is driving it — pair this with use_external_voltage.

I2cController.get_bus_voltage, SpiController.get_bus_voltage and Gpio.get_voltage return the very same number. Upstream implements all four by issuing one getAnalogMeasurements command and returning its vmeasVtargI2cSpiUartGpio_mV field (CosmicSDK.cpp); the four names exist to keep each protocol’s surface self-contained, not because four rails are measured. I3cController.get_bus_voltage is the genuine exception: I3C has its own HV and LV rails and reports both.

Returns:

The measured voltage in millivolts.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x04xx code, so this never surfaces as UartError.

send(data: bytes) → None

Transmit data over the UART.

A plain transmit. Reception is decided once at bring-up via initialize (armed by default; opt out with send_only_mode=True), so send does not touch the RX path — it behaves the same whether or not reception is armed.

Parameters:

data – Payload bytes. Must not exceed the device’s maximum UART message size (Device.capabilities.max_uart_message_bytes); a larger value raises ValidationError.

Raises:
  • ValidationError – If data exceeds the device’s maximum message size.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER otherwise. Neither is a 0x04xx code, so neither surfaces as UartError.

query(data: bytes, *, timeout: float | None = None) → bytes

Send data and return the reply, a request/response round-trip.

The primitive for command/response protocols. It arms reception, drains any RX buffered before the request, transmits, then waits for the next message, so the bytes returned are the reply to this data and not a stale unsolicited message. Prefer this over send + receive, which does not drain and so can return a stale message instead of the reply.

Note

Not safe to call concurrently, nor to mix with receive: the drain step would discard or steal data across overlapping callers.

Parameters:
  • data – Payload bytes to transmit. Must not exceed the device’s maximum UART message size; a larger value raises ValidationError.

  • timeout – Maximum seconds to wait for the reply. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The payload of the reply message.

Raises:
  • ValidationError – If data exceeds the device’s maximum message size, or timeout is negative.

  • DisconnectError – If the device is not open, or if Device.close is called while this call is blocked waiting for the reply.

  • CosmicTimeoutError – If no reply arrives within timeout (only when timeout is not None).

  • FirmwareError – On a firmware-reported transmit failure — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER otherwise. Neither is a 0x04xx code, so neither surfaces as UartError.

receive(*, timeout: float | None = None) → bytes

Return the payload of the oldest queued received message.

The pull path for unsolicited inbound data. UART reception is push-driven: the firmware reports incoming bytes as notifications buffered in an internal queue, and each call pops exactly one message (oldest first), never concatenating or draining. For a command/response round-trip use query instead; for continuous push delivery subscribe to UartRxNotification via Device.subscribe.

Note

Safe to call concurrently with competing-consumer semantics; each message is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait for a message when the queue is empty. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The payload of the oldest queued message.

Raises:
class pycosmicsdk.AsyncUart(*, device: AsyncDevice)

Bases: object

Asyncio UART interface for an open device.

The asyncio twin of Uart, returned by AsyncDevice.uart; the constructor is package-private. The interface is cached on the device, so repeated dev.uart() calls return the same instance. Argument validation matches the blocking interface; commands are awaited and correlated through the device’s engine rather than serialized under a lock.

Note

The rail surface matches Uart in full, get_bus_voltage included. CosmicAsyncSDK declares no uartGetBusVoltage, but it does declare getAnalogMeasurements, which carries the shared I2C/SPI/UART/GPIO rail — and that is the same command upstream’s blocking uartGetBusVoltage issues internally. So the async reader is built the same way as the blocking one, and reports the same value.

async initialize(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE, send_only_mode: bool = False) → AsyncUart

Bring the UART up and return self for chaining.

Not idempotent: a re-run on an already-up UART raises FirmwareError with FW_INTERFACE_ALREADY_INITIALIZED. The UART stays initialised across reconnects, so use configure to change parameters instead.

Parameters:
  • baudrate – Line speed.

  • hw_handshake – RTS/CTS hardware flow control.

  • parity – Parity bit mode.

  • data_size – Bits per character.

  • stop_bit – Number of stop bits.

  • send_only_mode – When True, do not arm reception at bring-up, for write-only traffic, and release the queue and subscription if an earlier bring-up armed them. A later receive/query still works but may have missed data that arrived before that call; a coroutine parked in one when reception is released is woken with DisconnectError, exactly as deinit wakes it.

Returns:

This interface, to allow chaining.

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

  • FirmwareError – If the UART is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), or the firmware rejects the configuration (FW_INVALID_PARAMETER). Neither code sits in the 0x04xx UART band, so neither surfaces as UartError.

  • UartError – If the port is currently held by the USB CDC (status_code FW_UART_IN_VCOM_PORT_MODE). This is the only firmware code in the UART band, and the only UART call upstream documents it for.

async bring_up(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE, send_only_mode: bool = False) → BringUpResult

Bring the UART up, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring the UART up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the UART stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live UART with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Two UART-specific details are worth knowing:

  • send_only_mode is a this-SDK setting, not a firmware one – configure does not take it, because it selects whether the RX queue and its dispatcher subscription are armed. initialize arms or releases them before it talks to the device, so the choice is honoured whichever path runs, in both directions, and is never the thing settings_applied is reporting about.

  • Unlike I2C, no capability guard gates UART reconfiguration and the UART does expose deinit, so in practice the fallback here succeeds. settings_applied is False in two cases: firmware rejected a line parameter it accepted at first bring-up, or hw_handshake was requested on a unit whose capabilities.uart_hw_handshake_effective is False – the one UART setting whose refusal is knowable before the call, and which initialize can only warn about because it returns nothing. Either way the UART is up, the other line parameters did apply, and refusal says which case it was. The two are independent and can both hold on one call; refusal then names both, keeping the reconfigure refusal’s own class and status_code so a caller branching on either still matches. See refused_with.

Parameters:
  • baudrate – Line speed requested for the UART.

  • hw_handshake – RTS/CTS hardware flow control.

  • parity – Parity bit mode.

  • data_size – Bits per character.

  • stop_bit – Number of stop bits.

  • send_only_mode – When True, do not arm reception, for write-only traffic. Honoured whichever path runs; see above.

Returns:

A BringUpResult. On return the UART is up and usable; consult settings_applied before relying on the requested line parameters actually being in force, and refusal for why they are not. It is a report, not a promise: nothing here guarantees a setting the device accepted is in force, only that a refusal this SDK can see is not reported as a success.

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

  • FirmwareError – On any firmware failure other than the two the fallback absorbs.

  • UartError – If the port is currently held by the USB CDC (status_code FW_UART_IN_VCOM_PORT_MODE), which only initialize reports.

async deinit() → None

Release the UART peripheral on the firmware.

After this call the UART is uninitialized on the firmware side; any subsequent UART call comes back as FW_INTERFACE_NOT_INITIALIZED until initialize is called again. Also cancels the internal RX subscription (if one exists) so no stale notifications fire after teardown, and wakes any coroutine parked in receive/query.

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

  • FirmwareError – If the UART was not initialized (status_code FW_INTERFACE_NOT_INITIALIZED, 0x0006). A generic firmware code, not a 0x04xx one, so it does not surface as UartError.

async configure(*, baudrate: UartBaudRate, hw_handshake: UartHwHandshake = UartHwHandshake.DISABLED, parity: UartParity = UartParity.NONE, data_size: UartDataSize = UartDataSize.BITS_8, stop_bit: UartStopBit = UartStopBit.ONE) → None

Reconfigure an already-initialised UART at runtime.

Updates the line parameters without tearing the UART down. It must have been brought up first with initialize; calling this on an uninitialised UART comes back as FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • baudrate – New line speed.

  • hw_handshake – New RTS/CTS hardware flow control setting.

  • parity – New parity bit mode.

  • data_size – New bits per character.

  • stop_bit – New number of stop bits.

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

  • FirmwareError – If the firmware rejects the request — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER when a line parameter is unsupported. Both are generic codes below the 0x04xx UART band, so neither surfaces as UartError.

async set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the UART bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. An out-of-range value is rejected by the firmware and surfaced as SystemFirmwareError.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as UartError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

async use_external_voltage(*, persist: bool = False) → None

Hand the UART rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, SPI and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:

Note

Read back what the adapter measured with get_bus_voltage.

async get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

The asyncio twin of Uart.get_bus_voltage, with the same return type and the same meaning. It is not a UART-specific measurement: the adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and AsyncI2cController.get_bus_voltage, AsyncSpiController.get_bus_voltage and AsyncGpio.get_voltage return the very same number. Pair this with use_external_voltage.

Implemented over getAnalogMeasurements, which reads every rail in one transaction; this keeps the shared-rail field and discards the rest, exactly as upstream’s blocking uartGetBusVoltage does. Use AsyncDevice.get_analog_measurements to get all six rails at once instead of one field per call.

Returns:

The measured voltage in millivolts.

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

  • ProtocolError – If the response was too short to carry the measurements, which is also how firmware that does not implement the command reports itself.

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

  • FirmwareError – On a firmware-reported failure. The measurement command reports no 0x04xx code, so this never surfaces as UartError.

async send(data: bytes) → None

Transmit data over the UART.

A plain transmit. Reception is decided once at bring-up via initialize (armed by default; opt out with send_only_mode=True), so send does not touch the RX path.

Parameters:

data – Payload bytes. Must not exceed the device’s maximum UART message size (AsyncDevice.capabilities.max_uart_message_bytes); a larger value raises ValidationError.

Raises:
  • ValidationError – If data exceeds the device’s maximum message size.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER otherwise. Neither is a 0x04xx code, so neither surfaces as UartError.

async query(data: bytes, *, timeout: float | None = None) → bytes

Send data and return the reply, a request/response round-trip.

The primitive for command/response protocols. It arms reception, drains any RX buffered before the request, transmits, then waits for the next message, so the bytes returned are the reply to this data and not a stale unsolicited message. Prefer this over send + receive, which does not drain and so can return a stale message instead of the reply.

Note

Not safe to call concurrently, nor to mix with receive: the drain step would discard or steal data across overlapping callers.

Parameters:
  • data – Payload bytes to transmit. Must not exceed the device’s maximum UART message size; a larger value raises ValidationError.

  • timeout – Maximum seconds to wait for the reply. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The payload of the reply message.

Raises:
  • ValidationError – If data exceeds the device’s maximum message size, or timeout is negative.

  • DisconnectError – If the device is not open, or if AsyncDevice.close is called while this call is awaiting the reply.

  • CosmicTimeoutError – If no reply arrives within timeout (only when timeout is not None).

  • FirmwareError – On a firmware-reported transmit failure — FW_INTERFACE_NOT_INITIALIZED when the UART has not been brought up, FW_INVALID_PARAMETER otherwise. Neither is a 0x04xx code, so neither surfaces as UartError.

async receive(*, timeout: float | None = None) → bytes

Return the payload of the oldest queued received message.

The pull path for unsolicited inbound data. UART reception is push-driven: the firmware reports incoming bytes as notifications buffered in an internal queue, and each call pops exactly one message (oldest first), never concatenating or draining. For a command/response round-trip use query instead; for continuous push delivery use iter_receive or subscribe via AsyncDevice.subscribe.

Note

Safe to call concurrently with competing-consumer semantics; each message is delivered to exactly one waiter, non-deterministically.

Parameters:

timeout – Maximum seconds to wait for a message when the queue is empty. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The payload of the oldest queued message.

Raises:
iter_receive() → _AsyncUartReceiveStream

Return an async iterator over received message payloads.

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

async with uart.iter_receive() as stream:
    async for message in stream:
        ...

The stream installs its own subscription and queue, independent of receive/query, so iterating does not compete with those for messages.

Returns:

An async context manager / iterator yielding message payloads.

Raises:

DisconnectError – From the iteration — not from this call — if the device is closed, rebooted or reconfigured while a consumer is parked on the stream, which is how receive ends in the same situation. The stream has no other ending. Wrap the async     for in except DisconnectError if a consumer task should end cleanly when the device closes.

Note

Async-only, but not because of any upstream limit. This is an async for idiom layered over the UartRxNotification subscription that both surfaces expose — there is no separate firmware command and nothing async-specific in L1 or L2. The blocking Uart covers the same ground with Uart.receive and Device.subscribe. The same holds for AsyncDevice.iter_notifications.

SPI

class pycosmicsdk.SpiController(*, device: Device)

Bases: object

Blocking SPI controller for an open device.

Obtain one from Device.spi; the constructor is package-private. The interface is cached on the device, so repeated dev.spi() calls return the same controller – which is also what makes “once per connection” hold for the transport negotiation and its log line: the controller lives as long as the open device does, and Device.close drops it.

initialize(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → SpiController

Bring the SPI controller up and return self for chaining.

Not idempotent: a re-run on an already-initialised interface reports FW_INTERFACE_ALREADY_INITIALIZED. The interface stays initialised across reconnects, so a re-run hits this unless the device is reset first. Use configure to change parameters of a live interface.

Parameters:
  • bit_order – Whether each frame goes out most- or least-significant bit first.

  • mode – Clock polarity and phase pair (SPI modes 0 to 3).

  • data_width – Frame size, 8 or 16 bits.

  • chip_select – Which chip-select line to drive.

  • chip_select_polarity – Whether the chip-select line is active low or active high.

  • frequency_hz – SCLK frequency in Hz. Both adapters accept 10_000-50_000_000, with the usable maximum depending on model and hardware revision; a rejected rate surfaces as FirmwareError.

Returns:

This controller, to allow chaining.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the interface is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), or if the firmware rejects the configuration.

bring_up(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → BringUpResult

Bring the SPI controller up, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring the SPI controller up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the interface stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live controller with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Two SPI-specific details are worth knowing:

  • configure reaches every parameter initialize takes, so the fallback path requests exactly what the fresh path would have. There is no SPI setting that survives only the initialize route.

  • No capability guard gates SPI reconfiguration today, so in practice the fallback succeeds and settings_applied is False only if firmware rejects a value it accepted at first bring-up. The CapabilityError arm below is not dead weight: it is what keeps this contract protocol-independent without an edit here if a guard lands.

A refusal raised by initialize itself is a different thing from a refused reconfigure and propagates untouched, whatever its type: nothing reached the device, so the interface is not up, and the first half of bring_up’s contract – on return the interface is usable – cannot be kept by a return value. Only the already-up path can report.

Parameters:
  • bit_order – Bit order requested for each frame – most- or least-significant bit first.

  • mode – Clock polarity and phase pair requested (SPI modes 0 to 3).

  • data_width – Frame size requested, 8 or 16 bits.

  • chip_select – Chip-select line requested.

  • chip_select_polarity – Chip-select active polarity requested.

  • frequency_hz – SCLK frequency in Hz requested. Both adapters accept 10_000-50_000_000, with the usable maximum depending on model and hardware revision.

Returns:

A BringUpResult. On return the controller is up and usable; consult settings_applied before relying on the requested parameters actually being in force.

Raises:
configure(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → None

Reconfigure an already-initialised SPI controller at runtime.

Updates every controller parameter without tearing the interface down. It must have been brought up first with initialize; an uninitialised interface reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • bit_order – New frame bit order.

  • mode – New clock polarity and phase pair.

  • data_width – New frame size, 8 or 16 bits.

  • chip_select – New chip-select line to drive.

  • chip_select_polarity – New chip-select active polarity.

  • frequency_hz – New SCLK frequency in Hz. Both adapters accept 10_000-50_000_000; a rejected rate surfaces as FirmwareError.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the request — notably if the interface has not been initialised.

release() → None

Release the SPI role this peripheral holds.

spiDeinit in upstream’s terms – the same parameterless command SpiTarget.release sends, because there is only one. Firmware dispatches it on the peripheral’s own stored role and tears down whichever role it finds, so it is not two commands and this is not a controller-specific one: after it the peripheral holds no role, and the other role initialises normally.

Why the method exists on this class as well. Not for symmetry. The release is role-agnostic, but the only wrapper over it used to be SpiTarget.release, and Device.spi_target refuses a rev-B Supernova outright – that revision’s SPI level shifter has its chip-select direction pin fixed as an output, so the board cannot receive an incoming chip select. On the pairing that revision forces, with the Supernova as the controller because it cannot be the target, there was no way from Python to hand back a role the device would happily have released. guard_spi_target says a unit cannot serve as a target; it says nothing about relinquishing a role, and firmware applies no revision check to the teardown. So the release belongs on the object that models the role such a unit can actually hold.

I2cController has no counterpart today, and the asymmetry is a reachability one rather than a design one: Device.i2c_target carries no revision gate, so I2cTarget.release is reachable on every unit that has the bus and already serves both of that bus’s roles.

This object keeps no role state, so nothing here is invalidated. The controller’s one latch is the once-per-connection transport log line, which describes the negotiation rather than the role, and the bulk negotiation itself is deliberately untouched for the reason SpiTarget.release gives – it records this host’s transport and the device’s advertised ceilings, neither of which a role release changes.

Note

A `SpiTarget` handle you also hold is not updated by this call. Releasing through here tears down a target role just as effectively, but the target object goes on reporting the SpiTarget.arena_size it was told and keeps its event capture armed for events that can no longer arrive. Call SpiTarget.release when a target is what is armed; this method is for the role this object drives, and for the unit where no SpiTarget can be built at all.

Note

Nothing is absorbed. A peripheral holding no role answers FW_INTERFACE_NOT_INITIALIZED rather than succeeding quietly, matching Uart.deinit and SpiTarget.release; a caller who wants an idempotent release catches that one code.

Note

The first transfer after a release is not proven byte-clean. Firmware carries an open issue on a first-transfer byte skew reachable only through controller → deinit → target – docs_dev/hardware_validation/firmware-role-and-target-mode.md §4 – and this method is one of the two ways to reach that order.

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

  • FirmwareError – If the peripheral held no role (FW_INTERFACE_NOT_INITIALIZED) or the firmware predates the command (FW_UNSUPPORTED_COMMAND). Both are generic codes below the 0x03xx band, so neither surfaces as SpiError, and there is no FW_BUS_NOT_SUPPORTED in the list because there is no bus to name.

set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the SPI bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface — a different band from (and unrelated to) the empty FW_SPI_* one described above. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

use_external_voltage(*, persist: bool = False) → None

Hand the SPI rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, UART and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead. Read back what it measured with get_bus_voltage.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:
get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

This is not an SPI-specific measurement. The adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and this reads that single rail, so the reading reflects whatever last set or is driving it — pair this with use_external_voltage.

I2cController.get_bus_voltage, Uart.get_bus_voltage and Gpio.get_voltage return the very same number. Upstream implements all four by issuing one getAnalogMeasurements command and returning its vmeasVtargI2cSpiUartGpio_mV field (CosmicSDK.cpp); the four names exist to keep each protocol’s surface self-contained, not because four rails are measured. I3cController.get_bus_voltage is the genuine exception: I3C has its own HV and LV rails and reports both.

Returns:

The measured voltage in millivolts.

Raises:
transfer(data: bytes) → bytes

Clock data out on MOSI and return what came back on MISO.

One full-duplex exchange: the transfer is as long as data, so the returned buffer carries the same number of frames. To read from a device that answers a command in later frames, append padding bytes to data and slice the reply out of the result.

Parameters:

data – Frames to transmit. Must not exceed the adapter’s maximum SPI transfer (1024 bytes); a larger value raises ValidationError.

Returns:

The frames received while data was clocked out.

Raises:
  • ValidationError – If data exceeds the maximum transfer size.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure — notably if the interface has not been initialised.

property max_transfer_bytes: int

Largest payload one write or read can carry on this connection.

1024 – MAX_SPI_PAYLOAD_BYTES, the ordinary HID route’s ceiling – unless this host and device can both carry the SPI controller bulk pair, in which case it is the ceiling the device advertised for its SPI command group. That figure differs by model – 32768 on a Supernova, 16384 on a Pulsar, each equal to that product’s whole bulk staging arena – which is why this is a property to read rather than a constant to assume, and why it is not upstream’s compile-time SPI_BULK_MAX_PAYLOAD: a host that hard-codes one product’s number has its oversized requests refused on the other.

It is exactly the ceiling write and read enforce, taken from the same _select_transport call they make, so the number cannot disagree with the refusal. A device that advertised less than the HID route’s 1024 is therefore reported as it advertised, not raised to 1024: the bulk route is the one SpiTransport.AUTO takes when it is available, and that route’s own bound is what applies.

`transfer` is not described by this number and cannot be. It is full-duplex, the bulk write command carries no MISO to hand back, and so there is no bulk command for it to ride: transfer is capped at MAX_SPI_PAYLOAD_BYTES (1024) on every connection, whatever this reports. One number per controller cannot cover a method with a transport choice and a method without one, so this covers the two that have one and names the third rather than averaging them.

Reading it runs the bulk negotiation if it has not run yet, which is at most one device query per connection – and none at all on a build whose extension has no bulk surface, since the seam is a getattr.

Returns:

The per-call ceiling in bytes for write and read.

Raises:

DisconnectError – If the device is no longer open.

property transport: SpiTransport

Which transport write and read use on this connection.

The answer to “did my transfer go over the fast path”, askable before the transfer rather than after it. It is a stable property of the connection, not a record of the last call: the negotiation is latched, and transport selection is otherwise a pure function of that latch and the transport= argument, so this cannot go stale between reading it and calling write.

Reading it runs the negotiation if it has not run yet, which is the only device traffic this property can cause and happens at most once per connection – and none at all on a build whose extension has no bulk surface, or once SpiTransport.HID has been forced, since both short-circuit before the query.

Returns:

SpiTransport.BULK or SpiTransport.HID – never SpiTransport.AUTO.

Raises:

DisconnectError – If the device is no longer open.

write(data: bytes, *, transport: SpiTransport = SpiTransport.AUTO) → None

Clock data out on MOSI and discard what comes back on MISO.

The half-duplex write a flash page program or a register store actually is. Chip select is asserted across the whole payload, exactly once, on either transport.

Prefer this over transfer when the reply frames are not wanted: transfer is fine and unchanged, but only write can reach the bulk transport’s larger payload ceiling, because transfer has to hand back MISO and the bulk write command does not carry it.

Parameters:
  • data – Frames to transmit. Must not be empty. Must not exceed the active transport’s ceiling – 1024 bytes on the HID path, or capabilities.bulk.max_spi_payload_bytes on the bulk one. A larger value raises ValidationError; it is never split across two calls.

  • transport – Which host transport to use. SpiTransport.AUTO, the default, takes the bulk plane when it is available on this host and device and the HID path otherwise.

Raises:
  • ValidationError – If data is empty or exceeds the active transport’s ceiling. No facade call is made.

  • CapabilityError – If transport is SpiTransport.BULK and the bulk transport is unavailable – most often because this host has not claimed the adapter’s vendor USB interface. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure – notably if the interface has not been initialised.

Note

Blocking-only. There is deliberately no AsyncSpiController.write – see AsyncSpiController for the reason.

read(command: bytes, length: int, *, transport: SpiTransport = SpiTransport.AUTO) → bytes

Clock command out, then read length bytes back under the same CS.

The half-duplex read a target’s “read register” or “read page” command actually is: a command phase out, then a data phase in, with chip select asserted across both. Only the data phase is returned – the command bytes are not echoed back, so no slicing is needed at the call site.

Parameters:
  • command – The command bytes to clock out first. May be empty for a target that needs no command phase. On the HID path len(command) + length must fit the 1024-byte transfer.

  • length – How many bytes to read back. Must be at least 1, and must not exceed the active transport’s ceiling. A larger value raises ValidationError; the read is never split, because continuing one requires re-issuing the target’s own read command at an incremented address and this SDK does not know that encoding.

  • transport – Which host transport to use. SpiTransport.AUTO, the default, takes the bulk plane when it is available on this host and device and the HID path otherwise.

Returns:

Exactly length bytes from the data phase.

Raises:
  • ValidationError – If length is less than 1, or the request exceeds the active transport’s ceiling. No facade call is made.

  • CapabilityError – If transport is SpiTransport.BULK and the bulk transport is unavailable – most often because this host has not claimed the adapter’s vendor USB interface. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure – notably if the interface has not been initialised.

  • ProtocolError – If the HID path’s reply is not as long as the frame that produced it, so the data phase this promises is not in it.

Note

The HID fallback issues a single spiTransfer of command + bytes(length) and slices the reply from len(command). That is believed to be an exact equivalent of the bulk read, because one spiTransfer is one frame and holds chip select across all of it – which is the property the bulk read’s two phases exist to preserve. Inferred from the transport’s structure, not confirmed on a bench, and the confirmation is a scope-comparison against a real target once the bulk path is reachable.

Note

⚠ The slice offset is not bench-confirmed either, and there is evidence against it. #99 records two sightings where the data phase did not start at len(command) on the HID path – once as an all-zero read while transfer() returned the target’s id on the same part. The length check above catches a reply that is the wrong size; it cannot catch one that is the right size with the data in the wrong place. Until #99 is settled, transfer() is the honest choice when a read must not be silently wrong, and the bulk path was the stable one in both sightings.

Note

Blocking-only. There is deliberately no AsyncSpiController.read – see AsyncSpiController for the reason.

class pycosmicsdk.AsyncSpiController(*, device: AsyncDevice)

Bases: object

Asyncio SPI controller for an open device.

The asyncio twin of SpiController, returned by AsyncDevice.spi; the constructor is package-private. The interface is cached on the device, so repeated dev.spi() calls return the same controller. Argument validation matches the blocking controller; commands are awaited and correlated through the device’s engine rather than serialized under a lock.

Warning

This class is deliberately NOT a complete mirror of `SpiController`, and it is the first such asymmetry in this package. Do not “finish the surface”.

SpiController.write, SpiController.read and SpiController.transport have no counterpart here, and that is a design decision with a specific cause, not an oversight or an unfinished task.

The reason is in CosmicAsyncSDK. Its spiWriteBulk pushes the payload by calling sendPayload inside the submit, immediately after sendRequest and before any reply, with a 2000 ms timeout (SPI_BULK_PAYLOAD_TIMEOUT_MS). So the “submit” can block for up to two seconds. Every *_async submit in this package is required to be non-blocking – one non-blocking submit per public operation is the whole contract of AsyncDeviceFacade, and it is what lets an awaiting coroutine yield instead of pinning a thread. A submit that can stall for two seconds would stall the asyncio event loop itself, and every other coroutine on it with it, which is worse than not offering the method.

Exposing the half-duplex pair here over the HID path only would be worse still: SpiController.write and SpiController.read are transport-negotiating methods, so an asyncio twin that could never reach the bulk plane would silently mean something different from its blocking namesake.

If an asyncio surface is wanted later, the honest shape is loop.run_in_executor around the blocking call – not a facade *_async submit. AsyncSpiController.transfer is unaffected and stays a full mirror of SpiController.transfer.

Note

The rail surface matches SpiController in full, get_bus_voltage included. CosmicAsyncSDK declares no spiGetBusVoltage, but it does declare getAnalogMeasurements, which carries the shared I2C/SPI/UART/GPIO rail — and that is the same command upstream’s blocking spiGetBusVoltage issues internally. So the async reader is built the same way as the blocking one, and reports the same value.

async initialize(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → AsyncSpiController

Bring the SPI controller up and return self for chaining.

Not idempotent: a re-run on an already-initialised interface reports FW_INTERFACE_ALREADY_INITIALIZED. The interface stays initialised across reconnects, so a re-run hits this unless the device is reset first. Use configure to change parameters of a live interface.

Parameters:
  • bit_order – Whether each frame goes out most- or least-significant bit first.

  • mode – Clock polarity and phase pair (SPI modes 0 to 3).

  • data_width – Frame size, 8 or 16 bits.

  • chip_select – Which chip-select line to drive.

  • chip_select_polarity – Whether the chip-select line is active low or active high.

  • frequency_hz – SCLK frequency in Hz. Both adapters accept 10_000-50_000_000, with the usable maximum depending on model and hardware revision; a rejected rate surfaces as FirmwareError.

Returns:

This controller, to allow chaining.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the interface is already initialised (status_code FW_INTERFACE_ALREADY_INITIALIZED), or if the firmware rejects the configuration.

async bring_up(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → BringUpResult

Bring the SPI controller up, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way to bring the SPI controller up. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

initialize is not idempotent and the interface stays initialised across a close()/reopen cycle, so the second run of any script is the common case rather than an edge case. This tries initialize first; on FW_INTERFACE_ALREADY_INITIALIZED it reconfigures the live controller with configure instead, and reports a refused reconfigure through the return value rather than raising.

Semantics are identical to I2cController.bring_up, and so is the return type, deliberately: one contract across protocols is worth more than a per-protocol shape. Two SPI-specific details are worth knowing:

  • configure reaches every parameter initialize takes, so the fallback path requests exactly what the fresh path would have. There is no SPI setting that survives only the initialize route.

  • No capability guard gates SPI reconfiguration today, so in practice the fallback succeeds and settings_applied is False only if firmware rejects a value it accepted at first bring-up. The CapabilityError arm below is not dead weight: it is what keeps this contract protocol-independent without an edit here if a guard lands.

A refusal raised by initialize itself is a different thing from a refused reconfigure and propagates untouched, whatever its type: nothing reached the device, so the interface is not up, and the first half of bring_up’s contract – on return the interface is usable – cannot be kept by a return value. Only the already-up path can report.

Parameters:
  • bit_order – Bit order requested for each frame – most- or least-significant bit first.

  • mode – Clock polarity and phase pair requested (SPI modes 0 to 3).

  • data_width – Frame size requested, 8 or 16 bits.

  • chip_select – Chip-select line requested.

  • chip_select_polarity – Chip-select active polarity requested.

  • frequency_hz – SCLK frequency in Hz requested. Both adapters accept 10_000-50_000_000, with the usable maximum depending on model and hardware revision.

Returns:

A BringUpResult. On return the controller is up and usable; consult settings_applied before relying on the requested parameters actually being in force.

Raises:
async configure(*, bit_order: SpiBitOrder, mode: SpiMode, data_width: SpiDataWidth, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, frequency_hz: int) → None

Reconfigure an already-initialised SPI controller at runtime.

Updates every controller parameter without tearing the interface down. It must have been brought up first with initialize; an uninitialised interface reports FW_INTERFACE_NOT_INITIALIZED and surfaces as FirmwareError.

Parameters:
  • bit_order – New frame bit order.

  • mode – New clock polarity and phase pair.

  • data_width – New frame size, 8 or 16 bits.

  • chip_select – New chip-select line to drive.

  • chip_select_polarity – New chip-select active polarity.

  • frequency_hz – New SCLK frequency in Hz. Both adapters accept 10_000-50_000_000; a rejected rate surfaces as FirmwareError.

Raises:
  • ValidationError – If frequency_hz does not fit a uint32.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the firmware rejects the request — notably if the interface has not been initialised.

async release() → None

Release the SPI role this peripheral holds.

The asyncio twin of SpiController.release, with the same semantics: no argument, the role released is whichever one the peripheral holds, this object keeps no role state to invalidate, and the shared bulk negotiation is left alone. Read that method for why the release lives on the controller as well as on the target – the short version is that AsyncDevice.spi_target refuses a rev-B Supernova, which leaves this the only way to hand back that unit’s SPI role – and for the first-transfer caveat, which is a bench question this call makes reachable.

Note

An AsyncSpiTarget handle you also hold is not updated by this call; use AsyncSpiTarget.release when a target is what is armed.

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

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

  • FirmwareError – If the peripheral held no role (FW_INTERFACE_NOT_INITIALIZED) or the firmware predates the command (FW_UNSUPPORTED_COMMAND). Neither is absorbed, and both are generic codes rather than SpiError.

async set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the SPI bus target voltage.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware and surfaced as FirmwareError.

  • persist – When True, store the setting across power cycles.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface — a different band from (and unrelated to) the empty FW_SPI_* one described in the module docstring. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

async use_external_voltage(*, persist: bool = False) → None

Hand the SPI rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, UART and GPIO — comes from outside, so it stops driving it itself and measures the supplied rail instead.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:

Note

Read back what the adapter measured with get_bus_voltage.

async get_bus_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

The asyncio twin of SpiController.get_bus_voltage, with the same return type and the same meaning. It is not an SPI-specific measurement: the adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and AsyncI2cController.get_bus_voltage, AsyncUart.get_bus_voltage and AsyncGpio.get_voltage return the very same number. Pair this with use_external_voltage.

Implemented over getAnalogMeasurements, which reads every rail in one transaction; this keeps the shared-rail field and discards the rest, exactly as upstream’s blocking spiGetBusVoltage does. Use AsyncDevice.get_analog_measurements to get all six rails at once instead of one field per call.

Returns:

The measured voltage in millivolts.

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

  • ProtocolError – If the response was too short to carry the measurements, which is also how firmware that does not implement the command reports itself.

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

  • FirmwareError – On a firmware-reported failure.

async transfer(data: bytes) → bytes

Clock data out on MOSI and return what came back on MISO.

One full-duplex exchange: the transfer is as long as data, so the returned buffer carries the same number of frames. To read from a device that answers a command in later frames, append padding bytes to data and slice the reply out of the result.

Parameters:

data – Frames to transmit. Must not exceed the adapter’s maximum SPI transfer (1024 bytes); a larger value raises ValidationError.

Returns:

The frames received while data was clocked out.

Raises:
  • ValidationError – If data exceeds the maximum transfer size.

  • ProtocolError – If the response is too short to carry the frames it claims.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – On a firmware-reported failure — notably if the interface has not been initialised.

SPI — target mode

class pycosmicsdk.SpiTarget(*, device: Device)

Bases: object

Blocking SPI target-mode interface for an open device.

In target mode the adapter stops driving the bus and answers an external SPI controller out of a memory arena drawn from the device’s bulk staging pool – 32 KB on a Supernova, 16 KB on a Pulsar, reported by bring-up and readable through arena_size. The constructor is package-private.

Read the comment block above this class before calling initialize: the role switch is exclusive, release is what reverses it, and because the arena is the device’s shared staging pool, an adapter serves one target protocol at a time – an I2C target and a SPI target cannot be armed at once, though either order works in one session with a release between.

Three differences from SpiController, each of which is the difference between the two roles rather than a gap to be tidied up later:

  • No ``frequency_hz`` and no ``data_width``. A target neither generates the clock nor chooses the frame width; the external controller does both. mode and bit_order are still here because the target has to agree with the controller about them.

  • The arena is bulk-only. There is no HID path for write_memory / read_memory, so initialize refuses outright on a host that has not claimed the adapter’s vendor USB interface. SpiController.transfer is unaffected and needs no bulk plane at all.

  • There is no ``add_register_region``. SpiTargetModel has a REGISTER_MAP member and firmware has a command code for the region table, but no CosmicSDK revision wraps it, and upstream lists it under known gaps with a reason: SPI’s register-map model is driven in band by the command byte, since SPI has no dedicated read/write address bit like I2C’s. A method raising NotImplementedError would advertise a feature the SDK cannot perform, so there is none. I2cTarget has one because I2C’s wrapper exists.

Note

release is the counterpart of initialize, and it is the one method here whose shape was decided before it could be written: this class was left deliberately ready for it. spiDeinit() turned out to take no argument, so the pre-planned signature stands – but two things about it were not anticipated. It releases whichever role the peripheral holds, a controller’s included, so it is not “the target role’s release”; and because firmware’s teardown hands the shared arena back with it, arena_size and the remembered model have to be invalidated rather than merely left alone. release documents both.

property arena_size: int | None

Bytes of staging arena the device reported for this target.

The value that decides whether a preload fits: 32768 on a Supernova and 16384 on a Pulsar today, but a runtime figure the device reports rather than a constant, so read it instead of assuming either. On a Pulsar a 16 KB write fills the arena exactly at offset 0 and overruns it at any other offset.

None until this object has brought the interface up – the same fail-open posture the I3C target takes towards its memory layout. Note that a bare initialize which raised FW_INTERFACE_ALREADY_INITIALIZED leaves it unset, because upstream leaves memorySize at 0 on that path; configure reports it, which makes bring_up the only single call that always leaves this populated.

Returns:

The reported arena size in bytes, or None if unknown.

property max_transfer_bytes: int

Largest payload one write_memory or read_memory call may carry.

min(arena_size, advertised ceiling) – the pair, never upstream’s compile-time SPI_BULK_MAX_PAYLOAD, because the advertised ceiling differs between products (32768 on a Supernova, 16384 on a Pulsar) and a host that assumes one product’s number gets its oversized requests refused on another. 0 when the arena is unreachable on this connection, since no size is honest then – which also makes this the one-call answer to “can this target’s memory be touched at all”, rather than leaving a caller to assemble it out of BulkTransportCapability.supports.

Reading it runs the bulk negotiation if it has not run yet, which is at most one device query per connection.

Returns:

The per-call ceiling in bytes, or 0.

Raises:

DisconnectError – If the device is no longer open.

initialize(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine, require_arena_access: bool = True) → SpiTarget

Switch the adapter into SPI target mode and return self for chaining.

This takes the SPI role, exclusively, and release is what gives it back – the comment block above this class says what that costs while it is held. initialize is also not idempotent: a re-run on an adapter already in SPI target mode reports FW_INTERFACE_ALREADY_INITIALIZED, so prefer bring_up unless you specifically need that faithful failure.

Two refusals happen before anything reaches the device, and both are CapabilityError:

  • A hardware revision that cannot receive a chip select at all. That refusal is this SDK’s own: firmware applies no revision check and would answer SUCCESS, after which the adapter arms and then reports nothing ever again. Read capabilities.spi_target_capable to branch on it.

  • A host or device that cannot reach the arena over the vendor bulk endpoints. Target memory has no HID fallback, so a target brought up without them would be inert. Read capabilities.bulk.

Parameters:
  • mode – Clock polarity and phase pair the external controller uses (SPI modes 0 to 3). The target does not generate the clock, but it has to sample on the same edge.

  • bit_order – Whether each frame arrives most- or least-significant bit first.

  • chip_select – Which chip-select line this target answers on. Echoed back in the bus-event notifications.

  • chip_select_polarity – Whether that line is active low or high.

  • model – Which part the target emulates – SpiTargetModel.MEMORY, SpiTargetModel.FLASH or SpiTargetModel.REGISTER_MAP. It is remembered, so a later configure_flash on a non-flash target is refused here rather than on the wire.

  • engine – Which servicing engine to use – SpiTargetEngine.DMA or SpiTargetEngine.ISR. Firmware reads it for the MEMORY model only and ignores it for the other two, which is why there is no validation pairing the two arguments: passing an engine with a FLASH model is legal and inert.

  • require_arena_access – Whether to refuse when this host and device cannot reach the target’s memory arena. True, the default, is right for every target the host seeds or drains, which is almost all of them: without the vendor bulk endpoints such a target services the bus perfectly while write_memory and read_memory are both unusable, and there is no HID fallback to demote to. Pass False to arm a bus-only target deliberately – one whose contents the external controller writes and this host never touches again.

Returns:

This interface, to allow chaining.

Raises:
  • CapabilityError – If this hardware revision cannot act as a SPI target, or if the arena would be unreachable from this host and device and require_arena_access is set. No facade call is made in either case.

  • DisconnectError – If the device is no longer open.

  • SpiError – If the SPI peripheral is owned by the controller role (status_code FW_SPI_ROLE_CONFLICT), or the shared bulk staging pool the arena comes from is already held – FW_SPI_TARGET_STAGING_UNAVAILABLE, which is what an I2C target currently armed looks like from here – release that one and this call succeeds. Both sit in the 0x03xx band, so unlike the I3C target’s failures these do surface as the protocol-specific class.

  • FirmwareError – If the adapter is already in SPI target mode (status_code FW_INTERFACE_ALREADY_INITIALIZED, a generic code rather than a 0x03xx one), or the firmware rejects the configuration.

bring_up(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine, require_arena_access: bool = True) → BringUpResult

Bring SPI target mode up, re-runnably, and report what took effect.

The composite over initialize and configure that a re-runnable script wants, and the recommended way in. Call it instead of initialize unless you specifically need the primitive’s faithful FW_INTERFACE_ALREADY_INITIALIZED.

It is also the only single call that always leaves arena_size populated: upstream leaves memorySize at 0 on the already-up path, so a bare initialize that raises there reports no capacity, while the reconfigure this falls back to does.

settings_applied is always True here, and that is honest rather than a placeholder – spiTargetSetParameters documents no refusal status to absorb and no capability gates it, so there is nothing for the second call to report. already_initialized varies, and it is the bit a re-run cares about. The comment block above this class explains at length why no refused-status filter belongs here, and tests/unit/test_spi_target.py pins its absence.

A firmware failure initialize itself raises propagates untouched – notably FW_SPI_ROLE_CONFLICT, which must never be absorbed: the adapter would still be driving the bus as a controller, so no return value could keep this method’s contract.

Parameters:
  • mode – Clock polarity and phase pair requested.

  • bit_order – Frame bit order requested.

  • chip_select – Chip-select line this target answers on.

  • chip_select_polarity – Chip-select active polarity requested.

  • model – Which part the target emulates. See initialize.

  • engine – Which servicing engine to use. See initialize.

  • require_arena_access – Whether to refuse when the target’s memory arena would be unreachable. See initialize.

Returns:

A BringUpResult. On return the target is up and the requested parameters are in force.

Raises:
  • CapabilityError – As initialize, and for the same two reasons. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • SpiError – On a 0x03xx failure from either call – a role conflict or an unavailable staging pool.

  • FirmwareError – On any firmware failure other than the one the fallback absorbs.

release() → None

Release the SPI role, and the arena with it.

spiDeinit in upstream’s terms, and the call the comment block above this class was written waiting for. After it the peripheral holds no role, so a controller bring-up succeeds – an in-session release, with no reboot and no fresh Device.

No argument, because the adapter has one SPI peripheral. That is the asymmetry with I2cTarget.release, which takes its bus from the interface: upstream follows the firmware’s request structs rather than symmetry, and spi_mgr.c reads only the header.

It releases whichever role the peripheral holds, not specifically a target one. A controller is torn down by this same call, with the armed transfer aborted and the FIFOs emptied. The method lives here because this is where the need arises.

What this object forgets. arena_size and the remembered model go back to None, because the arena they describe is gone – firmware’s teardown hands the shared bulk-staging pool back, which is what lets an I2C target arm afterwards. They are cleared even when the call fails, since a refusal or a timeout leaves the arena’s fate unknown and None is this class’s word for unknown. max_transfer_bytes therefore falls back to the advertised ceiling, exactly as before a bring-up.

Event capture is torn down too – the subscription is cancelled and any thread parked in wait_for_event is woken with DisconnectError, following Uart.deinit – but only when the release succeeded. A release that failed may have left a live target whose events are the only record that a transaction happened.

The bulk negotiation is deliberately not dropped: it records this host’s transport and the device’s advertised ceilings, neither of which a role release changes, and the latch is shared with SpiController.

Note

Nothing is absorbed. A peripheral holding no role answers FW_INTERFACE_NOT_INITIALIZED rather than succeeding quietly, matching Uart.deinit; a caller who wants an idempotent release catches that one code.

Note

The first transfer after a release is not proven byte-clean. Firmware carries an open issue on a first-transfer byte skew reachable only through controller → deinit → target, and the candidate fix is not confirmed closed – docs_dev/hardware_validation/firmware-role-and-target-mode.md §4. That is a bench question, and this method makes it reachable for the first time from Python.

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

  • FirmwareError – If the peripheral held no role (FW_INTERFACE_NOT_INITIALIZED) or the firmware predates the command (FW_UNSUPPORTED_COMMAND). Both are generic codes below the 0x03xx band, so neither surfaces as SpiError, and there is no FW_BUS_NOT_SUPPORTED in the list because there is no bus to name. The second is the one a caller cannot pre-empt: firmware’s version string is a compile-time constant, so a build that dispatches the command and one that does not both report the same version.

configure(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine) → None

Reapply the target parameters of an adapter already in SPI target mode.

The idempotent counterpart of initialize, and the call upstream names when spiTargetInit reports FW_INTERFACE_ALREADY_INITIALIZED. It keeps the arena’s contents and reports the arena’s size, so arena_size is populated afterwards either way.

The adapter must already hold the SPI target role; one that does not reports FW_INTERFACE_NOT_INITIALIZED.

Parameters:
  • mode – New clock polarity and phase pair.

  • bit_order – New frame bit order.

  • chip_select – New chip-select line to answer on.

  • chip_select_polarity – New chip-select active polarity.

  • model – Which part the target emulates. See initialize.

  • engine – Which servicing engine to use. See initialize.

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

  • FirmwareError – If the firmware rejects the request – notably when the adapter is not in SPI target mode (FW_INTERFACE_NOT_INITIALIZED). No `SpiError` here, and the absence is the point: spiTargetSetParameters documents no 0x03xx status at all – no refusal, no role conflict, no staging failure – which is exactly why bring_up’s reconfigure arm absorbs nothing.

configure_flash(*, address_bytes: int, page_size: int, write_cycle_us: int, jedec_id: bytes = b'', require_write_enable: bool = True) → None

Describe the flash part a FLASH-model target emulates.

Only meaningful on a target brought up with the FLASH model. When this object selected the model itself and it was a different one, the call is refused locally with ValidationError rather than sent for firmware to answer FW_SPI_TARGET_WRONG_MODEL; when the model is unknown – an adapter another process left in target mode – it is sent, and firmware answers.

Parameters:
  • address_bytes – Width of the address phase the emulated part expects, 2 or 3.

  • page_size – Program page size in bytes. Must be a power of two.

  • write_cycle_us – How long a page program takes, in microseconds, so a datasheet’s “5 ms write cycle” is 5000. Deliberately not float seconds like a host timeout: this is the emulated part’s own datasheet number.

  • jedec_id – The bytes answered to a read-id command, 0 to 8 of them. One argument rather than upstream’s (idLength, id[8]) pair, exactly as the I3C target’s pid already resolves – the length is derived from what you pass, so the two cannot disagree.

  • require_write_enable – Whether a program needs a write-enable latch set first, which is real flash behaviour. This is flags bit 0; every other bit of that byte is reserved and documented “send 0”, which is why this is a boolean and not a raw int.

Raises:
  • ValidationError – If address_bytes is not 2 or 3, page_size is not a positive power of two, jedec_id is longer than 8 bytes, page_size or write_cycle_us does not fit its field, or this target is known to be running a different model. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • SpiError – If firmware rejects the configuration (FW_SPI_TARGET_INVALID_CONFIG) or the target is running a different model (FW_SPI_TARGET_WRONG_MODEL).

write_memory(address: int, data: bytes) → None

Preload data into the target’s arena at byte offset address.

Seeds the bytes the external controller will read back. ``address`` is a byte offset into the arena, not a scaled register index – the opposite of I3cTarget.write_memory, which shares this parameter’s name and type and multiplies it by the configured register width. Nothing is scaled here.

Once this returns successfully the arena is bus-ready. Firmware defers its reply until the servicing engine has re-armed, which was a real race found during hardware validation and fixed there, so no caller-side settle delay should ever be necessary – needing one indicates a firmware regression rather than something to work around on the host.

Parameters:
  • address – Byte offset into the arena to start writing at. Must fit a uint16.

  • data – Payload bytes. Must not be empty, and must fit the arena from address onwards without exceeding the advertised bulk payload ceiling. It is never split.

Raises:
  • ValidationError – If address does not fit a uint16, data is empty, or the access runs past the smaller of the arena and the ceiling. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • SpiError – On a 0x03xx firmware failure – notably FW_SPI_TARGET_SURPASSED_MEMORY_SIZE when the device’s own bound is tighter than this SDK could know, or FW_SPI_TARGET_PAYLOAD_TRANSFER_FAILED.

  • FirmwareError – If the adapter is not in SPI target mode.

Note

Blocking-only, and there is deliberately no asyncio twin – see AsyncSpiTarget for the reason.

Note

Do not call this from inside a notification subscriber callback. Subscriber callbacks run on the dispatcher thread, which delivers every notification for the connection in order; a bulk transfer can hold it for the full payload timeout (2 s per phase), during which nothing else is delivered and the bounded notification queue starts dropping its oldest entries. It is not a deadlock and it is not forbidden, but it is congestion the caller controls and the SDK cannot. Record the event in the callback and move the bytes on your own thread.

read_memory(address: int, length: int) → bytes

Read length bytes back out of the target’s arena from address.

Reads what the external controller wrote. address is a byte offset, exactly as in write_memory, and is scaled by nothing.

Parameters:
  • address – Byte offset into the arena to start reading at. Must fit a uint16.

  • length – Number of bytes to read. Must be at least 1, and must fit the arena from address onwards without exceeding the advertised bulk payload ceiling.

Returns:

Exactly length bytes from the arena.

Raises:
  • ValidationError – If address does not fit a uint16, length is zero, or the access runs past the smaller of the arena and the ceiling. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • SpiError – On a 0x03xx firmware failure – notably FW_SPI_TARGET_SURPASSED_MEMORY_SIZE.

  • FirmwareError – If the adapter is not in SPI target mode.

Note

Blocking-only, and there is deliberately no asyncio twin – see AsyncSpiTarget for the reason.

Note

Do not call this from inside a notification subscriber callback, for the reason write_memory gives. The bus-event notification carries only metadata – chip select and payload length; its arena offset is a firmware placeholder, see SpiTargetBusEvent – and never the bytes, so fetching them always means a call like this one, which makes the dispatcher-thread trap easy to fall into here.

Note

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

wait_for_event(*, timeout: float | None = None) → SpiTargetBusEvent

Return the oldest queued bus transaction, waiting for one if none is queued.

The pull path for what the external controller does to this target. Events are queued as the firmware reports them and each call pops exactly one, oldest first; the queue is never trimmed, so a burst serviced between two calls is delivered in full. For push delivery, subscribe to SpiTargetBusEventNotification via Device.subscribe.

The event carries no bytes, and on current firmware it does not carry a usable offset either. Both are the device’s shape rather than omissions here. The notification is metadata only – upstream says so outright – and its memory_address is hard-coded to 0 at both firmware emit sites regardless of where in the arena the transaction landed, because the FLASH and REGISTER_MAP models track their address internally and never report it. So the event tells you that a transaction completed, on which chip select, and how many bytes it moved; where it landed is something you know from the protocol you are emulating, and you pass that offset to read_memory yourself.

There is deliberately no ``read_event_payload`` convenience. One was designed, and it cannot be written honestly while the offset is a placeholder: it would read arena byte 0 on every call while its signature implied it read the region the event named. It becomes worth adding if firmware starts populating the field.

Note

Safe to call concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Warning

The bus-event path has no upstream test. Both of upstream’s own hardware tests bypass the bus-event accessors and poll the arena directly, and upstream says so in its known-gaps list: anyone building a real target-mode application is the first to exercise this path. Test it against a real controller before trusting it, and do not assume the I3C target’s experience transfers.

Parameters:

timeout – Maximum seconds to wait when nothing is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:
class pycosmicsdk.AsyncSpiTarget(*, device: AsyncDevice)

Bases: object

Asyncio SPI target-mode interface for an open device.

The asyncio twin of SpiTarget, minus its arena’s bytes. Configuration in full, arena_size included; memory access not at all. Read the comment block above SpiTarget before calling initialize – the role switch is exclusive, and release is what reverses it.

Warning

The bulk payload methods are deliberately absent from every async class in this package, and this is a design decision with a specific cause. Do not “finish the surface”.

The vendor bulk endpoint is an unframed byte stream. A bulk reply announces a length on the HID path and the bytes then arrive on the bulk endpoint with no transaction id, no header and no delimiter; the transport drains whatever is left over once the announced count has been collected. So the only thing that associates a payload with the command that asked for it is that nothing else was in flight.

One bulk transfer therefore owns the connection from submit to drain. Two overlapping bulk reads do not merely queue: the first one’s drain discards the head of the second one’s payload and the second times out. The read after that can be prefixed by up to one endpoint packet of bytes that were never meant for it – they come back plausible, the status comes back successful, and there is nothing on the endpoint to detect it with.

That last sentence used to be justified by “a timed-out collection does not drain”, which stopped being true when CosmicSDK 712e433 made both failure paths fall through to the shared drain(). The exposure is narrower now and it is not gone: the drain is guarded only by readPayload having been entered, and a bulk read that firmware refuses on the command channel returns before that (CosmicSDK.cpp:2956), so nothing drains on that path at all. A firmware refusal on a bulk read is the live case, not a timeout.

An async def that has to hold a connection-exclusive lock across its own await is a blocking call with a coroutine’s syntax, and it would be advertising concurrency the transport cannot provide. Use the blocking Device for arena traffic, where the per-device lock already provides that exclusion and the signature does not promise otherwise.

Note that one adapter has one bulk plane: the vendor interface is claimed exclusively by whichever handle opened first, so a Device and an AsyncDevice on the same adapter cannot both use it. Choose one handle style per adapter.

Two consequences of that absence worth stating outright, because they are where this class and its blocking twin actually diverge – and the first is a narrower divergence than it looks, because the arena’s size survives even though its bytes do not:

  • `arena_size` is here, but `SpiTarget.max_transfer_bytes` is not. The size is reachable and the ceiling is not, and the split is the bulk plane’s asymmetry rather than an oversight. CosmicAsyncSDK’s target init carries no memorySize out-parameter, but the number is on the wire – SpiTargetInitResponseParameters is a packed {uint32 memorySize} immediately after the generic response header, and the async engine already delivers every response’s post-header bytes to this layer – so _decode_arena_size reads it here in pure Python, exactly as AsyncI2cTarget.arena_size reads its own {uint8 busId; uint32 memorySize}. max_transfer_bytes cannot follow: it is min(arena_size, advertised ceiling) or 0, and both of its other terms – the advertised per-call ceiling and the arena operation bits – live in the negotiated capability record, which no async facade can decode (next bullet). A property here could only relabel the arena size as a transfer ceiling, and its 0 could never carry the “unreachable arena” meaning that is the blocking property’s main reason to exist. Use SpiTarget.max_transfer_bytes on the handle that will move the bytes, which is the handle that can answer.

  • This class gates on only the *host* half of the bulk plane, where SpiTarget.initialize gates on the whole negotiated capability. That is not a preference: CosmicAsyncSDK’s getBulkCapabilities is a bare submit whose reply only the blocking CosmicSDK wrapper decodes, so no capability record is reachable from an async facade at all. initialize can therefore ask whether this machine holds the adapter’s vendor interface and nothing else. That is the half carrying the information – firmware derives supported from a group count that is never zero – but it is a real asymmetry between the two surfaces, and it is why a snapshot only ever touched through the asyncio surface never has its bulk capability negotiated at all. Pass require_arena_access=False to arm a bus-only target deliberately, accepting an arena nobody can reach – which on a single adapter is the situation regardless, since this handle would hold the plane. Bring the target up here, watch its bus events here, and do arena traffic from a blocking Device on a different adapter; or do the whole thing blocking.

property arena_size: int | None

Bytes of staging arena the device reported for this target.

The same figure and the same semantics as SpiTarget.arena_size, and deliberately not a weaker version of them: None until this object has armed or reconfigured the interface, then whatever the most recent init or reconfigure announced. Decoded from the response packet’s {uint32 memorySize} parameters, because CosmicAsyncSDK has no out-parameter to carry it.

A bare initialize that raised FW_INTERFACE_ALREADY_INITIALIZED leaves it unset – firmware announces no size on that path and the response never reaches the decode – while configure reports it, which makes bring_up the only single call that always leaves it populated. Identical to the blocking twin down to that case.

Returns:

The reported arena size in bytes, or None if unknown.

async initialize(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine, require_arena_access: bool = True) → AsyncSpiTarget

Switch the adapter into SPI target mode and return self for chaining.

The asyncio twin of SpiTarget.initialize, with the same arguments and the same non-idempotence: prefer bring_up. The hardware-revision refusal is identical and fires before the submit; the bulk-plane refusal is not made here, for the reason in this class’s docstring.

Parameters:
  • mode – Clock polarity and phase pair the external controller uses.

  • bit_order – Whether each frame arrives most- or least-significant bit first.

  • chip_select – Which chip-select line this target answers on.

  • chip_select_polarity – Whether that line is active low or high.

  • model – Which part the target emulates. See SpiTarget.initialize.

  • engine – Which servicing engine to use. Same.

  • require_arena_access – Whether to refuse when the target’s memory arena would be unreachable. See initialize.

Returns:

This interface, to allow chaining.

Raises:
  • CapabilityError – If this hardware revision cannot act as a SPI target. The refusal is this SDK’s own – firmware would answer SUCCESS – and nothing is submitted.

  • DisconnectError – If the device is no longer open.

  • ProtocolError – If the response is too short to carry the arena size. The role was taken – the submit succeeded – so configure is the way to recover a figure for arena_size.

  • SpiError – On a 0x03xx failure – FW_SPI_ROLE_CONFLICT or FW_SPI_TARGET_STAGING_UNAVAILABLE.

  • FirmwareError – If the adapter is already in SPI target mode (FW_INTERFACE_ALREADY_INITIALIZED), or the firmware rejects the configuration.

async bring_up(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine, require_arena_access: bool = True) → BringUpResult

Bring SPI target mode up, re-runnably, and report what took effect.

The asyncio twin of SpiTarget.bring_up, with the same contract: one absorbed status, no refused-status filter, and settings_applied always True because spiTargetSetParameters documents no refusal to absorb. FW_SPI_ROLE_CONFLICT propagates. It is also, here as there, the only single call that always leaves arena_size populated.

Parameters:
  • mode – Clock polarity and phase pair requested.

  • bit_order – Frame bit order requested.

  • chip_select – Chip-select line this target answers on.

  • chip_select_polarity – Chip-select active polarity requested.

  • model – Which part the target emulates. See SpiTarget.initialize.

  • engine – Which servicing engine to use. See SpiTarget.initialize.

  • require_arena_access – Whether to refuse when the target’s memory arena would be unreachable from this host. See initialize.

Returns:

A BringUpResult. On return the target is up and the requested parameters are in force.

Raises:
  • CapabilityError – If this hardware revision cannot act as a SPI target. Nothing is submitted.

  • DisconnectError – If the device is no longer open.

  • ProtocolError – If either response is too short to carry the arena size.

  • SpiError – On a 0x03xx failure from either call.

  • FirmwareError – On any firmware failure other than the one the fallback absorbs.

async release() → None

Release the SPI role, and the arena with it.

The asyncio twin of SpiTarget.release, with the same semantics: no argument, the role released is whichever one the peripheral holds, arena_size and the remembered model go back to None on every path, event capture is cancelled only on success, and the shared bulk negotiation is left alone. Read that method for the first-transfer caveat, which is a bench question this call makes reachable.

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

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

  • FirmwareError – If the peripheral held no role (FW_INTERFACE_NOT_INITIALIZED) or the firmware predates the command (FW_UNSUPPORTED_COMMAND). Neither is absorbed, and both are generic codes rather than SpiError.

async configure(*, mode: SpiMode, bit_order: SpiBitOrder, chip_select: SpiChipSelect, chip_select_polarity: SpiChipSelectPolarity, model: SpiTargetModel, engine: SpiTargetEngine) → None

Reapply the target parameters of an adapter already in SPI target mode.

The asyncio twin of SpiTarget.configure. It keeps the arena’s contents and, like the blocking twin, reports the arena’s size: the figure is on the response packet and _decode_arena_size reads it, so arena_size is populated afterwards either way.

Parameters:
  • mode – New clock polarity and phase pair.

  • bit_order – New frame bit order.

  • chip_select – New chip-select line to answer on.

  • chip_select_polarity – New chip-select active polarity.

  • model – Which part the target emulates. See SpiTarget.initialize.

  • engine – Which servicing engine to use. See SpiTarget.initialize.

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

  • ProtocolError – If the response is too short to carry the arena size.

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

  • FirmwareError – If the firmware rejects the request – notably when the adapter is not in SPI target mode (FW_INTERFACE_NOT_INITIALIZED). No `SpiError` here, for the reason SpiTarget.configure gives: this command documents no 0x03xx status at all.

async configure_flash(*, address_bytes: int, page_size: int, write_cycle_us: int, jedec_id: bytes = b'', require_write_enable: bool = True) → None

Describe the flash part a FLASH-model target emulates.

The asyncio twin of SpiTarget.configure_flash, with the same validation and the same model pre-guard. It carries no payload, so it is an ordinary non-blocking submit.

Parameters:
  • address_bytes – Width of the address phase the emulated part expects, 2 or 3.

  • page_size – Program page size in bytes. Must be a power of two.

  • write_cycle_us – How long a page program takes, in microseconds.

  • jedec_id – The bytes answered to a read-id command, 0 to 8 of them.

  • require_write_enable – Whether a program needs a write-enable latch set first. This is flags bit 0.

Raises:
  • ValidationError – If any argument is outside its documented range, or this target is known to be running a different model. Nothing is submitted.

  • DisconnectError – If the device is no longer open.

  • SpiError – If firmware rejects the configuration (FW_SPI_TARGET_INVALID_CONFIG) or the target is running a different model (FW_SPI_TARGET_WRONG_MODEL).

async wait_for_event(*, timeout: float | None = None) → SpiTargetBusEvent

Return the oldest queued bus transaction, awaiting one if none is queued.

The asyncio twin of SpiTarget.wait_for_event, and the half of the target surface the async side is genuinely good at: observation costs no payload transfer, so nothing here blocks the event loop. Events are queued as the firmware reports them and each call pops exactly one, oldest first; the queue is never trimmed. For push delivery, subscribe to SpiTargetBusEventNotification via AsyncDevice.subscribe, or iterate AsyncDevice.iter_notifications, whose async for body runs on the event loop rather than on the dispatcher thread.

The bytes are not here, and this class cannot fetch them. The event carries metadata only, by the device’s design, and the arena is reachable only from a blocking Device – see this class’s docstring. Nor is memory_address usable: firmware hard-codes it to 0. So an async target reports that a transaction completed, on which chip select, and how many bytes it moved; reading the bytes back is a blocking handle’s job, and knowing where they landed is the emulated protocol’s.

Note

Safe to await concurrently with competing-consumer semantics; each event is delivered to exactly one waiter, non-deterministically.

Warning

The bus-event path has no upstream test, for the reason SpiTarget.wait_for_event gives at length.

Parameters:

timeout – Maximum seconds to wait when nothing is queued. None waits indefinitely; 0 returns immediately; a positive value waits up to that long. A negative value is rejected.

Returns:

The oldest queued bus event.

Raises:

GPIO

class pycosmicsdk.Gpio(*, device: Device)

Bases: object

Blocking GPIO interface for an open device.

Obtain one from Device.gpio; the constructor is package-private. The interface is cached on the device, so repeated dev.gpio() calls return the same instance — and with it the recorded pin configuration.

set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the GPIO pins’ target voltage.

The GPIO pins share the multifunction rail with I2C, SPI and UART, so this moves all four together.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware.

  • persist – When True, store the setting across power cycles, so downstream hardware is powered from the moment the adapter starts up.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as GpioError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

use_external_voltage(*, persist: bool = False) → None

Hand the GPIO rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, SPI and UART — comes from outside, so it stops driving it itself and measures the supplied rail instead. Read back what it measured with get_voltage.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:
get_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

This is not a GPIO-specific measurement. The adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and this reads that single rail, so the reading reflects whatever last set or is driving it — pair this with use_external_voltage.

I2cController.get_bus_voltage, SpiController.get_bus_voltage and Uart.get_bus_voltage return the very same number. Upstream implements all four by issuing one getAnalogMeasurements command and returning its vmeasVtargI2cSpiUartGpio_mV field (CosmicSDK.cpp); the four names exist to keep each protocol’s surface self-contained, not because four rails are measured. The name differs here only because upstream calls it gpioGetVoltage — GPIO pins are not a bus — but the reading is identical. I3cController.get_bus_voltage is the genuine exception: I3C has its own HV and LV rails and reports both.

Returns:

The measured voltage in millivolts.

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

  • FirmwareError – On a firmware-reported failure. This is a system measurement command, so it reports no 0x05xx code and never surfaces as GpioError – catch the base class.

configure_pin(pin: GpioPinNumber, *, mode: GpioFunctionality, initial: GpioLogicLevel | None = None) → None

Configure pin as a digital input or output.

Required before any other operation on the pin: an unconfigured pin is rejected by write, read and configure_interrupt, and by the firmware with FW_GPIO_NOT_CONFIGURED. Re-running it on a configured pin is allowed and switches the direction, which drops any interrupt armed on it.

Parameters:
  • pin – The pin to configure.

  • mode – Whether the pin becomes a digital input or a digital output.

  • initial – Level to drive as soon as an output pin is configured. Defaults to GpioLogicLevel.LOW, and is ignored by the firmware for an input pin.

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

  • FirmwareError – If the firmware rejects the configuration (FW_INVALID_PARAMETER). Upstream gpioConfigurePin reports no 0x05xx code – the two that exist, FW_GPIO_NOT_CONFIGURED and FW_GPIO_WRONG_CONFIGURATION, are answers to operating a pin, not to configuring one – so this never surfaces as GpioError.

write(pin: GpioPinNumber, level: GpioLogicLevel) → None

Drive pin to level.

Parameters:
Raises:
read(pin: GpioPinNumber) → GpioLogicLevel

Return the logic level present on pin.

Parameters:

pin – The pin to sample. Must be configured as GpioFunctionality.DIGITAL_INPUT.

Returns:

The level the firmware read on the pin.

Raises:
  • ValidationError – If pin is unconfigured or configured as an output. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • GpioError – On a firmware-reported failure.

configure_interrupt(pin: GpioPinNumber, trigger: GpioTriggerType) → None

Arm the firmware interrupt on pin for trigger.

This configures the interrupt only. To receive the events, subscribe to GpioInterruptNotification on the device and filter by pin:

gpio.configure_interrupt(GpioPinNumber.PIN_3, GpioTriggerType.RISING_EDGE)
sub = dev.subscribe(
    GpioInterruptNotification,
    on_irq,
    where=lambda n: n.pin_number == GpioPinNumber.PIN_3,
)

Not every pin supports interrupts, and which do is a property of the adapter model and hardware revision rather than of this API. Ask dev.capabilities.supports_gpio_interrupt(pin) rather than discovering it by failing; a pin that answers False is refused here with CapabilityError before anything is sent to the device.

One such limit is bench-confirmed: on a Supernova hardware Rev B, PIN_3 cannot be armed for any trigger type — its firmware hard-codes the pin as interrupt-incapable, so no argument change makes it work; pick another pin. See docs_dev/hardware_validation/gpio-interrupt-pin3-supernova-revb.md. Other revisions and models read as supporting PIN_3 in the firmware source, and are therefore attempted rather than refused — but that reading is unconfirmed on hardware, so a FirmwareError naming FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE remains possible for a pin this SDK reports as capable.

Parameters:
  • pin – The pin to arm. Must be configured as GpioFunctionality.DIGITAL_INPUT, and must be one of dev.capabilities.interrupt_capable_gpio_pins.

  • trigger – Which edge (or both) raises the interrupt.

Raises:
  • CapabilityError – If the adapter’s model and hardware revision are known not to support an interrupt on pin — the Rev B PIN_3 limit above. The message names the pin, the model and the revision, and lists the pins that do work. No facade call is made, so this replaces the firmware round trip rather than following it. Checked before the direction guard, because a revision limit is unfixable while a missing or wrong configure_pin is not: reporting the fixable problem first would send the caller away to do work this method then refuses.

  • ValidationError – If pin is unconfigured or configured as an output. No facade call is made. On a pin this unit’s revision cannot arm at all, CapabilityError is raised instead — that guard runs first.

  • DisconnectError – If the device is no longer open.

  • FirmwareError – If the pin does not support interrupts (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE, 0x0005) or the pin or trigger is rejected (FW_INVALID_PARAMETER, 0x0004). Both are generic codes that sit below the 0x05xx GPIO band, so they arrive as a plain FirmwareError and are not caught by except GpioError. A pin whose limit this SDK already records raises CapabilityError instead, so reaching the 0x0005 case here means an interrupt limit the revision table does not yet know about.

  • GpioError – If the firmware reports the pin unconfigured (FW_GPIO_NOT_CONFIGURED, 0x0501) or configured as an output (FW_GPIO_WRONG_CONFIGURATION, 0x0502) — the 0x05xx GPIO band. Reaching these takes the device disagreeing with the pin state this interface mirrors, since a locally known-bad mode raises ValidationError before any call is made; a pin reconfigured out from under this handle does it.

Note

To catch every firmware-reported failure of this method in one clause, catch FirmwareError — GpioError is a subclass of it. CapabilityError is not: it is raised before any request is sent, and is a sibling of FirmwareError under CosmicError.

disable_interrupt(pin: GpioPinNumber) → None

Disarm the firmware interrupt on pin.

Leaves any GpioInterruptNotification subscription in place; it simply stops receiving events for this pin.

Parameters:

pin – The pin to disarm. Must be configured as GpioFunctionality.DIGITAL_INPUT.

Raises:
  • ValidationError – If pin is unconfigured or configured as an output. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • GpioError – On a firmware-reported failure.

class pycosmicsdk.AsyncGpio(*, device: AsyncDevice)

Bases: object

Asyncio GPIO interface for an open device.

The asyncio twin of Gpio, returned by AsyncDevice.gpio; the constructor is package-private. The interface is cached on the device, so repeated dev.gpio() calls return the same instance. Direction enforcement and argument validation match the blocking interface; commands are awaited and correlated through the device’s engine rather than serialized under a lock. Interrupt events are delivered through AsyncDevice.subscribe, which is synchronous on the async device too.

Note

The rail surface matches Gpio in full, get_voltage included. CosmicAsyncSDK declares no gpioGetVoltage, but it does declare getAnalogMeasurements, which carries the shared I2C/SPI/UART/GPIO rail — and that is the same command upstream’s blocking gpioGetVoltage issues internally. So the async reader is built the same way as the blocking one, and reports the same value.

async set_voltage(voltage_mv: int, *, persist: bool = False) → None

Set the GPIO pins’ target voltage.

The GPIO pins share the multifunction rail with I2C, SPI and UART, so this moves all four together.

Parameters:
  • voltage_mv – Target voltage in millivolts. The multifunction port accepts 0 (rail off) and 1200-3300 mV; an out-of-range value is rejected by the firmware.

  • persist – When True, store the setting across power cycles, so downstream hardware is powered from the moment the adapter starts up.

Raises:
  • ValidationError – If voltage_mv does not fit a uint16.

  • DisconnectError – If the device is no longer open.

  • SystemFirmwareError – If the firmware rejects the voltage. Voltage rejection is FW_VOLTAGE_OUT_OF_RANGE (0x0101), which sits in the 0x01xx system band shared by every interface, so it does not surface as GpioError. With firmware 4.7.0 or newer, a port that another source already powers is refused with FW_EXTERNAL_VOLTAGE_DETECTED (0x0102) and the rail stays off; call use_external_voltage instead.

async use_external_voltage(*, persist: bool = False) → None

Hand the GPIO rail over to an externally supplied voltage.

Tells the adapter that the multifunction VTARG rail — shared with I2C, SPI and UART — comes from outside, so it stops driving it itself and measures the supplied rail instead.

Parameters:

persist – When True, store the voltage measured now. After a power cycle the adapter drives the rail itself at that voltage; it does not store “external”.

Raises:

Note

Read back what the adapter measured with get_voltage.

async get_voltage() → int

Read the voltage measured on the shared multifunction VTARG rail.

The asyncio twin of Gpio.get_voltage, with the same return type and the same meaning. It is not a GPIO-specific measurement: the adapter has one multifunction VTARG rail shared by I2C, SPI, UART and GPIO, and AsyncI2cController.get_bus_voltage, AsyncSpiController.get_bus_voltage and AsyncUart.get_bus_voltage return the very same number. Pair this with use_external_voltage.

Implemented over getAnalogMeasurements, which reads every rail in one transaction; this keeps the shared-rail field and discards the rest, exactly as upstream’s blocking gpioGetVoltage does. Use AsyncDevice.get_analog_measurements to get all six rails at once instead of one field per call.

Returns:

The measured voltage in millivolts.

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

  • ProtocolError – If the response was too short to carry the measurements, which is also how firmware that does not implement the command reports itself.

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

  • FirmwareError – On a firmware-reported failure. This is a system measurement command, so it reports no 0x05xx code and never surfaces as GpioError – catch the base class.

async configure_pin(pin: GpioPinNumber, *, mode: GpioFunctionality, initial: GpioLogicLevel | None = None) → None

Configure pin as a digital input or output.

Required before any other operation on the pin: an unconfigured pin is rejected by write, read and configure_interrupt, and by the firmware with FW_GPIO_NOT_CONFIGURED. Re-running it on a configured pin is allowed and switches the direction, which drops any interrupt armed on it.

Parameters:
  • pin – The pin to configure.

  • mode – Whether the pin becomes a digital input or a digital output.

  • initial – Level to drive as soon as an output pin is configured. Defaults to GpioLogicLevel.LOW, and is ignored by the firmware for an input pin.

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

  • FirmwareError – If the firmware rejects the configuration (FW_INVALID_PARAMETER). Upstream gpioConfigurePin reports no 0x05xx code – the two that exist, FW_GPIO_NOT_CONFIGURED and FW_GPIO_WRONG_CONFIGURATION, are answers to operating a pin, not to configuring one – so this never surfaces as GpioError.

async write(pin: GpioPinNumber, level: GpioLogicLevel) → None

Drive pin to level.

Parameters:
Raises:
async read(pin: GpioPinNumber) → GpioLogicLevel

Return the logic level present on pin.

Parameters:

pin – The pin to sample. Must be configured as GpioFunctionality.DIGITAL_INPUT.

Returns:

The level the firmware read on the pin.

Raises:
async configure_interrupt(pin: GpioPinNumber, trigger: GpioTriggerType) → None

Arm the firmware interrupt on pin for trigger.

This configures the interrupt only. To receive the events, subscribe to GpioInterruptNotification on the device and filter by pin:

await gpio.configure_interrupt(GpioPinNumber.PIN_3, GpioTriggerType.RISING_EDGE)
sub = dev.subscribe(
    GpioInterruptNotification,
    on_irq,
    where=lambda n: n.pin_number == GpioPinNumber.PIN_3,
)

Not every pin supports interrupts, and which do is a property of the adapter model and hardware revision rather than of this API. Ask dev.capabilities.supports_gpio_interrupt(pin) rather than discovering it by failing; a pin that answers False is refused here with CapabilityError before anything is sent to the device.

One such limit is bench-confirmed: on a Supernova hardware Rev B, PIN_3 cannot be armed for any trigger type — its firmware hard-codes the pin as interrupt-incapable, so no argument change makes it work; pick another pin. See docs_dev/hardware_validation/gpio-interrupt-pin3-supernova-revb.md. Other revisions and models read as supporting PIN_3 in the firmware source, and are therefore attempted rather than refused — but that reading is unconfirmed on hardware, so a FirmwareError naming FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE remains possible for a pin this SDK reports as capable.

Parameters:
  • pin – The pin to arm. Must be configured as GpioFunctionality.DIGITAL_INPUT, and must be one of dev.capabilities.interrupt_capable_gpio_pins.

  • trigger – Which edge (or both) raises the interrupt.

Raises:
  • CapabilityError – If the adapter’s model and hardware revision are known not to support an interrupt on pin — the Rev B PIN_3 limit above. The message names the pin, the model and the revision, and lists the pins that do work. No facade call is made, so this replaces the firmware round trip rather than following it. Checked before the direction guard, because a revision limit is unfixable while a missing or wrong configure_pin is not: reporting the fixable problem first would send the caller away to do work this method then refuses.

  • ValidationError – If pin is unconfigured or configured as an output. No facade call is made. On a pin this unit’s revision cannot arm at all, CapabilityError is raised instead — that guard runs first.

  • DisconnectError – If the device is no longer open.

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

  • FirmwareError – If the pin does not support interrupts (FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE, 0x0005) or the pin or trigger is rejected (FW_INVALID_PARAMETER, 0x0004). Both are generic codes that sit below the 0x05xx GPIO band, so they arrive as a plain FirmwareError and are not caught by except GpioError. A pin whose limit this SDK already records raises CapabilityError instead, so reaching the 0x0005 case here means an interrupt limit the revision table does not yet know about.

  • GpioError – If the firmware reports the pin unconfigured (FW_GPIO_NOT_CONFIGURED, 0x0501) or configured as an output (FW_GPIO_WRONG_CONFIGURATION, 0x0502) — the 0x05xx GPIO band. Reaching these takes the device disagreeing with the pin state this interface mirrors, since a locally known-bad mode raises ValidationError before any call is made; a pin reconfigured out from under this handle does it.

Note

To catch every firmware-reported failure of this method in one clause, catch FirmwareError — GpioError is a subclass of it. CapabilityError is not: it is raised before any request is sent, and is a sibling of FirmwareError under CosmicError.

async disable_interrupt(pin: GpioPinNumber) → None

Disarm the firmware interrupt on pin.

Leaves any GpioInterruptNotification subscription in place; it simply stops receiving events for this pin.

Parameters:

pin – The pin to disarm. Must be configured as GpioFunctionality.DIGITAL_INPUT.

Raises:
  • ValidationError – If pin is unconfigured or configured as an output. No facade call is made.

  • DisconnectError – If the device is no longer open.

  • GpioError – On a firmware-reported failure.