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>( config: T ): InputGroup { 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 = {} 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 { 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 } }