> Complete documentation index: [llms.txt](/llms.txt). Markdown versions of all pages are available by appending `.md` to page URLs.

# PyCosmicSDK Examples

> Runnable PyCosmicSDK examples for the Binho Supernova, Pulsar and Atira, and copy-paste snippets for the tasks customers ask about most

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](https://binhollc.github.io/pycosmicsdk/). To install the SDK, see [PyCosmicSDK](/software/pycosmicsdk/).

## Download

Download [pycosmicsdk-examples.zip](https://binhollc.github.io/pycosmicsdk/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) |

:::note
The examples are built for the SDK version they ship with. Download the zip again after upgrading `pycosmicsdk`.

:::

## 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](/guides/firmware-updates/).



**Windows**

```powershell
python -m venv .venv
.venv\Scripts\activate
pip install pycosmicsdk
tar -xf pycosmicsdk-examples.zip
cd pycosmicsdk-examples
python examples\blocking_api\system\get_device_info.py
```



**macOS / Linux**

```sh
python3 -m venv .venv
source .venv/bin/activate
pip install pycosmicsdk
unzip pycosmicsdk-examples.zip
cd pycosmicsdk-examples
python examples/blocking_api/system/get_device_info.py
```

On Linux, set up USB access once first: [Solving USB Connection Issues on Linux](/support/troubleshooting/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

| 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

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 |

:::note
Some scripts select a model with `Device.open(model=DeviceModel.SUPERNOVA)` or `DeviceModel.PULSAR`. Unless the protocol is I3C, which only the Supernova has, change that line to run the script on the other adapter.

:::

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](https://binhollc.github.io/pycosmicsdk/guides/target-mode.html) |
| `i2c/target_arena_bulk.py`, `spi/bulk_flash_read.py`, `spi/transport_selection.py` | [Large transfers](https://binhollc.github.io/pycosmicsdk/guides/bulk.html) |
| `i2c/error_handling.py`, `i3c/capability_rejection.py` | [Errors](https://binhollc.github.io/pycosmicsdk/reference/errors.html) |
| `system/list_devices.py`, `i2c/blocking_manual_close.py` | [Device lifecycle](https://binhollc.github.io/pycosmicsdk/guides/lifecycle.html) |
| `examples/legacy/` | [Migrating from the legacy packages](https://binhollc.github.io/pycosmicsdk/migration.html) |

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

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

```python
import time

from pycosmicsdk import Device, I2cBus, I2cError, I2cPullUp, StatusCode

EEPROM = 0x50
MEMORY_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**

```python
import time

from pycosmicsdk import Device, I2cBus, I2cError, I2cPullUp, StatusCode

EEPROM = 0x50
MEMORY_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 `I2cError` with `FW_I2C_BUS_WITH_NO_TARGETS_CONNECTED`. It does not return an empty list, which is why the snippet catches it.
* `non_stop=True` sends 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](https://binhollc.github.io/pycosmicsdk/guides/i2c.html).

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

```python
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, use `spi.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 `000000` or `ffffff`.
* Guide: [SPI](https://binhollc.github.io/pycosmicsdk/guides/spi.html).

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

```python
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 example `i3c.ccc.getpid(address)` and `i3c.ccc.getbcr(address)`.
* Guide: [I3C](https://binhollc.github.io/pycosmicsdk/guides/i3c.html).

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

```python
"""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 an
SMBus Block Write with PEC. The SSD's response is not read here: under MCTP over
SMBus the SSD sends it back as its own Block Write to HOST_ADDR, which the
Supernova'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 = 1800
SMBUS_CLOCK_HZ = 100_000
SSD_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

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.

```python
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()` and `uart.receive()`. See `uart/send_receive.py` in the examples.
* Guide: [UART](https://binhollc.github.io/pycosmicsdk/guides/uart.html).

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

```python
import threading
import time

from pycosmicsdk import (
    Device,
    GpioFunctionality,
    GpioInterruptNotification,
    GpioLogicLevel,
    GpioPinNumber,
    GpioTriggerType,
)

OUT = GpioPinNumber.PIN_1
IN = 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)
```

:::caution
`GpioInterruptNotification.pin_number` is zero-based: GPIO 1 arrives as `0`, GPIO 4 as `3`. Compare it with the `GpioPinNumber` member, as above. Comparing it with the plain number `4` matches GPIO 5.

:::

* 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](https://binhollc.github.io/pycosmicsdk/guides/gpio.html).

### `asyncio`: Scan and Read an EEPROM

The same I2C task with `AsyncDevice`. Every call that talks to the adapter is awaited.

```python
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())
```

* `AsyncDevice` lets one program serve several adapters, or other I/O, while it waits on the bus. `examples/async_api/i2c/concurrent_ops.py` runs several I2C operations at once.
* Guide: [Blocking and asyncio](https://binhollc.github.io/pycosmicsdk/asyncio.html).

## Getting Help

If an example does not behave as described, [contact us](/support/contact-us/). Include the script name, its full output, and the output of `python examples/blocking_api/system/get_device_info.py`.