# Pins

High-level GPIO library for Bun using libgpiod v2 with automatic resource management.

## Features

- True event-driven GPIO with worker-based architecture (<10ms latency)
- Zero CPU usage when idle (blocking on hardware events)
- Type-safe TypeScript API
- Automatic resource cleanup with `using` keyword
- Hardware debouncing via kernel
- Callback-based event handling with multiple listeners
- 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 with callback
button.onChange((event) => {
  console.log(event.value === 0 ? "Pressed!" : "Released")
})

// Keep process running
await new Promise(() => {})
```

## 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.listChips()`

List available GPIO chips.

```typescript
const chips = await gpio.listChips()
console.log(chips)
// [{ path: '/dev/gpiochip0', name: 'pinctrl-bcm2835', label: '...', numLines: 58 }]
```

### Input

```typescript
using button = gpio.input(20)

// Read current state (cached from last event)
const value: 0 | 1 = button.value

// Listen for changes (returns unsubscribe function)
const unsubscribe = button.onChange((event) => {
  console.log(event.value, event.timestamp)
})

// Add multiple listeners
const unsub2 = button.onChange((event) => {
  console.log("Second listener:", event.value)
})

// Remove specific listener
unsubscribe()

// Wait for specific value
await button.waitForValue(0)        // wait for LOW
await button.waitForValue(1, 5000)  // wait for HIGH with 5s timeout
```

### OutputPin

```typescript
using led = gpio.output(17)

// Read/write state
led.value = 1
const value = led.value
led.toggle()
```

## Architecture

### Worker-Based Event Handling

Each input spawns a dedicated Web Worker that:

1. Blocks on `gpiod_line_request_wait_edge_events()` with `-1` timeout (infinite)
2. Wakes instantly when hardware GPIO edge event occurs
3. Reads event and posts message to main thread
4. Main thread fires registered callbacks

**Benefits:**

- **True blocking**: Zero CPU usage when idle
- **Low latency**: <10ms response time (vs 100ms with polling)
- **Independent inputs**: Each input has its own worker
- **Clean shutdown**: Workers terminated on close, kernel handles GPIO cleanup

## Resource Management

**IMPORTANT:** Always use the `using` keyword to ensure proper cleanup of GPIO resources.

```typescript
// Good - automatic cleanup
using led = gpio.output(17) // Automatically released because of `using`
led.value = 1
```

```typescript
// Meh - manual cleanup required
const led = gpio.output(17)
led.value = 1
led.close() // Must call manually
```

```typescript
// Bad - leaks resources
const led = gpio.output(17)
led.value = 1
// Forgot to close() - resource leak!
```

## 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 button = gpio.input(20, { pull: "up", debounce: 10 })
using switchInput = gpio.input(16, { pull: "up" })
using led = gpio.output(21)

// Set LED based on switch state
led.value = switchInput.value

// Toggle LED when button pressed
button.onChange((event) => {
  if (event.value === 0) {
    led.toggle()
  }
})

// Mirror switch to LED
switchInput.onChange((event) => {
  led.value = event.value
})

// Keep process running
await new Promise(() => {})
```

### Rotary Phone Dialer

```typescript
import { GPIO } from "@/pins"

const gpio = new GPIO()

using hook = gpio.input(27, { pull: "up", debounce: 3 })
using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 })
using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 })

let digit = 0

hook.onChange((event) => {
  console.log(event.value === 0 ? "Phone picked up" : "Phone hung up")
})

rotaryInUse.onChange((event) => {
  if (event.value === 0) {
    digit = 0
  } else {
    console.log(`Dialed digit: ${digit}`)
  }
})

rotaryNumber.onChange((event) => {
  if (event.value === 1) {
    digit += 1
  }
})

await new Promise(() => {})
```

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