Getting started

Install pycosmicsdk, satisfy the per-platform device prerequisites, and open a first adapter.

Install

Installing from PyPI is the whole setup:

pip install pycosmicsdk

The native CosmicSDK library is statically linked into the compiled extension the wheel ships, so there is no shared library to place on a search path and no environment variable to point at one. Device takes no lib_path, and no COSMICSDK_LIB_PATH is read.

Python ≥ 3.10 is required. Binary wheels are built for CPython 3.10–3.14 on Linux x86_64, macOS arm64 and Windows AMD64; musllinux is skipped, because CosmicSDK needs libudev. No sdist is uploaded, so on any other interpreter or platform pip install pycosmicsdk finds nothing. The only route there is a source checkout with the CosmicSDK submodule and a C++17 toolchain (gcc / clang / MSVC); see the README’s “Building from source”.

Verify the install and the version you got:

python -c "import pycosmicsdk; print(pycosmicsdk.__version__)"

From a source checkout

For contributors. It additionally needs uv for environment management, CMake >= 3.18, and access to the CosmicSDK git submodule, which is a separate private repository.

export SKBUILD_CMAKE_ARGS=-DCMAKE_POLICY_VERSION_MINIMUM=3.5   # required: hidapi predates CMake 4
git clone --recurse-submodules git@github.com:binhollc/pycosmicsdk.git
cd pycosmicsdk
uv sync

Without the SKBUILD_CMAKE_ARGS export a fresh configure fails inside CMake; the repository README.md explains why and gives the HTTPS clone variant. Development is pinned to Python 3.12 via .python-version.

Platform prerequisites

Supernova and Pulsar are USB HID devices: the operating system’s own HID stack claims them, so no vendor driver and no kernel module is involved on any platform.

Linux

Two things are needed.

A udev rule, so the adapter is reachable without root. From a checkout, ./submodules/CosmicSDK/install_udev_rules.sh installs it for you; the file it writes to /etc/udev/rules.d/ is:

# Binho Supernova (HID)
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="82fc", MODE="0660", GROUP="plugdev"
# Binho Supernova (USB/libusb)
SUBSYSTEM=="usb", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="82fc", MODE="0660", GROUP="plugdev"

# Binho Pulsar (HID)
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="82fd", MODE="0660", GROUP="plugdev"
# Binho Pulsar (USB/libusb)
SUBSYSTEM=="usb", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="82fd", MODE="0660", GROUP="plugdev"

# Binho Atira (HID)
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="8347", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# Binho Atira (USB/libusb)
SUBSYSTEM=="usb", ATTRS{idVendor}=="1fc9", ATTRS{idProduct}=="8347", MODE="0660", GROUP="plugdev", TAG+="uaccess"

Reload and re-plug the adapter afterwards:

sudo udevadm control --reload-rules && sudo udevadm trigger

The rule grants access to the plugdev group, so your user has to be in it (sudo usermod -aG plugdev "$USER", then log out and back in). The usb lines are not redundant with the hidraw ones: the bulk transfer plane reaches the adapter’s vendor interface through libusb rather than the HID stack, so without them bulk is unavailable even though ordinary transfers work.

libusb and libudev at runtime. The extension links both dynamically, and the manylinux wheel bundles its own copies under pycosmicsdk.libs/, so neither is a runtime prerequisite of a wheel install. Building from source needs the libraries and their headers: pkg-config, libusb-1.0-0-dev, libudev-dev on Debian / Ubuntu.

macOS

Nothing to install. HID access needs no special permission for these devices.

Windows

Nothing to install — the built-in HID class driver handles the adapter.

First device — blocking

The rest of this page opens a Supernova or Pulsar. For a Binho Atira, open a DebugProbe instead; the Atira debug probe guide starts from there.

Device is the blocking API: every call returns when the firmware has answered. Open it as a context manager and the handle closes on the way out, including when an exception unwinds the block.

from pycosmicsdk import Device

with Device.open() as dev:
    info = dev.info
    print(f"{info.manufacturer} {info.product_name}")
    print(f"model {info.model.name}, serial {info.serial_number}")
    print(f"firmware {info.fw_version}, hardware {info.hw_version}")

open with no arguments takes the first Binho adapter it enumerates and raises DeviceNotFoundError when none matches. With more than one plugged in, name the one you want:

with Device.open(serial="1234ABCD") as dev:  # or path=..., or model=...
    ...

The selectors are mutually exclusive; passing two raises ValidationError before any USB traffic happens.

info is the snapshot taken at open time — free to read, no device round-trip. capabilities reports what this adapter can do, also without touching the wire:

from pycosmicsdk import Device, I2cBus, ProtocolInterface

with Device.open() as dev:
    caps = dev.capabilities
    print(ProtocolInterface.I2C in caps.protocols)   # True on both models
    print(caps.has_i2c_bus(I2cBus.B))                # Pulsar only

Without a with block, close the handle yourself:

dev = Device.open()
try:
    ...
finally:
    dev.close()

close is idempotent — calling it twice is harmless — while any protocol call made after it raises DisconnectError instead of reaching for a stale handle. state reports where in the lifecycle the object currently is. The device lifecycle covers discovery, all three selectors, the state machine and rebooting in full.

First device — async

AsyncDevice is the same surface for asyncio: the device is opened with async with, and every operation is awaited. Use it when the program has other work (or other devices) to interleave, or when you want to consume notifications as an async stream.

import asyncio

from pycosmicsdk import AsyncDevice


async def main() -> None:
    async with AsyncDevice.open() as dev:
        info = dev.info
        print(f"{info.manufacturer} {info.product_name}")
        print(f"model {info.model.name}, serial {info.serial_number}")


asyncio.run(main())

The two APIs differ only where they must: properties that read cached state (info, model, capabilities, state) stay plain attributes on AsyncDevice, while everything that talks to the device — including close — is a coroutine. The four places where they are genuinely not mirror images are in Blocking and asyncio.

When something fails

Every error the SDK raises derives from CosmicError and carries the status_code that produced it, so one except keeps a script from crashing. The subclasses are what you act on:

ValidationError

the call was rejected before any USB traffic happened.

CapabilityError

the connected model, hardware revision or host cannot do it. Nothing was sent, so it is not a FirmwareError and except FirmwareError does not catch it.

FirmwareError

the device answered and the answer was a refusal. exc.status_code is the StatusCode it reported — FW_I2C_NACK_ADDRESS for a target that is not on the bus, say. The per-protocol subclasses (I2cError, UartError and the rest) are what you usually catch.

CosmicTimeoutError

also catchable as the built-in TimeoutError.

The full hierarchy is in the errors reference, and examples/blocking_api/i2c/error_handling.py provokes one of each on purpose.

Where to go next