200 lines
6.3 KiB
TypeScript
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
|
|
}
|
|
}
|