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

Make port_handler work with PWA (#3958)

* Make port_handler work with PWA

* Modify the port_handler more the Vue way

* Fixes after review

* Fix request permission option not being deselected

* Hide baud selection in port picker if virtual port

* Added port override option for manual

* Fix virtual port state when loading the page

* Fix request permission adds the same device several times

* Fix automatic selection of device under Linux
This commit is contained in:
Míguel Ángel Mulero Martínez 2024-05-17 10:24:44 +02:00 committed by GitHub
parent b91698d0f4
commit fce0c8305b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 246 additions and 166 deletions

View file

@ -62,6 +62,10 @@
"message": "Virtual Mode (Experimental)",
"description": "Configure a Virtual Flight Controller without the need of a physical FC."
},
"portsSelectPermission": {
"message": "--- I can't find my device ---",
"description": "Option in the port selection dropdown to allow the user to give permissions to the system to access the device."
},
"virtualMSPVersion": {
"message": "Virtual Firmware Version"
},

View file

@ -13,6 +13,7 @@ import StatusBar from "./status-bar/StatusBar.vue";
import BatteryIcon from "./quad-status/BatteryIcon.vue";
import FC from '../js/fc.js';
import MSP from '../js/msp.js';
import PortHandler from '../js/port_handler.js';
import PortUsage from '../js/port_usage.js';
import PortPicker from './port-picker/PortPicker.vue';
import CONFIGURATOR from '../js/data_storage.js';
@ -26,6 +27,7 @@ const betaflightModel = {
FC,
MSP,
PortUsage,
PortHandler,
};
i18next.on('initialized', function() {

View file

@ -1,13 +1,13 @@
<template>
<div
id="firmware-virtual-option"
:style="{ display: isVirtual ? 'block' : 'none' }"
>
<div class="dropdown dropdown-dark">
<select
id="firmware-version-dropdown"
class="dropdown-select"
:title="$t('virtualMSPVersion')"
v-model="value"
>
<option
v-for="(version, index) in firmwareVersions"
@ -24,9 +24,9 @@
<script>
export default {
props: {
isVirtual: {
type: Boolean,
default: true,
value: {
type: String,
default: "1.46.0",
},
},
data() {
@ -50,7 +50,7 @@ export default {
width: 180px;
margin-right: 15px;
margin-top: 0px;
display: none;
display: block;
}
.dropdown {
display: inline-block;

View file

@ -1,42 +1,43 @@
<template>
<div
id="port-override-option"
style="display: none"
:style="{ display: isManual ? 'flex' : 'none' }"
>
<div id="port-override-option">
<label
for="port-override"
><span>{{ $t("portOverrideText") }}</span>
<input
id="port-override"
type="text"
value="/dev/rfcomm0"
v-model="value"
></label>
</div>
</template>
<script>
export default {
props: {
isManual: {
type: Boolean,
default: true,
},
props: {
value: {
type: String,
default: "/dev/rfcomm0",
},
isManual: {
type: Boolean,
default: true,
},
},
};
</script>
<style scoped>
#port-override-option {
font-family: "Open Sans", "Segoe UI", Tahoma, sans-serif;
font-size: 12px;
margin-top: 16px;
margin-top: 4px;
margin-right: 15px;
label {
background-color: #2b2b2b;
border-radius: 3px;
padding: 3px;
color: var(--subtleAccent);
}
};
display: block;
}
#port-override-option label {

View file

@ -1,24 +1,50 @@
<template>
<div class="web-port-picker">
<FirmwareVirtualOption :is-virtual="port === 'virtual'" />
<PortsInput v-model="port" />
<PortOverrideOption
v-if="value.selectedPort === 'manual'"
v-model="value.portOverride"
/>
<FirmwareVirtualOption
v-if="value.selectedPort === 'virtual'"
v-model="value.virtualMspVersion"
/>
<PortsInput
v-model="value"
:connected-devices="connectedDevices"
:disabled="disabled" />
</div>
</template>
<script>
import PortOverrideOption from "./PortOverrideOption.vue";
import FirmwareVirtualOption from "./FirmwareVirtualOption.vue";
import PortsInput from "./PortsInput.vue";
export default {
props: {
value: {
type: Object,
default: {
selectedPort: "manual",
selectedBaud: 115200,
portOverride: "/dev/rfcomm0",
virtualMspVersion: "1.46.0",
},
},
connectedDevices: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
},
components: {
PortOverrideOption,
FirmwareVirtualOption,
PortsInput,
},
data() {
return {
port: 'manual',
};
},
};
</script>

View file

@ -8,8 +8,9 @@
id="port"
class="dropdown-select"
:title="$t('firmwareFlasherManualPort')"
@value="value"
@input="$emit('input', $event.target.value)"
:disabled="disabled"
v-model="value.selectedPort"
@change="onChange"
>
<option value="manual">
{{ $t("portsSelectManual") }}
@ -20,16 +21,29 @@
>
{{ $t("portsSelectVirtual") }}
</option>
<option
v-for="connectedDevice in connectedDevices"
:key="connectedDevice.path"
:value="connectedDevice.path"
>
{{ connectedDevice.displayName }}
</option>
<option value="requestpermission">
{{ $t("portsSelectPermission") }}
</option>
</select>
</div>
<div id="auto-connect-and-baud">
<div id="baudselect">
<div id="baudselect"
v-if="value.selectedPort !== 'virtual'"
>
<div class="dropdown dropdown-dark">
<select
id="baud"
v-model="selectedBaudRate"
v-model="value.selectedBauds"
class="dropdown-select"
:title="$t('firmwareFlasherBaudRate')"
:disabled="disabled"
>
<option
v-for="baudRate in baudRates"
@ -52,15 +66,25 @@ import { EventBus } from '../eventBus';
export default {
props: {
value: {
type: String,
default: 'manual',
type: Object,
default: {
selectedPort: 'manual',
selectedBaud: 115200,
},
},
connectedDevices: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
showVirtual: false,
selectedBaudRate: "115200",
baudRates: [
{ value: "1000000", label: "1000000" },
{ value: "500000", label: "500000" },
@ -80,13 +104,23 @@ export default {
},
mounted() {
EventBus.$on('config-storage:set', this.setShowVirtual);
this.setShowVirtual('showVirtualMode');
},
destroyed() {
EventBus.$off('config-storage:set', this.setShowVirtual);
},
methods: {
setShowVirtual() {
this.showVirtual = getConfig('showVirtualMode').showVirtualMode;
setShowVirtual(element) {
if (element === 'showVirtualMode') {
this.showVirtual = getConfig('showVirtualMode').showVirtualMode;
}
},
onChange(event) {
if (event.target.value === 'requestpermission') {
EventBus.$emit('ports-input:request-permission');
} else {
EventBus.$emit('ports-input:change', event.target.value);
}
},
},
};

View file

@ -26,7 +26,11 @@
:firmware-id="FC.CONFIG.flightControllerIdentifier"
:hardware-id="FC.CONFIG.hardwareName"
></betaflight-logo>
<port-picker></port-picker>
<port-picker
v-model="PortHandler.portPicker"
:connected-devices="PortHandler.currentPorts"
:disabled="PortHandler.portPickerDisabled"
></port-picker>
<div class="header-wrapper">
<div id="quad-status_wrapper">
<battery-icon

View file

@ -45,7 +45,7 @@ export function set(input) {
tmpObj[element] = input[element];
try {
localStorage.setItem(element, JSON.stringify(tmpObj));
EventBus.$emit(`config-storage:set`, 'element');
EventBus.$emit('config-storage:set', element);
} catch (e) {
console.error(e);
}

View file

@ -1,18 +1,28 @@
import GUI, { TABS } from "./gui";
import FC from "./fc";
import { i18n } from "./localization";
import { generateVirtualApiVersions, getTextWidth } from './utils/common';
import { get as getConfig } from "./ConfigStorage";
import serial from "./serial";
import MdnsDiscovery from "./mdns_discovery";
import { isWeb } from "./utils/isWeb";
import { usbDevices } from "./usb_devices";
import { serialShim } from "./serial_shim.js";
import { EventBus } from "../components/eventBus";
const TIMEOUT_CHECK = 500 ; // With 250 it seems that it produces a memory leak and slowdown in some versions, reason unknown
const serial = serialShim();
const DEFAULT_PORT = 'manual';
const DEFAULT_BAUDS = 115200;
const PortHandler = new function () {
this.currentPorts = [];
this.initialPorts = false;
this.portPicker = {
selectedPort: DEFAULT_PORT,
selectedBauds: DEFAULT_BAUDS,
portOverride: "/dev/rfcomm0",
virtualMspVersion: "1.46.0",
};
this.portPickerDisabled = false;
this.port_detected_callbacks = [];
this.port_removed_callbacks = [];
this.dfu_available = false;
@ -24,21 +34,13 @@ const PortHandler = new function () {
};
PortHandler.initialize = function () {
const self = this;
// currently web build doesn't need port handler,
// so just bail out.
if (isWeb()) {
return 'not implemented';
}
EventBus.$on('ports-input:request-permission', this.askPermissionPort.bind(this));
EventBus.$on('ports-input:change', this.onChangeSelectedPort.bind(this));
const portPickerElementSelector = "div#port-picker #port";
self.portPickerElement = $(portPickerElementSelector);
self.selectList = document.querySelector(portPickerElementSelector);
self.initialWidth = self.selectList.offsetWidth + 12;
serial.addEventListener("addedDevice", this.check_serial_devices.bind(this));
// fill dropdown with version numbers
generateVirtualApiVersions();
serial.addEventListener("removedDevice", this.check_serial_devices.bind(this));
this.reinitialize(); // just to prevent code redundancy
};
@ -73,15 +75,15 @@ PortHandler.check = function () {
self.check_serial_devices();
}
self.usbCheckLoop = setTimeout(() => {
self.check();
}, TIMEOUT_CHECK);
this.check_serial_devices();
};
PortHandler.check_serial_devices = function () {
const self = this;
serial.getDevices(function(cp) {
const updatePorts = function(cp) {
self.currentPorts = [];
if (self.useMdnsBrowser) {
@ -109,11 +111,25 @@ PortHandler.check_serial_devices = function () {
} else {
self.removePort();
self.detectPort();
self.selectActivePort();
}
});
};
serial.getDevices().then(updatePorts);
};
PortHandler.onChangeSelectedPort = function(port) {
this.portPicker.selectedPort = port;
};
PortHandler.check_usb_devices = function (callback) {
// TODO needs USB code refactor for web
if (isWeb()) {
return;
}
const self = this;
chrome.usb.getDevices(usbDevices, function (result) {
@ -137,7 +153,7 @@ PortHandler.check_usb_devices = function (callback) {
}));
self.portPickerElement.append($('<option/>', {
value: 'manual',
value: DEFAULT_PORT,
text: i18n.getMessage('portsSelectManual'),
/**
* @deprecated please avoid using `isDFU` and friends for new code.
@ -203,7 +219,6 @@ PortHandler.removePort = function() {
self.initialPorts.splice(self.initialPorts.indexOf(port, 1));
}
self.updatePortSelect(self.initialPorts);
self.portPickerElement.trigger('change');
}
};
@ -216,8 +231,8 @@ PortHandler.detectPort = function() {
console.log(`PortHandler - Found: ${JSON.stringify(newPorts)}`);
if (newPorts.length === 1) {
self.portPickerElement.val(newPorts[0].path);
} else if (newPorts.length > 1) {
this.portPicker.selectedPort = newPorts[0].path;
} else {
self.selectActivePort();
}
@ -227,8 +242,6 @@ PortHandler.detectPort = function() {
TABS.firmware_flasher.boardNeedsVerification = true;
}
self.portPickerElement.trigger('change');
// auto-connect if enabled
if (GUI.auto_connect && !GUI.connecting_to && !GUI.connected_to && GUI.active_tab !== 'firmware_flasher') {
// start connect procedure. We need firmware flasher protection over here
@ -263,105 +276,39 @@ PortHandler.sortPorts = function(ports) {
});
};
PortHandler.addNoPortSelection = function() {
if (!this.showVirtualMode && !this.showManualMode) {
this.portPickerElement.append($("<option/>", {
value: 'none',
text: i18n.getMessage('portsSelectNone'),
}));
}
};
PortHandler.updatePortSelect = function (ports) {
ports = this.sortPorts(ports);
this.portPickerElement.empty();
this.currentPorts = ports;
};
for (const port of ports) {
const portText = port.displayName ? `${port.path} - ${port.displayName}` : port.path;
this.portPickerElement.append($("<option/>", {
value: port.path,
text: portText,
/**
* @deprecated please avoid using `isDFU` and friends for new code.
*/
data: {isManual: false},
}));
}
if (this.showVirtualMode) {
this.portPickerElement.append($("<option/>", {
value: 'virtual',
text: i18n.getMessage('portsSelectVirtual'),
/**
* @deprecated please avoid using `isDFU` and friends for new code.
*/
data: {isVirtual: true},
}));
}
if (this.showManualMode) {
this.portPickerElement.append($("<option/>", {
value: 'manual',
text: i18n.getMessage('portsSelectManual'),
/**
* @deprecated please avoid using `isDFU` and friends for new code.
*/
data: {isManual: true},
}));
}
if (!ports.length) {
this.addNoPortSelection();
}
this.setPortsInputWidth();
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() {
const ports = this.currentPorts;
const OS = GUI.operating_system;
let selectedPort;
for (let i = 0; i < ports.length; i++) {
const portName = ports[i].displayName;
if (portName) {
const pathSelect = ports[i].path;
const isWindows = (OS === 'Windows');
const isTty = pathSelect.includes('tty');
const deviceFilter = ['AT32', 'CP210', 'SPR', 'STM32'];
const deviceFilter = ['AT32', 'CP210', 'SPR', 'STM'];
const deviceRecognized = deviceFilter.some(device => portName.includes(device));
const legacyDeviceRecognized = portName.includes('usb');
if (isWindows && deviceRecognized || isTty && (deviceRecognized || legacyDeviceRecognized)) {
this.portPickerElement.val(pathSelect);
if (deviceRecognized || legacyDeviceRecognized) {
selectedPort = pathSelect;
this.port_available = true;
console.log(`Porthandler detected device ${portName} on port: ${pathSelect}`);
}
}
}
};
PortHandler.setPortsInputWidth = function() {
function findMaxLengthOption(selectEl) {
let max = 0;
$(selectEl.options).each(function () {
const textSize = getTextWidth(this.textContent);
if (textSize > max) {
max = textSize;
}
});
return max;
}
const correction = 32; // account for up/down button and spacing
let width = findMaxLengthOption(this.selectList) + correction;
width = (width > this.initialWidth) ? width : this.initialWidth;
const portsInput = document.querySelector("div#port-picker #portsinput");
portsInput.style.width = `${width}px`;
this.portPicker.selectedPort = selectedPort || DEFAULT_PORT;
};
PortHandler.port_detected = function(name, code, timeout, ignore_timeout) {

View file

@ -26,6 +26,7 @@ import BuildApi from "./BuildApi";
import { isWeb } from "./utils/isWeb";
import { serialShim } from "./serial_shim.js";
import { EventBus } from "../components/eventBus";
let serial = serialShim();
@ -73,19 +74,16 @@ export function initializeSerialBackend() {
$('#port-override').val(data.portOverride);
}
$('div#port-picker #port').change(function (target) {
GUI.updateManualPortVisibility();
});
EventBus.$on('ports-input:change', () => GUI.updateManualPortVisibility());
$("div.connect_controls a.connect").on('click', function () {
const selectedPort = $('#port').val();
const selectedPort = PortHandler.portPicker.selectedPort;
let portName;
if (selectedPort === 'manual') {
portName = $('#port-override').val();
} else {
portName = String($('div#port-picker #port').val());
portName = selectedPort;
}
if (!GUI.connect_lock && selectedPort !== 'none') {
@ -93,8 +91,8 @@ export function initializeSerialBackend() {
GUI.configuration_loaded = false;
const selected_baud = parseInt($('div#port-picker #baud').val());
const selectedPort = $('#port').val();
const selected_baud = PortHandler.portPicker.selectedBauds;
const selectedPort = portName;
if (selectedPort === 'DFU') {
$('select#baud').hide();
@ -106,10 +104,10 @@ export function initializeSerialBackend() {
GUI.connecting_to = portName;
// lock port select & baud while we are connecting / connected
$('div#port-picker #port, div#port-picker #baud, div#port-picker #delay').prop('disabled', true);
PortHandler.portPickerDisabled = true;
$('div.connect_controls div.connect_state').text(i18n.getMessage('connecting'));
const baudRate = parseInt($('#baud').val());
const baudRate = selected_baud;
if (selectedPort === 'virtual') {
CONFIGURATOR.virtualMode = true;
CONFIGURATOR.virtualApiVersion = $('#firmware-version-dropdown').val();
@ -117,7 +115,7 @@ export function initializeSerialBackend() {
// Hack to get virtual working on the web
serial = serialShim();
serial.connect('virtual', {}, onOpenVirtual);
} else if (isWeb()) {
} else {
CONFIGURATOR.virtualMode = false;
serial = serialShim();
// Explicitly disconnect the event listeners before attaching the new ones.
@ -127,10 +125,7 @@ export function initializeSerialBackend() {
serial.removeEventListener('disconnect', disconnectHandler);
serial.addEventListener('disconnect', disconnectHandler);
serial.connect({ baudRate });
} else {
serial.connect(portName, { bitrate: selected_baud }, onOpen);
toggleStatus();
serial.connect(portName, { baudRate });
}
} else {
@ -230,8 +225,7 @@ function finishClose(finishedCallback) {
$('#dialogReportProblems-closebtn').click();
// unlock port select & baud
$('div#port-picker #port').prop('disabled', false);
if (!GUI.auto_connect) $('div#port-picker #baud').prop('disabled', false);
PortHandler.portPickerDisabled = false;
// reset connect / disconnect button
$('div.connect_controls a.connect').removeClass('active');
@ -275,7 +269,7 @@ function abortConnection() {
$('div#connectbutton a.connect').removeClass('active');
// unlock port select & baud
$('div#port-picker #port, div#port-picker #baud, div#port-picker #delay').prop('disabled', false);
PortHandler.portPickerDisabled = false;
// reset data
isConnected = false;

View file

@ -1,3 +1,10 @@
export const vendorIdNames = {
1027: "FTDI",
1155: "STM Electronics",
4292: "Silicon Labs",
0x2e3c: "AT32",
};
export const serialDevices = [
{ vendorId: 1027, productId: 24577 }, // FT232R USB UART
{ vendorId: 1155, productId: 22336 }, // STM Electronics Virtual COM Port

View file

@ -1,4 +1,4 @@
import { webSerialDevices } from "./serial_devices";
import { webSerialDevices, vendorIdNames } from "./serial_devices";
async function* streamAsyncIterable(reader, keepReadingFlag) {
try {
@ -30,12 +30,34 @@ class WebSerial extends EventTarget {
this.logHead = "SERIAL: ";
this.port_counter = 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) {
@ -44,16 +66,56 @@ class WebSerial extends EventTarget {
handleDisconnect() {
this.removeEventListener('receive', this.handleReceiveBytes);
this.removeEventListener('disconnect', this.handleDisconnect);
this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
}
async connect(options) {
this.openRequested = true;
this.port = await navigator.serial.requestPort({
getConnectedPort() {
return this.port;
}
createPort(port) {
return {
path: `D${this.port_counter}`,
displayName: `Betaflight ${vendorIdNames[port.getInfo().usbVendorId]}`,
vendorId: port.getInfo().usbVendorId,
productId: port.getInfo().usbProductId,
port: port,
};
}
async loadDevices() {
const ports = await navigator.serial.getPorts({
filters: webSerialDevices,
});
this.port_counter = 1;
this.ports = ports.map(function (port) {
return this.createPort(port);
}, this);
};
async requestPermissionDevice() {
const permissionPort = await navigator.serial.requestPort({
filters: webSerialDevices,
});
const found = this.ports.find(port => port.port === device);
if (!found) {
return this.handleNewDevice(permissionPort);
}
return null;
};
async getDevices() {
return this.ports;
}
async connect(path, options) {
this.openRequested = true;
this.port = this.ports.find(device => device.path === path).port;
await this.port.open(options);
const connectionInfo = this.port.getInfo();
this.connectionInfo = connectionInfo;
this.writer = this.port.writable.getWriter();
@ -69,7 +131,6 @@ class WebSerial extends EventTarget {
this.openRequested = false;
this.addEventListener("receive", this.handleReceiveBytes);
this.addEventListener('disconnect', this.handleDisconnect);
console.log(
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, Baud: ${options.baudRate}`,