Skip to content

Commit

Permalink
Update webSerial.js
Browse files Browse the repository at this point in the history
  • Loading branch information
haybnzz authored Jan 21, 2025
1 parent 5e26aaf commit 751875b
Showing 1 changed file with 100 additions and 122 deletions.
222 changes: 100 additions & 122 deletions src/js/webSerial.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { webSerialDevices, vendorIdNames } from "./serial_devices";
import { checkBrowserCompatibility } from "./utils/checkBrowserCompatibilty";
import { checkBrowserCompatibility } from "./utils/checkBrowserCompatibility";

async function* streamAsyncIterable(reader, keepReadingFlag) {
try {
Expand Down Expand Up @@ -54,14 +54,15 @@ class WebSerial extends EventTarget {
const added = this.createPort(device);
this.ports.push(added);
this.dispatchEvent(new CustomEvent("addedDevice", { detail: added }));

return added;
}

handleRemovedDevice(device) {
const removed = this.ports.find((port) => port.port === device);
this.ports = this.ports.filter((port) => port.port !== device);
this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed }));
if (removed) {
this.ports = this.ports.filter((port) => port.port !== device);
this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed }));
}
}

handleReceiveBytes(info) {
Expand All @@ -78,39 +79,32 @@ class WebSerial extends EventTarget {
}

createPort(port) {
const displayName = vendorIdNames[port.getInfo().usbVendorId]
? vendorIdNames[port.getInfo().usbVendorId]
: `VID:${port.getInfo().usbVendorId} PID:${port.getInfo().usbProductId}`;
const info = port.getInfo();
const displayName = vendorIdNames[info.usbVendorId]
? vendorIdNames[info.usbVendorId]
: `VID:${info.usbVendorId} PID:${info.usbProductId}`;
return {
path: `serial_${this.portCounter++}`,
displayName: `Betaflight ${displayName}`,
vendorId: port.getInfo().usbVendorId,
productId: port.getInfo().usbProductId,
vendorId: info.usbVendorId,
productId: info.usbProductId,
port: port,
};
}

async loadDevices() {
const ports = await navigator.serial.getPorts();

this.portCounter = 1;
this.ports = ports.map(function (port) {
return this.createPort(port);
}, this);
this.ports = ports.map(port => this.createPort(port));
}

async requestPermissionDevice(showAllSerialDevices = false) {
let newPermissionPort = null;

try {
const options = showAllSerialDevices ? {} : { filters: webSerialDevices };
const userSelectedPort = await navigator.serial.requestPort(options);

newPermissionPort = this.ports.find((port) => port.port === userSelectedPort);

if (!newPermissionPort) {
newPermissionPort = this.handleNewDevice(userSelectedPort);
}
newPermissionPort = this.ports.find(port => port.port === userSelectedPort) ||
this.handleNewDevice(userSelectedPort);
console.info(`${this.logHead}User selected SERIAL device from permissions:`, newPermissionPort.path);
} catch (error) {
console.error(`${this.logHead}User didn't select any SERIAL device when requesting permission:`, error);
Expand All @@ -125,131 +119,115 @@ class WebSerial extends EventTarget {
async connect(path, options) {
this.openRequested = true;
this.closeRequested = false;
this.port = this.ports.find(device => device.path === path)?.port;

if (this.port) {
try {
await this.port.open(options);
const connectionInfo = this.port.getInfo();
this.connectionInfo = connectionInfo;
this.writer = this.port.writable.getWriter();
this.reader = this.port.readable.getReader();

if (connectionInfo && !this.openCanceled) {
this.setConnectedState(true, path, options.baudRate);
this.setupEventListeners();
this.startReading();
} else if (connectionInfo && this.openCanceled) {
this.handleCanceledConnection(connectionInfo);
} else {
this.handleConnectionFailure();
}
} catch (error) {
console.error(`${this.logHead}Failed to open serial port:`, error);
this.handleConnectionFailure();
}
} else {
console.error(`${this.logHead}No port found for path: ${path}`);
this.handleConnectionFailure();
}
}

this.port = this.ports.find((device) => device.path === path).port;

await this.port.open(options);

const connectionInfo = this.port.getInfo();
this.connectionInfo = connectionInfo;
this.writer = this.port.writable.getWriter();
this.reader = this.port.readable.getReader();

if (connectionInfo && !this.openCanceled) {
this.connected = true;
this.connectionId = path;
this.bitrate = options.baudRate;
this.bytesReceived = 0;
this.bytesSent = 0;
this.failed = 0;
this.openRequested = false;

this.port.addEventListener("disconnect", this.handleDisconnect.bind(this));
this.addEventListener("receive", this.handleReceiveBytes);

console.log(
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, Baud: ${options.baudRate}`,
);
setConnectedState(connected, path, baudRate) {
this.connected = connected;
this.connectionId = path;
this.bitrate = baudRate;
this.bytesReceived = this.bytesSent = this.failed = 0;
this.openRequested = false;
console.log(`${this.logHead}Connection opened with ID: ${path}, Baud: ${baudRate}`);
this.dispatchEvent(new CustomEvent("connect", { detail: this.connectionInfo }));
}

this.dispatchEvent(new CustomEvent("connect", { detail: connectionInfo }));
// Check if we need the helper function or could polyfill
// the stream async iterable interface:
// https://web.dev/streams/#asynchronous-iteration
setupEventListeners() {
this.port.addEventListener("disconnect", this.handleDisconnect.bind(this));
this.addEventListener("receive", this.handleReceiveBytes);
}

this.reading = true;
startReading() {
this.reading = true;
(async () => {
for await (let value of streamAsyncIterable(this.reader, () => this.reading)) {
this.dispatchEvent(new CustomEvent("receive", { detail: value }));
}
} else if (connectionInfo && this.openCanceled) {
this.connectionId = connectionInfo.connectionId;

console.log(
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, but request was canceled, disconnecting`,
);
// some bluetooth dongles/dongle drivers really doesn't like to be closed instantly, adding a small delay
setTimeout(() => {
this.openRequested = false;
this.openCanceled = false;
this.disconnect(() => {
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
});
}, 150);
} else if (this.openCanceled) {
console.log(`${this.logHead} Connection didn't open and request was canceled`);
this.openRequested = false;
this.openCanceled = false;
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
} else {
this.openRequested = false;
console.log(`${this.logHead} Failed to open serial port`);
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
}
})();
}

async disconnect() {
this.connected = false;
this.transmitting = false;
this.reading = false;
this.bytesReceived = 0;
this.bytesSent = 0;
handleCanceledConnection(connectionInfo) {
this.connectionId = connectionInfo.connectionId;
console.log(`${this.logHead}Connection opened but request was canceled, disconnecting`);
setTimeout(() => {
this.openRequested = this.openCanceled = false;
this.disconnect(() => this.dispatchEvent(new CustomEvent("connect", { detail: false })));
}, 150);
}

// if we are already closing, don't do it again
if (this.closeRequested) {
return;
}
handleConnectionFailure() {
this.openRequested = false;
console.log(`${this.logHead}Failed to open serial port`);
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
}

const doCleanup = async () => {
this.removeEventListener("receive", this.handleReceiveBytes);
if (this.reader) {
this.reader.cancel();
this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.releaseLock();
this.writer = null;
}
if (this.port) {
this.port.removeEventListener("disconnect", this.handleDisconnect.bind(this));
await this.port.close();
this.port = null;
}
};
async disconnect() {
if (this.closeRequested) return;

try {
await doCleanup();
this.connected = this.transmitting = this.reading = false;
this.bytesReceived = this.bytesSent = 0;

console.log(
`${this.logHead}Connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
);
await this.cleanupResources();

this.connectionId = false;
this.bitrate = 0;
this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
} catch (error) {
console.error(error);
console.error(
`${this.logHead}Failed to close connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
);
this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
} finally {
if (this.openCanceled) {
this.openCanceled = false;
}
console.log(`${this.logHead}Connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`);

this.connectionId = false;
this.bitrate = 0;
this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
}

async cleanupResources() {
this.removeEventListener("receive", this.handleReceiveBytes);
if (this.reader) {
await this.reader.cancel();
this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.close();
this.writer = null;
}
if (this.port) {
this.port.removeEventListener("disconnect", this.handleDisconnect.bind(this));
await this.port.close();
this.port = null;
}
}

async send(data) {
// TODO: previous serial implementation had a buffer of 100, do we still need it with streams?
if (this.writer) {
await this.writer.write(data);
this.bytesSent += data.byteLength;
return { bytesSent: data.byteLength };
} else {
console.error(`${this.logHead}Failed to send data, serial port not open`);
}
return {
bytesSent: data.byteLength,
};
}
}

Expand Down

0 comments on commit 751875b

Please sign in to comment.