UNPKG

@robotical/ricjs

Version:

Javascript/TS library for Robotical RIC

345 lines (293 loc) 9.97 kB
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // RICJS // Communications Library // // Rob Dobson, Chris Greening & Alexander (Sandy) Enoch 2020-2023 // (C) 2020-2023 // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// import RICChannel from "./RICChannel"; import RICMsgHandler from "./RICMsgHandler"; import RICLog from "./RICLog"; //import RICUtils from "./RICUtils"; //import SerialPort from "@types/w3c-web-serial"; import { RICConnEvent, RICConnEventFn } from "./RICConnEvents"; export default class RICChannelWebSerial implements RICChannel { // Message handler private _ricMsgHandler: RICMsgHandler | null = null; private _port: SerialPort | null = null; private _reader?: ReadableStreamDefaultReader<Uint8Array>; // Last message tx time // private _msgTxTimeLast = Date.now(); // private _msgTxMinTimeBetweenMs = 15; // Is connected private _isConnected = false; private _connPaused = false; private _serialBuffer: number[] = []; private _escapeSeqCode = 0; private _OVERASCII_ESCAPE_1 = 0x85; private _OVERASCII_ESCAPE_2 = 0x8E; private _OVERASCII_ESCAPE_3 = 0x8F; private _OVERASCII_MOD_CODE = 0x20; // Conn event fn private _onConnEvent: RICConnEventFn | null = null; // File Handler parameters private _requestedBatchAckSize = 1; private _requestedFileBlockSize = 1200; fhBatchAckSize(): number { return this._requestedBatchAckSize; } fhFileBlockSize(): number { return this._requestedFileBlockSize; } // isConnected isConnected(): boolean { return this._isConnected; } // Set message handler setMsgHandler(ricMsgHandler: RICMsgHandler): void { this._ricMsgHandler = ricMsgHandler; } // Serial interface will require subscription, but don't start it by default requiresSubscription(): boolean { return false; } // Set onConnEvent handler setOnConnEvent(connEventFn: RICConnEventFn): void { this._onConnEvent = connEventFn; } // Get connected locator getConnectedLocator(): string | object { return this._port || ""; } // Connect to a device async connect(locator: string | object): Promise<boolean> { // Debug RICLog.debug("RICChannelWebSerial.connect " + locator.toString()); // Check already connected if (await this.isConnected()) { return true; } try { if (!this._port || locator != "reusePort"){ const port = await navigator.serial.requestPort(); this._port = port; } // Connect try { RICLog.info("opening port"); await this._port.open({ baudRate: 115200 }); } catch (err: any){ if (err.name == "InvalidStateError"){ RICLog.debug(`Opening port failed - already open ${err}`); } else { RICLog.error(`Opening port failed: ${err}`); throw err; } } this._isConnected = true; // start read loop this._readLoop(); this._port.addEventListener('disconnect', (event) => { RICLog.debug("WebSerial disconnect " + JSON.stringify(event)); if (this._onConnEvent) this._onConnEvent(RICConnEvent.CONN_DISCONNECTED_RIC); }); // TODO: handle errors } catch (err) { RICLog.error("RICChannelWebSerial.connect fail. Error: " + JSON.stringify(err)); return false; } return true; } // Disconnect async disconnect(): Promise<void> { // Not connected this._isConnected = false; RICLog.debug(`RICChannelWebSerial.disconnect attempting to close webserial`); while (this._reader){ await new Promise((resolve) => setTimeout(resolve, 100)); } // Disconnect webserial try { if (this._port) await this._port.close(); } catch (err){ console.debug(`Error closing port ${err}`); } RICLog.debug("WebSerial port closed"); return; } pauseConnection(pause: boolean): void { this._connPaused = pause; } private _overasciiDecodeByte(ch: number){ // Check if in escape sequence if (this._escapeSeqCode != 0) { const prevEscCode = this._escapeSeqCode; this._escapeSeqCode = 0; if (prevEscCode == 1){ return (ch ^ this._OVERASCII_MOD_CODE) & 0x7f; } else if (prevEscCode == 2) { return ch ^ this._OVERASCII_MOD_CODE; } return ch; } else if (ch == this._OVERASCII_ESCAPE_1) { this._escapeSeqCode = 1; return -1; } else if (ch == this._OVERASCII_ESCAPE_2) { this._escapeSeqCode = 2; return -1; } else if (ch == this._OVERASCII_ESCAPE_3) { this._escapeSeqCode = 3; return -1; } else { return ch & 0x7f; } } private _overasciiEncode(inData: Uint8Array){ /* Encode frame Values 0x00-0x0F map to ESCAPE_CODE_1, VALUE_XOR_20H_AND_MSB_SET Values 0x10-0x7f map to VALUE_WITH_MSB_SET Values 0x80-0x8F map to ESCAPE_CODE_2, VALUE_XOR_20H Values 0x90-0xff map to ESCAPE_CODE_3, VALUE Value ESCAPE_CODE_1 maps to ESCAPE_CODE_1 + VALUE Args: inData: data to encode (bytes) Returns: encoded frame (bytes) */ // Iterate over frame const encodedFrame: number[] = []; for (let i=0; i < inData.length; i++){ if (inData[i] <= 0x0f){ encodedFrame.push(this._OVERASCII_ESCAPE_1); encodedFrame.push((inData[i] ^ this._OVERASCII_MOD_CODE) | 0x80); } else if ((inData[i] >= 0x10) && (inData[i] <= 0x7f)){ encodedFrame.push(inData[i] | 0x80); } else if ((inData[i] >= 0x80) &&(inData[i] <= 0x8f)){ encodedFrame.push(this._OVERASCII_ESCAPE_2); encodedFrame.push(inData[i] ^ this._OVERASCII_MOD_CODE); } else { encodedFrame.push(this._OVERASCII_ESCAPE_3); encodedFrame.push(inData[i]); } } return new Uint8Array(encodedFrame); } // Handle notifications _onMsgRx(msg: Uint8Array | null): void { if (msg === null) return; // Debug //const decoder = new TextDecoder(); //RICLog.debug(`RICChannelWebSerial._onMsgRx ${decoder.decode(msg)}`); const overasciiBuffer: number[] = []; for (let i = 0; i<msg.length; i++){ if (msg[i] > 127){ const ch = this._overasciiDecodeByte(msg[i]); if (ch != -1){ overasciiBuffer.push(ch); } } else { this._serialBuffer.push(msg[i]); } } if (this._ricMsgHandler) this._ricMsgHandler.handleNewRxMsg(new Uint8Array(overasciiBuffer)); // any output over the non overascii channel will be delimited by a new line character // scan for this, and log any lines as they occur before removing them from the buffer if (this._serialBuffer.includes(0x0a)){ const decoder = new TextDecoder(); RICLog.debug(decoder.decode(new Uint8Array(this._serialBuffer.slice(0,this._serialBuffer.indexOf(0x0a))))); this._serialBuffer.splice(0, this._serialBuffer.indexOf(0x0a)+1); } } // Send a message async sendTxMsg( msg: Uint8Array, sendWithResponse: boolean ): Promise<boolean> { // Check connected if (!this._isConnected || !this._port) return false; // Debug const decoder = new TextDecoder(); RICLog.verbose(`RICChannelWebSerial.sendTxMsg ${msg.toString()} ${decoder.decode(msg)} sendWithResp ${sendWithResponse.toString()}`); try { if (this._port.writable != null){ const writer = this._port.writable.getWriter(); await writer.write(this._overasciiEncode(msg)); writer.releaseLock(); } } catch (err) { RICLog.warn("sendMsg error: " + JSON.stringify(err)); return false; } return true; } async sendTxMsgNoAwait( msg: Uint8Array, sendWithResponse: boolean ): Promise<boolean> { // Check connected if (!this._isConnected || !this._port) return false; // Debug RICLog.verbose(`RICChannelWebSerial.sendTxMsgNoAwait ${msg.toString()} sendWithResp ${sendWithResponse.toString()}`); try { if (this._port.writable != null){ const writer = this._port.writable.getWriter(); writer.write(msg).then(() => {writer.releaseLock();}); } } catch (err) { RICLog.error("sendMsg error: " + JSON.stringify(err)); } return true; } async _readLoop(){ RICLog.debug("Starting read loop"); if (!this._port) return; let retries = 10; try { if (!this._port.readable){ RICLog.error("RICChannelWebSerial _readLoop port is not readble"); return; } this._reader = this._port.readable.getReader(); while (this._port.readable && this._isConnected) { if (this._connPaused){ if (this._reader) { this._reader.releaseLock(); this._reader = undefined; } await new Promise(resolve => setTimeout(resolve, 100)); continue; } try { if (!this._reader) this._reader = this._port.readable.getReader(); const { value, done } = await this._reader.read(); if (done) { this._reader.releaseLock(); break; } if (!value || value.length === 0) { continue; } this._onMsgRx(new Uint8Array(value)); } catch (err){ RICLog.error("read loop issue: " + JSON.stringify(err)); retries -= 1; if (!retries) break; await new Promise(resolve => setTimeout(resolve, 100)); this._reader = this._port.readable.getReader(); } } if (this._reader) this._reader.releaseLock(); this._reader = undefined; } catch (err) { RICLog.error("Read loop got disconnected. err: " + JSON.stringify(err)); } // Disconnected! this._isConnected = false; RICLog.debug("Finished read loop"); } }