7.6 KiB
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
usingkeyword - 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
usingkeyword support)
Quick Start
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.
const gpio = new GPIO({ chip: "/dev/gpiochip0" }) // defaults to /dev/gpiochip0
gpio.output(pin, options?)
Configure a pin as output.
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.
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!
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.
const chips = await gpio.listChips()
console.log(chips)
// [{ path: '/dev/gpiochip0', name: 'pinctrl-bcm2835', label: '...', numLines: 58 }]
InputPin
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
using led = gpio.output(17)
// Read/write state
led.value = 1
const value = led.value
led.toggle()
InputGroup
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.
// 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:
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
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
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
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
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:
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:
ls /dev/gpiochip*
If you see "libgpiod v2 (libgpiod.so.3) not found", install it:
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 -
usingkeyword prevents leaks