// Homunculus / LeRobot PR #1713: newline-delimited, 16 whitespace-separated // integers at 115200 baud. No writes to the device are needed. export const CHANNEL_NAMES = ["thumb_cmc", "thumb_mcp", "thumb_pip", "thumb_dip", ...["index", "middle", "ring", "pinky"].flatMap(f => [`${f}_mcp_abduction`, `${f}_mcp_flexion`, `${f}_dip`])]; export class FrameParser { constructor(onFrame, onInvalid = () => {}) { this.onFrame = onFrame; this.onInvalid = onInvalid; this.reset(); } reset() { this.buffer = ""; this.discarding = false; this.decoder = new TextDecoder(); } push(chunk) { const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true }); for (const character of text) { if (character === "\n") { if (!this.discarding && this.buffer.trim()) { const fields = this.buffer.trim().split(/\s+/); if (fields.length === 16 && fields.every(s => /^\d{1,4}$/.test(s) && Number(s) <= 4096)) this.onFrame(fields.map(Number)); else this.onInvalid(); } this.buffer = ""; this.discarding = false; } else if (!this.discarding) { if (this.buffer.length >= 1024) { this.buffer = ""; this.discarding = true; this.onInvalid(); } else this.buffer += character; } } } } export class GloveSerial { constructor({ onFrame, onState = () => {}, onInvalid, serial = globalThis.navigator?.serial }) { this.serial = serial; this.onState = onState; this.parser = new FrameParser(onFrame, onInvalid); this.state = "disconnected"; this.port = null; this.reader = null; this.task = null; this.generation = 0; } emit(state, message = state) { this.state = state; this.onState(state, message); } async connect(baudRate = 115200) { if (this.state !== "disconnected") throw new Error("Serial connection is already active"); if (!this.serial) throw new Error("Web Serial is unavailable. Open localhost or HTTPS in desktop Chrome/Edge."); if (!Number.isInteger(baudRate) || baudRate <= 0) throw new Error("Invalid baud rate"); const generation = ++this.generation; this.emit("connecting", "Select the glove USB port…"); let candidate; try { candidate = await this.serial.requestPort(); if (generation !== this.generation) return; await candidate.open({ baudRate }); if (generation !== this.generation) { await candidate.close(); return; } if (!candidate.readable) throw new Error("The selected port is not readable"); this.port = candidate; this.parser.reset(); this.emit("connected", "Connected — waiting for a complete frame"); this.task = this.pump(candidate, generation); } catch (error) { if (candidate && !this.port) { try { await candidate.close(); } catch {} } if (generation === this.generation) this.emit("disconnected", error.name === "NotFoundError" ? "Connection cancelled" : error.message); } } async pump(port, generation) { let message = "Disconnected"; try { this.reader = port.readable.getReader(); while (generation === this.generation) { const { value, done } = await this.reader.read(); if (done) break; if (value && generation === this.generation) this.parser.push(value); } } catch (error) { message = `Serial stopped: ${error.message}`; } finally { this.reader?.releaseLock(); this.reader = null; try { await port.close(); } catch (error) { message += ` (${error.message})`; } this.port = null; this.parser.reset(); this.task = null; this.emit("disconnected", message); } } async disconnect() { ++this.generation; if (!this.task) { this.emit("disconnected", "Disconnected"); return; } this.emit("disconnecting", "Disconnecting…"); try { await this.reader?.cancel(); } catch {} await this.task; } }