# GPIO {py:obj}`~pycosmicsdk.Gpio` drives the general-purpose pins of an open adapter, from {py:obj}`Device.gpio() `. Both the Supernova and the Pulsar expose the **same six pins**, `GpioPinNumber.PIN_1` through `PIN_6`. The async twin is {py:obj}`~pycosmicsdk.AsyncGpio`. ## Digital I/O Every pin is configured once as an input or an output: ```python 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. {py:obj}`~pycosmicsdk.Gpio.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()`](index.md) 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 {py:obj}`~pycosmicsdk.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. {py:obj}`~pycosmicsdk.Gpio.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 {py:obj}`~pycosmicsdk.Gpio.configure_interrupt` only **arms** the firmware-level interrupt. The events themselves arrive as {py:obj}`~pycosmicsdk.GpioInterruptNotification` through {py:obj}`Device.subscribe ` — the same split every event-driven interface in this SDK uses. {py:obj}`~pycosmicsdk.Gpio.disable_interrupt` disarms it. ```python 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. {py:obj}`~pycosmicsdk.Gpio.configure_interrupt` sits behind a guard that refuses that pin with {py:obj}`~pycosmicsdk.CapabilityError` **before any USB traffic**. That is {py:obj}`~pycosmicsdk.CosmicError`, not {py:obj}`~pycosmicsdk.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](../reference/errors.md). 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 {py:obj}`~pycosmicsdk.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](../reference/types.md). ::: :::{warning} **Compare against the enum member, not a printed pin label.** {py:obj}`~pycosmicsdk.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 {py:obj}`~pycosmicsdk.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, {py:obj}`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 {py:obj}`~pycosmicsdk.GpioError`, but only the ones firmware actually reports in the 0x05xx band — `FW_GPIO_NOT_CONFIGURED` and `FW_GPIO_WRONG_CONFIGURATION`, from {py:obj}`~pycosmicsdk.Gpio.write`, {py:obj}`~pycosmicsdk.Gpio.read`, {py:obj}`~pycosmicsdk.Gpio.configure_interrupt` and {py:obj}`~pycosmicsdk.Gpio.disable_interrupt`. {py:obj}`~pycosmicsdk.Gpio.configure_pin` and {py:obj}`~pycosmicsdk.Gpio.get_voltage` report none of them, so their failures arrive as the base {py:obj}`~pycosmicsdk.FirmwareError`. A rejected voltage is {py:obj}`~pycosmicsdk.SystemFirmwareError` (0x01xx system band, shared rail). The direction guard and field-width checks are {py:obj}`~pycosmicsdk.ValidationError`, raised before any USB traffic. Catch {py:obj}`~pycosmicsdk.FirmwareError` to cover every firmware-reported GPIO failure. A revision-refused interrupt is none of those: it is {py:obj}`~pycosmicsdk.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 - {py:obj}`pycosmicsdk.Gpio` / {py:obj}`pycosmicsdk.AsyncGpio` - {py:obj}`pycosmicsdk.GpioPinNumber`, {py:obj}`pycosmicsdk.GpioFunctionality`, {py:obj}`pycosmicsdk.GpioLogicLevel`, {py:obj}`pycosmicsdk.GpioTriggerType` - {py:obj}`pycosmicsdk.GpioInterruptNotification`