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:
ValidationErrorthe call was rejected before any USB traffic happened.
CapabilityErrorthe connected model, hardware revision or host cannot do it. Nothing was sent, so it is not a
FirmwareErrorandexcept FirmwareErrordoes not catch it.FirmwareErrorthe device answered and the answer was a refusal.
exc.status_codeis theStatusCodeit reported —FW_I2C_NACK_ADDRESSfor a target that is not on the bus, say. The per-protocol subclasses (I2cError,UartErrorand the rest) are what you usually catch.CosmicTimeoutErroralso 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
Protocol guides — one page per protocol, plus the bulk transfer plane and target mode.
The device lifecycle — the handle every protocol guide sits inside.
Migrating from the legacy packages — for existing
binhosupernova,binhopulsarorsupernovacontrollercode.API reference — generated from the docstrings.