para:gpio
import gpio from "para:gpio";A small module wrapping the Linux GPIO character device interface (/dev/gpiochipN, uAPI v2). Same call shape across Raspberry Pi 4, Raspberry Pi 5 (where the new pinctrl-rp1 driver exposes the same uAPI), Jetson, and any other Linux SBC. No vendored libgpiod; pure ioctl on the kernel character device.
para:gpio is currently Linux-only. macOS / Windows hosts don’t have an equivalent kernel surface for SBC pin headers — those don’t ship without an external bridge.
chips()
Section titled “chips()”Synchronously enumerates /dev/gpiochipN entries with their driver label and line count.
gpio.chips();// [// { path: "/dev/gpiochip0", label: "gpio-brcmstb@107d508500", lines: 32 },// { path: "/dev/gpiochip4", label: "pinctrl-rp1", lines: 54 },// ...// ]On a Pi 5 the user-accessible header pins live on /dev/gpiochip4 (the RP1). On a Pi 4 they’re on /dev/gpiochip0 (BCM2711). The label field is how you tell them apart.
open(path)
Section titled “open(path)”Opens a chip and runs GPIO_GET_CHIPINFO_IOCTL to confirm it’s a real gpiochip. Returns a Chip.
await using chip = gpio.open("/dev/gpiochip4");chip.path; // "/dev/gpiochip4"chip.label; // "pinctrl-rp1"chip.lines; // 54Chip is AsyncDisposable — await using releases the chip fd at scope exit. Lines acquired through this chip stay open until they’re individually closed (you can drop the chip handle without affecting in-flight Line requests).
chip.line(offset, opts)
Section titled “chip.line(offset, opts)”Acquire a single line on this chip. Returns a Line you can read() / write() / toggle() / edges().
// Output:const led = chip.line(17, { mode: "out", initial: 0 });led.write(1);led.toggle();led.value.get(); // 0 | 1 — Signal of the most recent observed value
// Input with hardware debounce + edge events:const button = chip.line(27, { mode: "in", pull: "up", debounceMs: 5, edge: "falling",});for await (const e of button.edges()) { // e.kind: "rising" | "falling" // e.timestampNs: bigint (kernel monotonic) // e.value: 0 | 1}// Output:const led = chip.line(17, { mode: "out", initial: 0 });led.write(1);led.toggle();led.value; // 0 | 1 — Signal, bare reads desugar to .get()
// Input with hardware debounce + edge events:const button = chip.line(27, { mode: "in", pull: "up", debounceMs: 5, edge: "falling",});for await (const e of button.edges()) { // e.kind / e.timestampNs / e.value}Options:
mode: "in" | "out"— required.pull: "up" | "down" | "off"— input bias resistor. Default"off".debounceMs: number— hardware debounce.0disables. RPi 5 (RP1) supports this; RPi 4 (BCM2711) returnsENOTSUPat request time.edge: "rising" | "falling" | "both" | "none"— edge events to deliver viaedges(). Default"none".initial: 0 | 1— output starting value. Default0.
Line is AsyncDisposable. The reactive line.value Signal updates on read() and on edge events — pair with para:signals for effect { ... } / ~> composition.
chip.bank(offsets, opts)
Section titled “chip.bank(offsets, opts)”Acquire several lines as one atomic unit. Up to 64 lines per call (kernel uAPI v2 cap). All lines share mode / pull / edge / debounceMs. Reads + writes go through a single ioctl, so multi-pin transitions hit the bus simultaneously.
await using bank = chip.bank([17, 22, 23, 27], { mode: "out", initial: 0b1010n, // BigInt: bit i = offsets[i]});
bank.read(); // BigInt — bit i = current value of offsets[i]bank.write(0b0101n); // all linesbank.write(0b0001n, 0b0001n); // values + mask: only modify bit 0Named bank rather than lines because chip.lines is already the chip’s line count.
Quick example: button → LED, reactive
Section titled “Quick example: button → LED, reactive”The full IoT control loop in one effect block. chip.line({ pollHz: 50 }) updates button.value from the kernel; effect { ... } re-runs on every change and writes the LED — no manual setInterval, no callback wiring.
import gpio from "para:gpio";import { effect } from "para:signals";
await using chip = gpio.open("/dev/gpiochip4");await using button = chip.line(27, { mode: "in", pull: "up", debounceMs: 5, pollHz: 50,});await using led = chip.line(17, { mode: "out", initial: 0 });
effect(() => { const pressed = button.value.get() === 0; led.write(pressed ? 1 : 0);});
await new Promise(() => {}); // keep aliveimport gpio from "para:gpio";
await using chip = gpio.open("/dev/gpiochip4");await using button = chip.line(27, { mode: "in", pull: "up", debounceMs: 5, pollHz: 50,});await using led = chip.line(17, { mode: "out", initial: 0 });
effect { const pressed = button.value.get() === 0; led.write(pressed ? 1 : 0);}
await new Promise(() => {}); // keep alivePerformance
Section titled “Performance”Measured on a Pi 5 RP1 (bench/parabun-gpio-toggle):
- Single-line
write()— ~2.1 M writes/s, ~470 ns per ioctl - Single-line
toggle()— ~2.0 M toggles/s, ~490 ns per call - 4-line
bank.write()— ~920 k writes/s = ~3.7 M pin-writes/s (1.1 µs covers 4 lines atomically)
uAPI v2 is ioctl-bound at ~1 MHz toggle rate per pin (two writes per toggle cycle). For sustained > 1 MHz toggle rates or DMA-driven PWM, see bun:mmio (off by default — bypasses the kernel and pokes the controller’s GPIO registers directly).
See also
Section titled “See also”para:i2c— i2c-dev character device on the same Linux SBCs.para:spi— spidev wrapper for the same hardware family.para:signals— reactive composition overline.value.