# Pins High-level GPIO library for Bun using libgpiod v2 with automatic resource management. ## Features - Type-safe TypeScript API with autocomplete for pin names - Automatic resource cleanup with `using` keyword - Hardware debouncing via kernel - Event-driven input handling - Efficient multi-pin monitoring with input groups - Zero external dependencies (uses Bun FFI) ## Requirements - Bun 1.0+ - libgpiod v2 (libgpiod.so.3) - Linux system with GPIO support (Raspberry Pi, etc.) - TypeScript 5.2+ (for `using` keyword support) ## Quick Start ```typescript import { GPIO } from "pins" const gpio = new GPIO() // Blink an LED using led = gpio.output(17) for (let i = 0; i < 10; i++) { led.toggle() await Bun.sleep(500) } // Read a button using button = gpio.input(20, { pull: "up", debounce: 10 }) console.log(button.value) // Listen for button events for await (const event of button.events()) { console.log(event.value === 0 ? "Pressed!" : "Released") } ``` ## API ### GPIO Class #### `new GPIO(options?)` Create a GPIO instance. ```typescript const gpio = new GPIO({ chip: "/dev/gpiochip0" }) // defaults to /dev/gpiochip0 ``` #### `gpio.output(pin, options?)` Configure a pin as output. ```typescript using led = gpio.output(17, { initialValue: 0 }) led.value = 1 led.toggle() ``` Options: - `initialValue?: 0 | 1` - Initial pin state (default: 0) #### `gpio.input(pin, options?)` Configure a pin as input. ```typescript using button = gpio.input(20, { pull: "up", // 'up' | 'down' | 'none' debounce: 10, // milliseconds edge: "both", // 'rising' | 'falling' | 'both' }) ``` Options: - `pull?: 'up' | 'down' | 'none'` - Pull resistor (default: 'up') - `debounce?: number` - Debounce period in milliseconds (default: 0) - `edge?: 'rising' | 'falling' | 'both'` - Edge detection (default: 'both') #### `gpio.inputGroup(config)` Monitor multiple inputs efficiently with a single file descriptor. Pin names are fully type-safe! ```typescript using inputs = gpio.inputGroup({ hook: { pin: 20, pull: "up" }, rotary: { pin: 21, pull: "up", debounce: 1 }, button: { pin: 22, pull: "down" }, }) // Access individual pins (fully typed!) console.log(inputs.pins.hook.value) // TypeScript knows about .hook console.log(inputs.pins.button.value) // TypeScript knows about .button // Monitor all pins for await (const event of inputs.events()) { console.log(`${event.pin}: ${event.value}`) // event.pin is "hook" | "rotary" | "button" } ``` #### `gpio.listChips()` List available GPIO chips. ```typescript const chips = await gpio.listChips() console.log(chips) // [{ path: '/dev/gpiochip0', name: 'pinctrl-bcm2835', label: '...', numLines: 58 }] ``` ### InputPin ```typescript using button = gpio.input(20) // Read current state const value: 0 | 1 = button.value // Wait for specific value await button.waitForValue(0) // wait for LOW await button.waitForValue(1, 5000) // wait for HIGH with 5s timeout // Event stream for await (const event of button.events()) { console.log(event.value, event.timestamp) } ``` ### OutputPin ```typescript using led = gpio.output(17) // Read/write state led.value = 1 const value = led.value led.toggle() ``` ### InputGroup ```typescript using inputs = gpio.inputGroup({ switch: { pin: 16, pull: "up" }, button: { pin: 20, pull: "up", debounce: 10 } }) // Access pins with full type safety inputs.pins.switch.value // ✓ TypeScript autocomplete inputs.pins.button.value // ✓ TypeScript autocomplete // Wait for specific pin values await inputs.pins.button.waitForValue(0) // wait for button to go LOW await inputs.pins.switch.waitForValue(1, 3000) // wait for switch to go HIGH with timeout // Monitor all pins for await (const event of inputs.events()) { event.pin // Type: 'switch' | 'button' event.value // Type: 0 | 1 event.timestamp // Type: bigint (nanoseconds) } ``` ## Resource Management **IMPORTANT:** Always use the `using` keyword to ensure proper cleanup of GPIO resources. ```typescript // Good - automatic cleanup { using led = gpio.output(17) led.value = 1 } // Automatically released // Bad - manual cleanup required const led = gpio.output(17) led.value = 1 led.close() // Must call manually ``` ## Hardware Setup ### Pull Resistors Pull resistors prevent floating input values when nothing is connected to the pin. - **Pull-up + button to GND**: When released, pin reads HIGH (1). When pressed, pin reads LOW (0). - **Pull-down + button to VCC**: When released, pin reads LOW (0). When pressed, pin reads HIGH (1). **Important:** Match your pull resistor to your wiring: - Button to ground → use `pull: "up"` - Button to VCC (3.3V) → use `pull: "down"` ### Debouncing Mechanical buttons "bounce" - they make and break contact multiple times when pressed. Hardware debouncing eliminates these spurious events at the kernel level: ```typescript using button = gpio.input(20, { pull: "up", debounce: 10, // 10ms debounce - ignore edges within 10ms of previous edge }) ``` Typical debounce values: 5-50ms depending on your button quality. ## Error Handling ```typescript try { using led = gpio.output(17) } catch (err) { if (err instanceof PermissionError) { // Add user to gpio group: sudo usermod -aG gpio $USER } else if (err instanceof PinInUseError) { // Pin is already in use by another process } else if (err instanceof ChipNotFoundError) { // GPIO chip not found at path } } ``` ## Examples ### Simple LED Blink ```typescript import { GPIO } from "@/pins" const gpio = new GPIO() using led = gpio.output(17) for (let i = 0; i < 10; i++) { led.toggle() await Bun.sleep(500) } ``` ### Button and Switch ```typescript import { GPIO } from "@/pins" const gpio = new GPIO() using inputs = gpio.inputGroup({ button: { pin: 20, pull: "up", debounce: 10 }, switch: { pin: 16, pull: "up" } }) using led = gpio.output(21) // Set LED based on switch state if (inputs.pins.switch.value === 1) { led.value = 1 } // Toggle LED when button pressed for await (const event of inputs.events()) { if (event.pin === "button" && event.value === 0) { led.toggle() } else if (event.pin === "switch") { led.value = event.value } } ``` ### Rotary Phone Dialer ```typescript import { GPIO } from "@/pins" const gpio = new GPIO() using inputs = gpio.inputGroup({ hook: { pin: 20, pull: "up" }, rotary: { pin: 21, pull: "up", debounce: 1 }, }) for await (const event of inputs.events()) { if (event.pin === "hook") { console.log(event.value === 0 ? "Phone picked up" : "Phone hung up") } else if (event.pin === "rotary" && event.value === 0) { console.log("Rotary pulse") } } ``` ## Troubleshooting ### Permission Denied Add your user to the `gpio` group: ```bash sudo usermod -aG gpio $USER ``` Then log out and back in. ### Pin Already in Use Another process has claimed the pin. Stop that process or use a different pin. ### Chip Not Found Verify GPIO hardware is enabled: ```bash ls /dev/gpiochip* ``` If you see "libgpiod v2 (libgpiod.so.3) not found", install it: ```bash sudo apt-get install libgpiod-dev ``` ## Design Philosophy This library provides a simple, type-safe interface to GPIO: - **Low-level values** - Events return raw 0/1 values, let users interpret semantics - **Simple by default** - Sensible defaults for common use cases - **Explicit when needed** - Full control via options - **Type-safe** - TypeScript catches errors at compile time and provides autocomplete - **Resource-safe** - `using` keyword prevents leaks ## References - [libgpiod v2 documentation](https://libgpiod.readthedocs.io/) - [Bun FFI documentation](https://bun.sh/docs/api/ffi)