mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-21 15:25:22 +03:00
Add serial facade (#4402)
* Add serial facade * Fix websocket connection * Refactor * Fix unplug * Fix reboot / unplug reconnect issue * The real deal (detail has no value)
This commit is contained in:
parent
96a82d77f0
commit
4aad8c648b
21 changed files with 839 additions and 440 deletions
|
@ -1,8 +1,6 @@
|
|||
import GUI from "./gui.js";
|
||||
import CONFIGURATOR from "./data_storage.js";
|
||||
import { serialShim } from "./serial_shim.js";
|
||||
|
||||
let serial = serialShim();
|
||||
import { serial } from "./serial.js";
|
||||
|
||||
const MSP = {
|
||||
symbols: {
|
||||
|
@ -370,17 +368,12 @@ const MSP = {
|
|||
return bufferOut;
|
||||
},
|
||||
send_cli_command(str, callback) {
|
||||
serial = serialShim();
|
||||
|
||||
const bufferOut = this.encode_message_cli(str);
|
||||
this.cli_callback = callback;
|
||||
|
||||
serial.send(bufferOut);
|
||||
},
|
||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||
// Hack to make BT work
|
||||
serial = serialShim();
|
||||
|
||||
const connected = serial.connected;
|
||||
|
||||
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
||||
|
|
|
@ -4,18 +4,17 @@ import { i18n } from "../localization";
|
|||
import GUI from "../gui";
|
||||
import MSP from "../msp";
|
||||
import FC from "../fc";
|
||||
import { serialShim } from "../serial_shim";
|
||||
import { serial } from "../serial";
|
||||
import MSPCodes from "./MSPCodes";
|
||||
import CONFIGURATOR from "../data_storage";
|
||||
import { gui_log } from "../gui_log";
|
||||
|
||||
const serial = serialShim();
|
||||
/**
|
||||
* This seems to be mainly used in firmware flasher parts.
|
||||
*/
|
||||
|
||||
function readSerialAdapter(e) {
|
||||
read_serial(e.detail.buffer);
|
||||
read_serial(e.detail);
|
||||
}
|
||||
|
||||
function disconnectAndCleanup() {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { get as getConfig } from "./ConfigStorage";
|
||||
import { EventBus } from "../components/eventBus";
|
||||
import serial from "./webSerial";
|
||||
import usb from "./protocols/webusbdfu";
|
||||
import BT from "./protocols/bluetooth";
|
||||
import { serial } from "./serial.js";
|
||||
import WEBUSBDFU from "./protocols/webusbdfu";
|
||||
import WebBluetooth from "./protocols/WebBluetooth.js";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const DEFAULT_PORT = "noselection";
|
||||
|
@ -39,16 +39,39 @@ PortHandler.initialize = function () {
|
|||
EventBus.$on("ports-input:request-permission", this.askSerialPermissionPort.bind(this));
|
||||
EventBus.$on("ports-input:change", this.onChangeSelectedPort.bind(this));
|
||||
|
||||
BT.addEventListener("addedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
||||
BT.addEventListener("removedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
||||
// Use serial for all protocol events
|
||||
serial.addEventListener("addedDevice", (event) => {
|
||||
const detail = event.detail;
|
||||
|
||||
serial.addEventListener("addedDevice", (event) => this.addedSerialDevice(event.detail));
|
||||
serial.addEventListener("removedDevice", (event) => this.removedSerialDevice(event.detail));
|
||||
// Determine the device type based on its properties
|
||||
if (detail?.path?.startsWith("bluetooth")) {
|
||||
this.addedBluetoothDevice(detail);
|
||||
} else if (detail?.path?.startsWith("usb_")) {
|
||||
this.addedUsbDevice(detail);
|
||||
} else {
|
||||
this.addedSerialDevice(detail);
|
||||
}
|
||||
});
|
||||
|
||||
usb.addEventListener("addedDevice", (event) => this.addedUsbDevice(event.detail));
|
||||
serial.addEventListener("removedDevice", (event) => {
|
||||
const detail = event.detail;
|
||||
|
||||
this.addedBluetoothDevice();
|
||||
// Determine the device type based on its properties
|
||||
if (detail?.path?.startsWith("bluetooth")) {
|
||||
this.removedBluetoothDevice(detail);
|
||||
} else if (detail?.path?.startsWith("usb_")) {
|
||||
// Handle USB device removal if needed
|
||||
} else {
|
||||
this.removedSerialDevice(detail);
|
||||
}
|
||||
});
|
||||
|
||||
// Keep USB listener separate as it's not part of the serial protocols
|
||||
WEBUSBDFU.addEventListener("addedDevice", (event) => this.addedUsbDevice(event.detail));
|
||||
|
||||
// Initial device discovery
|
||||
this.addedSerialDevice();
|
||||
this.addedBluetoothDevice();
|
||||
this.addedUsbDevice();
|
||||
};
|
||||
|
||||
|
@ -125,7 +148,7 @@ PortHandler.updateCurrentSerialPortsList = async function () {
|
|||
};
|
||||
|
||||
PortHandler.updateCurrentUsbPortsList = async function () {
|
||||
const ports = await usb.getDevices();
|
||||
const ports = await WEBUSBDFU.getDevices();
|
||||
const orderedPorts = this.sortPorts(ports);
|
||||
this.dfuAvailable = orderedPorts.length > 0;
|
||||
console.log(`${this.logHead} Found DFU port`, orderedPorts);
|
||||
|
@ -133,8 +156,8 @@ PortHandler.updateCurrentUsbPortsList = async function () {
|
|||
};
|
||||
|
||||
PortHandler.updateCurrentBluetoothPortsList = async function () {
|
||||
if (BT.bluetooth) {
|
||||
const ports = await BT.getDevices();
|
||||
if (WebBluetooth.bluetooth) {
|
||||
const ports = await WebBluetooth.getDevices();
|
||||
const orderedPorts = this.sortPorts(ports);
|
||||
this.bluetoothAvailable = orderedPorts.length > 0;
|
||||
console.log(`${this.logHead} Found bluetooth port`, orderedPorts);
|
||||
|
@ -152,8 +175,8 @@ PortHandler.sortPorts = function (ports) {
|
|||
};
|
||||
|
||||
PortHandler.askBluetoothPermissionPort = function () {
|
||||
if (BT.bluetooth) {
|
||||
BT.requestPermissionDevice().then((port) => {
|
||||
if (WebBluetooth.bluetooth) {
|
||||
WebBluetooth.requestPermissionDevice().then((port) => {
|
||||
// When giving permission to a new device, the port is selected in the handleNewDevice method, but if the user
|
||||
// selects a device that had already permission, or cancels the permission request, we need to select the port
|
||||
// so do it here too
|
||||
|
@ -180,14 +203,14 @@ PortHandler.selectActivePort = function (suggestedDevice) {
|
|||
selectedPort = this.currentSerialPorts.find((device) => device === serial.getConnectedPort());
|
||||
}
|
||||
|
||||
// Return the same that is connected to usb (dfu mode)
|
||||
if (usb.usbDevice) {
|
||||
selectedPort = this.currentUsbPorts.find((device) => device === usb.getConnectedPort());
|
||||
// Return the same that is connected to WEBUSBDFU (dfu mode)
|
||||
if (WEBUSBDFU.usbDevice) {
|
||||
selectedPort = this.currentUsbPorts.find((device) => device === WEBUSBDFU.getConnectedPort());
|
||||
}
|
||||
|
||||
// Return the same that is connected to bluetooth
|
||||
if (BT.device) {
|
||||
selectedPort = this.currentBluetoothPorts.find((device) => device === BT.getConnectedPort());
|
||||
if (WebBluetooth.device) {
|
||||
selectedPort = this.currentBluetoothPorts.find((device) => device === WebBluetooth.getConnectedPort());
|
||||
}
|
||||
|
||||
// Return the suggested device (the new device that has been detected)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { serialShim } from "./serial_shim";
|
||||
|
||||
const serial = serialShim();
|
||||
import { serial } from "./serial";
|
||||
|
||||
const PortUsage = {
|
||||
previous_received: 0,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const VIRTUAL = "virtual";
|
||||
|
||||
/**
|
||||
* Stripped down version of our nwjs based serial port implementation
|
||||
* Stripped down version of previous nwjs based serial port implementation
|
||||
* which is required to still have virtual serial port support in the
|
||||
* browser.
|
||||
*/
|
||||
|
@ -38,6 +38,14 @@ class VirtualSerial {
|
|||
}
|
||||
}
|
||||
}
|
||||
getConnectedPort() {
|
||||
return this.connectionId;
|
||||
}
|
||||
getDevices() {
|
||||
return new Promise((resolve) => {
|
||||
resolve([{ path: VIRTUAL }]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new VirtualSerial();
|
||||
export default VirtualSerial;
|
|
@ -1,5 +1,6 @@
|
|||
import { i18n } from "../localization";
|
||||
import { gui_log } from "../gui_log";
|
||||
import { bluetoothDevices } from "./devices";
|
||||
|
||||
/* Certain flags needs to be enabled in the browser to use BT
|
||||
*
|
||||
|
@ -9,64 +10,10 @@ import { gui_log } from "../gui_log";
|
|||
*
|
||||
*/
|
||||
|
||||
const bluetoothDevices = [
|
||||
{
|
||||
name: "CC2541",
|
||||
serviceUuid: "0000ffe0-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HC-05",
|
||||
serviceUuid: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HM-10",
|
||||
serviceUuid: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HM-11",
|
||||
serviceUuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
writeCharacteristic: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
readCharacteristic: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
},
|
||||
{
|
||||
name: "Nordic NRF",
|
||||
serviceUuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
writeCharacteristic: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
readCharacteristic: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
},
|
||||
{
|
||||
name: "SpeedyBee V1",
|
||||
serviceUuid: "00001000-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "00001001-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "00001002-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "SpeedyBee V2",
|
||||
serviceUuid: "0000abf0-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000abf1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000abf2-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
];
|
||||
|
||||
class BT extends EventTarget {
|
||||
class WebBluetooth extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.logHead = "[BLUETOOTH]";
|
||||
|
||||
if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) {
|
||||
this.bluetooth = navigator.bluetooth;
|
||||
} else {
|
||||
console.error(`${this.logHead} Bluetooth API not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.connected = false;
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
|
@ -83,6 +30,15 @@ class BT extends EventTarget {
|
|||
this.devices = [];
|
||||
this.device = null;
|
||||
|
||||
this.logHead = "[BLUETOOTH]";
|
||||
|
||||
if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) {
|
||||
this.bluetooth = navigator.bluetooth;
|
||||
} else {
|
||||
console.error(`${this.logHead} Bluetooth API not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.connect = this.connect.bind(this);
|
||||
|
||||
this.bluetooth.addEventListener("connect", (e) => this.handleNewDevice(e.target));
|
||||
|
@ -395,4 +351,4 @@ class BT extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
export default new BT();
|
||||
export default WebBluetooth;
|
372
src/js/protocols/WebSerial.js
Normal file
372
src/js/protocols/WebSerial.js
Normal file
|
@ -0,0 +1,372 @@
|
|||
import { webSerialDevices, vendorIdNames } from "./devices";
|
||||
import { checkBrowserCompatibility } from "../utils/checkBrowserCompatibilty";
|
||||
|
||||
async function* streamAsyncIterable(reader, keepReadingFlag) {
|
||||
try {
|
||||
while (keepReadingFlag()) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
yield value;
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSerial protocol implementation for the Serial base class
|
||||
*/
|
||||
class WebSerial extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
checkBrowserCompatibility();
|
||||
|
||||
this.connected = false;
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
this.closeRequested = 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);
|
||||
this.disconnect = this.disconnect.bind(this);
|
||||
this.handleDisconnect = this.handleDisconnect.bind(this);
|
||||
this.handleReceiveBytes = this.handleReceiveBytes.bind(this);
|
||||
|
||||
// Initialize device connection/disconnection listeners
|
||||
if (navigator.serial) {
|
||||
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() {
|
||||
console.log(`${this.logHead} Device disconnected externally`);
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
getConnectedPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
createPort(port) {
|
||||
const portInfo = port.getInfo();
|
||||
const displayName = vendorIdNames[portInfo.usbVendorId]
|
||||
? vendorIdNames[portInfo.usbVendorId]
|
||||
: `VID:${portInfo.usbVendorId} PID:${portInfo.usbProductId}`;
|
||||
return {
|
||||
path: `serial_${this.portCounter++}`,
|
||||
displayName: `Betaflight ${displayName}`,
|
||||
vendorId: portInfo.usbVendorId,
|
||||
productId: portInfo.usbProductId,
|
||||
port: port,
|
||||
};
|
||||
}
|
||||
|
||||
async loadDevices() {
|
||||
if (!navigator.serial) {
|
||||
console.error(`${this.logHead} Web Serial API not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ports = await navigator.serial.getPorts();
|
||||
this.portCounter = 1;
|
||||
this.ports = ports.map((port) => this.createPort(port));
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error loading devices:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissionDevice(showAllSerialDevices = false) {
|
||||
if (!navigator.serial) {
|
||||
console.error(`${this.logHead} Web Serial API not available`);
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
return newPermissionPort;
|
||||
}
|
||||
|
||||
async getDevices() {
|
||||
return this.ports;
|
||||
}
|
||||
|
||||
async connect(path, options = { baudRate: 115200 }) {
|
||||
// Prevent double connections
|
||||
if (this.connected) {
|
||||
console.log(`${this.logHead} Already connected, not connecting again`);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.openRequested = true;
|
||||
this.closeRequested = false;
|
||||
|
||||
try {
|
||||
const device = this.ports.find((device) => device.path === path);
|
||||
if (!device) {
|
||||
console.error(`${this.logHead} Device not found:`, path);
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
return false;
|
||||
}
|
||||
|
||||
this.port = device.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);
|
||||
this.addEventListener("receive", this.handleReceiveBytes);
|
||||
|
||||
console.log(
|
||||
`${this.logHead} Connection opened with ID: ${this.connectionId}, Baud: ${options.baudRate}`,
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: connectionInfo }));
|
||||
|
||||
// Start reading from the port
|
||||
this.reading = true;
|
||||
this.readLoop();
|
||||
|
||||
return true;
|
||||
} else if (connectionInfo && this.openCanceled) {
|
||||
this.connectionId = path;
|
||||
|
||||
console.log(
|
||||
`${this.logHead} Connection opened with ID: ${path}, 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);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.openRequested = false;
|
||||
console.log(`${this.logHead} Failed to open serial port`);
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error connecting:`, error);
|
||||
this.openRequested = false;
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async readLoop() {
|
||||
try {
|
||||
for await (let value of streamAsyncIterable(this.reader, () => this.reading)) {
|
||||
this.dispatchEvent(new CustomEvent("receive", { detail: value }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error reading:`, error);
|
||||
if (this.connected) {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(callback) {
|
||||
// If already disconnected, just call callback and return
|
||||
if (!this.connected) {
|
||||
if (callback) {
|
||||
try {
|
||||
callback(true);
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error calling disconnect callback:`, error);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mark as disconnected immediately to prevent race conditions
|
||||
this.connected = false;
|
||||
this.transmitting = false;
|
||||
this.reading = false;
|
||||
|
||||
// if we are already closing, don't do it again
|
||||
if (this.closeRequested) {
|
||||
if (callback) {
|
||||
try {
|
||||
callback(true);
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error calling disconnect callback:`, error);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
this.closeRequested = true;
|
||||
|
||||
try {
|
||||
this.removeEventListener("receive", this.handleReceiveBytes);
|
||||
|
||||
if (this.reader) {
|
||||
await 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);
|
||||
await this.port.close();
|
||||
this.port = null;
|
||||
}
|
||||
|
||||
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.closeRequested = false;
|
||||
|
||||
this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
|
||||
|
||||
if (callback) {
|
||||
try {
|
||||
callback(true);
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error calling disconnect callback:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error disconnecting:`, error);
|
||||
console.error(
|
||||
`${this.logHead} Failed to close connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
|
||||
);
|
||||
|
||||
this.closeRequested = false;
|
||||
this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
|
||||
|
||||
if (callback) {
|
||||
try {
|
||||
callback(false);
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error calling disconnect callback:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (this.openCanceled) {
|
||||
this.openCanceled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data, callback) {
|
||||
if (!this.connected || !this.writer) {
|
||||
console.error(`${this.logHead} Failed to send data, serial port not open`);
|
||||
if (callback) {
|
||||
callback({ bytesSent: 0 });
|
||||
}
|
||||
return { bytesSent: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.writer.write(data);
|
||||
this.bytesSent += data.byteLength;
|
||||
|
||||
const result = { bytesSent: data.byteLength };
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error sending data:`, error);
|
||||
if (callback) {
|
||||
callback({ bytesSent: 0 });
|
||||
}
|
||||
return { bytesSent: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the protocol is no longer needed
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.connected) {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class itself, not an instance
|
||||
export default WebSerial;
|
|
@ -1,9 +1,7 @@
|
|||
class WebsocketSerial extends EventTarget {
|
||||
class Websocket extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.logHead = "[WEBSOCKET]";
|
||||
|
||||
this.connected = false;
|
||||
this.connectionInfo = null;
|
||||
|
||||
|
@ -12,6 +10,8 @@ class WebsocketSerial extends EventTarget {
|
|||
this.bytesReceived = 0;
|
||||
this.failed = 0;
|
||||
|
||||
this.logHead = "[WEBSOCKET]";
|
||||
|
||||
this.address = "ws://localhost:5761";
|
||||
|
||||
this.ws = null;
|
||||
|
@ -123,4 +123,4 @@ class WebsocketSerial extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
export default new WebsocketSerial();
|
||||
export default Websocket;
|
78
src/js/protocols/devices.js
Normal file
78
src/js/protocols/devices.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
export const bluetoothDevices = [
|
||||
{
|
||||
name: "CC2541",
|
||||
serviceUuid: "0000ffe0-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HC-05",
|
||||
serviceUuid: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "00001101-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HM-10",
|
||||
serviceUuid: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000ffe1-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "HM-11",
|
||||
serviceUuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
writeCharacteristic: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
readCharacteristic: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
},
|
||||
{
|
||||
name: "Nordic NRF",
|
||||
serviceUuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
writeCharacteristic: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
readCharacteristic: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
|
||||
},
|
||||
{
|
||||
name: "SpeedyBee V1",
|
||||
serviceUuid: "00001000-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "00001001-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "00001002-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
name: "SpeedyBee V2",
|
||||
serviceUuid: "0000abf0-0000-1000-8000-00805f9b34fb",
|
||||
writeCharacteristic: "0000abf1-0000-1000-8000-00805f9b34fb",
|
||||
readCharacteristic: "0000abf2-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
];
|
||||
|
||||
export const serialDevices = [
|
||||
{ vendorId: 1027, productId: 24577 }, // FT232R USB UART
|
||||
{ vendorId: 1155, productId: 12886 }, // STM32 in HID mode
|
||||
{ vendorId: 1155, productId: 14158 }, // 0483:374e STM Electronics STLink Virtual COM Port (NUCLEO boards)
|
||||
{ vendorId: 1155, productId: 22336 }, // STM Electronics Virtual COM Port
|
||||
{ vendorId: 4292, productId: 60000 }, // CP210x
|
||||
{ vendorId: 4292, productId: 60001 }, // CP210x
|
||||
{ vendorId: 4292, productId: 60002 }, // CP210x
|
||||
{ vendorId: 11836, productId: 22336 }, // AT32 VCP
|
||||
{ vendorId: 12619, productId: 22336 }, // APM32 VCP
|
||||
];
|
||||
|
||||
export const usbDevices = {
|
||||
filters: [
|
||||
{ vendorId: 1155, productId: 57105 }, // STM Device in DFU Mode || Digital Radio in USB mode
|
||||
{ vendorId: 10473, productId: 393 }, // GD32 DFU Bootloader
|
||||
{ vendorId: 0x2e3c, productId: 0xdf11 }, // AT32F435 DFU Bootloader
|
||||
{ vendorId: 12619, productId: 262 }, // APM32 DFU Bootloader
|
||||
],
|
||||
};
|
||||
|
||||
export const vendorIdNames = {
|
||||
1027: "FTDI",
|
||||
1155: "STM Electronics",
|
||||
4292: "Silicon Labs",
|
||||
0x2e3c: "AT32",
|
||||
0x314b: "Geehy Semiconductor",
|
||||
};
|
||||
|
||||
export const webSerialDevices = serialDevices.map(({ vendorId, productId }) => ({
|
||||
usbVendorId: vendorId,
|
||||
usbProductId: productId,
|
||||
}));
|
|
@ -15,7 +15,7 @@ import { gui_log } from "../gui_log";
|
|||
import MSPCodes from "../msp/MSPCodes";
|
||||
import PortUsage from "../port_usage";
|
||||
import $ from "jquery";
|
||||
import serial from "../webSerial";
|
||||
import { serial } from "../serial";
|
||||
import DFU from "../protocols/webusbdfu";
|
||||
import { read_serial } from "../serial_backend";
|
||||
import NotificationManager from "../utils/notifications";
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import GUI, { TABS } from "../gui";
|
||||
import { i18n } from "../localization";
|
||||
import { gui_log } from "../gui_log";
|
||||
import { usbDevices } from "../usb_devices";
|
||||
import { usbDevices } from "./devices";
|
||||
import NotificationManager from "../utils/notifications";
|
||||
import { get as getConfig } from "../ConfigStorage";
|
||||
|
||||
|
|
290
src/js/serial.js
Normal file
290
src/js/serial.js
Normal file
|
@ -0,0 +1,290 @@
|
|||
import CONFIGURATOR from "./data_storage";
|
||||
import WebSerial from "./protocols/WebSerial.js";
|
||||
import WebBluetooth from "./protocols/WebBluetooth.js";
|
||||
import Websocket from "./protocols/WebSocket.js";
|
||||
import VirtualSerial from "./protocols/VirtualSerial.js";
|
||||
|
||||
/**
|
||||
* Base Serial class that manages all protocol implementations
|
||||
* and handles event forwarding.
|
||||
*/
|
||||
class Serial extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._protocol = null;
|
||||
this._eventHandlers = {};
|
||||
|
||||
this.logHead = "[SERIAL]";
|
||||
|
||||
// Initialize the available protocols
|
||||
this._webSerial = new WebSerial();
|
||||
this._bluetooth = new WebBluetooth();
|
||||
this._websocket = new Websocket();
|
||||
this._virtual = new VirtualSerial();
|
||||
|
||||
// Initialize with default protocol
|
||||
this.selectProtocol(false);
|
||||
|
||||
// Forward events from all protocols to the Serial class
|
||||
this._setupEventForwarding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event forwarding from all protocols to the Serial class
|
||||
*/
|
||||
_setupEventForwarding() {
|
||||
const protocols = [this._webSerial, this._bluetooth, this._websocket, this._virtual];
|
||||
const events = ["addedDevice", "removedDevice", "connect", "disconnect", "receive"];
|
||||
|
||||
protocols.forEach((protocol) => {
|
||||
if (protocol && typeof protocol.addEventListener === "function") {
|
||||
events.forEach((eventType) => {
|
||||
protocol.addEventListener(eventType, (event) => {
|
||||
let newDetail;
|
||||
|
||||
// Special handling for 'receive' events to ensure data is properly passed through
|
||||
if (eventType === "receive") {
|
||||
// If it's already a Uint8Array or ArrayBuffer, keep it as is
|
||||
newDetail = {
|
||||
data: event.detail,
|
||||
protocolType:
|
||||
protocol === this._webSerial
|
||||
? "webSerial"
|
||||
: protocol === this._bluetooth
|
||||
? "bluetooth"
|
||||
: protocol === this._websocket
|
||||
? "websocket"
|
||||
: protocol === this._virtual
|
||||
? "virtual"
|
||||
: "unknown",
|
||||
};
|
||||
} else {
|
||||
// For all other events, pass through the detail as is
|
||||
newDetail = event.detail;
|
||||
}
|
||||
// Dispatch the event with the new detail
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(event.type, {
|
||||
detail: newDetail,
|
||||
bubbles: event.bubbles,
|
||||
cancelable: event.cancelable,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the appropriate protocol based on port path or CONFIGURATOR settings
|
||||
* @param {string|null} portPath - Optional port path to determine protocol
|
||||
* @param {boolean} forceDisconnect - Whether to force disconnect from current protocol
|
||||
*/
|
||||
selectProtocol(portPath = null, forceDisconnect = true) {
|
||||
// Determine which protocol to use based on port path first, then fall back to CONFIGURATOR
|
||||
let newProtocol;
|
||||
|
||||
if (portPath) {
|
||||
// Select protocol based on port path
|
||||
if (portPath === "virtual") {
|
||||
console.log(`${this.logHead} Using virtual protocol (based on port path)`);
|
||||
newProtocol = this._virtual;
|
||||
// Update CONFIGURATOR flags for consistency
|
||||
CONFIGURATOR.virtualMode = true;
|
||||
CONFIGURATOR.bluetoothMode = false;
|
||||
CONFIGURATOR.manualMode = false;
|
||||
} else if (portPath === "manual") {
|
||||
console.log(`${this.logHead} Using websocket protocol (based on port path)`);
|
||||
newProtocol = this._websocket;
|
||||
// Update CONFIGURATOR flags for consistency
|
||||
CONFIGURATOR.virtualMode = false;
|
||||
CONFIGURATOR.bluetoothMode = false;
|
||||
CONFIGURATOR.manualMode = true;
|
||||
} else if (portPath.startsWith("bluetooth")) {
|
||||
console.log(`${this.logHead} Using bluetooth protocol (based on port path)`);
|
||||
newProtocol = this._bluetooth;
|
||||
// Update CONFIGURATOR flags for consistency
|
||||
CONFIGURATOR.virtualMode = false;
|
||||
CONFIGURATOR.bluetoothMode = true;
|
||||
CONFIGURATOR.manualMode = false;
|
||||
} else {
|
||||
console.log(`${this.logHead} Using web serial protocol (based on port path)`);
|
||||
newProtocol = this._webSerial;
|
||||
// Update CONFIGURATOR flags for consistency
|
||||
CONFIGURATOR.virtualMode = false;
|
||||
CONFIGURATOR.bluetoothMode = false;
|
||||
CONFIGURATOR.manualMode = false;
|
||||
}
|
||||
} else {
|
||||
// Fall back to CONFIGURATOR flags if no port path is provided
|
||||
if (CONFIGURATOR.virtualMode) {
|
||||
console.log(`${this.logHead} Using virtual protocol (based on CONFIGURATOR flags)`);
|
||||
newProtocol = this._virtual;
|
||||
} else if (CONFIGURATOR.manualMode) {
|
||||
console.log(`${this.logHead} Using websocket protocol (based on CONFIGURATOR flags)`);
|
||||
newProtocol = this._websocket;
|
||||
} else if (CONFIGURATOR.bluetoothMode) {
|
||||
console.log(`${this.logHead} Using bluetooth protocol (based on CONFIGURATOR flags)`);
|
||||
newProtocol = this._bluetooth;
|
||||
} else {
|
||||
console.log(`${this.logHead} Using web serial protocol (based on CONFIGURATOR flags)`);
|
||||
newProtocol = this._webSerial;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're switching to a different protocol
|
||||
if (this._protocol !== newProtocol) {
|
||||
// Clean up previous protocol if exists
|
||||
if (this._protocol && forceDisconnect) {
|
||||
// Disconnect if connected
|
||||
if (this._protocol.connected) {
|
||||
console.log(`${this.logHead} Disconnecting from current protocol before switching`);
|
||||
this._protocol.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Set new protocol
|
||||
this._protocol = newProtocol;
|
||||
console.log(`${this.logHead} Protocol switched successfully to:`, this._protocol);
|
||||
} else {
|
||||
console.log(`${this.logHead} Same protocol selected, no switch needed`);
|
||||
}
|
||||
|
||||
return this._protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the specified port with options
|
||||
* @param {string|function} path - Port path or callback for virtual mode
|
||||
* @param {object} options - Connection options (baudRate, etc.)
|
||||
*/
|
||||
connect(path, options) {
|
||||
if (!this._protocol) {
|
||||
console.error(`${this.logHead} No protocol selected, cannot connect`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If path is a function, it's a callback for virtual mode
|
||||
const isCallback = typeof path === "function";
|
||||
|
||||
// In virtual mode, a callback is passed as the first parameter
|
||||
if (isCallback && CONFIGURATOR.virtualMode) {
|
||||
console.log(`${this.logHead} Connecting in virtual mode`);
|
||||
return this._protocol.connect(path);
|
||||
}
|
||||
|
||||
// Check if already connected
|
||||
if (this._protocol.connected) {
|
||||
console.warn(`${this.logHead} Protocol already connected, not connecting again`);
|
||||
|
||||
// If we're already connected to the requested port, return success
|
||||
const connectedPort = this._protocol.getConnectedPort?.();
|
||||
if (connectedPort && connectedPort.path === path) {
|
||||
console.log(`${this.logHead} Already connected to the requested port`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we're connected to a different port, disconnect first
|
||||
console.log(`${this.logHead} Connected to a different port, disconnecting first`);
|
||||
this.disconnect((success) => {
|
||||
if (success) {
|
||||
// Now connect to the new port
|
||||
console.log(`${this.logHead} Reconnecting to new port:`, path);
|
||||
this._protocol.connect(path, options);
|
||||
} else {
|
||||
console.error(`${this.logHead} Failed to disconnect before reconnecting`);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`${this.logHead} Connecting to port:`, path, "with options:", options);
|
||||
return this._protocol.connect(path, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the current connection
|
||||
*/
|
||||
disconnect(callback) {
|
||||
if (!this._protocol) {
|
||||
console.warn(`${this.logHead} No protocol selected, nothing to disconnect`);
|
||||
if (callback) callback(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._protocol.connected) {
|
||||
console.warn(`${this.logHead} Protocol not connected, nothing to disconnect`);
|
||||
if (callback) callback(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`${this.logHead} Disconnecting from current protocol`);
|
||||
|
||||
try {
|
||||
// Disconnect from the protocol
|
||||
const result = this._protocol.disconnect((success) => {
|
||||
if (success) {
|
||||
// Ensure our connection state is updated
|
||||
console.log(`${this.logHead} Disconnection successful`);
|
||||
} else {
|
||||
console.error(`${this.logHead} Disconnection failed`);
|
||||
}
|
||||
|
||||
// Call the callback with the result
|
||||
if (callback) callback(success);
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} Error during disconnect:`, error);
|
||||
if (callback) callback(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data through the serial connection
|
||||
*/
|
||||
send(data, callback) {
|
||||
if (!this._protocol || !this._protocol.connected) {
|
||||
console.warn(`${this.logHead} Cannot send data - not connected`);
|
||||
if (callback) callback({ bytesSent: 0 });
|
||||
return { bytesSent: 0 };
|
||||
}
|
||||
|
||||
return this._protocol.send(data, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get devices from the current protocol
|
||||
*/
|
||||
async getDevices() {
|
||||
return this._protocol?.getDevices() || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permission for a device
|
||||
*/
|
||||
async requestPermissionDevice(showAllSerialDevices = false) {
|
||||
return this._protocol?.requestPermissionDevice(showAllSerialDevices) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently connected port
|
||||
*/
|
||||
getConnectedPort() {
|
||||
return this._protocol?.getConnectedPort() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
get connected() {
|
||||
return this._protocol ? this._protocol.connected : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const serial = new Serial();
|
|
@ -23,14 +23,12 @@ import CryptoES from "crypto-es";
|
|||
import $ from "jquery";
|
||||
import BuildApi from "./BuildApi";
|
||||
|
||||
import { serialShim } from "./serial_shim.js";
|
||||
import { serial } from "./serial.js";
|
||||
import { EventBus } from "../components/eventBus";
|
||||
import { ispConnected } from "./utils/connection";
|
||||
|
||||
const logHead = "[SERIAL-BACKEND]";
|
||||
|
||||
let serial = serialShim();
|
||||
|
||||
let mspHelper;
|
||||
let connectionTimestamp;
|
||||
let liveDataRefreshTimerId = false;
|
||||
|
@ -80,7 +78,7 @@ export function initializeSerialBackend() {
|
|||
}
|
||||
});
|
||||
|
||||
// Using serialShim for serial and bluetooth we don't know which event we need before we connect
|
||||
// Using serial and bluetooth we don't know which event we need before we connect
|
||||
// Perhaps we should implement a Connection class that handles the connection and events for bluetooth, serial and sockets
|
||||
// TODO: use event gattserverdisconnected for save and reboot and device removal.
|
||||
|
||||
|
@ -118,39 +116,32 @@ function connectDisconnect() {
|
|||
PortHandler.portPickerDisabled = true;
|
||||
$("div.connection_button__label").text(i18n.getMessage("connecting"));
|
||||
|
||||
// Set configuration flags for consistency with other code
|
||||
CONFIGURATOR.virtualMode = selectedPort === "virtual";
|
||||
CONFIGURATOR.bluetoothMode = selectedPort.startsWith("bluetooth");
|
||||
CONFIGURATOR.manualMode = selectedPort === "manual";
|
||||
|
||||
// Select the appropriate protocol based directly on the port path
|
||||
serial.selectProtocol(selectedPort);
|
||||
console.log("Serial protocol selected:", serial._protocol, "using port", portName);
|
||||
|
||||
if (CONFIGURATOR.virtualMode) {
|
||||
CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion;
|
||||
|
||||
// Hack to get virtual working on the web
|
||||
serial = serialShim();
|
||||
// Virtual mode uses a callback instead of port path
|
||||
serial.connect(onOpenVirtual);
|
||||
} else if (selectedPort === "manual") {
|
||||
serial = serialShim();
|
||||
// Explicitly disconnect the event listeners before attaching the new ones.
|
||||
serial.removeEventListener("connect", connectHandler);
|
||||
serial.addEventListener("connect", connectHandler);
|
||||
|
||||
serial.removeEventListener("disconnect", disconnectHandler);
|
||||
serial.addEventListener("disconnect", disconnectHandler);
|
||||
|
||||
serial.connect(portName, { baudRate });
|
||||
} else {
|
||||
CONFIGURATOR.virtualMode = false;
|
||||
serial = serialShim();
|
||||
// Explicitly disconnect the event listeners before attaching the new ones.
|
||||
// Set up event listeners for all non-virtual connections
|
||||
serial.removeEventListener("connect", connectHandler);
|
||||
serial.addEventListener("connect", connectHandler);
|
||||
|
||||
serial.removeEventListener("disconnect", disconnectHandler);
|
||||
serial.addEventListener("disconnect", disconnectHandler);
|
||||
|
||||
// All non-virtual modes pass the port path and options
|
||||
serial.connect(portName, { baudRate });
|
||||
}
|
||||
} else {
|
||||
// If connected, start disconnection sequence
|
||||
GUI.timeout_kill_all();
|
||||
GUI.interval_kill_all();
|
||||
GUI.tab_switch_cleanup(() => (GUI.tab_switch_in_progress = false));
|
||||
|
@ -165,7 +156,11 @@ function connectDisconnect() {
|
|||
// show CLI panel on Control+I
|
||||
document.onkeydown = function (e) {
|
||||
if (e.code === "KeyI" && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
if (isConnected && GUI.active_tab !== "cli" && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) {
|
||||
if (
|
||||
serial.connected &&
|
||||
GUI.active_tab !== "cli" &&
|
||||
semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)
|
||||
) {
|
||||
GUI.showCliPanel();
|
||||
}
|
||||
}
|
||||
|
@ -184,7 +179,6 @@ function finishClose(finishedCallback) {
|
|||
$("#dialogResetToCustomDefaults")[0].close();
|
||||
}
|
||||
|
||||
// serialShim calls the disconnect method for selected connection type.
|
||||
serial.disconnect(onClosed);
|
||||
|
||||
MSP.disconnect_cleanup();
|
||||
|
@ -249,8 +243,6 @@ function resetConnection() {
|
|||
|
||||
// unlock port select & baud
|
||||
PortHandler.portPickerDisabled = false;
|
||||
// reset data
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
function abortConnection() {
|
||||
|
@ -271,7 +263,7 @@ function abortConnection() {
|
|||
* when serial events are handled.
|
||||
*/
|
||||
function read_serial_adapter(event) {
|
||||
read_serial(event.detail.buffer);
|
||||
read_serial(event.detail.data);
|
||||
}
|
||||
|
||||
function onOpen(openInfo) {
|
||||
|
@ -290,7 +282,7 @@ function onOpen(openInfo) {
|
|||
const result = getConfig("expertMode")?.expertMode ?? false;
|
||||
$('input[name="expertModeCheckbox"]').prop("checked", result).trigger("change");
|
||||
|
||||
// serialShim adds event listener for selected connection type
|
||||
// serial adds event listener for selected connection type
|
||||
serial.removeEventListener("receive", read_serial_adapter);
|
||||
serial.addEventListener("receive", read_serial_adapter);
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
export const vendorIdNames = {
|
||||
1027: "FTDI",
|
||||
1155: "STM Electronics",
|
||||
4292: "Silicon Labs",
|
||||
0x2e3c: "AT32",
|
||||
0x314b: "Geehy Semiconductor",
|
||||
};
|
||||
|
||||
export const serialDevices = [
|
||||
{ vendorId: 1027, productId: 24577 }, // FT232R USB UART
|
||||
{ vendorId: 1155, productId: 12886 }, // STM32 in HID mode
|
||||
{ vendorId: 1155, productId: 14158 }, // 0483:374e STM Electronics STLink Virtual COM Port (NUCLEO boards)
|
||||
{ vendorId: 1155, productId: 22336 }, // STM Electronics Virtual COM Port
|
||||
{ vendorId: 4292, productId: 60000 }, // CP210x
|
||||
{ vendorId: 4292, productId: 60001 }, // CP210x
|
||||
{ vendorId: 4292, productId: 60002 }, // CP210x
|
||||
{ vendorId: 11836, productId: 22336 }, // AT32 VCP
|
||||
{ vendorId: 12619, productId: 22336 }, // APM32 VCP
|
||||
];
|
||||
|
||||
export const webSerialDevices = serialDevices.map(({ vendorId, productId }) => ({
|
||||
usbVendorId: vendorId,
|
||||
usbProductId: productId,
|
||||
}));
|
|
@ -1,18 +0,0 @@
|
|||
import CONFIGURATOR from "./data_storage";
|
||||
import serialWeb from "./webSerial.js";
|
||||
import BT from "./protocols/bluetooth.js";
|
||||
import websocketSerial from "./protocols/websocket.js";
|
||||
import virtualSerial from "./virtualSerial.js";
|
||||
|
||||
export const serialShim = () => {
|
||||
if (CONFIGURATOR.virtualMode) {
|
||||
return virtualSerial;
|
||||
}
|
||||
if (CONFIGURATOR.manualMode) {
|
||||
return websocketSerial;
|
||||
}
|
||||
if (CONFIGURATOR.bluetoothMode) {
|
||||
return BT;
|
||||
}
|
||||
return serialWeb;
|
||||
};
|
|
@ -10,12 +10,10 @@ import CliAutoComplete from "../CliAutoComplete";
|
|||
import { gui_log } from "../gui_log";
|
||||
import jBox from "jbox";
|
||||
import $ from "jquery";
|
||||
import { serialShim } from "../serial_shim";
|
||||
import { serial } from "../serial";
|
||||
import FileSystem from "../FileSystem";
|
||||
import { ispConnected } from "../utils/connection";
|
||||
|
||||
const serial = serialShim();
|
||||
|
||||
const cli = {
|
||||
lineDelayMs: 5,
|
||||
profileSwitchDelayMs: 100,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export const usbDevices = {
|
||||
filters: [
|
||||
{ vendorId: 1155, productId: 57105 }, // STM Device in DFU Mode || Digital Radio in USB mode
|
||||
{ vendorId: 10473, productId: 393 }, // GD32 DFU Bootloader
|
||||
{ vendorId: 0x2e3c, productId: 0xdf11 }, // AT32F435 DFU Bootloader
|
||||
{ vendorId: 12619, productId: 262 }, // APM32 DFU Bootloader
|
||||
],
|
||||
};
|
|
@ -3,7 +3,7 @@ import FileSystem from "../FileSystem";
|
|||
import { generateFilename } from "./generate_filename";
|
||||
import { gui_log } from "../gui_log";
|
||||
import { i18n } from "../localization";
|
||||
import serial from "../webSerial";
|
||||
import { serial } from "../serial";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -62,7 +62,7 @@ class AutoBackup {
|
|||
}
|
||||
|
||||
readSerialAdapter(info) {
|
||||
const data = new Uint8Array(info.detail.buffer);
|
||||
const data = new Uint8Array(info.detail.data);
|
||||
|
||||
for (const charCode of data) {
|
||||
const currentChar = String.fromCharCode(charCode);
|
||||
|
|
|
@ -8,7 +8,7 @@ import MSP from "../msp";
|
|||
import MSPCodes from "../msp/MSPCodes";
|
||||
import semver from "semver";
|
||||
import { API_VERSION_1_45, API_VERSION_1_46 } from "../data_storage";
|
||||
import serial from "../webSerial";
|
||||
import { serial } from "../serial";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -19,7 +19,7 @@ import serial from "../webSerial";
|
|||
let mspHelper = null;
|
||||
|
||||
function readSerialAdapter(event) {
|
||||
MSP.read(event.detail.buffer);
|
||||
MSP.read(event.detail);
|
||||
}
|
||||
|
||||
class AutoDetect {
|
||||
|
@ -109,7 +109,7 @@ class AutoDetect {
|
|||
);
|
||||
}
|
||||
|
||||
serial.disconnect(this.onClosed);
|
||||
serial.disconnect(this.onClosed.bind(this));
|
||||
MSP.disconnect_cleanup();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,256 +0,0 @@
|
|||
import { webSerialDevices, vendorIdNames } from "./serial_devices";
|
||||
import { checkBrowserCompatibility } from "./utils/checkBrowserCompatibilty";
|
||||
|
||||
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();
|
||||
|
||||
checkBrowserCompatibility();
|
||||
|
||||
this.connected = false;
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
this.closeRequested = 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();
|
||||
this.closeRequested = true;
|
||||
}
|
||||
|
||||
getConnectedPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
createPort(port) {
|
||||
const displayName = vendorIdNames[port.getInfo().usbVendorId]
|
||||
? vendorIdNames[port.getInfo().usbVendorId]
|
||||
: `VID:${port.getInfo().usbVendorId} PID:${port.getInfo().usbProductId}`;
|
||||
return {
|
||||
path: `serial_${this.portCounter++}`,
|
||||
displayName: `Betaflight ${displayName}`,
|
||||
vendorId: port.getInfo().usbVendorId,
|
||||
productId: port.getInfo().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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
return newPermissionPort;
|
||||
}
|
||||
|
||||
async getDevices() {
|
||||
return this.ports;
|
||||
}
|
||||
|
||||
async connect(path, options) {
|
||||
this.openRequested = true;
|
||||
this.closeRequested = false;
|
||||
|
||||
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;
|
||||
|
||||
// if we are already closing, don't do it again
|
||||
if (this.closeRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
|
@ -3,9 +3,7 @@ import { i18n } from "../../js/localization";
|
|||
import CONFIGURATOR from "../../js/data_storage";
|
||||
import { reinitializeConnection } from "../../js/serial_backend";
|
||||
import { gui_log } from "../../js/gui_log";
|
||||
import { serialShim } from "../../js/serial_shim";
|
||||
|
||||
const serial = serialShim();
|
||||
import { serial } from "../../js/serial";
|
||||
|
||||
export default class CliEngine {
|
||||
constructor(currentTab) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue