1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-18 22:05:13 +03:00

Serial refactor part 2 (#4404)

* Serial refactor part 2

* Fix callback

* Fix some sonar complaints

* Does no longer block dfu

* Excempt presets

* Add interval for reboot dialog

* Update message after review Vitroid
This commit is contained in:
Mark Haslinghuis 2025-03-31 22:54:06 +02:00 committed by GitHub
parent c842b9d60b
commit 2d36e62474
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 402 additions and 135 deletions

View file

@ -320,6 +320,12 @@
"configuratorUpdateWebsite": {
"message": "Go to Release Website"
},
"rebootFlightController": {
"message": "Rebooting flight controller, reconnect when ready"
},
"rebootFlightControllerReady": {
"message": "Flight Controller is ready"
},
"deviceRebooting": {
"message": "Device - <span class=\"message-negative\">Rebooting</span>"
},

View file

@ -2,7 +2,6 @@ import { get as getConfig } from "./ConfigStorage";
import { EventBus } from "../components/eventBus";
import { serial } from "./serial.js";
import WEBUSBDFU from "./protocols/webusbdfu";
import WebBluetooth from "./protocols/WebBluetooth.js";
import { reactive } from "vue";
const DEFAULT_PORT = "noselection";
@ -35,44 +34,42 @@ const PortHandler = new (function () {
})();
PortHandler.initialize = function () {
EventBus.$on("ports-input:request-permission-bluetooth", this.askBluetoothPermissionPort.bind(this));
EventBus.$on("ports-input:request-permission", this.askSerialPermissionPort.bind(this));
EventBus.$on("ports-input:request-permission-bluetooth", () => this.requestDevicePermission("bluetooth"));
EventBus.$on("ports-input:request-permission", () => this.requestDevicePermission("serial"));
EventBus.$on("ports-input:change", this.onChangeSelectedPort.bind(this));
// Use serial for all protocol events
serial.addEventListener("addedDevice", (event) => {
const detail = 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);
this.handleDeviceAdded(detail, "bluetooth");
} else {
this.addedSerialDevice(detail);
this.handleDeviceAdded(detail, "serial");
}
});
serial.addEventListener("removedDevice", (event) => {
const detail = event.detail;
// 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);
}
this.removedSerialDevice(event.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();
// Initial device discovery using the serial facade
this.refreshAllDeviceLists();
};
// Refactored refreshAllDeviceLists to use updateDeviceList
PortHandler.refreshAllDeviceLists = async function () {
// Update all device lists in parallel
return Promise.all([
this.updateDeviceList("serial"),
this.updateDeviceList("bluetooth"),
this.updateDeviceList("usb"),
]).then(() => {
this.selectActivePort();
});
};
PortHandler.setShowVirtualMode = function (showVirtualMode) {
@ -89,44 +86,40 @@ PortHandler.setShowAllSerialDevices = function (showAllSerialDevices) {
this.showAllSerialDevices = showAllSerialDevices;
};
PortHandler.addedSerialDevice = function (device) {
this.updateCurrentSerialPortsList().then(() => {
const selectedPort = this.selectActivePort(device);
if (!device || selectedPort === device.path) {
// Send this event when the port handler auto selects a new device
EventBus.$emit("port-handler:auto-select-serial-device", selectedPort);
}
});
};
PortHandler.removedSerialDevice = function (device) {
this.updateCurrentSerialPortsList().then(() => {
if (this.portPicker.selectedPort === device.path) {
this.selectActivePort();
}
});
};
console.log(`${this.logHead} Device removal event received:`, device);
PortHandler.addedBluetoothDevice = function (device) {
this.updateCurrentBluetoothPortsList().then(() => {
const selectedPort = this.selectActivePort(device);
if (!device || selectedPort === device.path) {
// Send this event when the port handler auto selects a new device
EventBus.$emit("port-handler:auto-select-bluetooth-device", selectedPort);
}
});
};
// Get device path safely
const devicePath = device?.path || (typeof device === "string" ? device : null);
PortHandler.removedBluetoothDevice = function (device) {
this.updateCurrentBluetoothPortsList().then(() => {
if (this.portPicker.selectedPort === device.path) {
if (!devicePath) {
console.warn(`${this.logHead} Device removal event missing path information`, device);
// Still update ports, but don't try to use the undefined path
this.updateDeviceList("serial").then(() => {
this.selectActivePort();
});
return;
}
// Update the appropriate ports list based on the device type
const updatePromise = devicePath.startsWith("bluetooth")
? this.updateDeviceList("bluetooth")
: this.updateDeviceList("serial");
const wasSelectedPort = this.portPicker.selectedPort === devicePath;
updatePromise.then(() => {
if (wasSelectedPort) {
this.selectActivePort();
// Send event for UI components that might need to update
EventBus.$emit("port-handler:device-removed", devicePath);
}
});
};
PortHandler.addedUsbDevice = function (device) {
this.updateCurrentUsbPortsList().then(() => {
this.updateDeviceList("usb").then(() => {
const selectedPort = this.selectActivePort(device);
if (!device || selectedPort === device.path) {
// Send this event when the port handler auto selects a new device
@ -139,30 +132,28 @@ PortHandler.onChangeSelectedPort = function (port) {
this.portPicker.selectedPort = port;
};
PortHandler.updateCurrentSerialPortsList = async function () {
const ports = await serial.getDevices();
const orderedPorts = this.sortPorts(ports);
this.portAvailable = orderedPorts.length > 0;
console.log(`${this.logHead} Found serial port`, orderedPorts);
this.currentSerialPorts = [...orderedPorts];
};
/**
* Request permission for a device of the specified type
* @param {string} deviceType - Type of device ('serial' or 'bluetooth')
*/
PortHandler.requestDevicePermission = function (deviceType = "serial") {
// Determine whether to show all devices based on device type
const showAllDevices = deviceType === "serial" ? this.showAllSerialDevices : false;
PortHandler.updateCurrentUsbPortsList = async function () {
const ports = await WEBUSBDFU.getDevices();
const orderedPorts = this.sortPorts(ports);
this.dfuAvailable = orderedPorts.length > 0;
console.log(`${this.logHead} Found DFU port`, orderedPorts);
this.currentUsbPorts = [...orderedPorts];
};
PortHandler.updateCurrentBluetoothPortsList = async function () {
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);
this.currentBluetoothPorts = [...orderedPorts];
}
// Use serial facade to request permission
serial
.requestPermissionDevice(showAllDevices, deviceType)
.then((port) => {
if (port) {
console.log(`${this.logHead} Permission granted for ${deviceType} device:`, port);
this.selectActivePort(port);
} else {
console.log(`${this.logHead} Permission request cancelled or failed for ${deviceType}`);
}
})
.catch((error) => {
console.error(`${this.logHead} Error requesting permission for ${deviceType}:`, error);
});
};
PortHandler.sortPorts = function (ports) {
@ -174,31 +165,11 @@ PortHandler.sortPorts = function (ports) {
});
};
PortHandler.askBluetoothPermissionPort = function () {
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
this.selectActivePort(port);
});
}
};
PortHandler.askSerialPermissionPort = function () {
serial.requestPermissionDevice(this.showAllSerialDevices).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
this.selectActivePort(port);
});
};
PortHandler.selectActivePort = function (suggestedDevice) {
PortHandler.selectActivePort = function (suggestedDevice = false) {
const deviceFilter = ["AT32", "CP210", "SPR", "STM"];
let selectedPort;
// Return the same that is connected to serial
// First check for active connections
if (serial.connected) {
selectedPort = this.currentSerialPorts.find((device) => device === serial.getConnectedPort());
}
@ -208,10 +179,22 @@ PortHandler.selectActivePort = function (suggestedDevice) {
selectedPort = this.currentUsbPorts.find((device) => device === WEBUSBDFU.getConnectedPort());
}
// Return the same that is connected to bluetooth
if (WebBluetooth.device) {
selectedPort = this.currentBluetoothPorts.find((device) => device === WebBluetooth.getConnectedPort());
}
// If there is a connection, return it
// if (selectedPort) {
// console.log(`${this.logHead} Using connected device: ${selectedPort.path}`);
// selectedPort = selectedPort.path;
// return selectedPort;
// }
// If there is no connection, check for the last used device
// Check if the device is already connected
// if (this.portPicker.selectedPort && this.portPicker.selectedPort !== DEFAULT_PORT) {
// selectedPort = this.currentSerialPorts.find((device) => device.path === this.portPicker.selectedPort);
// if (selectedPort) {
// console.log(`${this.logHead} Using previously selected device: ${selectedPort.path}`);
// return selectedPort.path;
// }
// }
// Return the suggested device (the new device that has been detected)
if (!selectedPort && suggestedDevice) {
@ -269,6 +252,81 @@ PortHandler.selectActivePort = function (suggestedDevice) {
return selectedPort;
};
// Create a unified handler for device addition
PortHandler.handleDeviceAdded = function (device, deviceType) {
if (!device) {
console.warn(`${this.logHead} Invalid ${deviceType} device added event`);
return;
}
console.log(`${this.logHead} ${deviceType} device added:`, device);
// Update the appropriate device list
const updatePromise =
deviceType === "bluetooth" ? this.updateDeviceList("bluetooth") : this.updateDeviceList("serial");
updatePromise.then(() => {
const selectedPort = this.selectActivePort(device);
if (selectedPort === device.path) {
// Emit an event with the proper type for backward compatibility
EventBus.$emit(`port-handler:auto-select-${deviceType}-device`, selectedPort);
}
});
};
/**
* Update device list with common implementation
* @param {string} deviceType - Type of device ('serial', 'bluetooth', 'usb')
* @returns {Promise} - Promise that resolves after updating the ports list
*/
PortHandler.updateDeviceList = async function (deviceType) {
let ports = [];
try {
switch (deviceType) {
case "bluetooth":
ports = await serial.getDevices("bluetooth");
break;
case "usb":
ports = await WEBUSBDFU.getDevices();
break;
case "serial":
default:
ports = await serial.getDevices("serial");
break;
}
// Sort the ports
const orderedPorts = this.sortPorts(ports);
// Update the appropriate properties based on device type
switch (deviceType) {
case "bluetooth":
this.bluetoothAvailable = orderedPorts.length > 0;
this.currentBluetoothPorts = [...orderedPorts];
console.log(`${this.logHead} Found bluetooth port(s)`, orderedPorts);
break;
case "usb":
this.dfuAvailable = orderedPorts.length > 0;
this.currentUsbPorts = [...orderedPorts];
console.log(`${this.logHead} Found DFU port(s)`, orderedPorts);
break;
case "serial":
default:
this.portAvailable = orderedPorts.length > 0;
this.currentSerialPorts = [...orderedPorts];
console.log(`${this.logHead} Found serial port(s)`, orderedPorts);
break;
}
return orderedPorts;
} catch (error) {
console.error(`${this.logHead} Error updating ${deviceType} devices:`, error);
return [];
}
};
// We need to explicit make it reactive. If not, Vue3 does not detect correctly changes in array properties
// like currentSerialPorts, currentUsbPorts, currentBluetoothPorts
export default reactive(PortHandler);

View file

@ -272,7 +272,7 @@ class WebSerial extends EventTarget {
if (this.reader) {
await this.reader.cancel();
this.reader.releaseLock();
await this.reader.releaseLock();
this.reader = null;
}

View file

@ -22,6 +22,14 @@ class Serial extends EventTarget {
this._websocket = new Websocket();
this._virtual = new VirtualSerial();
// Update protocol map to use consistent naming
this._protocolMap = {
serial: this._webSerial, // TODO: should be 'webserial'
bluetooth: this._bluetooth, // TODO: should be 'webbluetooth'
websocket: this._websocket,
virtual: this._virtual,
};
// Initialize with default protocol
this.selectProtocol(false);
@ -29,6 +37,42 @@ class Serial extends EventTarget {
this._setupEventForwarding();
}
// Add a getter method to safely access the protocol map
_getProtocolByType(type) {
if (!type) {
return this._protocol;
}
const protocol = this._protocolMap[type.toLowerCase()];
if (!protocol) {
console.warn(`${this.logHead} Unknown protocol type: ${type}`);
}
return protocol || null;
}
/**
* Get the protocol type as a string
* @param {Object} protocol - Protocol instance
* @returns {string} - Protocol type name
*/
_getProtocolType(protocol) {
if (protocol === this._webSerial) {
return "webserial";
}
if (protocol === this._bluetooth) {
return "webbluetooth";
}
if (protocol === this._websocket) {
return "websocket";
}
if (protocol === this._virtual) {
return "virtual";
}
return "unknown";
}
/**
* Set up event forwarding from all protocols to the Serial class
*/
@ -41,27 +85,20 @@ class Serial extends EventTarget {
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
if (event.type === "receive") {
// For 'receive' events, we need to handle the data differently
newDetail = {
data: event.detail,
protocolType:
protocol === this._webSerial
? "webSerial"
: protocol === this._bluetooth
? "bluetooth"
: protocol === this._websocket
? "websocket"
: protocol === this._virtual
? "virtual"
: "unknown",
protocolType: this._getProtocolType(protocol),
};
} else {
// For all other events, pass through the detail as is
newDetail = event.detail;
// For other events, we can use the detail directly
newDetail = {
...event.detail,
protocolType: this._getProtocolType(protocol),
};
}
// Dispatch the event with the new detail
this.dispatchEvent(
new CustomEvent(event.type, {
@ -102,14 +139,14 @@ class Serial extends EventTarget {
CONFIGURATOR.bluetoothMode = false;
CONFIGURATOR.manualMode = true;
} else if (portPath.startsWith("bluetooth")) {
console.log(`${this.logHead} Using bluetooth protocol (based on port path)`);
console.log(`${this.logHead} Using bluetooth protocol (based on port path: ${portPath})`);
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)`);
console.log(`${this.logHead} Using web serial protocol (based on port path: ${portPath})`);
newProtocol = this._webSerial;
// Update CONFIGURATOR flags for consistency
CONFIGURATOR.virtualMode = false;
@ -258,17 +295,58 @@ class Serial extends EventTarget {
}
/**
* Get devices from the current protocol
* Get devices from a specific protocol type or current protocol
* @param {string} protocolType - Optional protocol type ('serial', 'bluetooth', 'websocket', 'virtual')
* @returns {Promise<Array>} - List of devices
*/
async getDevices() {
return this._protocol?.getDevices() || [];
async getDevices(protocolType = null) {
try {
// Get the appropriate protocol
const targetProtocol = this._getProtocolByType(protocolType);
if (!targetProtocol) {
console.warn(`${this.logHead} No valid protocol for getting devices`);
return [];
}
if (typeof targetProtocol.getDevices !== "function") {
console.error(`${this.logHead} Selected protocol does not implement getDevices`);
return [];
}
return targetProtocol.getDevices() || [];
} catch (error) {
console.error(`${this.logHead} Error getting devices:`, error);
return [];
}
}
/**
* Request permission for a device
* Request permission to access a device
* @param {boolean} showAllDevices - Whether to show all devices or only those with filters
* @param {string} protocolType - Optional protocol type ('serial', 'bluetooth', etc.)
* @returns {Promise<Object>} - Promise resolving to the selected device
*/
async requestPermissionDevice(showAllSerialDevices = false) {
return this._protocol?.requestPermissionDevice(showAllSerialDevices) || null;
async requestPermissionDevice(showAllDevices = false, protocolType = null) {
try {
// Get the appropriate protocol
const targetProtocol = this._getProtocolByType(protocolType);
if (!targetProtocol) {
console.warn(`${this.logHead} No valid protocol for permission request`);
return null;
}
if (typeof targetProtocol.requestPermissionDevice !== "function") {
console.error(`${this.logHead} Selected protocol does not support permission requests`);
return null;
}
return targetProtocol.requestPermissionDevice(showAllDevices);
} catch (error) {
console.error(`${this.logHead} Error requesting device permission:`, error);
return null;
}
}
/**

View file

@ -810,12 +810,16 @@ function startLiveDataRefreshTimer() {
export function reinitializeConnection(callback) {
// In virtual mode reconnect when autoconnect is enabled
if (CONFIGURATOR.virtualMode && PortHandler.portPicker.autoConnect) {
return setTimeout(function () {
$("a.connection_button__link").trigger("click");
}, 500);
if (CONFIGURATOR.virtualMode) {
connectDisconnect();
if (PortHandler.portPicker.autoConnect) {
return setTimeout(function () {
$("a.connection_button__link").trigger("click");
}, 500);
}
}
// Send reboot command to the flight controller
rebootTimestamp = Date.now();
MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false);
@ -824,14 +828,131 @@ export function reinitializeConnection(callback) {
connectDisconnect();
}
// Show reboot progress modal except for presets tab
if (GUI.active_tab === "presets") {
console.log("Rebooting in presets tab, skipping reboot dialog", GUI.active_tab);
gui_log(i18n.getMessage("deviceRebooting"));
gui_log(i18n.getMessage("deviceReady"));
if (callback && typeof callback === "function") {
callback();
}
return;
}
// Show reboot progress modal
showRebootDialog(callback);
}
function showRebootDialog(callback) {
gui_log(i18n.getMessage("deviceRebooting"));
// wait for the device to reboot
setTimeout(function () {
gui_log(i18n.getMessage("deviceReady"));
}, 2000);
// Show reboot progress modal
const rebootDialog = document.getElementById("rebootProgressDialog") || createRebootProgressDialog();
rebootDialog.querySelector(".reboot-progress-bar").style.width = "0%";
rebootDialog.querySelector(".reboot-status").textContent = i18n.getMessage("rebootFlightController");
rebootDialog.showModal();
if (callback && typeof callback === "function") {
callback();
// Update progress during reboot
let progress = 0;
const progressInterval = setInterval(() => {
progress += 5;
if (progress <= 100) {
rebootDialog.querySelector(".reboot-progress-bar").style.width = `${progress}%`;
}
}, 100);
// Check for successful connection every 100ms
const connectionCheckInterval = setInterval(() => {
if (CONFIGURATOR.connectionValid) {
// Connection established, device is ready
clearInterval(connectionCheckInterval);
clearInterval(progressInterval);
rebootDialog.querySelector(".reboot-progress-bar").style.width = "100%";
rebootDialog.querySelector(".reboot-status").textContent = i18n.getMessage("rebootFlightControllerReady");
// Close the dialog after showing "ready" message briefly
setTimeout(() => {
rebootDialog.close();
}, 1000);
gui_log(i18n.getMessage("deviceReady"));
if (callback && typeof callback === "function") {
callback();
}
}
}, 100);
// Set a maximum timeout for the reboot process (5 seconds)
setTimeout(function () {
clearInterval(connectionCheckInterval);
clearInterval(progressInterval);
rebootDialog.querySelector(".reboot-progress-bar").style.width = "100%";
rebootDialog.querySelector(".reboot-status").textContent = i18n.getMessage("rebootFlightControllerReady");
// Close the dialog after showing "ready" message briefly
setTimeout(() => {
rebootDialog.close();
}, 1000);
gui_log(i18n.getMessage("deviceReady"));
if (callback && typeof callback === "function") {
callback();
}
}, 5000);
// Helper function to create the reboot dialog if it doesn't exist
function createRebootProgressDialog() {
const dialog = document.createElement("dialog");
dialog.id = "rebootProgressDialog";
dialog.className = "dialogReboot";
dialog.innerHTML = `
<div class="content">
<div class="reboot-status">${i18n.getMessage("rebootFlightController")}</div>
<div class="reboot-progress-container">
<div class="reboot-progress-bar"></div>
</div>
</div>
`;
document.body.appendChild(dialog);
// Add styles if not already defined
if (!document.getElementById("rebootProgressStyle")) {
const style = document.createElement("style");
style.id = "rebootProgressStyle";
style.textContent = `
.dialogReboot {
border: 1px solid #3f4241;
border-radius: 5px;
background-color: #2d3233;
color: #fff;
padding: 20px;
max-width: 400px;
}
.reboot-progress-container {
width: 100%;
background-color: #424546;
border-radius: 3px;
margin: 15px 0 5px;
height: 10px;
}
.reboot-progress-bar {
height: 100%;
background-color: #ffbb00;
border-radius: 3px;
transition: width 0.1s ease-in-out;
width: 0%;
}
.reboot-status {
text-align: center;
margin: 10px 0;
}
`;
document.head.appendChild(style);
}
return dialog;
}
}

View file

@ -19,6 +19,7 @@ import AutoBackup from "../utils/AutoBackup.js";
import AutoDetect from "../utils/AutoDetect.js";
import { EventBus } from "../../components/eventBus";
import { ispConnected } from "../utils/connection.js";
import FC from "../fc";
const firmware_flasher = {
targets: null,
@ -255,6 +256,9 @@ firmware_flasher.initialize = function (callback) {
}
// extract osd protocols from general options and add to osdProtocols
console.log(`${self.logHead} buildOptions`, FC.CONFIG.buildOptions);
self.cloudBuildOptions = FC.CONFIG.buildOptions || [];
console.log(`${self.logHead} generalOptions`, self.cloudBuildOptions);
data.osdProtocols = data.generalOptions
.filter((option) => option.group === "OSD")
.map((option) => {