Compare commits
No commits in common. "dc902565f31ff8261496e44da5ee0688b1499458" and "843f3680e1a54543f0a75a837c9cc161f4ea129c" have entirely different histories.
dc902565f3
...
843f3680e1
47
src/hq.ts
Normal file
47
src/hq.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { GPIO } from "./pins"
|
||||
|
||||
console.log(`kill -9 ${process.pid}`)
|
||||
|
||||
const gpio = new GPIO({ resetOnClose: true })
|
||||
|
||||
// // Blink an LED
|
||||
using led = gpio.output(21)
|
||||
|
||||
// Read a button
|
||||
using inputs = gpio.inputGroup({
|
||||
button: { pin: 20, pull: "up", debounce: 10 },
|
||||
switch: { pin: 16, pull: "up", debounce: 10 }
|
||||
})
|
||||
|
||||
led.value = inputs.pins.button.value
|
||||
|
||||
const iteratorEvents = new Promise(async (resolve) => {
|
||||
for await (const event of inputs.events()) {
|
||||
if (event.pin === "button") {
|
||||
console.log(`🌭`, event.value)
|
||||
led.value = event.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const switchEvent = new Promise<void>(async (resolve) => {
|
||||
await inputs.pins.switch.waitForValue(0)
|
||||
console.log("Switch pressed!")
|
||||
resolve()
|
||||
})
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
inputs.close()
|
||||
led.close()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
inputs.close()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
await Promise.race([iteratorEvents, switchEvent])
|
||||
|
||||
console.log(`👋 Goodbye!`)
|
||||
39
src/phone.ts
39
src/phone.ts
|
|
@ -22,19 +22,25 @@ type PhoneContext = {
|
|||
cancelAgent?: CancelableTask
|
||||
}
|
||||
|
||||
const gpio = new GPIO()
|
||||
using ringer = gpio.output(17, { resetOnClose: true })
|
||||
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 })
|
||||
const gpio = new GPIO({ resetOnClose: true })
|
||||
using ringer = gpio.output(17)
|
||||
using inputs = gpio.inputGroup({
|
||||
hook: { pin: 27, debounce: 50 },
|
||||
rotaryInUse: { pin: 22, debounce: 50 },
|
||||
rotaryNumber: { pin: 23, debounce: 10 },
|
||||
})
|
||||
|
||||
export const startPhone = async (agentId: string, apiKey: string) => {
|
||||
await Buzz.setVolume(0.4)
|
||||
log.info(`📞 Hook ${hook.value}`)
|
||||
log.info(`📞 Hook ${inputs.pins.hook.value}`)
|
||||
await handleInputEvents()
|
||||
}
|
||||
|
||||
const handleInputEvents = async () => {
|
||||
let digit = 0
|
||||
|
||||
hook.onChange((event) => {
|
||||
for await (const event of inputs.events()) {
|
||||
switch (event.pin) {
|
||||
case "hook":
|
||||
const type = event.value == 0 ? "hang_up" : "pick_up"
|
||||
log.info(`📞 Hook ${event.value} sending ${type}`)
|
||||
if (type === "hang_up") {
|
||||
|
|
@ -42,24 +48,27 @@ export const startPhone = async (agentId: string, apiKey: string) => {
|
|||
} else {
|
||||
ringer.value = 0
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
rotaryInUse.onChange((event) => {
|
||||
case "rotaryInUse":
|
||||
if (event.value === 0) {
|
||||
digit = 0
|
||||
} else {
|
||||
log.info(`📞 Dialed digit: ${digit}`)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
rotaryNumber.onChange((event) => {
|
||||
case "rotaryNumber":
|
||||
if (event.value === 1) {
|
||||
digit += 1
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
// Keep process running
|
||||
await new Promise(() => {})
|
||||
default:
|
||||
log.error(`📞 Unknown pin event: ${event.pin}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const apiKey = process.env.ELEVEN_API_KEY
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ High-level GPIO library for Bun using libgpiod v2 with automatic resource manage
|
|||
|
||||
## Features
|
||||
|
||||
- True event-driven GPIO with worker-based architecture (<10ms latency)
|
||||
- Zero CPU usage when idle (blocking on hardware events)
|
||||
- Type-safe TypeScript API
|
||||
- Type-safe TypeScript API with autocomplete for pin names
|
||||
- Automatic resource cleanup with `using` keyword
|
||||
- Hardware debouncing via kernel
|
||||
- Callback-based event handling with multiple listeners
|
||||
- Event-driven input handling
|
||||
- Efficient multi-pin monitoring with input groups
|
||||
- Zero external dependencies (uses Bun FFI)
|
||||
|
||||
## Requirements
|
||||
|
|
@ -37,13 +36,10 @@ for (let i = 0; i < 10; i++) {
|
|||
using button = gpio.input(20, { pull: "up", debounce: 10 })
|
||||
console.log(button.value)
|
||||
|
||||
// Listen for button events with callback
|
||||
button.onChange((event) => {
|
||||
// Listen for button events
|
||||
for await (const event of button.events()) {
|
||||
console.log(event.value === 0 ? "Pressed!" : "Released")
|
||||
})
|
||||
|
||||
// Keep process running
|
||||
await new Promise(() => {})
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
|
@ -90,6 +86,27 @@ Options:
|
|||
- `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.
|
||||
|
|
@ -100,30 +117,22 @@ console.log(chips)
|
|||
// [{ path: '/dev/gpiochip0', name: 'pinctrl-bcm2835', label: '...', numLines: 58 }]
|
||||
```
|
||||
|
||||
### Input
|
||||
### InputPin
|
||||
|
||||
```typescript
|
||||
using button = gpio.input(20)
|
||||
|
||||
// Read current state (cached from last event)
|
||||
// Read current state
|
||||
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
|
||||
|
||||
// Event stream
|
||||
for await (const event of button.events()) {
|
||||
console.log(event.value, event.timestamp)
|
||||
}
|
||||
```
|
||||
|
||||
### OutputPin
|
||||
|
|
@ -137,23 +146,29 @@ const value = led.value
|
|||
led.toggle()
|
||||
```
|
||||
|
||||
## Architecture
|
||||
### InputGroup
|
||||
|
||||
### Worker-Based Event Handling
|
||||
```typescript
|
||||
using inputs = gpio.inputGroup({
|
||||
switch: { pin: 16, pull: "up" },
|
||||
button: { pin: 20, pull: "up", debounce: 10 }
|
||||
})
|
||||
|
||||
Each input spawns a dedicated Web Worker that:
|
||||
// Access pins with full type safety
|
||||
inputs.pins.switch.value // ✓ TypeScript autocomplete
|
||||
inputs.pins.button.value // ✓ TypeScript autocomplete
|
||||
|
||||
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
|
||||
// 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
|
||||
|
||||
**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
|
||||
// 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
|
||||
|
||||
|
|
@ -161,24 +176,17 @@ Each input spawns a dedicated Web Worker that:
|
|||
|
||||
```typescript
|
||||
// Good - automatic cleanup
|
||||
using led = gpio.output(17) // Automatically released because of `using`
|
||||
{
|
||||
using led = gpio.output(17)
|
||||
led.value = 1
|
||||
```
|
||||
} // Automatically released
|
||||
|
||||
```typescript
|
||||
// Meh - manual cleanup required
|
||||
// Bad - 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
|
||||
|
|
@ -189,7 +197,6 @@ Pull resistors prevent floating input values when nothing is connected to the pi
|
|||
- **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"`
|
||||
|
||||
|
|
@ -245,27 +252,26 @@ 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 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
|
||||
led.value = switchInput.value
|
||||
if (inputs.pins.switch.value === 1) {
|
||||
led.value = 1
|
||||
}
|
||||
|
||||
// Toggle LED when button pressed
|
||||
button.onChange((event) => {
|
||||
if (event.value === 0) {
|
||||
for await (const event of inputs.events()) {
|
||||
if (event.pin === "button" && event.value === 0) {
|
||||
led.toggle()
|
||||
}
|
||||
})
|
||||
|
||||
// Mirror switch to LED
|
||||
switchInput.onChange((event) => {
|
||||
} else if (event.pin === "switch") {
|
||||
led.value = event.value
|
||||
})
|
||||
|
||||
// Keep process running
|
||||
await new Promise(() => {})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rotary Phone Dialer
|
||||
|
|
@ -275,31 +281,18 @@ 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 })
|
||||
using inputs = gpio.inputGroup({
|
||||
hook: { pin: 20, pull: "up" },
|
||||
rotary: { pin: 21, pull: "up", debounce: 1 },
|
||||
})
|
||||
|
||||
let digit = 0
|
||||
|
||||
hook.onChange((event) => {
|
||||
for await (const event of inputs.events()) {
|
||||
if (event.pin === "hook") {
|
||||
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}`)
|
||||
} else if (event.pin === "rotary" && event.value === 0) {
|
||||
console.log("Rotary pulse")
|
||||
}
|
||||
})
|
||||
|
||||
rotaryNumber.onChange((event) => {
|
||||
if (event.value === 1) {
|
||||
digit += 1
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise(() => {})
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
import type { Pointer } from "bun:ffi"
|
||||
import { gpiod, GPIOD_LINE_DIRECTION_INPUT, GPIOD_LINE_DIRECTION_OUTPUT } from "./ffi"
|
||||
import { cstr, mapPullToLibgpiod, mapEdgeToLibgpiod } from "./utils"
|
||||
import type { PullMode, EdgeMode } from "./types"
|
||||
|
||||
type LineRequestResult = {
|
||||
chip: Pointer
|
||||
request: Pointer
|
||||
}
|
||||
|
||||
export type InputLineConfig = {
|
||||
chipPath: string
|
||||
offset: number
|
||||
pull: PullMode
|
||||
debounce: number
|
||||
edge: EdgeMode
|
||||
}
|
||||
|
||||
export type OutputLineConfig = {
|
||||
chipPath: string
|
||||
offset: number
|
||||
initialValue: 0 | 1
|
||||
}
|
||||
|
||||
const cleanup = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const requestLine = (
|
||||
chipPath: string,
|
||||
offset: number,
|
||||
consumer: string,
|
||||
configureSettings: (settings: Pointer) => void
|
||||
): LineRequestResult => {
|
||||
const chip = gpiod.gpiod_chip_open(cstr(chipPath))
|
||||
if (!chip) cleanup("Failed to open GPIO chip")
|
||||
|
||||
const settings = gpiod.gpiod_line_settings_new()
|
||||
if (!settings) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to create line settings")
|
||||
}
|
||||
|
||||
configureSettings(settings!)
|
||||
|
||||
const lineConfig = gpiod.gpiod_line_config_new()
|
||||
if (!lineConfig) {
|
||||
gpiod.gpiod_line_settings_free(settings)
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to create line config")
|
||||
}
|
||||
|
||||
const offsets = new Uint32Array([offset])
|
||||
const ret = gpiod.gpiod_line_config_add_line_settings(lineConfig, offsets, 1, settings)
|
||||
gpiod.gpiod_line_settings_free(settings)
|
||||
|
||||
if (ret !== 0) {
|
||||
gpiod.gpiod_line_config_free(lineConfig)
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to add line settings")
|
||||
}
|
||||
|
||||
const requestConfig = gpiod.gpiod_request_config_new()
|
||||
if (!requestConfig) {
|
||||
gpiod.gpiod_line_config_free(lineConfig)
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to create request config")
|
||||
}
|
||||
|
||||
gpiod.gpiod_request_config_set_consumer(requestConfig, cstr(consumer))
|
||||
|
||||
const request = gpiod.gpiod_chip_request_lines(chip, requestConfig, lineConfig)
|
||||
gpiod.gpiod_request_config_free(requestConfig)
|
||||
gpiod.gpiod_line_config_free(lineConfig)
|
||||
|
||||
if (!request) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to request GPIO line")
|
||||
}
|
||||
|
||||
return { chip: chip!, request: request! }
|
||||
}
|
||||
|
||||
export const requestInputLine = (config: InputLineConfig): LineRequestResult => {
|
||||
return requestLine(config.chipPath, config.offset, "bun-gpio-input", (settings) => {
|
||||
gpiod.gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT)
|
||||
gpiod.gpiod_line_settings_set_bias(settings, mapPullToLibgpiod(config.pull))
|
||||
gpiod.gpiod_line_settings_set_edge_detection(settings, mapEdgeToLibgpiod(config.edge))
|
||||
|
||||
if (config.debounce > 0) {
|
||||
gpiod.gpiod_line_settings_set_debounce_period_us(settings, config.debounce * 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const requestOutputLine = (config: OutputLineConfig): LineRequestResult => {
|
||||
return requestLine(config.chipPath, config.offset, "bun-gpio", (settings) => {
|
||||
gpiod.gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT)
|
||||
gpiod.gpiod_line_settings_set_output_value(settings, config.initialValue)
|
||||
})
|
||||
}
|
||||
154
src/pins/gpio.ts
154
src/pins/gpio.ts
|
|
@ -1,23 +1,157 @@
|
|||
import { ptr } from "bun:ffi"
|
||||
import { readdir } from "node:fs/promises"
|
||||
import { gpiod, cstr } from "./ffi"
|
||||
import { Output } from "./output"
|
||||
import { Input } from "./input"
|
||||
import { ChipNotFoundError } from "./errors"
|
||||
import type { OutputOptions, InputOptions, ChipInfo } from "./types"
|
||||
import { gpiod, cstr, GPIOD_LINE_DIRECTION_OUTPUT, GPIOD_LINE_DIRECTION_INPUT } from "./ffi"
|
||||
import { OutputPin } from "./output"
|
||||
import { InputPin } from "./input"
|
||||
import { InputGroup } from "./input-group"
|
||||
import { ChipNotFoundError, PinInUseError } from "./errors"
|
||||
import { mapPullToLibgpiod, mapEdgeToLibgpiod, hashInputConfig } from "./utils"
|
||||
import type { OutputOptions, InputOptions, PullMode, ChipInfo } from "./types"
|
||||
|
||||
export class GPIO {
|
||||
#chipPath: string
|
||||
#resetOnClose: boolean
|
||||
|
||||
constructor(options?: { chip?: string }) {
|
||||
constructor(options?: { chip?: string; resetOnClose?: boolean }) {
|
||||
this.#chipPath = options?.chip ?? "/dev/gpiochip0"
|
||||
this.#resetOnClose = options?.resetOnClose ?? false
|
||||
}
|
||||
|
||||
output(pin: number, options?: OutputOptions): Output {
|
||||
return new Output(this.#chipPath, pin, options)
|
||||
output(pin: number, options?: OutputOptions): OutputPin {
|
||||
const initialValue = options?.initialValue ?? 0
|
||||
|
||||
const chip = gpiod.gpiod_chip_open(cstr(this.#chipPath))
|
||||
if (!chip) {
|
||||
throw new ChipNotFoundError(this.#chipPath)
|
||||
}
|
||||
|
||||
input(pin: number, options?: InputOptions): Input {
|
||||
return new Input(this.#chipPath, pin, options)
|
||||
try {
|
||||
const reqConfig = gpiod.gpiod_request_config_new()
|
||||
gpiod.gpiod_request_config_set_consumer(reqConfig, cstr("bun-gpio"))
|
||||
|
||||
const lineSettings = gpiod.gpiod_line_settings_new()
|
||||
gpiod.gpiod_line_settings_set_direction(lineSettings, GPIOD_LINE_DIRECTION_OUTPUT)
|
||||
gpiod.gpiod_line_settings_set_output_value(lineSettings, initialValue)
|
||||
|
||||
const lineConfig = gpiod.gpiod_line_config_new()
|
||||
const offsets = new Uint32Array([pin])
|
||||
gpiod.gpiod_line_config_add_line_settings(lineConfig, ptr(offsets), 1, lineSettings)
|
||||
|
||||
const request = gpiod.gpiod_chip_request_lines(chip, reqConfig, lineConfig)
|
||||
|
||||
gpiod.gpiod_line_settings_free(lineSettings)
|
||||
gpiod.gpiod_line_config_free(lineConfig)
|
||||
gpiod.gpiod_request_config_free(reqConfig)
|
||||
|
||||
if (!request) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
throw new PinInUseError(pin)
|
||||
}
|
||||
|
||||
let resetValue: 0 | 1 | undefined
|
||||
if (this.#resetOnClose) {
|
||||
const currentValue = gpiod.gpiod_line_request_get_value(request, pin)
|
||||
if (currentValue === -1) {
|
||||
console.warn(`Failed to read initial value for pin ${pin}, assuming 0`)
|
||||
resetValue = 0
|
||||
} else {
|
||||
resetValue = currentValue as 0 | 1
|
||||
}
|
||||
}
|
||||
|
||||
return new OutputPin(chip, request, pin, resetValue)
|
||||
} catch (err) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
input(pin: number, options?: InputOptions): InputPin<"pin"> {
|
||||
const group = this.inputGroup({
|
||||
pin: { pin, ...options },
|
||||
})
|
||||
|
||||
return new InputPin(group, "pin")
|
||||
}
|
||||
|
||||
inputGroup<T extends Record<string, { pin: number } & InputOptions>>(
|
||||
config: T
|
||||
): InputGroup<keyof T & string> {
|
||||
const chip = gpiod.gpiod_chip_open(cstr(this.#chipPath))
|
||||
if (!chip) {
|
||||
throw new ChipNotFoundError(this.#chipPath)
|
||||
}
|
||||
|
||||
try {
|
||||
const reqConfig = gpiod.gpiod_request_config_new()
|
||||
gpiod.gpiod_request_config_set_consumer(reqConfig, cstr("bun-gpio"))
|
||||
|
||||
const lineConfig = gpiod.gpiod_line_config_new()
|
||||
|
||||
const groups = new Map<
|
||||
string,
|
||||
Array<{ name: string; pin: number; pull: PullMode; options: InputOptions }>
|
||||
>()
|
||||
|
||||
for (const [name, pinConfig] of Object.entries(config)) {
|
||||
const pull = pinConfig.pull ?? "up"
|
||||
const debounce = pinConfig.debounce ?? 0
|
||||
const edge = pinConfig.edge ?? "both"
|
||||
|
||||
const hash = hashInputConfig(pull, debounce, edge)
|
||||
if (!groups.has(hash)) groups.set(hash, [])
|
||||
groups.get(hash)!.push({ name, pin: pinConfig.pin, pull, options: pinConfig })
|
||||
}
|
||||
|
||||
for (const [hash, pins] of groups) {
|
||||
const firstPin = pins[0]
|
||||
if (!firstPin) continue
|
||||
|
||||
const pull = firstPin.options.pull ?? "up"
|
||||
const debounce = firstPin.options.debounce ?? 0
|
||||
const edge = firstPin.options.edge ?? "both"
|
||||
|
||||
const lineSettings = gpiod.gpiod_line_settings_new()
|
||||
gpiod.gpiod_line_settings_set_direction(lineSettings, GPIOD_LINE_DIRECTION_INPUT)
|
||||
gpiod.gpiod_line_settings_set_bias(lineSettings, mapPullToLibgpiod(pull))
|
||||
gpiod.gpiod_line_settings_set_edge_detection(lineSettings, mapEdgeToLibgpiod(edge))
|
||||
gpiod.gpiod_line_settings_set_debounce_period_us(lineSettings, debounce * 1000)
|
||||
|
||||
const offsets = new Uint32Array(pins.map((p) => p.pin))
|
||||
gpiod.gpiod_line_config_add_line_settings(
|
||||
lineConfig,
|
||||
ptr(offsets),
|
||||
pins.length,
|
||||
lineSettings
|
||||
)
|
||||
|
||||
gpiod.gpiod_line_settings_free(lineSettings)
|
||||
}
|
||||
|
||||
const request = gpiod.gpiod_chip_request_lines(chip, reqConfig, lineConfig)
|
||||
|
||||
gpiod.gpiod_line_config_free(lineConfig)
|
||||
gpiod.gpiod_request_config_free(reqConfig)
|
||||
|
||||
if (!request) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
const firstConfig = Object.values(config)[0]
|
||||
throw new PinInUseError(firstConfig?.pin ?? 0)
|
||||
}
|
||||
|
||||
const pinMap: Record<string, { offset: number; pull: PullMode }> = {}
|
||||
for (const [name, pinConfig] of Object.entries(config)) {
|
||||
pinMap[name] = {
|
||||
offset: pinConfig.pin,
|
||||
pull: pinConfig.pull ?? "up",
|
||||
}
|
||||
}
|
||||
|
||||
return new InputGroup(chip, request, pinMap)
|
||||
} catch (err) {
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listChips(): Promise<ChipInfo[]> {
|
||||
|
|
|
|||
205
src/pins/input-group.ts
Normal file
205
src/pins/input-group.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import type { Pointer } from "bun:ffi"
|
||||
import { gpiod } from "./ffi"
|
||||
import { mapLibgpiodEdgeToPressedState } from "./utils"
|
||||
import type { PullMode, InputEvent, InputGroupEvent, PinConfig } from "./types"
|
||||
|
||||
export class InputGroup<T extends string = string> {
|
||||
#closed = false
|
||||
#chip: Pointer
|
||||
#request: Pointer
|
||||
#pinMap: Map<string, { offset: number; pull: PullMode }>
|
||||
#offsetMap: Map<number, { name: string; pull: PullMode }>
|
||||
#eventBuffer: Pointer | undefined
|
||||
#eventListeners: Array<(event: InputGroupEvent<T>) => void> = []
|
||||
#closeHandlers: Array<() => void> = []
|
||||
|
||||
constructor(chip: Pointer, request: Pointer, pinConfig: PinConfig) {
|
||||
this.#chip = chip
|
||||
this.#request = request
|
||||
|
||||
this.#pinMap = new Map()
|
||||
this.#offsetMap = new Map()
|
||||
|
||||
for (const [name, config] of Object.entries(pinConfig)) {
|
||||
this.#pinMap.set(name, config)
|
||||
this.#offsetMap.set(config.offset, { name, pull: config.pull })
|
||||
}
|
||||
}
|
||||
|
||||
get pins(): Record<
|
||||
T,
|
||||
{ readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }
|
||||
> {
|
||||
const result = {} as Record<
|
||||
T,
|
||||
{
|
||||
readonly value: 0 | 1
|
||||
waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void>
|
||||
}
|
||||
>
|
||||
|
||||
for (const [name, config] of this.#pinMap) {
|
||||
const offset = config.offset
|
||||
const closed = () => this.#closed
|
||||
const request = this.#request
|
||||
const pinName = name
|
||||
|
||||
Object.defineProperty(result, name, {
|
||||
get: () => ({
|
||||
get value(): 0 | 1 {
|
||||
if (closed()) throw new Error("InputGroup is closed")
|
||||
const ret = gpiod.gpiod_line_request_get_value(request, offset)
|
||||
if (ret === -1) throw new Error("Failed to get pin value")
|
||||
return ret as 0 | 1
|
||||
},
|
||||
waitForValue: (targetValue: 0 | 1, timeout?: number) =>
|
||||
this.#waitForPinValue(pinName as T, targetValue, timeout),
|
||||
}),
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async #waitForPinValue(pinName: T, targetValue: 0 | 1, timeout?: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.#closed) {
|
||||
reject(new Error("InputGroup is closed"))
|
||||
return
|
||||
}
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
|
||||
this.#closeHandlers = this.#closeHandlers.filter((h) => h !== onClose)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
cleanup()
|
||||
reject(new Error("InputGroup closed while waiting"))
|
||||
}
|
||||
|
||||
const listener = (event: InputGroupEvent<T>) => {
|
||||
if (event.pin !== pinName) return
|
||||
if (event.value !== targetValue) return
|
||||
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error(`Timeout waiting for pin ${pinName} to become ${targetValue}`))
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
this.#eventListeners.push(listener)
|
||||
this.#closeHandlers.push(onClose)
|
||||
this.#startEventLoop()
|
||||
})
|
||||
}
|
||||
|
||||
async *events(): AsyncGenerator<InputGroupEvent<T>> {
|
||||
if (this.#closed) throw new Error("InputGroup is closed")
|
||||
|
||||
const eventQueue: InputGroupEvent<T>[] = []
|
||||
const listener = (event: InputGroupEvent<T>) => {
|
||||
eventQueue.push(event)
|
||||
}
|
||||
|
||||
this.#eventListeners.push(listener)
|
||||
this.#startEventLoop()
|
||||
|
||||
try {
|
||||
while (!this.#closed) {
|
||||
if (eventQueue.length > 0) {
|
||||
for (const event of eventQueue) {
|
||||
yield event
|
||||
}
|
||||
eventQueue.length = 0
|
||||
} else {
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
|
||||
#startEventLoop() {
|
||||
if (this.#eventBuffer !== undefined) return
|
||||
|
||||
const buffer = gpiod.gpiod_edge_event_buffer_new(1)
|
||||
if (!buffer) throw new Error("Failed to create event buffer")
|
||||
|
||||
this.#eventBuffer = buffer
|
||||
this.#runEventLoop()
|
||||
}
|
||||
|
||||
async #runEventLoop() {
|
||||
try {
|
||||
while (!this.#closed && this.#eventListeners.length > 0) {
|
||||
const ret = gpiod.gpiod_line_request_wait_edge_events(this.#request, 100_000_000)
|
||||
|
||||
if (ret === -1 || ret === 0) {
|
||||
await Bun.sleep(0)
|
||||
continue
|
||||
}
|
||||
|
||||
const numEvents = gpiod.gpiod_line_request_read_edge_events(
|
||||
this.#request,
|
||||
this.#eventBuffer!,
|
||||
1
|
||||
)
|
||||
|
||||
if (numEvents > 0) {
|
||||
const event = gpiod.gpiod_edge_event_buffer_get_event(this.#eventBuffer!, 0)
|
||||
const edgeType = gpiod.gpiod_edge_event_get_event_type(event)
|
||||
const timestamp = gpiod.gpiod_edge_event_get_timestamp_ns(event)
|
||||
const offset = gpiod.gpiod_edge_event_get_line_offset(event)
|
||||
|
||||
const pinInfo = this.#offsetMap.get(offset)
|
||||
if (!pinInfo) continue
|
||||
|
||||
const pressed = mapLibgpiodEdgeToPressedState(edgeType, pinInfo.pull)
|
||||
const value = (
|
||||
pressed ? (pinInfo.pull === "up" ? 0 : 1) : pinInfo.pull === "up" ? 1 : 0
|
||||
) as 0 | 1
|
||||
const inputEvent: InputGroupEvent<T> = { pin: pinInfo.name as T, value, timestamp }
|
||||
|
||||
for (const listener of this.#eventListeners) {
|
||||
listener(inputEvent)
|
||||
}
|
||||
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (this.#eventBuffer) {
|
||||
gpiod.gpiod_edge_event_buffer_free(this.#eventBuffer)
|
||||
this.#eventBuffer = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.#closed) return
|
||||
this.#closed = true
|
||||
|
||||
for (const handler of this.#closeHandlers) {
|
||||
handler()
|
||||
}
|
||||
this.#closeHandlers = []
|
||||
|
||||
gpiod.gpiod_line_request_release(this.#request)
|
||||
gpiod.gpiod_chip_close(this.#chip)
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { gpiod, GPIOD_EDGE_EVENT_RISING_EDGE } from "./ffi"
|
||||
import { requestInputLine } from "./gpio-helpers"
|
||||
import type { PullMode, EdgeMode } from "./types"
|
||||
|
||||
type WorkerConfig = {
|
||||
chipPath: string
|
||||
offset: number
|
||||
pull: PullMode
|
||||
debounce: number
|
||||
edge: EdgeMode
|
||||
}
|
||||
|
||||
type WorkerMessage =
|
||||
| { type: "ready"; initialValue: 0 | 1 }
|
||||
| { type: "event"; value: 0 | 1; timestamp: bigint }
|
||||
| { type: "error"; message: string }
|
||||
|
||||
const postMessage = (message: WorkerMessage) => {
|
||||
self.postMessage(message)
|
||||
}
|
||||
|
||||
const cleanup = (message: string): never => {
|
||||
postMessage({ type: "error", message })
|
||||
self.close()
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const mapEdgeToValue = (edgeType: number, pull: PullMode): 0 | 1 => {
|
||||
// Pull-up: rising edge = released (1), falling edge = pressed (0)
|
||||
// Pull-down: rising edge = pressed (1), falling edge = released (0)
|
||||
if (pull === "up") {
|
||||
return edgeType === GPIOD_EDGE_EVENT_RISING_EDGE ? 1 : 0
|
||||
}
|
||||
return edgeType === GPIOD_EDGE_EVENT_RISING_EDGE ? 1 : 0
|
||||
}
|
||||
|
||||
const run = (config: WorkerConfig) => {
|
||||
const { chip, request } = requestInputLine(config)
|
||||
|
||||
const initialValue = gpiod.gpiod_line_request_get_value(request, config.offset)
|
||||
if (initialValue === -1) {
|
||||
gpiod.gpiod_line_request_release(request)
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to read initial value")
|
||||
}
|
||||
|
||||
postMessage({ type: "ready", initialValue: initialValue as 0 | 1 })
|
||||
|
||||
const buffer = gpiod.gpiod_edge_event_buffer_new(1)
|
||||
if (!buffer) {
|
||||
gpiod.gpiod_line_request_release(request)
|
||||
gpiod.gpiod_chip_close(chip)
|
||||
cleanup("Failed to create event buffer")
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// Block forever (-1 timeout) until edge event occurs
|
||||
const waitResult = gpiod.gpiod_line_request_wait_edge_events(request, -1)
|
||||
|
||||
if (waitResult === 1) {
|
||||
const numEvents = gpiod.gpiod_line_request_read_edge_events(request, buffer, 1)
|
||||
if (numEvents === -1) cleanup("Failed to read edge events")
|
||||
|
||||
const event = gpiod.gpiod_edge_event_buffer_get_event(buffer, 0)
|
||||
const edgeType = gpiod.gpiod_edge_event_get_event_type(event)
|
||||
const timestamp = gpiod.gpiod_edge_event_get_timestamp_ns(event)
|
||||
|
||||
const value = mapEdgeToValue(edgeType, config.pull)
|
||||
|
||||
postMessage({ type: "event", value, timestamp })
|
||||
} else if (waitResult === -1) {
|
||||
cleanup("GPIO wait_edge_events failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Worker terminates - kernel cleans up GPIO resources automatically
|
||||
}
|
||||
|
||||
self.onmessage = (event: MessageEvent<WorkerConfig>) => {
|
||||
self.onmessage = () => {
|
||||
throw new Error("Worker already initialized")
|
||||
}
|
||||
run(event.data)
|
||||
}
|
||||
|
|
@ -1,90 +1,36 @@
|
|||
import type { InputEvent, InputOptions } from "./types"
|
||||
import type { Pointer } from "bun:ffi"
|
||||
import { InputGroup } from "./input-group"
|
||||
import type { PullMode, InputEvent } from "./types"
|
||||
|
||||
type WorkerMessage =
|
||||
| { type: "ready"; initialValue: 0 | 1 }
|
||||
| { type: "event"; value: 0 | 1; timestamp: bigint }
|
||||
| { type: "error"; message: string }
|
||||
export class InputPin<T extends string = string> {
|
||||
#group: InputGroup<T>
|
||||
#pinName: T
|
||||
|
||||
export class Input {
|
||||
#worker: Worker
|
||||
#callbacks = new Set<(event: InputEvent) => void>()
|
||||
#closed = false
|
||||
#lastValue: 0 | 1 = 0
|
||||
|
||||
constructor(chipPath: string, offset: number, options: InputOptions = {}) {
|
||||
const pull = options.pull ?? "up"
|
||||
const debounce = options.debounce ?? 0
|
||||
const edge = options.edge ?? "both"
|
||||
|
||||
this.#worker = new Worker(new URL("./input-worker.ts", import.meta.url).href)
|
||||
|
||||
this.#worker.onmessage = (msg: MessageEvent<WorkerMessage>) => {
|
||||
if (this.#closed) return
|
||||
|
||||
const data = msg.data
|
||||
|
||||
if (data.type === "ready") {
|
||||
this.#lastValue = data.initialValue
|
||||
} else if (data.type === "event") {
|
||||
this.#lastValue = data.value
|
||||
for (const callback of this.#callbacks) {
|
||||
callback({ value: data.value, timestamp: data.timestamp })
|
||||
}
|
||||
} else if (data.type === "error") {
|
||||
console.error(`GPIO Input Error (pin ${offset}):`, data.message)
|
||||
}
|
||||
}
|
||||
|
||||
this.#worker.postMessage({ chipPath, offset, pull, debounce, edge })
|
||||
constructor(group: InputGroup<T>, pinName: T) {
|
||||
this.#group = group
|
||||
this.#pinName = pinName
|
||||
}
|
||||
|
||||
get value(): 0 | 1 {
|
||||
if (this.#closed) throw new Error("Input is closed")
|
||||
return this.#lastValue
|
||||
}
|
||||
|
||||
onChange(callback: (event: InputEvent) => void): () => void {
|
||||
if (this.#closed) throw new Error("Input is closed")
|
||||
this.#callbacks.add(callback)
|
||||
return () => this.#callbacks.delete(callback)
|
||||
return this.#group.pins[this.#pinName]!.value
|
||||
}
|
||||
|
||||
async waitForValue(targetValue: 0 | 1, timeout?: number): Promise<void> {
|
||||
if (this.#closed) throw new Error("Input is closed")
|
||||
|
||||
if (this.#lastValue === targetValue) return
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
const unsubscribe = this.onChange((event) => {
|
||||
for await (const event of this.#group.events()) {
|
||||
if (event.value === targetValue) {
|
||||
cleanup()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!timeout) return
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error(`Timeout waiting for value ${targetValue}`))
|
||||
}, timeout)
|
||||
})
|
||||
async *events(): AsyncGenerator<InputEvent> {
|
||||
for await (const event of this.#group.events()) {
|
||||
yield { value: event.value, timestamp: event.timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.#closed) return
|
||||
this.#closed = true
|
||||
|
||||
this.#callbacks.clear()
|
||||
this.#worker.onmessage = null
|
||||
this.#worker.terminate()
|
||||
this.#group.close()
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,30 @@
|
|||
import type { Pointer } from "bun:ffi"
|
||||
import { gpiod } from "./ffi"
|
||||
import { requestOutputLine } from "./gpio-helpers"
|
||||
import type { OutputOptions } from "./types"
|
||||
|
||||
export class Output {
|
||||
export class OutputPin {
|
||||
#closed = false
|
||||
#chip: Pointer
|
||||
#request: Pointer
|
||||
#offset: number
|
||||
#pin: number
|
||||
#resetValue?: 0 | 1
|
||||
|
||||
constructor(chipPath: string, offset: number, options: OutputOptions = {}) {
|
||||
const initialValue = options.initialValue ?? 0
|
||||
const { chip, request } = requestOutputLine({ chipPath, offset, initialValue })
|
||||
|
||||
constructor(chip: Pointer, request: Pointer, pin: number, resetValue?: 0 | 1) {
|
||||
this.#chip = chip
|
||||
this.#request = request
|
||||
this.#offset = offset
|
||||
|
||||
if (options.resetOnClose) {
|
||||
const currentValue = gpiod.gpiod_line_request_get_value(request, offset)
|
||||
this.#resetValue = currentValue === -1 ? 0 : (currentValue as 0 | 1)
|
||||
}
|
||||
this.#pin = pin
|
||||
this.#resetValue = resetValue
|
||||
}
|
||||
|
||||
get value(): 0 | 1 {
|
||||
if (this.#closed) throw new Error("Output is closed")
|
||||
const ret = gpiod.gpiod_line_request_get_value(this.#request, this.#offset)
|
||||
if (this.#closed) throw new Error("OutputPin is closed")
|
||||
const ret = gpiod.gpiod_line_request_get_value(this.#request, this.#pin)
|
||||
if (ret === -1) throw new Error("Failed to get pin value")
|
||||
return ret as 0 | 1
|
||||
}
|
||||
|
||||
set value(val: 0 | 1) {
|
||||
if (this.#closed) throw new Error("Output is closed")
|
||||
const ret = gpiod.gpiod_line_request_set_value(this.#request, this.#offset, val)
|
||||
if (this.#closed) throw new Error("OutputPin is closed")
|
||||
const ret = gpiod.gpiod_line_request_set_value(this.#request, this.#pin, val)
|
||||
if (ret === -1) throw new Error("Failed to set pin value")
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +37,7 @@ export class Output {
|
|||
this.#closed = true
|
||||
|
||||
if (this.#resetValue !== undefined) {
|
||||
gpiod.gpiod_line_request_set_value(this.#request, this.#offset, this.#resetValue)
|
||||
gpiod.gpiod_line_request_set_value(this.#request, this.#pin, this.#resetValue)
|
||||
}
|
||||
|
||||
gpiod.gpiod_line_request_release(this.#request)
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "WebWorker"]
|
||||
},
|
||||
"include": ["input-worker.ts"]
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ export type InputOptions = {
|
|||
|
||||
export type OutputOptions = {
|
||||
initialValue?: 0 | 1 // default: 0
|
||||
resetOnClose?: boolean // default: false
|
||||
}
|
||||
|
||||
export type InputEvent = {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ export const mapEdgeToLibgpiod = (edge: EdgeMode): number => {
|
|||
// Hardware logic:
|
||||
// - Pull-up + button to GND: pressing pulls line LOW (falling edge = pressed)
|
||||
// - Pull-down + button to VCC: pressing pulls line HIGH (rising edge = pressed)
|
||||
export const mapLibgpiodEdgeToPressedState = (edgeType: number, pull: PullMode): boolean => {
|
||||
export const mapLibgpiodEdgeToPressedState = (
|
||||
edgeType: number,
|
||||
pull: PullMode
|
||||
): boolean => {
|
||||
if (pull === "up") {
|
||||
return edgeType === GPIOD_EDGE_EVENT_FALLING_EDGE
|
||||
} else if (pull === "down") {
|
||||
|
|
@ -48,6 +51,10 @@ export const mapLibgpiodEdgeToPressedState = (edgeType: number, pull: PullMode):
|
|||
}
|
||||
}
|
||||
|
||||
export const hashInputConfig = (pull: PullMode, debounce: number, edge: EdgeMode): string => {
|
||||
export const hashInputConfig = (
|
||||
pull: PullMode,
|
||||
debounce: number,
|
||||
edge: EdgeMode
|
||||
): string => {
|
||||
return `${pull}-${debounce}-${edge}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,38 +2,45 @@ import { GPIO } from "./pins"
|
|||
|
||||
console.log(`kill -9 ${process.pid}`)
|
||||
|
||||
const gpio = new GPIO()
|
||||
const gpio = new GPIO({ resetOnClose: true })
|
||||
|
||||
// // Blink an LED
|
||||
using led = gpio.output(21)
|
||||
using button = gpio.input(20, { pull: "up", debounce: 10 })
|
||||
using switchInput = gpio.input(16, { pull: "up", debounce: 10 })
|
||||
|
||||
led.value = button.value
|
||||
// Read a button
|
||||
using inputs = gpio.inputGroup({
|
||||
button: { pin: 20, pull: "up", debounce: 10 },
|
||||
switch: { pin: 16, pull: "up", debounce: 10 }
|
||||
})
|
||||
|
||||
button.onChange((event) => {
|
||||
led.value = inputs.pins.button.value
|
||||
|
||||
const iteratorEvents = new Promise(async (resolve) => {
|
||||
for await (const event of inputs.events()) {
|
||||
if (event.pin === "button") {
|
||||
led.value = event.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const switchEvent = new Promise<void>(async (resolve) => {
|
||||
await switchInput.waitForValue(0)
|
||||
await inputs.pins.switch.waitForValue(0)
|
||||
console.log("Switch pressed!")
|
||||
resolve()
|
||||
})
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
button.close()
|
||||
switchInput.close()
|
||||
inputs.close()
|
||||
led.close()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
button.close()
|
||||
switchInput.close()
|
||||
led.close()
|
||||
inputs.close()
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
await switchEvent
|
||||
await Promise.race([iteratorEvents, switchEvent])
|
||||
|
||||
console.log(`👋 Goodbye!`)
|
||||
|
|
@ -26,6 +26,5 @@
|
|||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"exclude": ["src/pins/input-worker.ts"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user