mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-21 23:35: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 GUI from "./gui.js";
|
||||||
import CONFIGURATOR from "./data_storage.js";
|
import CONFIGURATOR from "./data_storage.js";
|
||||||
import { serialShim } from "./serial_shim.js";
|
import { serial } from "./serial.js";
|
||||||
|
|
||||||
let serial = serialShim();
|
|
||||||
|
|
||||||
const MSP = {
|
const MSP = {
|
||||||
symbols: {
|
symbols: {
|
||||||
|
@ -370,17 +368,12 @@ const MSP = {
|
||||||
return bufferOut;
|
return bufferOut;
|
||||||
},
|
},
|
||||||
send_cli_command(str, callback) {
|
send_cli_command(str, callback) {
|
||||||
serial = serialShim();
|
|
||||||
|
|
||||||
const bufferOut = this.encode_message_cli(str);
|
const bufferOut = this.encode_message_cli(str);
|
||||||
this.cli_callback = callback;
|
this.cli_callback = callback;
|
||||||
|
|
||||||
serial.send(bufferOut);
|
serial.send(bufferOut);
|
||||||
},
|
},
|
||||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||||
// Hack to make BT work
|
|
||||||
serial = serialShim();
|
|
||||||
|
|
||||||
const connected = serial.connected;
|
const connected = serial.connected;
|
||||||
|
|
||||||
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
||||||
|
|
|
@ -4,18 +4,17 @@ import { i18n } from "../localization";
|
||||||
import GUI from "../gui";
|
import GUI from "../gui";
|
||||||
import MSP from "../msp";
|
import MSP from "../msp";
|
||||||
import FC from "../fc";
|
import FC from "../fc";
|
||||||
import { serialShim } from "../serial_shim";
|
import { serial } from "../serial";
|
||||||
import MSPCodes from "./MSPCodes";
|
import MSPCodes from "./MSPCodes";
|
||||||
import CONFIGURATOR from "../data_storage";
|
import CONFIGURATOR from "../data_storage";
|
||||||
import { gui_log } from "../gui_log";
|
import { gui_log } from "../gui_log";
|
||||||
|
|
||||||
const serial = serialShim();
|
|
||||||
/**
|
/**
|
||||||
* This seems to be mainly used in firmware flasher parts.
|
* This seems to be mainly used in firmware flasher parts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function readSerialAdapter(e) {
|
function readSerialAdapter(e) {
|
||||||
read_serial(e.detail.buffer);
|
read_serial(e.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectAndCleanup() {
|
function disconnectAndCleanup() {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { get as getConfig } from "./ConfigStorage";
|
import { get as getConfig } from "./ConfigStorage";
|
||||||
import { EventBus } from "../components/eventBus";
|
import { EventBus } from "../components/eventBus";
|
||||||
import serial from "./webSerial";
|
import { serial } from "./serial.js";
|
||||||
import usb from "./protocols/webusbdfu";
|
import WEBUSBDFU from "./protocols/webusbdfu";
|
||||||
import BT from "./protocols/bluetooth";
|
import WebBluetooth from "./protocols/WebBluetooth.js";
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
|
|
||||||
const DEFAULT_PORT = "noselection";
|
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:request-permission", this.askSerialPermissionPort.bind(this));
|
||||||
EventBus.$on("ports-input:change", this.onChangeSelectedPort.bind(this));
|
EventBus.$on("ports-input:change", this.onChangeSelectedPort.bind(this));
|
||||||
|
|
||||||
BT.addEventListener("addedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
// Use serial for all protocol events
|
||||||
BT.addEventListener("removedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
serial.addEventListener("addedDevice", (event) => {
|
||||||
|
const detail = event.detail;
|
||||||
|
|
||||||
serial.addEventListener("addedDevice", (event) => this.addedSerialDevice(event.detail));
|
// Determine the device type based on its properties
|
||||||
serial.addEventListener("removedDevice", (event) => this.removedSerialDevice(event.detail));
|
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.addedSerialDevice();
|
||||||
|
this.addedBluetoothDevice();
|
||||||
this.addedUsbDevice();
|
this.addedUsbDevice();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -125,7 +148,7 @@ PortHandler.updateCurrentSerialPortsList = async function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
PortHandler.updateCurrentUsbPortsList = async function () {
|
PortHandler.updateCurrentUsbPortsList = async function () {
|
||||||
const ports = await usb.getDevices();
|
const ports = await WEBUSBDFU.getDevices();
|
||||||
const orderedPorts = this.sortPorts(ports);
|
const orderedPorts = this.sortPorts(ports);
|
||||||
this.dfuAvailable = orderedPorts.length > 0;
|
this.dfuAvailable = orderedPorts.length > 0;
|
||||||
console.log(`${this.logHead} Found DFU port`, orderedPorts);
|
console.log(`${this.logHead} Found DFU port`, orderedPorts);
|
||||||
|
@ -133,8 +156,8 @@ PortHandler.updateCurrentUsbPortsList = async function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
PortHandler.updateCurrentBluetoothPortsList = async function () {
|
PortHandler.updateCurrentBluetoothPortsList = async function () {
|
||||||
if (BT.bluetooth) {
|
if (WebBluetooth.bluetooth) {
|
||||||
const ports = await BT.getDevices();
|
const ports = await WebBluetooth.getDevices();
|
||||||
const orderedPorts = this.sortPorts(ports);
|
const orderedPorts = this.sortPorts(ports);
|
||||||
this.bluetoothAvailable = orderedPorts.length > 0;
|
this.bluetoothAvailable = orderedPorts.length > 0;
|
||||||
console.log(`${this.logHead} Found bluetooth port`, orderedPorts);
|
console.log(`${this.logHead} Found bluetooth port`, orderedPorts);
|
||||||
|
@ -152,8 +175,8 @@ PortHandler.sortPorts = function (ports) {
|
||||||
};
|
};
|
||||||
|
|
||||||
PortHandler.askBluetoothPermissionPort = function () {
|
PortHandler.askBluetoothPermissionPort = function () {
|
||||||
if (BT.bluetooth) {
|
if (WebBluetooth.bluetooth) {
|
||||||
BT.requestPermissionDevice().then((port) => {
|
WebBluetooth.requestPermissionDevice().then((port) => {
|
||||||
// When giving permission to a new device, the port is selected in the handleNewDevice method, but if the user
|
// 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
|
// selects a device that had already permission, or cancels the permission request, we need to select the port
|
||||||
// so do it here too
|
// so do it here too
|
||||||
|
@ -180,14 +203,14 @@ PortHandler.selectActivePort = function (suggestedDevice) {
|
||||||
selectedPort = this.currentSerialPorts.find((device) => device === serial.getConnectedPort());
|
selectedPort = this.currentSerialPorts.find((device) => device === serial.getConnectedPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the same that is connected to usb (dfu mode)
|
// Return the same that is connected to WEBUSBDFU (dfu mode)
|
||||||
if (usb.usbDevice) {
|
if (WEBUSBDFU.usbDevice) {
|
||||||
selectedPort = this.currentUsbPorts.find((device) => device === usb.getConnectedPort());
|
selectedPort = this.currentUsbPorts.find((device) => device === WEBUSBDFU.getConnectedPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the same that is connected to bluetooth
|
// Return the same that is connected to bluetooth
|
||||||
if (BT.device) {
|
if (WebBluetooth.device) {
|
||||||
selectedPort = this.currentBluetoothPorts.find((device) => device === BT.getConnectedPort());
|
selectedPort = this.currentBluetoothPorts.find((device) => device === WebBluetooth.getConnectedPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the suggested device (the new device that has been detected)
|
// Return the suggested device (the new device that has been detected)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { serialShim } from "./serial_shim";
|
import { serial } from "./serial";
|
||||||
|
|
||||||
const serial = serialShim();
|
|
||||||
|
|
||||||
const PortUsage = {
|
const PortUsage = {
|
||||||
previous_received: 0,
|
previous_received: 0,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const VIRTUAL = "virtual";
|
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
|
* which is required to still have virtual serial port support in the
|
||||||
* browser.
|
* 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 { i18n } from "../localization";
|
||||||
import { gui_log } from "../gui_log";
|
import { gui_log } from "../gui_log";
|
||||||
|
import { bluetoothDevices } from "./devices";
|
||||||
|
|
||||||
/* Certain flags needs to be enabled in the browser to use BT
|
/* Certain flags needs to be enabled in the browser to use BT
|
||||||
*
|
*
|
||||||
|
@ -9,64 +10,10 @@ import { gui_log } from "../gui_log";
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const bluetoothDevices = [
|
class WebBluetooth extends EventTarget {
|
||||||
{
|
|
||||||
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 {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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.connected = false;
|
||||||
this.openRequested = false;
|
this.openRequested = false;
|
||||||
this.openCanceled = false;
|
this.openCanceled = false;
|
||||||
|
@ -83,6 +30,15 @@ class BT extends EventTarget {
|
||||||
this.devices = [];
|
this.devices = [];
|
||||||
this.device = null;
|
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.connect = this.connect.bind(this);
|
||||||
|
|
||||||
this.bluetooth.addEventListener("connect", (e) => this.handleNewDevice(e.target));
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.logHead = "[WEBSOCKET]";
|
|
||||||
|
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.connectionInfo = null;
|
this.connectionInfo = null;
|
||||||
|
|
||||||
|
@ -12,6 +10,8 @@ class WebsocketSerial extends EventTarget {
|
||||||
this.bytesReceived = 0;
|
this.bytesReceived = 0;
|
||||||
this.failed = 0;
|
this.failed = 0;
|
||||||
|
|
||||||
|
this.logHead = "[WEBSOCKET]";
|
||||||
|
|
||||||
this.address = "ws://localhost:5761";
|
this.address = "ws://localhost:5761";
|
||||||
|
|
||||||
this.ws = null;
|
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 MSPCodes from "../msp/MSPCodes";
|
||||||
import PortUsage from "../port_usage";
|
import PortUsage from "../port_usage";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import serial from "../webSerial";
|
import { serial } from "../serial";
|
||||||
import DFU from "../protocols/webusbdfu";
|
import DFU from "../protocols/webusbdfu";
|
||||||
import { read_serial } from "../serial_backend";
|
import { read_serial } from "../serial_backend";
|
||||||
import NotificationManager from "../utils/notifications";
|
import NotificationManager from "../utils/notifications";
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import GUI, { TABS } from "../gui";
|
import GUI, { TABS } from "../gui";
|
||||||
import { i18n } from "../localization";
|
import { i18n } from "../localization";
|
||||||
import { gui_log } from "../gui_log";
|
import { gui_log } from "../gui_log";
|
||||||
import { usbDevices } from "../usb_devices";
|
import { usbDevices } from "./devices";
|
||||||
import NotificationManager from "../utils/notifications";
|
import NotificationManager from "../utils/notifications";
|
||||||
import { get as getConfig } from "../ConfigStorage";
|
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 $ from "jquery";
|
||||||
import BuildApi from "./BuildApi";
|
import BuildApi from "./BuildApi";
|
||||||
|
|
||||||
import { serialShim } from "./serial_shim.js";
|
import { serial } from "./serial.js";
|
||||||
import { EventBus } from "../components/eventBus";
|
import { EventBus } from "../components/eventBus";
|
||||||
import { ispConnected } from "./utils/connection";
|
import { ispConnected } from "./utils/connection";
|
||||||
|
|
||||||
const logHead = "[SERIAL-BACKEND]";
|
const logHead = "[SERIAL-BACKEND]";
|
||||||
|
|
||||||
let serial = serialShim();
|
|
||||||
|
|
||||||
let mspHelper;
|
let mspHelper;
|
||||||
let connectionTimestamp;
|
let connectionTimestamp;
|
||||||
let liveDataRefreshTimerId = false;
|
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
|
// 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.
|
// TODO: use event gattserverdisconnected for save and reboot and device removal.
|
||||||
|
|
||||||
|
@ -118,39 +116,32 @@ function connectDisconnect() {
|
||||||
PortHandler.portPickerDisabled = true;
|
PortHandler.portPickerDisabled = true;
|
||||||
$("div.connection_button__label").text(i18n.getMessage("connecting"));
|
$("div.connection_button__label").text(i18n.getMessage("connecting"));
|
||||||
|
|
||||||
|
// Set configuration flags for consistency with other code
|
||||||
CONFIGURATOR.virtualMode = selectedPort === "virtual";
|
CONFIGURATOR.virtualMode = selectedPort === "virtual";
|
||||||
CONFIGURATOR.bluetoothMode = selectedPort.startsWith("bluetooth");
|
CONFIGURATOR.bluetoothMode = selectedPort.startsWith("bluetooth");
|
||||||
CONFIGURATOR.manualMode = selectedPort === "manual";
|
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) {
|
if (CONFIGURATOR.virtualMode) {
|
||||||
CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion;
|
CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion;
|
||||||
|
// Virtual mode uses a callback instead of port path
|
||||||
// Hack to get virtual working on the web
|
|
||||||
serial = serialShim();
|
|
||||||
serial.connect(onOpenVirtual);
|
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 {
|
} else {
|
||||||
CONFIGURATOR.virtualMode = false;
|
// Set up event listeners for all non-virtual connections
|
||||||
serial = serialShim();
|
|
||||||
// Explicitly disconnect the event listeners before attaching the new ones.
|
|
||||||
serial.removeEventListener("connect", connectHandler);
|
serial.removeEventListener("connect", connectHandler);
|
||||||
serial.addEventListener("connect", connectHandler);
|
serial.addEventListener("connect", connectHandler);
|
||||||
|
|
||||||
serial.removeEventListener("disconnect", disconnectHandler);
|
serial.removeEventListener("disconnect", disconnectHandler);
|
||||||
serial.addEventListener("disconnect", disconnectHandler);
|
serial.addEventListener("disconnect", disconnectHandler);
|
||||||
|
|
||||||
|
// All non-virtual modes pass the port path and options
|
||||||
serial.connect(portName, { baudRate });
|
serial.connect(portName, { baudRate });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// If connected, start disconnection sequence
|
||||||
GUI.timeout_kill_all();
|
GUI.timeout_kill_all();
|
||||||
GUI.interval_kill_all();
|
GUI.interval_kill_all();
|
||||||
GUI.tab_switch_cleanup(() => (GUI.tab_switch_in_progress = false));
|
GUI.tab_switch_cleanup(() => (GUI.tab_switch_in_progress = false));
|
||||||
|
@ -165,7 +156,11 @@ function connectDisconnect() {
|
||||||
// show CLI panel on Control+I
|
// show CLI panel on Control+I
|
||||||
document.onkeydown = function (e) {
|
document.onkeydown = function (e) {
|
||||||
if (e.code === "KeyI" && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
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();
|
GUI.showCliPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +179,6 @@ function finishClose(finishedCallback) {
|
||||||
$("#dialogResetToCustomDefaults")[0].close();
|
$("#dialogResetToCustomDefaults")[0].close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialShim calls the disconnect method for selected connection type.
|
|
||||||
serial.disconnect(onClosed);
|
serial.disconnect(onClosed);
|
||||||
|
|
||||||
MSP.disconnect_cleanup();
|
MSP.disconnect_cleanup();
|
||||||
|
@ -249,8 +243,6 @@ function resetConnection() {
|
||||||
|
|
||||||
// unlock port select & baud
|
// unlock port select & baud
|
||||||
PortHandler.portPickerDisabled = false;
|
PortHandler.portPickerDisabled = false;
|
||||||
// reset data
|
|
||||||
isConnected = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function abortConnection() {
|
function abortConnection() {
|
||||||
|
@ -271,7 +263,7 @@ function abortConnection() {
|
||||||
* when serial events are handled.
|
* when serial events are handled.
|
||||||
*/
|
*/
|
||||||
function read_serial_adapter(event) {
|
function read_serial_adapter(event) {
|
||||||
read_serial(event.detail.buffer);
|
read_serial(event.detail.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen(openInfo) {
|
function onOpen(openInfo) {
|
||||||
|
@ -290,7 +282,7 @@ function onOpen(openInfo) {
|
||||||
const result = getConfig("expertMode")?.expertMode ?? false;
|
const result = getConfig("expertMode")?.expertMode ?? false;
|
||||||
$('input[name="expertModeCheckbox"]').prop("checked", result).trigger("change");
|
$('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.removeEventListener("receive", read_serial_adapter);
|
||||||
serial.addEventListener("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 { gui_log } from "../gui_log";
|
||||||
import jBox from "jbox";
|
import jBox from "jbox";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import { serialShim } from "../serial_shim";
|
import { serial } from "../serial";
|
||||||
import FileSystem from "../FileSystem";
|
import FileSystem from "../FileSystem";
|
||||||
import { ispConnected } from "../utils/connection";
|
import { ispConnected } from "../utils/connection";
|
||||||
|
|
||||||
const serial = serialShim();
|
|
||||||
|
|
||||||
const cli = {
|
const cli = {
|
||||||
lineDelayMs: 5,
|
lineDelayMs: 5,
|
||||||
profileSwitchDelayMs: 100,
|
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 { generateFilename } from "./generate_filename";
|
||||||
import { gui_log } from "../gui_log";
|
import { gui_log } from "../gui_log";
|
||||||
import { i18n } from "../localization";
|
import { i18n } from "../localization";
|
||||||
import serial from "../webSerial";
|
import { serial } from "../serial";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -62,7 +62,7 @@ class AutoBackup {
|
||||||
}
|
}
|
||||||
|
|
||||||
readSerialAdapter(info) {
|
readSerialAdapter(info) {
|
||||||
const data = new Uint8Array(info.detail.buffer);
|
const data = new Uint8Array(info.detail.data);
|
||||||
|
|
||||||
for (const charCode of data) {
|
for (const charCode of data) {
|
||||||
const currentChar = String.fromCharCode(charCode);
|
const currentChar = String.fromCharCode(charCode);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import MSP from "../msp";
|
||||||
import MSPCodes from "../msp/MSPCodes";
|
import MSPCodes from "../msp/MSPCodes";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { API_VERSION_1_45, API_VERSION_1_46 } from "../data_storage";
|
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;
|
let mspHelper = null;
|
||||||
|
|
||||||
function readSerialAdapter(event) {
|
function readSerialAdapter(event) {
|
||||||
MSP.read(event.detail.buffer);
|
MSP.read(event.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoDetect {
|
class AutoDetect {
|
||||||
|
@ -109,7 +109,7 @@ class AutoDetect {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
serial.disconnect(this.onClosed);
|
serial.disconnect(this.onClosed.bind(this));
|
||||||
MSP.disconnect_cleanup();
|
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 CONFIGURATOR from "../../js/data_storage";
|
||||||
import { reinitializeConnection } from "../../js/serial_backend";
|
import { reinitializeConnection } from "../../js/serial_backend";
|
||||||
import { gui_log } from "../../js/gui_log";
|
import { gui_log } from "../../js/gui_log";
|
||||||
import { serialShim } from "../../js/serial_shim";
|
import { serial } from "../../js/serial";
|
||||||
|
|
||||||
const serial = serialShim();
|
|
||||||
|
|
||||||
export default class CliEngine {
|
export default class CliEngine {
|
||||||
constructor(currentTab) {
|
constructor(currentTab) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue