phone/src/pins/README.md
2025-11-18 14:33:25 -08:00

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

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

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 - using keyword prevents leaks

References