305 lines
11 KiB
JavaScript
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; |