Files
DroneCan_WebTools/src/web_serial.js
2025-07-29 09:25:01 +08:00

305 lines
11 KiB
JavaScript

class WebSerial {
constructor(port, baudRate) {
this.port = port;
this.baudRate = baudRate;
this.reader = null;
this.writer = null;
this.messageHandlers = [];
this.openHandlers = [];
this.errorHandlers = [];
this.closeHandlers = [];
this.isClosed = false;
this.readLoopActive = false; // Track if read loop is running
this.connected = false; // Track if the connection is established
this.connectionStatusHandlers = []; // Handlers for connection status changes
}
// Add getter for connection status
isConnected() {
return this.connected;
}
// Add method to register connection status change handlers
addConnectionStatusHandler(handler) {
this.connectionStatusHandlers.push(handler);
}
// Update connection status and notify handlers
updateConnectionStatus(status) {
const previousStatus = this.connected;
this.connected = status;
// Only notify if there was a change
if (previousStatus !== status) {
// console.log(`Connection status changed: ${status ? 'Connected' : 'Disconnected'}`);
this.connectionStatusHandlers.forEach(handler => handler(status));
}
}
static async requestPort() {
try {
const port = await navigator.serial.requestPort();
return port;
} catch (error) {
console.error('Error selecting port:', error);
throw error;
}
}
static async listPorts() {
const ports = await navigator.serial.getPorts();
// console.log('Serial ports:', ports);
return ports;
}
async connect() {
try {
if (!this.port) {
console.error('No port available to connect.');
return;
}
// console.log('Port selected:', this.port);
const baudRate = this.baudRate;
await this.port.open({ baudRate });
// console.log('Port opened:', this.port);
this.handleOpen();
this.isClosed = false;
if (this.port.writable) {
this.writer = this.port.writable.getWriter();
}
if (this.port.readable) {
// Store reader as instance property so we can access it during close
this.reader = this.port.readable.getReader();
const messageHandlers = this.messageHandlers;
// Set flag to indicate read loop is active
this.readLoopActive = true;
// Update connection status
this.updateConnectionStatus(true);
const readLoop = async () => {
try {
while (this.readLoopActive && this.reader) {
const { value, done } = await this.reader.read();
if (done) {
break;
}
messageHandlers.forEach(handler => handler(value));
}
} catch (error) {
if (error.name === 'BreakError') {
// console.log('BreakError received - this is expected during certain operations');
if (this.readLoopActive && !this.isClosed) {
// Release the current reader
try {
if (this.reader) {
this.reader.releaseLock();
}
} catch (releaseError) {
console.warn('Error releasing reader after BreakError:', releaseError);
}
// Wait a moment before attempting to restart
await new Promise(resolve => setTimeout(resolve, 150));
// Only try to restart if we're still supposed to be reading
if (this.readLoopActive && !this.isClosed && this.port && this.port.readable) {
// console.log('Restarting read loop after BreakError...');
try {
// Get a new reader
this.reader = this.port.readable.getReader();
// Restart the read loop with the new reader
readLoop(); // Recursively call readLoop to continue reading
} catch (restartError) {
console.error('Failed to restart reader after BreakError:', restartError);
this.handleError(restartError);
// If we can't restart, mark as disconnected
if (!this.isClosed) {
this.updateConnectionStatus(false);
}
}
}
}
} else {
console.error('Error reading from serial port:', error);
this.handleError(error);
// Update connection status if there's a critical error
if (!['BreakError', 'NetworkError'].includes(error.name)) {
this.updateConnectionStatus(false);
}
}
}
};
readLoop();
} else {
console.error('Port is not readable or writable.');
this.updateConnectionStatus(false);
}
} catch (error) {
this.handleError(error);
this.updateConnectionStatus(false);
}
}
handleOpen() {
this.openHandlers.forEach(handler => handler());
}
addOpenHandler(handler) {
this.openHandlers.push(handler);
}
handleMessage(data) {
if (this.messageHandlers && this.messageHandlers.length > 0) {
this.messageHandlers.forEach(handler => handler(data));
} else {
console.error('Message handlers are not initialized or empty.');
}
}
addMessageHandler(handler) {
this.messageHandlers.push(handler);
}
handleError(error) {
if (error.name === 'BreakError') {
// console.error('BreakError: Break received');
} else {
console.error('Serial port error observed:', error);
this.errorHandlers.forEach(handler => handler(error));
}
}
addErrorHandler(handler) {
this.errorHandlers.push(handler);
}
handleClose() {
if (!this.isClosed) { // Check if the port is already closed
console.log('Serial port is closed now.');
this.isClosed = true; // Set the flag to true
this.updateConnectionStatus(false); // Update connection status
this.closeHandlers.forEach(handler => handler());
}
}
addCloseHandler(handler) {
this.closeHandlers.push(handler);
}
async write(data) {
try {
if (!this.connected) {
console.error('Cannot write: not connected to serial port');
return;
}
if (!(data instanceof ArrayBuffer || ArrayBuffer.isView(data))) {
// Convert data to ArrayBuffer if it's not already an ArrayBuffer or ArrayBufferView
if (typeof data === 'string') {
data = new TextEncoder().encode(data);
} else if (data instanceof Blob) {
data = await data.arrayBuffer();
} else if (Array.isArray(data)) {
data = new Uint8Array(data);
} else {
throw new TypeError('The provided value is not of type (ArrayBuffer or ArrayBufferView)');
}
}
await this.writer.write(data);
} catch (error) {
this.handleError(error);
// Update connection status if write fails
if (error.message && error.message.includes('locked') === false) {
this.updateConnectionStatus(false);
}
}
}
async close() {
try {
// First, set the flag to prevent multiple close attempts
if (this.isClosed) {
console.log('Port already closed');
return;
}
this.isClosed = true;
// Signal read loop to stop
this.readLoopActive = false;
// Update connection status
this.updateConnectionStatus(false);
// Cancel and release reader if it exists
if (this.reader) {
try {
await this.reader.cancel();
this.reader.releaseLock();
this.reader = null;
} catch (readerError) {
console.warn('Error while closing reader:', readerError);
// Even if cancel failed, try to release the lock
try {
this.reader.releaseLock();
} catch (e) {
console.warn('Failed to release reader lock:', e);
}
this.reader = null;
}
}
// Close and release writer if it exists
if (this.writer) {
try {
await this.writer.close();
this.writer.releaseLock();
this.writer = null;
} catch (writerError) {
console.warn('Error while closing writer:', writerError);
// Even if close failed, try to release the lock
try {
this.writer.releaseLock();
} catch (e) {
console.warn('Failed to release writer lock:', e);
}
this.writer = null;
}
}
// Wait a bit for locks to be fully released
await new Promise(resolve => setTimeout(resolve, 50));
// Close the port last
if (this.port) {
try {
console.log('Closing port...');
await this.port.close();
console.log('Port closed successfully');
} catch (portError) {
console.error('Error while closing port:', portError);
}
this.port = null;
}
this.handleClose();
} catch (error) {
console.error('Error in close method:', error);
this.handleError(error);
}
}
}
export default WebSerial;