phone/src/pins/input.ts
2025-11-19 13:00:39 -08:00

94 lines
2.5 KiB
TypeScript

import type { InputEvent, InputOptions } from "./types"
type WorkerMessage =
| { type: "ready"; initialValue: 0 | 1 }
| { type: "event"; value: 0 | 1; timestamp: bigint }
| { type: "error"; message: string }
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 })
}
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)
}
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) => {
if (event.value === targetValue) {
cleanup()
resolve()
}
})
if (!timeout) return
timeoutId = setTimeout(() => {
cleanup()
reject(new Error(`Timeout waiting for value ${targetValue}`))
}, timeout)
})
}
close() {
if (this.#closed) return
this.#closed = true
this.#callbacks.clear()
this.#worker.onmessage = null
this.#worker.terminate()
}
[Symbol.dispose]() {
this.close()
}
}