phone/src/pins/gpio.ts
2025-11-18 14:33:25 -08:00

200 lines
6.3 KiB
TypeScript

import { ptr } from "bun:ffi"
import { readdir } from "node:fs/promises"
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; resetOnClose?: boolean }) {
this.#chipPath = options?.chip ?? "/dev/gpiochip0"
this.#resetOnClose = options?.resetOnClose ?? false
}
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)
}
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[]> {
const chips: ChipInfo[] = []
try {
const files = await readdir("/dev")
const chipFiles = files.filter((f) => f.startsWith("gpiochip"))
for (const file of chipFiles) {
const path = `/dev/${file}`
try {
const chip = gpiod.gpiod_chip_open(cstr(path))
if (!chip) continue
const info = gpiod.gpiod_chip_get_info(chip)
if (!info) {
gpiod.gpiod_chip_close(chip)
continue
}
const name = gpiod.gpiod_chip_info_get_name(info)
const label = gpiod.gpiod_chip_info_get_label(info)
const numLines = gpiod.gpiod_chip_info_get_num_lines(info)
chips.push({
path,
name: String(name || ""),
label: String(label || ""),
numLines: Number(numLines),
})
gpiod.gpiod_chip_close(chip)
} catch {
continue
}
}
} catch {
// /dev might not be accessible, return empty array
}
return chips
}
}