Spaces:
Running
Running
Download src/serial.js from lerobot/glove_visualizer: direct link, hf CLI and curl.
- Browser
- Download file 3.86 kB
-
https://huggingface.co/spaces/lerobot/glove_visualizer/resolve/main/src/serial.js
- Command line
-
hf download hf://spaces/lerobot/glove_visualizer/src/serial.js
-
curl -L -o serial.js https://huggingface.co/spaces/lerobot/glove_visualizer/resolve/main/src/serial.js
3.86 kB
| // 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; | |
| } | |
| } | |