UNPKG

ngx-serial-console

Version:

This is an angular library, to create a component that would connect to a serial port and display the data from the port.

223 lines (218 loc) 16.8 kB
import * as i0 from '@angular/core'; import { signal, computed, Input, ViewChild, Component } from '@angular/core'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; // NgxSerialConsole // Works only on Chromium based browsers, allows users to select the usb port and baud rate // to connect with serial interface. The output of the serial interface can be monitored using this tool. // When debugging Arduino devices, ESP32, ESP8266 and other devices with a serial port. class NgxSerialConsole { scrollContainer; theme = "Plain"; // state as a signal, so the component to react to state changes immediately. state = signal({ serialAvailable: false, connected: false, maxLines: 500, outputLines: [], baudRate: 115200, vendorId: "", productId: "", theme: "Plain" }, ...(ngDevMode ? [{ debugName: "state" }] : [])); // computed signal, which listens on state changes, and updates the consoleOutput consoleOutput = computed(() => this.state().outputLines.join(''), ...(ngDevMode ? [{ debugName: "consoleOutput" }] : [])); serial; themeStyles = ["CRT", "Plain"]; baudRates = [300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1000000, 1500000]; isUserScrolling = false; // triggered when user scrolls using mouse or touch autoScrollEnabled = true; // when user scrolls to any other position, auto scroll is disabled keepConnectionAlive = true; constructor() { let serialAvailable = false; if ('serial' in navigator) { this.serial = navigator.serial; serialAvailable = true; console.info("Serial device is available"); } else { this.serial = null; console.info("Serial device not available, use google chrome browser"); } this.state.update(state => ({ ...state, serialAvailable: serialAvailable, theme: this.theme, })); } ngAfterViewChecked() { if (!this.serial) return; // there will be no view to check the scroll const element = this.scrollContainer.nativeElement; element.addEventListener('scroll', () => { // Ignore scroll events triggered by programmatic scroll if (!this.isUserScrolling) return; // adding a threshold of 100 to scroll bottom, so user does not have to go to the fag end bottom const nearBottom = element.scrollHeight - element.scrollTop < (element.clientHeight + 100); this.autoScrollEnabled = nearBottom; // console.log("Auto scroll enabled ", this.autoScrollEnabled); }); // Enable this flag only on user interaction, for example with mouse or touch: element.addEventListener('wheel', () => this.isUserScrolling = true); element.addEventListener('touchmove', () => this.isUserScrolling = true); } // clear the output of the console window clear() { this.state.update(state => ({ ...state, outputLines: [] })); } unsetConnection() { this.state.update(state => ({ ...state, connected: false, vendorId: "", productId: "", })); this.appendOutput('\nDisconnected from serial port\n'); } async disconnectSerialPort() { this.keepConnectionAlive = false; } // initiate the serial connection // this will open a dialog, where the user has to select the console port // because of security reasons, we cannot auto connect to a serial port from browser connectSerialPort() { if (this.serial) { this.handleSerialEvents(); this.openSerialPort(); } } // handle events, so we know if the serial device disconnected handleSerialEvents() { this.serial?.addEventListener('disconnect', (event) => { //console.log('Serial port disconnected:', event.target); this.unsetConnection(); this.appendOutput('\n\nConnection lost, check device or cable. Please reconnect again using the "Connect Button above" \n'); }); this.serial?.addEventListener('connect', (event) => { //console.log('Serial port connected:', event.target); this.appendOutput('\n\nConnection was reset, check device or cable. Please reconnect again using the "Connect Button above" \n'); }); } async openSerialPort() { try { if (this.serial) { // Request the user to select a serial port. this.appendOutput(`Waiting for user input to connect to serial port\n`); const port = await this.serial.requestPort(); await port.open({ baudRate: this.state().baudRate }); if (port) { this.state.update(state => ({ ...state, connected: true, vendorId: port.getInfo().vendorId, productId: port.getInfo().productId, })); this.readSerialPort(port); } } } catch (error) { if (error instanceof Error) { // this.unsetConnection(); // TODO, depending on error we have to terminate connection this.appendOutput(`Error received: ${error.message} \n`); } else { this.unsetConnection(); this.appendOutput(`Unexpected error: ${String(error)}`); } } } // read stream from the serial port, till a user disconnects // or the serial device disconnects // handle disconnections gracefully async readSerialPort(port) { this.appendOutput(`Connected serial port with baud rate ${this.state().baudRate}\n`); // Set up a text decoder stream to read from the serial port. const textDecoder = new TextDecoderStream(); let readableStreamClosed = port.readable.pipeTo(textDecoder.writable); let reader = textDecoder.readable.getReader(); this.appendOutput('Connected and reading data:\n'); // if we want to stop the read loop, set this var to false, in other functions this.keepConnectionAlive = true; // we are using zoneless, // #TODO binu, revisit why PendingTasks is not needed here while (this.keepConnectionAlive) { const { value, done } = await reader.read(); if (done) { // console.log("Serial Reader closed"); reader.releaseLock(); this.keepConnectionAlive = false; break; } if (value) { this.appendOutput(value); } } const textEncoder = new TextEncoderStream(); const writableStreamClosed = textEncoder.readable.pipeTo(port.writable); let writer = textEncoder.writable.getWriter(); // close the reader, write and cleanup states reader.cancel(); await readableStreamClosed.catch(() => { }); writer.close(); await writableStreamClosed; await port.close(); this.unsetConnection(); } // appends the new data from serial monitor to the output array // if the number of lines exceed maxLines, we trim the array to maxLines length appendOutput(newData) { this.state.update(state => { // Concatenate existing lines with new lines const updatedLines = [...state.outputLines, newData]; // Trim the array if it exceeds maxLines const trimmedLines = updatedLines.length > state.maxLines ? updatedLines.slice(updatedLines.length - state.maxLines) : updatedLines; return { ...state, outputLines: trimmedLines }; }); this.scrollToBottomSerialOut(); } scrollToBottomSerialOut() { if (!this.autoScrollEnabled) return; this.isUserScrolling = false; // unset flag, till user feedback is present try { this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight; } catch (err) { // Handle errors if any } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: NgxSerialConsole, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: NgxSerialConsole, isStandalone: true, selector: "ngx-serial-console", inputs: { theme: "theme" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollSerialContainer"], descendants: true }], ngImport: i0, template: "@if (state().serialAvailable) {\n <div class=\"card console-window\" [class]=\"state().theme\">\n \n <div class=\"input-group input-group-sm mb-3\">\n <span class=\"input-group-text\">\n Serial Monitor\n @if (state().connected) {\n <i class=\"fa-solid fa-link px-2\"></i>\n } @else {\n <i class=\"fa-solid fa-link-slash px-2\"></i>\n }\n </span>\n @if (state().connected) {\n <button id=\"serialConnectButton\" class=\"btn btn-outline-secondary\" (click)=\"disconnectSerialPort()\">Disconnect</button>\n } @else {\n <button id=\"serialConnectButton\" class=\"btn btn-outline-secondary\" (click)=\"connectSerialPort()\">Connect</button>\n }\n \n <span class=\"input-group-text ps-4\">Theme</span>\n <select class=\"form-select\" id=\"themeStyleSelect\" name=\"themeStyle\" [(ngModel)]=\"state().theme\">\n @for (rate of themeStyles; track rate) {\n <option [value]=\"rate\">{{ rate }}</option>\n }\n </select>\n\n <span class=\"input-group-text ps-4\">Baud Rate</span>\n <select class=\"form-select\" id=\"baudRateSelect\" name=\"baudRate\" [(ngModel)]=\"state().baudRate\">\n @for (rate of baudRates; track rate) {\n <option [value]=\"rate\">{{ rate }}</option>\n }\n </select>\n <button class=\"btn btn-outline-secondary\" (click)=\"clear()\" tooltip=\"Clear History\">\n Clear\n </button>\n </div>\n\n \n <div class=\"card-body\" #scrollSerialContainer>\n <pre class=\"serial-out-text\">{{ consoleOutput() }}</pre>\n </div>\n\n </div>\n} @else {\n <div class=\"alert alert-info\" role=\"alert\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Your browser does not support serial port access.<br/>\n This feature is only available on chromium based browsers.<br/>\n Please use one the below browsers to access this feature.<br/>\n eg: Google Chrome, Opera, Edge<br/>\n <a href=\"https://caniuse.com/?search=usb\" target=\"_blank\">Browser support</a>\n </div>\n}\n\n", styles: [".console-window{width:100%;white-space:pre-wrap;font-family:monospace;background-color:#0b0f1a}.CRT.console-window{border-radius:15px;box-shadow:inset 0 0 20px #3f3,0 0 20px #0f0;padding:20px}.card-body{min-height:500px;max-height:500px;overflow-y:auto}.serial-out-text{font-size:1.1rem;color:#f5f5f5;white-space:pre-wrap}.CRT .serial-out-text{color:#3f3;text-shadow:0 0 10px #33ff33,0 0 20px #33ff33}:host-context([data-bs-theme=\"light\"]) .output-window{background-color:#f5f5f5;box-shadow:inset 0 0 20px #999,0 0 20px #666}:host-context([data-bs-theme=\"light\"]) .serial-out-text{color:#222;text-shadow:0 0 5px #444444,0 0 10px #666666}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: NgxSerialConsole, decorators: [{ type: Component, args: [{ selector: 'ngx-serial-console', imports: [FormsModule], template: "@if (state().serialAvailable) {\n <div class=\"card console-window\" [class]=\"state().theme\">\n \n <div class=\"input-group input-group-sm mb-3\">\n <span class=\"input-group-text\">\n Serial Monitor\n @if (state().connected) {\n <i class=\"fa-solid fa-link px-2\"></i>\n } @else {\n <i class=\"fa-solid fa-link-slash px-2\"></i>\n }\n </span>\n @if (state().connected) {\n <button id=\"serialConnectButton\" class=\"btn btn-outline-secondary\" (click)=\"disconnectSerialPort()\">Disconnect</button>\n } @else {\n <button id=\"serialConnectButton\" class=\"btn btn-outline-secondary\" (click)=\"connectSerialPort()\">Connect</button>\n }\n \n <span class=\"input-group-text ps-4\">Theme</span>\n <select class=\"form-select\" id=\"themeStyleSelect\" name=\"themeStyle\" [(ngModel)]=\"state().theme\">\n @for (rate of themeStyles; track rate) {\n <option [value]=\"rate\">{{ rate }}</option>\n }\n </select>\n\n <span class=\"input-group-text ps-4\">Baud Rate</span>\n <select class=\"form-select\" id=\"baudRateSelect\" name=\"baudRate\" [(ngModel)]=\"state().baudRate\">\n @for (rate of baudRates; track rate) {\n <option [value]=\"rate\">{{ rate }}</option>\n }\n </select>\n <button class=\"btn btn-outline-secondary\" (click)=\"clear()\" tooltip=\"Clear History\">\n Clear\n </button>\n </div>\n\n \n <div class=\"card-body\" #scrollSerialContainer>\n <pre class=\"serial-out-text\">{{ consoleOutput() }}</pre>\n </div>\n\n </div>\n} @else {\n <div class=\"alert alert-info\" role=\"alert\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Your browser does not support serial port access.<br/>\n This feature is only available on chromium based browsers.<br/>\n Please use one the below browsers to access this feature.<br/>\n eg: Google Chrome, Opera, Edge<br/>\n <a href=\"https://caniuse.com/?search=usb\" target=\"_blank\">Browser support</a>\n </div>\n}\n\n", styles: [".console-window{width:100%;white-space:pre-wrap;font-family:monospace;background-color:#0b0f1a}.CRT.console-window{border-radius:15px;box-shadow:inset 0 0 20px #3f3,0 0 20px #0f0;padding:20px}.card-body{min-height:500px;max-height:500px;overflow-y:auto}.serial-out-text{font-size:1.1rem;color:#f5f5f5;white-space:pre-wrap}.CRT .serial-out-text{color:#3f3;text-shadow:0 0 10px #33ff33,0 0 20px #33ff33}:host-context([data-bs-theme=\"light\"]) .output-window{background-color:#f5f5f5;box-shadow:inset 0 0 20px #999,0 0 20px #666}:host-context([data-bs-theme=\"light\"]) .serial-out-text{color:#222;text-shadow:0 0 5px #444444,0 0 10px #666666}\n"] }] }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: ViewChild, args: ['scrollSerialContainer'] }], theme: [{ type: Input }] } }); /* * Public API Surface of ngx-serial-console */ /** * Generated bundle index. Do not edit. */ export { NgxSerialConsole }; //# sourceMappingURL=ngx-serial-console.mjs.map