mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-26 09:45:28 +03:00
* Refactor port handler and fix reconnect * Fix as per review * Don't auto-connect for virtual or manual * Fix auto-connect switch state * Move auto-connect title to the parent div The checkbox is "hidden" under the switchary library, so move to the parent to be able to show it. * Select active port when request permission port exists before * Fix retun value for webserial requestPemission
251 lines
8.1 KiB
JavaScript
251 lines
8.1 KiB
JavaScript
import { webSerialDevices, vendorIdNames } from "./serial_devices";
|
|
|
|
async function* streamAsyncIterable(reader, keepReadingFlag) {
|
|
try {
|
|
while (keepReadingFlag()) {
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
return;
|
|
}
|
|
yield value;
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
|
|
class WebSerial extends EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.connected = false;
|
|
this.openRequested = false;
|
|
this.openCanceled = false;
|
|
this.transmitting = false;
|
|
this.connectionInfo = null;
|
|
|
|
this.bitrate = 0;
|
|
this.bytesSent = 0;
|
|
this.bytesReceived = 0;
|
|
this.failed = 0;
|
|
|
|
this.logHead = "SERIAL: ";
|
|
|
|
this.portCounter = 0;
|
|
this.ports = [];
|
|
this.port = null;
|
|
this.reader = null;
|
|
this.writer = null;
|
|
this.reading = false;
|
|
|
|
this.connect = this.connect.bind(this);
|
|
|
|
navigator.serial.addEventListener("connect", e => this.handleNewDevice(e.target));
|
|
navigator.serial.addEventListener("disconnect", e => this.handleRemovedDevice(e.target));
|
|
|
|
this.loadDevices();
|
|
}
|
|
|
|
handleNewDevice(device) {
|
|
|
|
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 }));
|
|
}
|
|
|
|
handleReceiveBytes(info) {
|
|
this.bytesReceived += info.detail.byteLength;
|
|
}
|
|
|
|
handleDisconnect() {
|
|
this.disconnect();
|
|
}
|
|
|
|
getConnectedPort() {
|
|
return this.port;
|
|
}
|
|
|
|
createPort(port) {
|
|
return {
|
|
path: `serial_${this.portCounter++}`,
|
|
displayName: `Betaflight ${vendorIdNames[port.getInfo().usbVendorId]}`,
|
|
vendorId: port.getInfo().usbVendorId,
|
|
productId: port.getInfo().usbProductId,
|
|
port: port,
|
|
};
|
|
}
|
|
|
|
async loadDevices() {
|
|
const ports = await navigator.serial.getPorts({
|
|
filters: webSerialDevices,
|
|
});
|
|
|
|
this.portCounter = 1;
|
|
this.ports = ports.map(function (port) {
|
|
return this.createPort(port);
|
|
}, this);
|
|
}
|
|
|
|
async requestPermissionDevice() {
|
|
let newPermissionPort = null;
|
|
try {
|
|
const userSelectedPort = await navigator.serial.requestPort({
|
|
filters: webSerialDevices,
|
|
});
|
|
newPermissionPort = this.ports.find(port => port.port === userSelectedPort);
|
|
if (!newPermissionPort) {
|
|
newPermissionPort = this.handleNewDevice(userSelectedPort);
|
|
}
|
|
console.info("User selected device from permissions:", newPermissionPort.path);
|
|
} catch (error) {
|
|
console.error("User didn't select any device when requesting permission:", error);
|
|
}
|
|
return newPermissionPort;
|
|
}
|
|
|
|
async getDevices() {
|
|
return this.ports;
|
|
}
|
|
|
|
async connect(path, options) {
|
|
this.openRequested = true;
|
|
|
|
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}`,
|
|
);
|
|
|
|
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
|
|
|
|
|
|
this.reading = true;
|
|
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;
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
try {
|
|
await doCleanup();
|
|
|
|
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 }));
|
|
} 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
} else {
|
|
console.error(
|
|
`${this.logHead}Failed to send data, serial port not open`,
|
|
);
|
|
}
|
|
return {
|
|
bytesSent: data.byteLength,
|
|
};
|
|
}
|
|
}
|
|
|
|
export default new WebSerial();
|