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: 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 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 ( |
yes |
yes |
yes |
yes |
yes |
Binho Pulsar |
2 buses ( |
— |
— |
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 and Target mode.
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
i3c raises
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:
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 DebugProbe (async twin
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.
Install
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 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:
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:
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
withblock closes the native handle. The explicitcloseform works too — seeexamples/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
bring_upis the answer to it.initializeis not idempotent: an already-initialised bus reportsFW_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_uptriesinitialize, falls back toconfigurewhen the bus is already up, and hands back aBringUpResult. 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_appliedis 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_appliedanswers is whether it is running at your settings. It isFalsein 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.refusalturns the report back into an exception.- A capability refusal is not a
FirmwareError. The second thing that will bite you, and the reason
bring_upexists 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 —configureandset_pull_upon a Supernova whose revision is anything butC, an unsupportedI2cPullUpvalue, an interrupt on a GPIO pin this revision cannot arm. Those raiseCapabilityError, which derives fromCosmicErrorand not fromFirmwareError, soexcept FirmwareErrordoes 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 onstatus_code: nothing was sent, so the code carried isSDK_ERROR_WRONG_REQUESTrather than any firmware rejection code. Better still, ask first —DeviceCapabilitiesanswers every one of these in advance, with no USB traffic:dev.capabilities.i2c_runtime_reconfigurable,dev.capabilities.supports_i2c_pull_up(pull_up),dev.capabilities.supports_gpio_interrupt(pin),dev.capabilities.spi_target_capableanddev.capabilities.supports_spi_bulk(). See Capabilities you can ask about in advance.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 takebytes. No lists, nobytearrayceremony.- Failures are exceptions.
Nothing returns a status code. Everything the SDK raises derives from
CosmicError— see Errors.
The same script in asyncio form is async with AsyncDevice.open() as dev:
with an await on each operation; see Blocking and asyncio.
Examples
Download the runnable scripts as
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 describe the layout.
Where to go next
The device lifecycle — discovery, the three
open()selectors, the state machine, capabilities, rebooting, and the USB configuration. The handle every protocol guide sits inside.Protocol guides — one page per protocol, each pointing at the runnable scripts under
examples/, plus two cross-cutting pages: The bulk transfer plane and Target mode.Atira debug probe — target power, SWD debug, SPI flash programming and the UART bridge on a Binho Atira.
Blocking and asyncio — how the two APIs relate, and the four places where they are genuinely not mirror images.
API reference — generated from the docstrings in
src/pycosmicsdk/.Migrating from a legacy SDK — for existing
binhosupernova,binhopulsarorsupernovacontrollercode.