PyCosmicSDK Examples
The PyCosmicSDK examples are more than 50 runnable Python scripts. Each one opens an adapter, drives one protocol, and prints what happened. This page shows how to download and run them. It also has seven short snippets to paste into other code.
For the full API, see the PyCosmicSDK documentation. To install the SDK, see PyCosmicSDK.
Download
Section titled “Download”Download pycosmicsdk-examples.zip and unzip it.
The zip holds:
| Folder | Contents |
|---|---|
examples/blocking_api/ | Scripts for the blocking Device API, one folder per protocol |
examples/async_api/ | The same scenarios for the asyncio AsyncDevice API |
examples/legacy/ | Scripts that use the BinhoSupernova, BinhoPulsar and SupernovaController compatibility layers |
examples/helpers.py | Shared code the scripts import (console output, bring-up, skip handling) |
Run an Example
Section titled “Run an Example”Python 3.10 to 3.14 and a Supernova or Pulsar running firmware 4.x, or an Atira running Atira firmware 1.0 or newer, are required. Use the latest firmware: see Updating Firmware.
Windows
python -m venv .venv.venv\Scripts\activatepip install pycosmicsdktar -xf pycosmicsdk-examples.zipcd pycosmicsdk-examplespython examples\blocking_api\system\get_device_info.pymacOS / Linux
python3 -m venv .venvsource .venv/bin/activatepip install pycosmicsdkunzip pycosmicsdk-examples.zipcd pycosmicsdk-examplespython examples/blocking_api/system/get_device_info.pyOn Linux, set up USB access once first: Solving USB Connection Issues on Linux.
Keep the folder layout as it is. Each script finds helpers.py by searching its parent folders.
Docstrings show uv run python; plain python works the same.
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | Success |
1 | The adapter could not be opened, or a check failed |
2 | Another SDK error |
3 | A scenario-specific failure, such as a timeout waiting for data |
4 | Skipped: the script needs hardware that is not connected, and says which |
Code 4 is not a failure. For example, the I2C round-trip script needs an EEPROM at address 0x50. With no EEPROM connected, it prints what it needs and exits with 4.
What Each Example Shows
Section titled “What Each Example Shows”The list below covers examples/blocking_api/. Most scenarios also exist in examples/async_api/ with the same file name.
| Script | What it shows | Needs |
|---|---|---|
system/get_device_info.py | Open the first adapter and print model, serial number and firmware version | Any adapter |
system/list_devices.py | List connected adapters, then open one by path and by serial number | Any adapter |
system/usb_config.py | Read the adapter’s saved USB interface configuration | Supernova |
i2c/blocking.py | Write and read back an I2C memory, then scan the bus | EEPROM or FRAM at 0x50, 2-byte addressing |
i2c/blocking_manual_close.py | The same round trip with explicit open() and close() | EEPROM or FRAM at 0x50 |
i2c/duty_cycle.py | Set the SCL duty cycle and print the timing the adapter reports | Firmware 4.6.0 or later |
i2c/error_handling.py | Cause I2C errors on purpose and show how each one is reported | Any adapter |
i2c/target_eeprom_driven.py | One adapter emulates a 24Cxx EEPROM, a second adapter reads it | Two adapters: SDA, SCL and GND wired together, external 2.2 kΩ pull-ups |
i2c/target_arena_bulk.py | Move 16 KB in and out of the adapter’s I2C target memory | Any adapter |
i3c/discovery.py | Bring up the I3C bus and assign dynamic addresses | Supernova; I3C targets optional |
i3c/ccc_queries.py | Read GETPID, GETBCR and GETDCR from each target | Supernova, I3C target |
i3c/legacy_i2c_target.py | Talk to an I2C or SMBus device on the I3C port | Supernova, I2C target on the I3C port |
i3c/i2c_clock_stretching.py | Talk to a clock-stretching I2C target on the I3C port | Supernova on firmware 4.6.0 or later, I2C target at 0x50 on the I3C port |
i3c/target_mode_setup.py | Run the Supernova as an I3C target and use its memory | Supernova |
i3c/target_mode_bus_event.py | Wait for a bus event while acting as an I3C target | Supernova |
i3c/capability_rejection.py | Show the error raised when asking a Pulsar for I3C | Pulsar |
spi/transfer.py | Bring up SPI and run a full-duplex transfer | Any adapter |
spi/mode_frequency_sweep.py | Sweep SPI modes, clock rates and chip selects | Supernova |
spi/transport_selection.py | Show which USB path a large SPI payload takes | Any adapter |
spi/bulk_flash_read.py | Read 16 KB from a SPI NOR flash in one call | SPI flash on CS0 |
spi/target_flash_driven.py | A Pulsar emulates a SPI NOR flash, a Supernova reads it | Supernova and Pulsar: MOSI (13), MISO (15), CLK (17), CS0 (19) and GND wired straight through |
uart/query.py | Send a request and wait for the reply, at two baud rates | TX-to-RX loopback wire |
uart/send_receive.py | Separate send() and receive() calls, like a serial console | TX-to-RX loopback wire |
uart/receive_stream.py | Read a continuous stream from a device that pushes data | TX-to-RX loopback wire |
uart/subscribe.py | Receive UART data through a callback | TX-to-RX loopback wire |
uart/send_only.py | Transmit only, then close | Any adapter |
gpio/digital_io.py | Configure pins, drive outputs and read inputs | Supernova |
gpio/interrupt_subscribe.py | Arm a GPIO interrupt and wait for it | Pulsar; a signal on GPIO 3 to see an event |
The I2C and SPI target-mode scripts (i2c/target_eeprom_driven.py, i2c/target_arena_bulk.py, spi/target_flash_driven.py) skip with code 4 unless PYCOSMICSDK_EXAMPLES_ALLOW_TARGET_MODE=1 is set. They name the adapters they use by serial number, in the environment variables PYCOSMICSDK_EXAMPLES_TARGET_SERIAL and PYCOSMICSDK_EXAMPLES_CONTROLLER_SERIAL. Each script’s docstring lists what it needs.
Guides behind the scripts that go beyond one protocol:
| Scripts | Guide |
|---|---|
i2c/target_*.py, i3c/target_mode_*.py, spi/target_flash_driven.py | Target mode |
i2c/target_arena_bulk.py, spi/bulk_flash_read.py, spi/transport_selection.py | Large transfers |
i2c/error_handling.py, i3c/capability_rejection.py | Errors |
system/list_devices.py, i2c/blocking_manual_close.py | Device lifecycle |
examples/legacy/ | Migrating from the legacy packages |
Worked Examples
Section titled “Worked Examples”Each snippet is complete: save it as a .py file and run it with python. They use the blocking API unless the heading says otherwise.
I2C: Scan the Bus and Read and Write an EEPROM
Section titled “I2C: Scan the Bus and Read and Write an EEPROM”This scans the bus, writes 4 bytes to an I2C EEPROM at address 0x50, and reads them back. It assumes a 24-series EEPROM with a 2-byte memory address, such as a 24LC256.
Supernova
import time
from pycosmicsdk import Device, I2cBus, I2cError, I2cPullUp, StatusCode
EEPROM = 0x50MEMORY_ADDRESS = b"\x00\x10" # 2-byte memory address 0x0010
with Device.open() as dev: i2c = dev.i2c(bus=I2cBus.A) # the Supernova has one I2C bus i2c.set_voltage(voltage_mv=3300) # set the target voltage (VTARG) before bring-up i2c.bring_up(frequency_hz=400_000, pull_up=I2cPullUp.KOHM_2_2)
try: found = i2c.scan().addresses_7bit except I2cError as exc: if exc.status_code is not StatusCode.FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED: raise found = [] print("Targets:", [hex(a) for a in found])
i2c.write(EEPROM, b"\xde\xad\xbe\xef", subaddress=MEMORY_ADDRESS) time.sleep(0.01) # the EEPROM ignores its address until the write cycle ends data = i2c.read(EEPROM, 4, subaddress=MEMORY_ADDRESS, non_stop=True) print("Read back:", data.hex())Pulsar
import time
from pycosmicsdk import Device, I2cBus, I2cError, I2cPullUp, StatusCode
EEPROM = 0x50MEMORY_ADDRESS = b"\x00\x10" # 2-byte memory address 0x0010
with Device.open() as dev: # The Pulsar has two separate I2C buses: A (Qwiic port) and B (header pins). i2c = dev.i2c(bus=I2cBus.B) i2c.set_voltage(voltage_mv=3300) # set the target voltage before bring-up i2c.bring_up(frequency_hz=400_000, pull_up=I2cPullUp.KOHM_2_2)
try: found = i2c.scan().addresses_7bit except I2cError as exc: if exc.status_code is not StatusCode.FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED: raise found = [] print("Targets:", [hex(a) for a in found])
i2c.write(EEPROM, b"\xde\xad\xbe\xef", subaddress=MEMORY_ADDRESS) time.sleep(0.01) # the EEPROM ignores its address until the write cycle ends data = i2c.read(EEPROM, 4, subaddress=MEMORY_ADDRESS, non_stop=True) print("Read back:", data.hex())- A scan of an empty bus raises
I2cErrorwithFW_I2C_BUS_WITH_NO_TARGETS_CONNECTED. It does not return an empty list, which is why the snippet catches it. non_stop=Truesends a repeated START between the address phase and the data phase. Most EEPROMs and FRAMs expect that for a random read.bring_up()is safe to run again: if the bus is already up, it applies the new settings instead of failing.- Guide: I2C.
SPI: Read a Flash JEDEC ID
Section titled “SPI: Read a Flash JEDEC ID”This reads the 3-byte JEDEC ID of a SPI NOR flash on chip select 0. A Winbond W25Q128JV answers ef4018. Both adapters run this code unchanged.
from pycosmicsdk import ( Device, SpiBitOrder, SpiChipSelect, SpiChipSelectPolarity, SpiDataWidth, SpiMode,)
with Device.open() as dev: spi = dev.spi() spi.set_voltage(voltage_mv=3300) spi.bring_up( bit_order=SpiBitOrder.MSB, mode=SpiMode.MODE_0, data_width=SpiDataWidth.BITS_8, chip_select=SpiChipSelect.CS_0, chip_select_polarity=SpiChipSelectPolarity.ACTIVE_LOW, frequency_hz=10_000_000, )
jedec_id = spi.read(b"\x9f", 3) # command 0x9F out, 3 bytes back print("JEDEC ID:", jedec_id.hex())spi.read()returns only the data phase. To get every byte clocked in on MISO, usespi.transfer(b"\x9f\x00\x00\x00"), which returns 4 bytes: the first byte answers the command and the last three are the ID.- With nothing connected, MISO reads a constant such as
000000orffffff. - Guide: SPI.
I3C: Assign Dynamic Addresses and Talk to a Target
Section titled “I3C: Assign Dynamic Addresses and Talk to a Target”I3C is available on the Supernova only. This brings up the bus, runs dynamic address assignment (RSTDAA, then ENTDAA), and then writes and reads a register on the first target.
from pycosmicsdk import ( Device, DeviceModel, I2cOpenDrainRate, I3cOpenDrainRate, I3cPushPullRate,)
REGISTER = b"\x00" # register address on your target
with Device.open(model=DeviceModel.SUPERNOVA) as dev: i3c = dev.i3c() i3c.set_voltage(voltage_mv=1800) # 1200-3300 mV selects the high-voltage I3C port i3c.bring_up( push_pull_rate=I3cPushPullRate.PUSH_PULL_5_MHZ_DC_50, open_drain_rate=I3cOpenDrainRate.OPEN_DRAIN_400_KHZ, i2c_open_drain_rate=I2cOpenDrainRate.FAST_MODE, )
targets = i3c.init_bus() for t in targets: print(f"dynamic {t.dynamic_address:#04x} PID {t.pid.hex()} BCR {t.bcr:#04x} DCR {t.dcr:#04x}")
if targets: address = targets[0].dynamic_address i3c.sdr_write(address, b"\x01", subaddress=REGISTER) print("Register:", i3c.sdr_read(address, 1, subaddress=REGISTER).hex())- The voltage selects the port: 800-1199 mV drives the low-voltage I3C port, 1200-3300 mV the high-voltage port. Set it to the target’s I/O voltage.
- Common Command Codes are under
i3c.ccc, for examplei3c.ccc.getpid(address)andi3c.ccc.getbcr(address). - Guide: I3C.
I3C: Send an MCTP-over-SMBus Packet (Supernova)
Section titled “I3C: Send an MCTP-over-SMBus Packet (Supernova)”Needs PyCosmicSDK 1.6.0 or later and Supernova firmware 4.6.0 or later. This sends an MCTP Get Endpoint ID request to an SSD on the I3C port, as an SMBus Block Write with PEC, with I2C clock stretching enabled. Set SSD_ADDR, HOST_ADDR and BUS_VOLTAGE_MV for the setup.
"""Send one MCTP-over-SMBus packet (DSP0237) through the Supernova's I3C port.
The packet is a Get Endpoint ID request (MCTP control message 0x02), sent as anSMBus Block Write with PEC. The SSD's response is not read here: under MCTP overSMBus the SSD sends it back as its own Block Write to HOST_ADDR, which theSupernova's I3C port cannot receive today.
Requires pycosmicsdk >= 1.6.0 and Supernova firmware >= 4.6.0.Run: python mctp_smbus_send.py"""
import sys
from pycosmicsdk import ( CosmicError, Device, DeviceModel, I2cOpenDrainRate, I3cOpenDrainRate, I3cPushPullRate,)
# Adjust to your setup.BUS_VOLTAGE_MV = 1800SMBUS_CLOCK_HZ = 100_000SSD_ADDR = 0x1D # SSD's MCTP SMBus address (7-bit)HOST_ADDR = 0x10 # address the SSD should answer to (7-bit)
def pec(data: bytes) -> int: """SMBus PEC: CRC-8, polynomial 0x07, initial value 0.""" crc = 0 for b in data: crc ^= b for _ in range(8): crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF return crc
def mctp_smbus_frame(dest_addr: int, src_addr: int, mctp_packet: bytes) -> bytes: """Wrap an MCTP packet (4-byte transport header + message) in an SMBus Block Write.
Returns everything after the address byte: command 0x0F, byte count, source address, the MCTP packet, and the PEC. """ body = bytes([0x0F, len(mctp_packet) + 1, (src_addr << 1) | 1]) + mctp_packet return body + bytes([pec(bytes([dest_addr << 1]) + body)])
# MCTP transport header: version 1, dest EID 0 (null), src EID 0 (null),# SOM=1 EOM=1 seq=0 TO=1 tag=0. Message: type 0x00 (control), Rq=1 instance 0,# command 0x02 (Get Endpoint ID).GET_ENDPOINT_ID = bytes([0x01, 0x00, 0x00, 0xC8, 0x00, 0x80, 0x02])
def main() -> int: frame = mctp_smbus_frame(SSD_ADDR, HOST_ADDR, GET_ENDPOINT_ID) try: with Device.open(model=DeviceModel.SUPERNOVA) as dev: i3c = dev.i3c() i3c.set_voltage(BUS_VOLTAGE_MV) i3c.bring_up( push_pull_rate=I3cPushPullRate.PUSH_PULL_5_MHZ_DC_50, open_drain_rate=I3cOpenDrainRate.OPEN_DRAIN_400_KHZ, i2c_open_drain_rate=I2cOpenDrainRate.STANDARD_MODE, ) i3c.enable_i2c_clock_stretching(SMBUS_CLOCK_HZ) try: print(f"write to 0x{SSD_ADDR:02X}: {frame.hex(' ')}") i3c.legacy_i2c_write(SSD_ADDR, frame) print("sent (the SSD ACKed every byte)") finally: # I3C SDR, HDR and CCC calls are refused while stretching is on. i3c.disable_i2c_clock_stretching() except CosmicError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 return 0
if __name__ == "__main__": sys.exit(main())- The response is not read. The SSD answers with its own Block Write to
HOST_ADDR, and the I3C port cannot receive it today. - With no SSD attached, the write ends with a NACK and the script prints
ERROR: ….
UART: Loopback Test
Section titled “UART: Loopback Test”Connect one wire from pin 7 (TX) to pin 5 (RX) on the adapter’s multifunction port. The pinout is the same on the Supernova and the Pulsar. The adapter then receives every byte it sends.
from pycosmicsdk import Device, UartBaudRate
with Device.open() as dev: uart = dev.uart() uart.set_voltage(voltage_mv=3300) uart.bring_up(baudrate=UartBaudRate.BAUD_115200) # 8-N-1, no flow control
reply = uart.query(b"Hello, world!", timeout=2.0) print("Received:", reply)query()clears old received data, sends, and waits for the reply. Use it for command-and-response protocols such as AT commands.- For a serial-console pattern, use
uart.send()anduart.receive(). Seeuart/send_receive.pyin the examples. - Guide: UART.
GPIO: Toggle an Output and Catch an Interrupt
Section titled “GPIO: Toggle an Output and Catch an Interrupt”Connect a wire from GPIO 1 to GPIO 4. GPIO 1 is an output; GPIO 4 is an input with a rising-edge interrupt. Both adapters have the same six GPIO pins.
import threadingimport time
from pycosmicsdk import ( Device, GpioFunctionality, GpioInterruptNotification, GpioLogicLevel, GpioPinNumber, GpioTriggerType,)
OUT = GpioPinNumber.PIN_1IN = GpioPinNumber.PIN_4
with Device.open() as dev: gpio = dev.gpio() gpio.set_voltage(voltage_mv=3300) gpio.configure_pin(OUT, mode=GpioFunctionality.DIGITAL_OUTPUT, initial=GpioLogicLevel.LOW) gpio.configure_pin(IN, mode=GpioFunctionality.DIGITAL_INPUT)
fired = threading.Event() with dev.subscribe( GpioInterruptNotification, lambda n: fired.set(), where=lambda n: n.pin_number == IN, # compare with the enum, not the number 4 ): gpio.configure_interrupt(IN, GpioTriggerType.RISING_EDGE) for _ in range(3): gpio.write(OUT, GpioLogicLevel.HIGH) time.sleep(0.1) print("Input reads:", gpio.read(IN).name) gpio.write(OUT, GpioLogicLevel.LOW) time.sleep(0.1) print("Interrupt seen:", fired.wait(timeout=1.0)) gpio.disable_interrupt(IN)- GPIO 3 cannot raise an interrupt on a Supernova hardware revision B; the SDK refuses it with
CapabilityError.dev.capabilities.supports_gpio_interrupt(pin)reports this before the call. - Guide: GPIO.
asyncio: Scan and Read an EEPROM
Section titled “asyncio: Scan and Read an EEPROM”The same I2C task with AsyncDevice. Every call that talks to the adapter is awaited.
import asyncio
from pycosmicsdk import AsyncDevice, I2cBus, I2cError, I2cPullUp, StatusCode
EEPROM = 0x50
async def main() -> None: async with AsyncDevice.open() as dev: i2c = dev.i2c(bus=I2cBus.A) await i2c.set_voltage(voltage_mv=3300) await i2c.bring_up(frequency_hz=400_000, pull_up=I2cPullUp.KOHM_2_2)
try: found = (await i2c.scan()).addresses_7bit except I2cError as exc: if exc.status_code is not StatusCode.FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED: raise found = [] print("Targets:", [hex(a) for a in found])
if EEPROM in found: data = await i2c.read(EEPROM, 16, subaddress=b"\x00\x00", non_stop=True) print("First 16 bytes:", data.hex())
asyncio.run(main())AsyncDevicelets one program serve several adapters, or other I/O, while it waits on the bus.examples/async_api/i2c/concurrent_ops.pyruns several I2C operations at once.- Guide: Blocking and asyncio.
Getting Help
Section titled “Getting Help”If an example does not behave as described, contact us. Include the script name, its full output, and the output of python examples/blocking_api/system/get_device_info.py.

