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
JavaScript
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