UNPKG

@ridge18/web-serial-monitor

Version:

JavaScript library to interact with serial devices using the Web Serial API.

240 lines (205 loc) 7.09 kB
/** * Web Serial Monitor module. * @module SerialMonitor */ /** * Class representing a serial connection through Web Serial API. * @extends EventTarget */ class WebSerialMonitor extends EventTarget { // private members #byteMode = false; #byteHex = false; #parseLines = false; #port= null; #reader; #inputDone; #outputDone; #inputStream; #outputStream; async #readLoop() { while (true) { try { const { value, done } = await this.#reader.read(); if (value) { const event = new CustomEvent("serial-data", { detail: value }); this.dispatchEvent(event); } if (done) { console.log('[readLoop] DONE', done); this.#reader.releaseLock(); break; } } catch(error) { const event = new CustomEvent("serial-error", { detail: error }); this.dispatchEvent(event); break; } } } // public members /** * Create an SerialMonitor instance * @param {object} options - instance options. */ constructor(options) { super(); if(options) { if(options.mode === "byte") { this.#byteMode = true; if(options.hex) this.#byteHex = true; } else if(options.parseLines) this.#parseLines = true; } this.#port = null; this.#reader = null; this.#inputDone = null; this.#outputDone = null; this.#inputStream = null; this.#outputStream = null; } /** * Get the mode of the SerialMonitor instance */ get mode() { if(this.#byteMode) return "byte"; else return "text"; } /** * Connect to a serial device * @param {number} bauds - baud rate of the serial connection. */ async connect(bauds) { if(this.#port != null) { const event = new CustomEvent("serial-error", { detail: "Close current connection first" }); this.dispatchEvent(event); return Promise.reject(new Error("Close current connection first")); } const p = await navigator.serial.requestPort().catch((error) => { const event = new CustomEvent("serial-error", { detail: "No port selected" }); this.dispatchEvent(event); return Promise.reject(new Error(error)); }); this.#port = p; await this.#port.open({baudRate: bauds}).then(() => { if(this.#byteMode) { if(this.#byteHex) { this.#inputStream = this.#port.readable.pipeThrough(new TransformStream(new DecToHexTransformer())); this.#reader = this.#inputStream.getReader(); } else { this.#reader = this.#port.readable.getReader(); } } else { // eslint-disable-next-line no-undef const decoder = new TextDecoderStream(); this.#inputDone = this.#port.readable.pipeTo(decoder.writable); if(this.#parseLines) this.#inputStream = decoder.readable.pipeThrough(new TransformStream(new LineBreakTransformer())); else this.#inputStream = decoder.readable; this.#reader = this.#inputStream.getReader(); } // eslint-disable-next-line no-undef const encoder = new TextEncoderStream(); this.#outputDone = encoder.readable.pipeTo(this.#port.writable); this.#outputStream = encoder.writable; const event = new CustomEvent("serial-connected", { detail: "Serial port connected" }); this.dispatchEvent(event); this.#readLoop(); }).catch((error) => { const event = new CustomEvent("serial-error", { detail: error.message }); this.dispatchEvent(event); return Promise.reject(error); }); return "Serial port connected"; } /** * Disconnect from a serial device */ async disconnect() { if(this.#port == null) return; if(this.#reader) { await this.#reader.cancel().catch((error) => { console.log(error); }); if(this.#inputDone) await this.#inputDone.catch(() => {}); this.#reader = null; this.#inputDone = null; } if (this.#outputStream) { await this.#outputStream.getWriter().close().catch((error) => { console.log(error); }); await this.#outputDone.catch((error) => { console.log(error); }); this.#outputStream = null; this.#outputDone = null; } await this.#port.close().catch((error) => { const event = new CustomEvent("serial-error", { detail: error.message }); this.dispatchEvent(event); return Promise.reject(error); }); this.#port = null; const event = new CustomEvent("serial-disconnected", { detail: "Serial port disconnected" }); this.dispatchEvent(event); return "Serial port disconnected"; } /** * Send data to a serial device * @param {string} data - data string to send. */ send(data) { if(this.#port != null) { const writer = this.#outputStream.getWriter(); writer.write(data); writer.releaseLock(); } else { const event = new CustomEvent("serial-error", { detail: "No serial port" }); this.dispatchEvent(event); } } } // TransformStream to parse the stream into lines. class LineBreakTransformer { constructor() { this.container = ''; } transform(chunk, controller) { this.container += chunk; const lines = this.container.split('\n'); this.container = lines.pop(); lines.forEach(line => controller.enqueue(line)); } flush(controller) { controller.enqueue(this.container); } } // TransformStream to parse the stream into hex bytes values. class DecToHexTransformer { constructor() { this.container = ''; } transform(chunk, controller) { this.container += chunk; const bytes = this.container.split(','); bytes.forEach(byte => { const number = parseInt(byte); if(number <= 15) controller.enqueue(number.toString(16).padStart(2, '0') + ' '); else controller.enqueue(number.toString(16) + ' '); }); } flush(controller) { controller.enqueue(Number(this.container).toString(16) + ' '); } } export default WebSerialMonitor;