GPIO

Gpio drives the general-purpose pins of an open adapter, from Device.gpio(). Both the Supernova and the Pulsar expose the same six pins, GpioPinNumber.PIN_1 through PIN_6. The async twin is AsyncGpio.

Digital I/O

Every pin is configured once as an input or an output:

from pycosmicsdk import Device, GpioFunctionality, GpioLogicLevel, GpioPinNumber

with Device.open() as dev:
    gpio = dev.gpio()
    gpio.set_voltage(3300)

    gpio.configure_pin(
        GpioPinNumber.PIN_3,
        mode=GpioFunctionality.DIGITAL_OUTPUT,
        initial=GpioLogicLevel.LOW,
    )
    gpio.write(GpioPinNumber.PIN_3, GpioLogicLevel.HIGH)

    gpio.configure_pin(GpioPinNumber.PIN_4, mode=GpioFunctionality.DIGITAL_INPUT)
    print(gpio.read(GpioPinNumber.PIN_4))

initial sets the output level as part of the configure, avoiding a glitch between configuring a pin and driving it.

GPIO has no bring_up(), and its absence is a decision rather than a gap. The other four protocols have one because their initialize() is not idempotent and needs a configure() fallback on a re-run; GPIO has no interface-level initialize/configure pair at all for a composite to sit on top of. configure_pin is idempotent in the way that matters — re-running a script re-configures a pin without complaint — so there is nothing for a re-runnable wrapper to absorb. See Bring-up is bring_up() for the four that do have one.

The direction is tracked Python-side. Writing to a pin configured as an input, or reading one configured as an output, is rejected before the call reaches the device — a ValidationError, not a firmware round trip. examples/blocking_api/gpio/digital_io.py calls this the “direction guard” and demonstrates it.

The rail is the shared multifunction VTARG — the same supply as I2C, SPI and UART. get_voltage exists on both twins (note the name: it is get_voltage, not get_bus_voltage like the other four readers), and reads the same shared rail the other four report.

Interrupts

configure_interrupt only arms the firmware-level interrupt. The events themselves arrive as GpioInterruptNotification through Device.subscribe — the same split every event-driven interface in this SDK uses. disable_interrupt disarms it.

import threading
from pycosmicsdk import GpioInterruptNotification, GpioPinNumber, GpioTriggerType

PIN = GpioPinNumber.PIN_3
fired = threading.Event()

# Not every pin can raise an interrupt on every hardware revision -- ask before
# arming, rather than catching CapabilityError afterwards.
if not dev.capabilities.supports_gpio_interrupt(PIN):
    armable = sorted(p.name for p in dev.capabilities.interrupt_capable_gpio_pins)
    raise SystemExit(f"{PIN.name} cannot be armed on this unit; try one of {armable}")

with dev.subscribe(
    GpioInterruptNotification,
    lambda n: fired.set(),
    where=lambda n: n.pin_number == PIN,
):
    gpio.configure_interrupt(PIN, GpioTriggerType.RISING_EDGE)
    fired.wait(timeout=5)

Important

Arming an interrupt is revision-gated, and the gate is one cell wide. Exactly one pin on one model at one revision is known not to arm: PIN_3 on a Supernova whose hw_version reads B. Firmware’s gpioInitConfiguration() clears that pin’s interrupt-enable flag in its Rev B branch alone, and gpioSetInterrupt() checks the flag before it even looks at the trigger — so no trigger type avoids it. Digital read and write on PIN_3 are unaffected. configure_interrupt sits behind a guard that refuses that pin with CapabilityError before any USB traffic. That is CosmicError, not FirmwareError, so except FirmwareError does not catch it; and it carries StatusCode.SDK_ERROR_WRONG_REQUEST rather than a firmware code, so a handler must not filter on status_code.

Every other pin, model and revision is attempted, including a revision this SDK has never seen. The table behind this guard lists the combinations known to be broken, so absence from it means “no known limit” rather than “unknown, so refuse” — see Refusals fail open. Two consequences worth stating plainly: a Supernova Rev A is not gated here (firmware splits Supernova only into Rev B and Rev C branches, so what Rev A does is simply not known), and interrupt_capable_gpio_pins reporting all six pins on a Rev C Supernova or on a Pulsar rests on reading the firmware source, not on a bench run. If one of those refuses on the wire, you get a FirmwareError carrying FW_FEATURE_NOT_SUPPORTED_BY_HARDWARE — the loud outcome, not a wrong answer.

dev.capabilities.supports_gpio_interrupt(pin) and dev.capabilities.interrupt_capable_gpio_pins are the way to avoid the question — see Capabilities you can ask about in advance.

Warning

Compare against the enum member, not a printed pin label. GpioInterruptNotification carries pin_number, triggered_edge and logic_level as plain int — the fields are copied out of a firmware-populated struct, so no enum survives the crossing.

The GPIO enums are bound arithmetic enums (IntEnum, and IntFlag for GpioTriggerType), which is what makes n.pin_number == GpioPinNumber.PIN_3 hold. Against a plain enum.Enum it would be unconditionally False and the subscription would silently never fire.

Firmware reports the zero-based pin index, not the label: PIN_1 is 0, so GpioPinNumber.PIN_3 is 2 and n.pin_number == 3 matches PIN_4.

Do not write GpioPinNumber(n.pin_number) inside a where= predicate. Predicates run on the dispatcher thread, where an exception is logged and swallowed — an adapter reporting a pin index this build does not enumerate would turn a ValueError into a silently dropped event. Compare directly.

On the async side, AsyncDevice.subscribe takes a coroutine function; registration is still synchronous, but the coroutine is scheduled onto the running loop from the dispatcher thread, so it runs concurrently with your own code rather than inline. An asyncio.Event replaces the threading.Event above.

Errors

Firmware-reported GPIO failures are GpioError, but only the ones firmware actually reports in the 0x05xx band — FW_GPIO_NOT_CONFIGURED and FW_GPIO_WRONG_CONFIGURATION, from write, read, configure_interrupt and disable_interrupt. configure_pin and get_voltage report none of them, so their failures arrive as the base FirmwareError. A rejected voltage is SystemFirmwareError (0x01xx system band, shared rail). The direction guard and field-width checks are ValidationError, raised before any USB traffic. Catch FirmwareError to cover every firmware-reported GPIO failure.

A revision-refused interrupt is none of those: it is CapabilityError, raised before the wire, and except FirmwareError will not catch it — see the note under Interrupts above.

Examples

examples/blocking_api/gpio/

digital_io.py — configure, write, read, and the direction guard. Requires no wiring. interrupt_subscribe.py — arm an interrupt and wait for it via Device.subscribe.

examples/async_api/gpio/

The async twins of both, which pick the opposite adapter model from their blocking counterparts to show the flow is identical on either.

Reference