refactor serial connection to base class and web serial class

This commit is contained in:
liamcottle 2025-04-08 11:52:32 +12:00
parent c18496f788
commit de4e1d713b
3 changed files with 156 additions and 130 deletions

View file

@ -190,7 +190,7 @@
<script type="module">
import Constants from "./src/constants.js";
import SerialConnection from "./src/connection/serial_connection.js";
import WebSerialConnection from "./src/connection/web_serial_connection.js";
import BleConnection from "./src/connection/ble_connection.js";
import BufferUtils from "./src/buffer_utils.js";
Vue.createApp({
@ -209,7 +209,7 @@
},
methods: {
async askForSerialPort() {
this.connection = await SerialConnection.open();
this.connection = await WebSerialConnection.open();
this.connection.on("connected", () => this.onConnected());
this.connection.on("disconnected", () => this.onDisconnected());
// this.connection.on("tx", (data) => console.log("tx", data));

View file

@ -5,75 +5,16 @@ import Connection from "./connection.js";
class SerialConnection extends Connection {
constructor(serialPort) {
constructor() {
super();
this.serialPort = serialPort;
this.reader = serialPort.readable.getReader();
this.writable = serialPort.writable;
this.readBuffer = [];
this.readLoop();
// listen for disconnect
this.serialPort.addEventListener("disconnect", () => {
this.onDisconnected();
});
// fire connected callback after constructor has returned
setTimeout(() => {
this.onConnected();
}, 0);
}
static async open() {
// ensure browser supports web serial
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
if(this.constructor === SerialConnection){
throw new Error("SerialConnection is an abstract class and can't be instantiated.");
}
// ask user to select device
const serialPort = await navigator.serial.requestPort({
filters: [],
});
// open port
await serialPort.open({
baudRate: 115200,
});
return new SerialConnection(serialPort);
}
async close() {
// release reader lock
try {
this.reader.releaseLock();
} catch(e) {
// console.log("failed to release lock on serial port readable, ignoring...", e);
}
// close serial port
try {
await this.serialPort.close();
} catch(e) {
// console.log("failed to close serial port, ignoring...", e);
}
}
async write(bytes) {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
} finally {
writer.releaseLock();
}
throw new Error("Not Implemented: write must be implemented by SerialConnection sub class.");
}
async writeFrame(frameType, frameData) {
@ -99,79 +40,58 @@ class SerialConnection extends Connection {
await this.writeFrame(0x3c, data);
}
async readLoop() {
try {
while(true){
async onDataReceived(value) {
// read bytes until reader indicates it's done
const { value, done } = await this.reader.read();
if(done){
// append received bytes to read buffer
this.readBuffer = [
...this.readBuffer,
...value,
];
// process read buffer while there is enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
const frameHeaderLength = 3;
while(this.readBuffer.length >= frameHeaderLength){
try {
// extract frame header
const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength));
// ensure frame type supported
const frameType = frameHeader.readByte();
if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// ensure frame length valid
const frameLength = frameHeader.readUInt16LE();
if(!frameLength){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// check if we have received enough bytes for this frame, otherwise wait until more bytes received
const requiredLength = frameHeaderLength + frameLength;
if(this.readBuffer.length < requiredLength){
break;
}
// append received bytes to read buffer
this.readBuffer = [
...this.readBuffer,
...value,
];
// get frame data, and remove it and its frame header from the read buffer
const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength);
this.readBuffer = this.readBuffer.slice(requiredLength);
// process read buffer while there is enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
const frameHeaderLength = 3;
while(this.readBuffer.length >= frameHeaderLength){
try {
// extract frame header
const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength));
// ensure frame type supported
const frameType = frameHeader.readByte();
if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// ensure frame length valid
const frameLength = frameHeader.readUInt16LE();
if(!frameLength){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// check if we have received enough bytes for this frame, otherwise wait until more bytes received
const requiredLength = frameHeaderLength + frameLength;
if(this.readBuffer.length < requiredLength){
break;
}
// get frame data, and remove it and its frame header from the read buffer
const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength);
this.readBuffer = this.readBuffer.slice(requiredLength);
// handle received frame
this.onFrameReceived(frameData);
} catch(e) {
console.error("Failed to process frame", e);
break;
}
}
// handle received frame
this.onFrameReceived(frameData);
} catch(e) {
console.error("Failed to process frame", e);
break;
}
} catch(error) {
// ignore error if reader was released
if(error instanceof TypeError){
return;
}
console.error('Error reading from serial port: ', error);
} finally {
this.reader.releaseLock();
}
}
}

View file

@ -0,0 +1,106 @@
import SerialConnection from "./serial_connection.js";
class WebSerialConnection extends SerialConnection {
constructor(serialPort) {
super();
this.serialPort = serialPort;
this.reader = serialPort.readable.getReader();
this.writable = serialPort.writable;
this.readLoop();
// listen for disconnect
this.serialPort.addEventListener("disconnect", () => {
this.onDisconnected();
});
// fire connected callback after constructor has returned
setTimeout(() => {
this.onConnected();
}, 0);
}
static async open() {
// ensure browser supports web serial
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// ask user to select device
const serialPort = await navigator.serial.requestPort({
filters: [],
});
// open port
await serialPort.open({
baudRate: 115200,
});
return new WebSerialConnection(serialPort);
}
async close() {
// release reader lock
try {
this.reader.releaseLock();
} catch(e) {
// console.log("failed to release lock on serial port readable, ignoring...", e);
}
// close serial port
try {
await this.serialPort.close();
} catch(e) {
// console.log("failed to close serial port, ignoring...", e);
}
}
// override
async write(bytes) {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
} finally {
writer.releaseLock();
}
}
async readLoop() {
try {
while(true){
// read bytes until reader indicates it's done
const { value, done } = await this.reader.read();
if(done){
break;
}
// pass to super class handler
await this.onDataReceived(value);
}
} catch(error) {
// ignore error if reader was released
if(error instanceof TypeError){
return;
}
console.error('Error reading from serial port: ', error);
} finally {
this.reader.releaseLock();
}
}
}
export default WebSerialConnection;