CoolFace
Apppublic

lerobot/glove_visualizer

sourceHugging Faceapache-2.0updated 8d agoView on Hugging Face
5likes
serial.js79 linesDownload Raw Back to src
1// Homunculus / LeRobot PR #1713: newline-delimited, 16 whitespace-separated2// integers at 115200 baud. No writes to the device are needed.3export const CHANNEL_NAMES = ["thumb_cmc", "thumb_mcp", "thumb_pip", "thumb_dip",4  ...["index", "middle", "ring", "pinky"].flatMap(f => [`${f}_mcp_abduction`, `${f}_mcp_flexion`, `${f}_dip`])];5 6export class FrameParser {7  constructor(onFrame, onInvalid = () => {}) { this.onFrame = onFrame; this.onInvalid = onInvalid; this.reset(); }8  reset() { this.buffer = ""; this.discarding = false; this.decoder = new TextDecoder(); }9  push(chunk) {10    const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });11    for (const character of text) {12      if (character === "\n") {13        if (!this.discarding && this.buffer.trim()) {14          const fields = this.buffer.trim().split(/\s+/);15          if (fields.length === 16 && fields.every(s => /^\d{1,4}$/.test(s) && Number(s) <= 4096)) this.onFrame(fields.map(Number));16          else this.onInvalid();17        }18        this.buffer = ""; this.discarding = false;19      } else if (!this.discarding) {20        if (this.buffer.length >= 1024) { this.buffer = ""; this.discarding = true; this.onInvalid(); }21        else this.buffer += character;22      }23    }24  }25}26 27export class GloveSerial {28  constructor({ onFrame, onState = () => {}, onInvalid, serial = globalThis.navigator?.serial }) {29    this.serial = serial; this.onState = onState; this.parser = new FrameParser(onFrame, onInvalid);30    this.state = "disconnected"; this.port = null; this.reader = null; this.task = null; this.generation = 0;31  }32  emit(state, message = state) { this.state = state; this.onState(state, message); }33  async connect(baudRate = 115200) {34    if (this.state !== "disconnected") throw new Error("Serial connection is already active");35    if (!this.serial) throw new Error("Web Serial is unavailable. Open localhost or HTTPS in desktop Chrome/Edge.");36    if (!Number.isInteger(baudRate) || baudRate <= 0) throw new Error("Invalid baud rate");37    const generation = ++this.generation;38    this.emit("connecting", "Select the glove USB port…");39    let candidate;40    try {41      candidate = await this.serial.requestPort();42      if (generation !== this.generation) return;43      await candidate.open({ baudRate });44      if (generation !== this.generation) { await candidate.close(); return; }45      if (!candidate.readable) throw new Error("The selected port is not readable");46      this.port = candidate; this.parser.reset();47      this.emit("connected", "Connected — waiting for a complete frame");48      this.task = this.pump(candidate, generation);49    } catch (error) {50      if (candidate && !this.port) { try { await candidate.close(); } catch {} }51      if (generation === this.generation) this.emit("disconnected", error.name === "NotFoundError" ? "Connection cancelled" : error.message);52    }53  }54  async pump(port, generation) {55    let message = "Disconnected";56    try {57      this.reader = port.readable.getReader();58      while (generation === this.generation) {59        const { value, done } = await this.reader.read();60        if (done) break;61        if (value && generation === this.generation) this.parser.push(value);62      }63    } catch (error) { message = `Serial stopped: ${error.message}`; }64    finally {65      this.reader?.releaseLock(); this.reader = null;66      try { await port.close(); } catch (error) { message += ` (${error.message})`; }67      this.port = null; this.parser.reset(); this.task = null;68      this.emit("disconnected", message);69    }70  }71  async disconnect() {72    ++this.generation;73    if (!this.task) { this.emit("disconnected", "Disconnected"); return; }74    this.emit("disconnecting", "Disconnecting…");75    try { await this.reader?.cancel(); } catch {}76    await this.task;77  }78}79