1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-25 17:25:16 +03:00

Refactor port handler (#3984)

* Refactor port handler and fix reconnect

* Fix as per review

* Don't auto-connect for virtual or manual

* Fix auto-connect switch state

* Move auto-connect title to the parent div

The checkbox is "hidden" under the switchary
library, so move to the parent to be able to show
it.

* Select active port when request permission port exists before

* Fix retun value for webserial requestPemission
This commit is contained in:
Míguel Ángel Mulero Martínez 2024-05-30 20:10:09 +02:00 committed by GitHub
parent a6e3761c26
commit ff83600a43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 228 additions and 386 deletions

View file

@ -44,13 +44,15 @@
</select>
</div>
<div id="auto-connect-and-baud">
<div id="auto-connect-switch">
<div
id="auto-connect-switch"
:title="value.autoConnect ? $t('autoConnectEnabled') : $t('autoConnectDisabled')"
>
<input
id="auto-connect"
class="auto_connect togglesmall"
type="checkbox"
:value="value.autoConnect"
:title="value.autoConnect ? $t('autoConnectEnabled') : $t('autoConnectDisabled')"
:checked="value.autoConnect"
@change="onChangeAutoConnect"
>
<span class="auto_connect">

View file

@ -14,7 +14,6 @@ const DEFAULT_BAUDS = 115200;
const PortHandler = new function () {
this.currentPorts = [];
this.initialPorts = false;
this.portPicker = {
selectedPort: DEFAULT_PORT,
selectedBauds: DEFAULT_BAUDS,
@ -23,24 +22,29 @@ const PortHandler = new function () {
autoConnect: getConfig('autoConnect').autoConnect,
};
this.portPickerDisabled = false;
this.port_detected_callbacks = [];
this.port_removed_callbacks = [];
this.dfu_available = false;
this.port_available = false;
this.dfuAvailable = false;
this.portAvailable = false;
this.showAllSerialDevices = false;
this.showVirtualMode = getConfig('showVirtualMode').showVirtualMode;
this.showManualMode = getConfig('showManualMode').showManualMode;
this.showAllSerialDevices = getConfig('showAllSerialDevices').showAllSerialDevices;
};
PortHandler.initialize = function () {
EventBus.$on('ports-input:request-permission', this.askPermissionPort.bind(this));
EventBus.$on('ports-input:request-permission', this.askSerialPermissionPort.bind(this));
EventBus.$on('ports-input:change', this.onChangeSelectedPort.bind(this));
serial.addEventListener("addedDevice", this.check_serial_devices.bind(this));
serial.addEventListener("removedDevice", this.check_serial_devices.bind(this));
serial.addEventListener("addedDevice", (event) => this.addedSerialDevice(event.detail));
serial.addEventListener("removedDevice", (event) => this.removedSerialDevice(event.detail));
this.reinitialize(); // just to prevent code redundancy
if (!this.portAvailable) {
this.check_usb_devices();
}
if (!this.dfuAvailable) {
this.addedSerialDevice();
}
};
PortHandler.setShowVirtualMode = function (showVirtualMode) {
@ -53,57 +57,103 @@ PortHandler.setShowManualMode = function (showManualMode) {
this.selectActivePort();
};
PortHandler.reinitialize = function () {
this.initialPorts = false;
this.showAllSerialDevices = getConfig('showAllSerialDevices').showAllSerialDevices;
this.check(); // start listening, check after TIMEOUT_CHECK ms
PortHandler.addedSerialDevice = function (device) {
this.updateCurrentPortsList()
.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-device', selectedPort);
}
});
};
PortHandler.check = function () {
const self = this;
if (!self.port_available) {
self.check_usb_devices();
PortHandler.removedSerialDevice = function (device) {
this.updateCurrentPortsList()
.then(() => {
if (this.portPicker.selectedPort === device.path) {
this.selectActivePort();
}
if (!self.dfu_available) {
self.check_serial_devices();
}
};
PortHandler.check_serial_devices = function () {
const self = this;
const updatePorts = function(cp) {
self.currentPorts = cp;
// auto-select port (only during initialization)
if (!self.initialPorts) {
self.updatePortSelect(self.currentPorts);
self.selectActivePort();
self.initialPorts = {...self.currentPorts};
GUI.updateManualPortVisibility();
self.detectPort();
} else {
self.removePort();
self.detectPort();
// already done in detectPort
// self.selectActivePort();
}
};
serial.getDevices().then(updatePorts);
});
};
PortHandler.onChangeSelectedPort = function(port) {
this.portPicker.selectedPort = port;
};
PortHandler.updateCurrentPortsList = function () {
return serial.getDevices()
.then((ports) => {
ports = this.sortPorts(ports);
this.currentPorts = ports;
});
};
PortHandler.sortPorts = function(ports) {
return ports.sort(function(a, b) {
return a.path.localeCompare(b.path, window.navigator.language, {
numeric: true,
sensitivity: 'base',
});
});
};
PortHandler.askSerialPermissionPort = function() {
serial.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.selectActivePort = function(suggestedDevice) {
// Return the same that is connected
if (serial.connected) {
return serial.getConnectedPort();
}
let selectedPort;
const deviceFilter = ['AT32', 'CP210', 'SPR', 'STM'];
if (suggestedDevice) {
selectedPort = suggestedDevice.path;
this.portAvailable = true;
} else {
for (let port of this.currentPorts) {
const portName = port.displayName;
const pathSelect = port.path;
const deviceRecognized = deviceFilter.some(device => portName.includes(device));
const legacyDeviceRecognized = portName.includes('usb');
if (deviceRecognized || legacyDeviceRecognized) {
selectedPort = pathSelect;
this.portAvailable = true;
console.log(`Porthandler detected device ${portName} on port: ${pathSelect}`);
break;
}
}
if (!selectedPort) {
this.portAvailable = false;
if (this.showVirtualMode) {
selectedPort = "virtual";
} else if (this.showManualMode) {
selectedPort = "manual";
}
}
}
this.portPicker.selectedPort = selectedPort || DEFAULT_PORT;
console.log(`Porthandler default device is '${this.portPicker.selectedPort}'`);
return selectedPort;
};
/************************************
// TODO all the methods from here need to be refactored or removed
************************************/
PortHandler.check_usb_devices = function (callback) {
// TODO needs USB code refactor for web
@ -144,243 +194,34 @@ PortHandler.check_usb_devices = function (callback) {
self.portPickerElement.val('DFU').trigger('change');
self.setPortsInputWidth();
self.dfu_available = true;
self.dfuAvailable = true;
}
} else if (dfuElement.length) {
dfuElement.remove();
self.setPortsInputWidth();
self.dfu_available = false;
self.dfuAvailable = false;
if ($('option:selected', self.portPickerElement).val() !== 'DFU') {
if (!(GUI.connected_to || GUI.connect_lock)) {
FC.resetState();
}
if (self.dfu_available) {
if (self.dfuAvailable) {
self.portPickerElement.trigger('change');
}
}
}
if (callback) {
callback(self.dfu_available);
callback(self.dfuAvailable);
}
});
};
PortHandler.removePort = function() {
const self = this;
const removePorts = self.array_difference(self.initialPorts, self.currentPorts);
if (removePorts.length) {
console.log(`PortHandler - Removed: ${JSON.stringify(removePorts)}`);
self.port_available = false;
// disconnect "UI" - routine can't fire during atmega32u4 reboot procedure !!!
if (removePorts.some(port => port.path === GUI.connected_to)) {
$('div.connect_controls a.connect').click();
$('div.connect_controls a.connect.active').click();
}
// trigger callbacks (only after initialization)
for (let i = (self.port_removed_callbacks.length - 1); i >= 0; i--) {
const obj = self.port_removed_callbacks[i];
// remove timeout
clearTimeout(obj.timer);
// trigger callback
obj.code(removePorts);
// remove object from array
const index = self.port_removed_callbacks.indexOf(obj);
if (index > -1) {
self.port_removed_callbacks.splice(index, 1);
}
}
for (const port of removePorts) {
self.initialPorts.splice(self.initialPorts.indexOf(port, 1));
}
self.updatePortSelect(self.initialPorts);
}
};
PortHandler.detectPort = function() {
const self = this;
const newPorts = self.array_difference(self.currentPorts, self.initialPorts);
if (newPorts.length) {
self.updatePortSelect(self.currentPorts);
console.log(`PortHandler - Found: ${JSON.stringify(newPorts)}`);
if (newPorts.length === 1) {
this.portPicker.selectedPort = newPorts[0].path;
} else {
self.selectActivePort();
}
self.port_available = true;
// auto-connect if enabled
if (this.portPicker.autoConnect && !GUI.connecting_to && !GUI.connected_to && GUI.active_tab !== 'firmware_flasher') {
// start connect procedure. We need firmware flasher protection over here
$('div.connect_controls a.connect').click();
}
// trigger callbacks
for (let i = (self.port_detected_callbacks.length - 1); i >= 0; i--) {
const obj = self.port_detected_callbacks[i];
// remove timeout
clearTimeout(obj.timer);
// trigger callback
obj.code(newPorts);
// remove object from array
const index = self.port_detected_callbacks.indexOf(obj);
if (index > -1) {
self.port_detected_callbacks.splice(index, 1);
}
}
self.initialPorts = self.currentPorts;
}
};
PortHandler.sortPorts = function(ports) {
return ports.sort(function(a, b) {
return a.path.localeCompare(b.path, window.navigator.language, {
numeric: true,
sensitivity: 'base',
});
});
};
PortHandler.updatePortSelect = function (ports) {
ports = this.sortPorts(ports);
this.currentPorts = ports;
};
PortHandler.askPermissionPort = function() {
serial.requestPermissionDevice().then(() => {
this.check_serial_devices();
}).catch(() => {
// In the catch we call the check_serial_devices too to change the request permission option from the select for other
this.check_serial_devices();
});
};
PortHandler.selectActivePort = function() {
let selectedPort;
const deviceFilter = ['AT32', 'CP210', 'SPR', 'STM'];
for (let port of this.currentPorts) {
const portName = port.displayName;
if (portName) {
const pathSelect = port.path;
const deviceRecognized = deviceFilter.some(device => portName.includes(device));
const legacyDeviceRecognized = portName.includes('usb');
if (deviceRecognized || legacyDeviceRecognized) {
selectedPort = pathSelect;
this.port_available = true;
console.log(`Porthandler detected device ${portName} on port: ${pathSelect}`);
}
}
}
if (!selectedPort) {
if (this.showVirtualMode) {
selectedPort = "virtual";
} else if (this.showManualMode) {
selectedPort = "manual";
}
}
this.portPicker.selectedPort = selectedPort || DEFAULT_PORT;
console.log(`Porthandler default device is '${this.portPicker.selectedPort}'`);
};
PortHandler.port_detected = function(name, code, timeout, ignore_timeout) {
const self = this;
const obj = {'name': name,
'code': code,
'timeout': (timeout) ? timeout : 10000,
};
if (!ignore_timeout) {
obj.timer = setTimeout(function() {
console.log(`PortHandler - timeout - ${obj.name}`);
// trigger callback
code(false);
// remove object from array
const index = self.port_detected_callbacks.indexOf(obj);
if (index > -1) {
self.port_detected_callbacks.splice(index, 1);
}
}, (timeout) ? timeout : 10000);
} else {
obj.timer = false;
obj.timeout = false;
}
this.port_detected_callbacks.push(obj);
return obj;
};
PortHandler.port_removed = function (name, code, timeout, ignore_timeout) {
const self = this;
const obj = {'name': name,
'code': code,
'timeout': (timeout) ? timeout : 10000,
};
if (!ignore_timeout) {
obj.timer = setTimeout(function () {
console.log(`PortHandler - timeout - ${obj.name}`);
// trigger callback
code(false);
// remove object from array
const index = self.port_removed_callbacks.indexOf(obj);
if (index > -1) {
self.port_removed_callbacks.splice(index, 1);
}
}, (timeout) ? timeout : 10000);
} else {
obj.timer = false;
obj.timeout = false;
}
this.port_removed_callbacks.push(obj);
return obj;
};
// accepting single level array with "value" as key
PortHandler.array_difference = function (firstArray, secondArray) {
const cloneArray = [];
// create hardcopy
for (let i = 0; i < firstArray.length; i++) {
cloneArray.push(firstArray[i]);
}
for (let i = 0; i < secondArray.length; i++) {
const elementExists = cloneArray.findIndex(element => element.path === secondArray[i].path);
if (elementExists !== -1) {
cloneArray.splice(elementExists, 1);
}
}
return cloneArray;
};
PortHandler.flush_callbacks = function () {
let killed = 0;
for (let i = this.port_detected_callbacks.length - 1; i >= 0; i--) {
for (let i = this.port_detected_callbacks?.length - 1; i >= 0; i--) {
if (this.port_detected_callbacks[i].timer) {
clearTimeout(this.port_detected_callbacks[i].timer);
}
@ -389,7 +230,7 @@ PortHandler.flush_callbacks = function () {
killed++;
}
for (let i = this.port_removed_callbacks.length - 1; i >= 0; i--) {
for (let i = this.port_removed_callbacks?.length - 1; i >= 0; i--) {
if (this.port_removed_callbacks[i].timer) {
clearTimeout(this.port_removed_callbacks[i].timer);
}
@ -401,6 +242,4 @@ PortHandler.flush_callbacks = function () {
return killed;
};
// temp workaround till everything is in modules
window.PortHandler = PortHandler;
export default PortHandler;

View file

@ -136,7 +136,7 @@ STM32_protocol.prototype.connect = function (port, baud, hex, options, callback)
// wait until board boots into bootloader mode
// MacOs may need 5 seconds delay
function waitForDfu() {
if (PortHandler.dfu_available) {
if (PortHandler.dfuAvailable) {
console.log(`DFU available after ${failedAttempts / 10} seconds`);
clearInterval(dfuWaitInterval);
startFlashing();

View file

@ -36,6 +36,9 @@ let liveDataRefreshTimerId = false;
let isConnected = false;
const REBOOT_CONNECT_MAX_TIME_MS = 10000;
let rebootTimestamp = 0;
const toggleStatus = function () {
isConnected = !isConnected;
};
@ -50,38 +53,55 @@ function disconnectHandler(event) {
}
export function initializeSerialBackend() {
GUI.updateManualPortVisibility = function() {
if(isWeb()) {
return;
}
const selected_port = $('#port').val();
$('#port-override-option').toggle(selected_port === 'manual');
$('#firmware-virtual-option').toggle(selected_port === 'virtual');
$('#auto-connect-and-baud').toggle(selected_port !== 'DFU');
};
GUI.updateManualPortVisibility();
// TODO move to Vue
$('#port-override').change(function () {
setConfig({'portOverride': $('#port-override').val()});
});
// TODO move to Vue
const data = getConfig('portOverride');
if (data.portOverride) {
$('#port-override').val(data.portOverride);
}
EventBus.$on('ports-input:change', () => GUI.updateManualPortVisibility());
$("div.connect_controls a.connect").on('click', connectDisconnect);
$("div.connect_controls a.connect").on('click', function () {
EventBus.$on('port-handler:auto-select-device', function(device) {
if (!GUI.connected_to && !GUI.connecting_to
&& ((PortHandler.portPicker.autoConnect && !["manual", "virtual"].includes(device))
|| Date.now() - rebootTimestamp < REBOOT_CONNECT_MAX_TIME_MS)) {
connectDisconnect();
}
});
serial.addEventListener("removedDevice", (event) => {
if (event.detail.path === GUI.connected_to) {
connectDisconnect();
}
});
$('div.open_firmware_flasher a.flash').click(function () {
if ($('div#flashbutton a.flash_state').hasClass('active') && $('div#flashbutton a.flash').hasClass('active')) {
$('div#flashbutton a.flash_state').removeClass('active');
$('div#flashbutton a.flash').removeClass('active');
$('#tabs ul.mode-disconnected .tab_landing a').click();
} else {
$('#tabs ul.mode-disconnected .tab_firmware_flasher a').click();
$('div#flashbutton a.flash_state').addClass('active');
$('div#flashbutton a.flash').addClass('active');
}
});
PortHandler.initialize();
PortUsage.initialize();
}
function connectDisconnect() {
const selectedPort = PortHandler.portPicker.selectedPort;
let portName;
if (selectedPort === 'manual') {
portName = $('#port-override').val();
portName = PortHandler.portPicker.portOverride;
} else {
portName = selectedPort;
}
@ -110,7 +130,7 @@ export function initializeSerialBackend() {
const baudRate = selected_baud;
if (selectedPort === 'virtual') {
CONFIGURATOR.virtualMode = true;
CONFIGURATOR.virtualApiVersion = $('#firmware-version-dropdown').val();
CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion;
// Hack to get virtual working on the web
serial = serialShim();
@ -144,36 +164,6 @@ export function initializeSerialBackend() {
mspHelper?.setArmingEnabled(true, false, onFinishCallback);
}
}
});
$('div.open_firmware_flasher a.flash').click(function () {
if ($('div#flashbutton a.flash_state').hasClass('active') && $('div#flashbutton a.flash').hasClass('active')) {
$('div#flashbutton a.flash_state').removeClass('active');
$('div#flashbutton a.flash').removeClass('active');
$('#tabs ul.mode-disconnected .tab_landing a').click();
} else {
$('#tabs ul.mode-disconnected .tab_firmware_flasher a').click();
$('div#flashbutton a.flash_state').addClass('active');
$('div#flashbutton a.flash').addClass('active');
}
});
// auto-connect
const result = PortHandler.portPicker.autoConnect;
if (result === undefined || result) {
$('input.auto_connect').prop('checked', true);
$('input.auto_connect, span.auto_connect').prop('title', i18n.getMessage('autoConnectEnabled'));
$('select#baud').val(115200).prop('disabled', true);
} else {
$('input.auto_connect').prop('checked', false);
$('input.auto_connect, span.auto_connect').prop('title', i18n.getMessage('autoConnectDisabled'));
}
PortHandler.initialize();
PortUsage.initialize();
}
function finishClose(finishedCallback) {
@ -228,7 +218,7 @@ function setConnectionTimeout() {
if (!CONFIGURATOR.connectionValid) {
gui_log(i18n.getMessage('noConfigurationReceived'));
$('div.connect_controls a.connect').click(); // disconnect
connectDisconnect();
}
}, 10000);
}
@ -396,7 +386,7 @@ function processCustomDefaults() {
dialog.close();
GUI.timeout_add('disconnect', function () {
$('div.connect_controls a.connect').click(); // disconnect
connectDisconnect(); // disconnect
}, 0);
});
@ -469,7 +459,7 @@ function checkReportProblems() {
abort = true;
GUI.timeout_remove('connecting'); // kill connecting timer
$('div.connect_controls a.connect').click(); // disconnect
connectDisconnect(); // disconnect
}
if (!abort) {
@ -792,6 +782,7 @@ export function reinitializeConnection(callback) {
}, 500);
}
rebootTimestamp = Date.now();
MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false);
gui_log(i18n.getMessage('deviceRebooting'));
@ -805,3 +796,4 @@ export function reinitializeConnection(callback) {
callback();
}
}

View file

@ -1112,6 +1112,7 @@ firmware_flasher.initialize = function (callback) {
if (status) {
const catch_new_port = function () {
// TODO modify by listen to a new event
PortHandler.port_detected('flash_detected_device', function (resultPort) {
const port = resultPort[0];
@ -1156,7 +1157,7 @@ firmware_flasher.initialize = function (callback) {
firmware_flasher.isSerialPortAvailable = function() {
return PortHandler.port_available && !GUI.connect_lock;
return PortHandler.portAvailable && !GUI.connect_lock;
};
firmware_flasher.updateDetectBoardButton = function() {
@ -1437,7 +1438,7 @@ firmware_flasher.backupConfig = function (callback) {
// Allow reboot after CLI exit
const waitOnReboot = () => {
const disconnect = setInterval(function() {
if (PortHandler.port_available) {
if (PortHandler.portAvailable) {
console.log(`Connection ready for flashing in ${count / 10} seconds`);
clearInterval(disconnect);
if (callback) {

View file

@ -30,7 +30,7 @@ class WebSerial extends EventTarget {
this.logHead = "SERIAL: ";
this.port_counter = 0;
this.portCounter = 0;
this.ports = [];
this.port = null;
this.reader = null;
@ -65,8 +65,7 @@ class WebSerial extends EventTarget {
}
handleDisconnect() {
this.removeEventListener('receive', this.handleReceiveBytes);
this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
this.disconnect();
}
getConnectedPort() {
@ -75,7 +74,7 @@ class WebSerial extends EventTarget {
createPort(port) {
return {
path: `D${this.port_counter++}`,
path: `serial_${this.portCounter++}`,
displayName: `Betaflight ${vendorIdNames[port.getInfo().usbVendorId]}`,
vendorId: port.getInfo().usbVendorId,
productId: port.getInfo().usbProductId,
@ -88,21 +87,27 @@ class WebSerial extends EventTarget {
filters: webSerialDevices,
});
this.port_counter = 1;
this.portCounter = 1;
this.ports = ports.map(function (port) {
return this.createPort(port);
}, this);
}
async requestPermissionDevice() {
const permissionPort = await navigator.serial.requestPort({
let newPermissionPort = null;
try {
const userSelectedPort = await navigator.serial.requestPort({
filters: webSerialDevices,
});
const found = this.ports.find(port => port.port === permissionPort);
if (!found) {
return this.handleNewDevice(permissionPort);
newPermissionPort = this.ports.find(port => port.port === userSelectedPort);
if (!newPermissionPort) {
newPermissionPort = this.handleNewDevice(userSelectedPort);
}
return null;
console.info("User selected device from permissions:", newPermissionPort.path);
} catch (error) {
console.error("User didn't select any device when requesting permission:", error);
}
return newPermissionPort;
}
async getDevices() {
@ -123,13 +128,14 @@ class WebSerial extends EventTarget {
if (connectionInfo && !this.openCanceled) {
this.connected = true;
this.connectionId = connectionInfo.connectionId;
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(
@ -186,6 +192,7 @@ class WebSerial extends EventTarget {
this.bytesSent = 0;
const doCleanup = async () => {
this.removeEventListener('receive', this.handleReceiveBytes);
if (this.reader) {
// this.reader.cancel();
this.reader.releaseLock();
@ -196,6 +203,7 @@ class WebSerial extends EventTarget {
this.writer = null;
}
if (this.port) {
this.port.removeEventListener("disconnect", this.handleDisconnect.bind(this));
await this.port.close();
this.port = null;
}
@ -235,7 +243,7 @@ class WebSerial extends EventTarget {
);
}
return {
bytesSent: this.bytesSent,
bytesSent: data.byteLength,
};
}
}