@tomneutens/serial_monitor
Version:
A web based serial monitor for communicating with serial devices (like Arduino). The library is based on native web components and uses the WebSerial API to communicate with the external device.
310 lines (273 loc) • 11.8 kB
text/typescript
/**
* @author Tom Neutens <tomneutens@gmail.com>
*/
import { LitElement, css, html, CSSResult, CSSResultGroup, nothing } from "lit";
import {customElement, property, state} from 'lit/decorators.js';
import { msg } from '@lit/localize';
import SerialMonitorConfig from "../state/SerialMonitorConfig";
import FileIOController from "../utils/FileIOController";
import WebSerialConnection from "../utils/WebSerialConnection";
enum ConnectionState {
DISC = 1,
DISC_DATA = 2,
CON = 3,
CON_DATA = 4
}
("send-receive-serial-controls")
class SendReceiveSerialControls extends LitElement {
static styles?: CSSResultGroup = css`
:host {
display: flex;
flex-direction: row;
gap: 5px;
width: 100%;
background-color: var(--component-background-color);
color: var(--component-foreground-color);
padding: 0 5px;
box-sizing: border-box;
font-size: var(--component-base-font-size);
font-family: var(--component-base-font-family);
margin: 5px 0;
border-top: 1px solid;
border-color: var(--component-accent-color);
}
:host > textarea {
flex-grow: 1;
vertical-align: middle;
padding-left: 5px;
rows: 1;
background-color: var(--component-background-color);
color: var(--component-foreground-color);
border: 1px solid white;
resize: none;
border-radius: 3px;
}
:host > textarea::placeholder {
position: absolute;
top: 50%;
left: 0;
transform: translate(0, -50%);
padding-left: 5px;
color: var(--component-foreground-color-textarea-disabled);
}
:host > * {
margin: 5px 0;
font-size: var(--component-base-font-size);
}
:host > button {
background-color: var(--component-background-color-button);
color: var(--component-foreground-color-button);
text-decoration: none;
border-radius: 3px;
border: none;
}
:host > button:disabled {
background-color: var(--component-background-color-disabled);
color: var(--component-foreground-color-disabled)
}
:host > textarea:disabled {
background-color: var(--component-background-color);
color: var(--component-foreground-color-textarea-disabled)
}
`
()
inputData: string = ""
()
connectPossible: boolean = true
()
disconnectPossible: boolean = false
()
sendPossible: boolean = false
()
downloadPossible: boolean = false
()
textInputPossible: boolean = false
()
datalog: Array<string> = new Array<string>()
({
type: SerialMonitorConfig,
converter: (value: any, type) => {
let conv: any = JSON.parse(value)
conv.__proto__ = SerialMonitorConfig.prototype
return conv
}
})
config: SerialMonitorConfig
private currentState: ConnectionState = ConnectionState.DISC
private radixMap: {[index: string]: number} = {
"bin": 2,
"oct": 8,
"dec": 10,
"hex": 16
}
private radixPrefix: {[index: string]: string} = {
"bin": "0b",
"oct": "0",
"dec": "",
"hex": "0x"
}
private readBuffer:number[] = [];
private byteInterpreter:any = {
"string": (value:number) => {
let strValue = String.fromCharCode(value);
if (strValue == "\r") return // Ignore carrige return
if (strValue == "\n"){
this.serialReadBufferPrintLn();
} else {
this.serialReadBufferPrint(strValue);
}
},
"byte": (value:number) => {
this.serialReadBufferPrint(this.radixPrefix[this.config.getDisplayType()] + value.toString(this.radixMap[this.config.getDisplayType()]));
this.serialReadBufferPrintLn();
},
"int": (value:number) => { // Javascript uses 32 bit integers, Arduino 16 bit integers => padding required
this.readBuffer.push(value);
if (this.readBuffer.length >= 2){
let bytes = this.readBuffer;
this.readBuffer = [];
let sign = bytes[0] & (1 << 7);
let combined = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
combined = sign ? 0xFFFF0000 & combined : combined; // Add ones to beginning for sign in two complement representation
this.serialReadBufferPrint(combined.toString(this.radixMap[this.config.getDisplayType()]))
this.serialReadBufferPrintLn();
}
},
"long": (value:number) => {
this.readBuffer.push(value);
if (this.readBuffer.length >= 4){
let bytes = this.readBuffer;
this.readBuffer = [];
let combined = ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
this.serialReadBufferPrint(combined.toString(this.radixMap[this.config.getDisplayType()]));
this.serialReadBufferPrintLn();
}
}
}
serialConnection: WebSerialConnection
constructor(){
super();
/*this.serialConnected = false
this.openPort = null
this.serialDataEventHandlers = new Array<Function>()
this.serialDisconnectEventHandlers = new Array<Function>()
this.serialConnectEventHandlers = new Array<Function>()
this.sendQueue = new Array<number>()*/
this.serialConnection = new WebSerialConnection();
this.serialConnection.addSerialDisconnectEventHandler(() => this.handleDisconnect());
this.serialConnection.addSerialConnectEventHandler(() => this.handleConnect())
this.serialConnection.addSerialDataEventHandler((byte: number) => this.handleReceiveData(byte))
this.serialConnection.setupWebSerial()
}
serialReadBufferPrint(value:string){
// If an entry in the data log exists, concat value to the current string
if (this.datalog.length > 0){
this.datalog[this.datalog.length-1] = this.datalog[this.datalog.length-1].concat(value)
} else { // Push the value to the datalog
this.datalog.push(value)
}
}
serialReadBufferPrintLn(){
this.datalog.push("")
}
private async handleConnect(){
if (this.currentState === ConnectionState.DISC || this.currentState === ConnectionState.DISC_DATA){
this.connectPossible = false
this.disconnectPossible = true
this.sendPossible = true
this.downloadPossible = false
this.textInputPossible = true
this.datalog = new Array<string>()
this.currentState = ConnectionState.CON
} else {
console.error("Trying to connect but already connected?!?")
}
}
private handleDisconnect(){
if (this.currentState === ConnectionState.CON){
this.connectPossible = true
this.disconnectPossible = false
this.sendPossible = false
this.downloadPossible = false
this.textInputPossible = false
this.datalog = new Array<string>()
this.currentState = ConnectionState.DISC
} else if (this.currentState === ConnectionState.CON_DATA){
this.connectPossible = true
this.disconnectPossible = false
this.sendPossible = false
this.downloadPossible = true
this.textInputPossible = false
this.currentState = ConnectionState.DISC_DATA
} else {
console.error("Trying to disconnect but not in a connected state?!?")
}
}
handleReceiveData(byte:number){
if (this.currentState === ConnectionState.CON || this.currentState === ConnectionState.CON_DATA){
this.downloadPossible = true
this.processData(byte)
this.currentState = ConnectionState.CON_DATA
let e = new CustomEvent("new-data-received", {bubbles: false, composed: true, detail: {data: this.datalog }})
this.dispatchEvent(e)
} else {
console.error("Received data in disconnected state?!?")
}
}
private processData(byte:number): string{
this.byteInterpreter[this.config.getDataType()](byte);
return ""
}
private handleSend(){
this.writeSerialValue(this.inputData)
this.inputData = ""
}
private writeSerialValue(value: string){
let valueArray:number[] = [];
if (this.config.getDataType() == "byte"){ // When sending bytes check if input is in byte format => send as byte
let parsedValue = parseInt(value, this.radixMap[this.config.getDisplayType()]);
if (!Number.isNaN(parsedValue) && parsedValue >= -128 && parsedValue <= 256 ){
valueArray.push(parsedValue);
} else { // If it does not fit in the byte format => send as array of characters
valueArray = value.split("").map((str) => { return str.charCodeAt(0)});
}
} else { // When sent as string => send as array of characters with newline at end.
valueArray = `${value}\n`.split("").map((str) => { return str.charCodeAt(0)});
}
valueArray.forEach(value => this.serialConnection.sendByte(value))
}
private handleDownload(){
FileIOController.download("data.csv", this.datalog.join("\n"))
}
private handleInput(data: string){
if (data.charAt(data.length - 1) == "\n"){
this.inputData = this.inputData.trim()
this.handleSend()
}else{
this.inputData = data
}
}
protected render() {
return html`
<button @click=${async () => {
this.serialConnection.connect(
parseInt(this.config.getBaudRate()),
this.config.getSerialPortFilters())
.catch(e => {
const ce = new CustomEvent("open-port-failed", {bubbles: true, composed: true, detail: {error: e }})
this.dispatchEvent(ce)
})}
} ?disabled="${!this.connectPossible}">${msg("Connect")}</button>
<button @click=${() => this.serialConnection.disconnect() } ?disabled=${!this.disconnectPossible}>${msg("Disconnect")}</button>
<textarea ?disabled=${!this.textInputPossible} rows="1" @input=${(e:any) => { this.handleInput(e.target.value) }} .value=${this.inputData} placeholder="${msg("Enter the data you want to send to the device!")}"></textarea>
<button @click=${ this.handleSend } ?disabled=${!this.sendPossible}>${msg("Send")}</button>
<button @click=${this.handleDownload } class="fas fa-download" ?disabled=${!this.downloadPossible}>${msg("Download csv")}</button>
`
}
}
declare global {
interface HTMLElementTagNameMap {
"send-receive-serial-controls": SendReceiveSerialControls;
}
}
export default SendReceiveSerialControls;