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 viaDevice.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.