# pycosmicsdk `pycosmicsdk` is the Python SDK for the **Binho Supernova** and **Binho Pulsar** USB host adapters and the **Binho Atira** debug probe. One package speaks I2C, I3C, UART, SPI and GPIO on the adapters, drives the Atira's SWD debug, SPI flash programming and target power, and offers each from either a blocking or an `asyncio` API. It is a thin, typed Python layer over the C/C++ [CosmicSDK](https://github.com/binhollc/CosmicSDK): a compiled `nanobind` extension ships inside the wheel, so there is no separate native install and no `ctypes` glue to configure. It replaces three legacy packages — `binhosupernova`, `binhopulsar` and `supernovacontroller`. If you have a working script against one of those, start at the [migration guide](migration.md) instead of here; the legacy compatibility shims under `pycosmicsdk.legacy` are a migration path, not the recommended API for new code. ## Supported hardware | Adapter | I2C | I3C controller | I3C target | UART | SPI | GPIO | |---|---|---|---|---|---|---| | **Binho Supernova** | 1 bus (`I2cBus.A`) | yes | yes | yes | yes | yes | | **Binho Pulsar** | 2 buses (`I2cBus.A`, `I2cBus.B`) | — | — | yes | yes | yes | Both adapters can also act as an I2C or SPI **target** rather than a controller, and can move large payloads over a vendor **bulk** USB endpoint instead of the ordinary 1024-byte HID request. Both are per-model and, for bulk, per-*host*: | Adapter | I2C target | SPI target | I2C / SPI bulk ceiling | I3C bulk ceiling | |---|---|---|---|---| | **Binho Supernova** | yes | **not rev B** | 32768 bytes | 32767 bytes | | **Binho Pulsar** | yes | yes | 16384 bytes | — | Three things in that table are easy to read past. The I2C and SPI ceilings are each that model's whole bulk staging arena; **the I3C one is one byte short of it**, because it is a hardware frame limit — the peripheral's transaction-length field is 15 bits — rather than a buffer size. A rev-B Supernova cannot receive an incoming chip select, so it cannot be a SPI target; firmware does not refuse it, and this SDK does. Only that one revision is refused — an unrecognised model or a revision string this SDK cannot read is attempted, so the refusal is always a specific claim rather than a default. And bulk additionally needs the adapter's vendor USB interface claimed on *this machine*, so the same adapter answers differently on two PCs — which is why it is negotiated per connection rather than looked up per model. See [The bulk transfer plane](guides/bulk.md) and [Target mode](guides/target-mode.md). Both adapters expose I2C, SPI, UART and the GPIO pins on a shared *multifunction port*, which is also a shared VTARG rail — setting the target voltage through any one of those four interfaces sets it for all four. Only the Supernova has I3C hardware; asking a Pulsar for {py:obj}`~pycosmicsdk.Device.i3c` raises {py:obj}`~pycosmicsdk.CapabilityError` rather than failing on the wire. The Supernova's Qwiic port and multifunction-port I2C pins are internally shorted into the single logical `I2cBus.A`. The Pulsar's two I2C ports are electrically separate buses that share only VTARG and ground. Query it at runtime instead of hard-coding it: ```python from pycosmicsdk import Device, ProtocolInterface with Device.open() as dev: print(dev.model) # DeviceModel.SUPERNOVA print(sorted(dev.capabilities.protocols)) print(ProtocolInterface.I3C in dev.capabilities.protocols) ``` ### Binho Atira debug probe The Atira is driven by {py:obj}`~pycosmicsdk.DebugProbe` (async twin {py:obj}`~pycosmicsdk.AsyncDebugProbe`), not by `Device`, and can be open in the same process as a Supernova or Pulsar. It needs fw-atira v1.0 or newer. | Function | Available | |---|---| | Target power (3.3 V rail, voltage / current readings) | yes | | SWD debug on Arm Cortex-M (attach, halt, step, registers, memory, breakpoints) | yes | | SPI controller and SPI NOR flash (detect, read, erase, program, verify) | yes | | USB-to-UART bridge (port name, saved line settings) | yes | | RTT, SWO, UPDI, JTAG | not in this release | See the [Atira debug probe guide](guides/debugprobe.md). ## Install ```bash pip install pycosmicsdk ``` That is the whole setup — the native library is statically linked into the compiled extension the wheel ships. Linux additionally needs a udev rule before an adapter or probe is reachable without `root`. [Getting started](getting-started.md) has all of it: the install, the per-platform prerequisites, opening a first adapter on both APIs, and what the errors mean. ## Quickstart: an I2C read Plug in a Supernova or Pulsar, wire an I2C target to bus A, and run: ```python from pycosmicsdk import Device, I2cBus, I2cPullUp with Device.open() as dev: i2c = dev.i2c(bus=I2cBus.A) i2c.set_voltage(voltage_mv=3300) i2c.bring_up(frequency_hz=400_000, pull_up=I2cPullUp.OHM_330) print(i2c.read(0x50, 4, subaddress=b"\x00\x00").hex(" ")) ``` That is the whole program: open the first adapter found (pass `serial=` to pick one), power the bus, bring it up, read four bytes from register 0x0000 of the target at 0x50. A target that does not answer raises `I2cError` with the firmware's reason in the message. The fuller version below adds device info, a bus scan and the `bring_up` result: ```python from pycosmicsdk import Device, I2cBus, I2cPullUp with Device.open() as dev: print(f"{dev.model.name} {dev.info.serial_number}") i2c = dev.i2c(bus=I2cBus.A) i2c.set_voltage(voltage_mv=3300) # Re-runnable: brings the bus up, or reconfigures it if an earlier run # left it up. On return the bus is usable either way. result = i2c.bring_up(frequency_hz=400_000, pull_up=I2cPullUp.OHM_330) if not result.settings_applied: print(f"bus kept its earlier settings: {result.refusal}") # scan() raises I2cError on an empty bus; a mis-wired target fails here. print(f"targets on bus A: {[hex(a) for a in i2c.scan().addresses_7bit]}") # Read 16 bytes from register 0x0100 of the target at 0x50. data = i2c.read(address=0x50, length=16, subaddress=b"\x01\x00") print(data.hex(" ")) ``` Six things in there are worth knowing up front: `Device.open()` is a context manager. : Leaving the `with` block closes the native handle. The explicit {py:obj}`~pycosmicsdk.Device.close` form works too — see `examples/blocking_api/i2c/blocking_manual_close.py`. `bring_up()` is the re-runnable bring-up. : This is the first thing that will bite you, and {py:obj}`~pycosmicsdk.I2cController.bring_up` is the answer to it. {py:obj}`~pycosmicsdk.I2cController.initialize` is **not idempotent**: an already-initialised bus reports `FW_INTERFACE_ALREADY_INITIALIZED`, and the bus stays initialised across reconnects — so the *second run of any script* hits it unless the adapter was reset in between. `bring_up` tries `initialize`, falls back to {py:obj}`~pycosmicsdk.I2cController.configure` when the bus is already up, and hands back a {py:obj}`~pycosmicsdk.BringUpResult`. I2C, I3C, UART and SPI all have one, and so do the I2C and SPI *target* interfaces — six in all, on both the blocking class and its asyncio twin. `BringUpResult.settings_applied` is the field to branch on. : On return the bus is up and usable — anything that would leave it down raises, so there is no half-success to check for. What `settings_applied` answers is whether it is running at *your* settings. It is `False` in exactly one case: the bus was already up and the adapter refused to reconfigure it, which leaves the bus working at settings you did not choose and cannot read back. A script that just needs *a* working bus can ignore the return value; a measurement that depends on the frequency must not. `raise result.refusal` turns the report back into an exception. A capability refusal is not a `FirmwareError`. : The second thing that will bite you, and the reason `bring_up` exists in library code rather than as a snippet to copy. Some operations are refused *before* anything reaches the device, because the connected model or hardware revision cannot do them — {py:obj}`~pycosmicsdk.I2cController.configure` and {py:obj}`~pycosmicsdk.I2cController.set_pull_up` on a Supernova whose revision is anything but `C`, an unsupported {py:obj}`~pycosmicsdk.I2cPullUp` value, an interrupt on a GPIO pin this revision cannot arm. Those raise {py:obj}`~pycosmicsdk.CapabilityError`, which derives from {py:obj}`~pycosmicsdk.CosmicError` and **not** from {py:obj}`~pycosmicsdk.FirmwareError`, so `except FirmwareError` does not catch it. Two rules follow if you ever write such a handler yourself. Catch it in its own clause, and do **not** filter that clause on `status_code`: nothing was sent, so the code carried is `SDK_ERROR_WRONG_REQUEST` rather than any firmware rejection code. Better still, ask first — {py:obj}`~pycosmicsdk.DeviceCapabilities` answers every one of these in advance, with no USB traffic: {py:obj}`dev.capabilities.i2c_runtime_reconfigurable `, `dev.capabilities.supports_i2c_pull_up(pull_up)`, `dev.capabilities.supports_gpio_interrupt(pin)`, `dev.capabilities.spi_target_capable` and `dev.capabilities.supports_spi_bulk()`. See [Capabilities you can ask about in advance](reference/types.md). `initialize()` returns the interface. : So `dev.i2c(bus=I2cBus.A).initialize(...)` chains when you know the bus is down. `bring_up()` returns the report instead, which is the trade: chaining or knowing what took effect. Payloads are `bytes`, in and out. : Reads return `bytes`; writes take `bytes`. No lists, no `bytearray` ceremony. Failures are exceptions. : Nothing returns a status code. Everything the SDK raises derives from {py:obj}`~pycosmicsdk.CosmicError` — see [Errors](reference/errors.md). The same script in `asyncio` form is `async with AsyncDevice.open() as dev:` with an `await` on each operation; see [Blocking and asyncio](asyncio.md). ## Examples Download the runnable scripts as {download}`pycosmicsdk-examples.zip <_generated/pycosmicsdk-examples.zip>` — blocking and `asyncio` versions for every protocol, the Atira debug probe and the legacy shims. Unzip, `pip install pycosmicsdk`, and run one with `python pycosmicsdk-examples/blocking_api/system/list_devices.py`. Each script's docstring lists the wiring it needs; the [protocol guides](guides/index.md) describe the layout. ## Where to go next - **[The device lifecycle](guides/lifecycle.md)** — discovery, the three `open()` selectors, the state machine, capabilities, rebooting, and the USB configuration. The handle every protocol guide sits inside. - **[Protocol guides](guides/index.md)** — one page per protocol, each pointing at the runnable scripts under `examples/`, plus two cross-cutting pages: [The bulk transfer plane](guides/bulk.md) and [Target mode](guides/target-mode.md). - **[Atira debug probe](guides/debugprobe.md)** — target power, SWD debug, SPI flash programming and the UART bridge on a Binho Atira. - **[Blocking and asyncio](asyncio.md)** — how the two APIs relate, and the four places where they are genuinely not mirror images. - **[API reference](reference/index.md)** — generated from the docstrings in `src/pycosmicsdk/`. - **[Migrating from a legacy SDK](migration.md)** — for existing `binhosupernova`, `binhopulsar` or `supernovacontroller` code. ```{toctree} :hidden: :maxdepth: 2 :caption: Guides getting-started guides/index asyncio migration ``` ```{toctree} :hidden: :maxdepth: 1 :caption: API reference reference/index reference/device reference/debugprobe reference/interfaces reference/errors reference/enums reference/types reference/notifications ``` ```{toctree} :hidden: :maxdepth: 1 :caption: Project information about support ```