1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-24 00:35:26 +03:00

Allow execution when either Web Serial, Bluetooth or USB API is present (#4470)

* Allow execution when either webserial, webbluetooth or webusb API is available

* Fix todo

* Add passing missing properties

* Update browser detection

* Remove commented code

* Indentation

* Update presentation of errorMessage

* cleanup

* Consistency

* Nitpicks

* Remove writeValueWithResponse

* More nitpicks
This commit is contained in:
Mark Haslinghuis 2025-05-15 19:03:53 +02:00 committed by GitHub
parent 77a719dd2d
commit 9baef76904
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 188 additions and 63 deletions

View file

@ -18,6 +18,9 @@
:disabled="disabled" :disabled="disabled"
:show-virtual-option="showVirtualOption" :show-virtual-option="showVirtualOption"
:show-manual-option="showManualOption" :show-manual-option="showManualOption"
:show-bluetooth-option="showBluetoothOption"
:show-serial-option="showSerialOption"
:show-usb-option="showUsbOption"
@update:modelValue="updateModelValue(null, $event)" @update:modelValue="updateModelValue(null, $event)"
/> />
</div> </div>
@ -62,6 +65,18 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showBluetoothOption: {
type: Boolean,
default: true,
},
showSerialOption: {
type: Boolean,
default: true,
},
showUsbOption: {
type: Boolean,
default: true,
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,

View file

@ -19,6 +19,7 @@
{{ $t("portsSelectVirtual") }} {{ $t("portsSelectVirtual") }}
</option> </option>
<option <option
v-if="showBluetoothOption"
v-for="connectedBluetoothDevice in connectedBluetoothDevices" v-for="connectedBluetoothDevice in connectedBluetoothDevices"
:key="connectedBluetoothDevice.path" :key="connectedBluetoothDevice.path"
:value="connectedBluetoothDevice.path" :value="connectedBluetoothDevice.path"
@ -26,6 +27,7 @@
{{ connectedBluetoothDevice.displayName }} {{ connectedBluetoothDevice.displayName }}
</option> </option>
<option <option
v-if="showSerialOption"
v-for="connectedSerialDevice in connectedSerialDevices" v-for="connectedSerialDevice in connectedSerialDevices"
:key="connectedSerialDevice.path" :key="connectedSerialDevice.path"
:value="connectedSerialDevice.path" :value="connectedSerialDevice.path"
@ -33,19 +35,20 @@
{{ connectedSerialDevice.displayName }} {{ connectedSerialDevice.displayName }}
</option> </option>
<option <option
v-if="showUsbOption"
v-for="connectedUsbDevice in connectedUsbDevices" v-for="connectedUsbDevice in connectedUsbDevices"
:key="connectedUsbDevice.path" :key="connectedUsbDevice.path"
:value="connectedUsbDevice.path" :value="connectedUsbDevice.path"
> >
{{ connectedUsbDevice.displayName }} {{ connectedUsbDevice.displayName }}
</option> </option>
<option value="requestpermission"> <option v-if="showSerialOption" value="requestpermission">
{{ $t("portsSelectPermission") }} {{ $t("portsSelectPermission") }}
</option> </option>
<option value="requestpermissionbluetooth"> <option v-if="showBluetoothOption" value="requestpermissionbluetooth">
{{ $t("portsSelectPermissionBluetooth") }} {{ $t("portsSelectPermissionBluetooth") }}
</option> </option>
<option value="requestpermissionusb"> <option v-if="showUsbOption" value="requestpermissionusb">
{{ $t("portsSelectPermissionDFU") }} {{ $t("portsSelectPermissionDFU") }}
</option> </option>
</select> </select>
@ -121,6 +124,18 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showBluetoothOption: {
type: Boolean,
default: true,
},
showSerialOption: {
type: Boolean,
default: true,
},
showUsbOption: {
type: Boolean,
default: true,
},
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(props, { emit }) { setup(props, { emit }) {

View file

@ -33,6 +33,9 @@
:connected-usb-devices="PortHandler.currentUsbPorts" :connected-usb-devices="PortHandler.currentUsbPorts"
:show-virtual-option="PortHandler.showVirtualMode" :show-virtual-option="PortHandler.showVirtualMode"
:show-manual-option="PortHandler.showManualMode" :show-manual-option="PortHandler.showManualMode"
:show-bluetooth-option="PortHandler.showBluetoothOption"
:show-serial-option="PortHandler.showSerialOption"
:show-usb-option="PortHandler.showUsbOption"
:disabled="PortHandler.portPickerDisabled" :disabled="PortHandler.portPickerDisabled"
></port-picker> ></port-picker>
<div class="header-wrapper"> <div class="header-wrapper">

View file

@ -3,7 +3,7 @@ import MSP from "./msp";
import Switchery from "switchery-latest"; import Switchery from "switchery-latest";
import jBox from "jbox"; import jBox from "jbox";
import $ from "jquery"; import $ from "jquery";
import { getOS } from "./utils/checkBrowserCompatibilty"; import { getOS } from "./utils/checkBrowserCompatibility";
const TABS = {}; const TABS = {};

View file

@ -3,6 +3,12 @@ import { EventBus } from "../components/eventBus";
import { serial } from "./serial.js"; import { serial } from "./serial.js";
import WEBUSBDFU from "./protocols/webusbdfu"; import WEBUSBDFU from "./protocols/webusbdfu";
import { reactive } from "vue"; import { reactive } from "vue";
import {
checkBrowserCompatibility,
checkWebBluetoothSupport,
checkWebSerialSupport,
checkWebUSBSupport,
} from "./utils/checkBrowserCompatibility.js";
const DEFAULT_PORT = "noselection"; const DEFAULT_PORT = "noselection";
const DEFAULT_BAUDS = 115200; const DEFAULT_BAUDS = 115200;
@ -27,7 +33,17 @@ const PortHandler = new (function () {
this.bluetoothAvailable = false; this.bluetoothAvailable = false;
this.dfuAvailable = false; this.dfuAvailable = false;
this.portAvailable = false; this.portAvailable = false;
this.showAllSerialDevices = false;
checkBrowserCompatibility();
this.showBluetoothOption = checkWebBluetoothSupport();
this.showSerialOption = checkWebSerialSupport();
this.showUsbOption = checkWebUSBSupport();
console.log(`${this.logHead} Bluetooth available: ${this.showBluetoothOption}`);
console.log(`${this.logHead} Serial available: ${this.showSerialOption}`);
console.log(`${this.logHead} DFU available: ${this.showUsbOption}`);
this.showVirtualMode = getConfig("showVirtualMode", false).showVirtualMode; this.showVirtualMode = getConfig("showVirtualMode", false).showVirtualMode;
this.showManualMode = getConfig("showManualMode", false).showManualMode; this.showManualMode = getConfig("showManualMode", false).showManualMode;
this.showAllSerialDevices = getConfig("showAllSerialDevices", false).showAllSerialDevices; this.showAllSerialDevices = getConfig("showAllSerialDevices", false).showAllSerialDevices;
@ -183,21 +199,21 @@ PortHandler.selectActivePort = function (suggestedDevice = false) {
} }
// If there is a connection, return it // If there is a connection, return it
// if (selectedPort) { if (selectedPort) {
// console.log(`${this.logHead} Using connected device: ${selectedPort.path}`); console.log(`${this.logHead} Using connected device: ${selectedPort.path}`);
// selectedPort = selectedPort.path; selectedPort = selectedPort.path;
// return selectedPort; return selectedPort;
// } }
// If there is no connection, check for the last used device // If there is no connection, check for the last used device
// Check if the device is already connected // Check if the device is already connected
// if (this.portPicker.selectedPort && this.portPicker.selectedPort !== DEFAULT_PORT) { if (this.portPicker.selectedPort && this.portPicker.selectedPort !== DEFAULT_PORT) {
// selectedPort = this.currentSerialPorts.find((device) => device.path === this.portPicker.selectedPort); selectedPort = this.currentSerialPorts.find((device) => device.path === this.portPicker.selectedPort);
// if (selectedPort) { if (selectedPort) {
// console.log(`${this.logHead} Using previously selected device: ${selectedPort.path}`); console.log(`${this.logHead} Using previously selected device: ${selectedPort.path}`);
// return selectedPort.path; return selectedPort.path;
// } }
// } }
// Return the suggested device (the new device that has been detected) // Return the suggested device (the new device that has been detected)
if (!selectedPort && suggestedDevice) { if (!selectedPort && suggestedDevice) {
@ -289,15 +305,23 @@ PortHandler.updateDeviceList = async function (deviceType) {
try { try {
switch (deviceType) { switch (deviceType) {
case "bluetooth": case "bluetooth":
ports = await serial.getDevices("bluetooth"); if (this.showBluetoothOption) {
ports = await serial.getDevices("bluetooth");
}
break; break;
case "usb": case "usb":
ports = await WEBUSBDFU.getDevices(); if (this.showUsbOption) {
ports = await WEBUSBDFU.getDevices();
}
break; break;
case "serial": case "serial":
default: if (this.showSerialOption) {
ports = await serial.getDevices("serial"); ports = await serial.getDevices("serial");
}
break; break;
default:
console.warn(`${this.logHead} Unknown device type: ${deviceType}`);
return [];
} }
// Sort the ports // Sort the ports

View file

@ -20,6 +20,7 @@ class WebBluetooth extends EventTarget {
this.closeRequested = false; this.closeRequested = false;
this.transmitting = false; this.transmitting = false;
this.connectionInfo = null; this.connectionInfo = null;
this.lastWrite = null;
this.bitrate = 0; this.bitrate = 0;
this.bytesSent = 0; this.bytesSent = 0;
@ -32,10 +33,10 @@ class WebBluetooth extends EventTarget {
this.logHead = "[BLUETOOTH]"; this.logHead = "[BLUETOOTH]";
if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) { this.bluetooth = navigator?.bluetooth;
this.bluetooth = navigator.bluetooth;
} else { if (!this.bluetooth) {
console.error(`${this.logHead} Bluetooth API not available`); console.error(`${this.logHead} Web Bluetooth API not supported`);
return; return;
} }
@ -86,10 +87,14 @@ class WebBluetooth extends EventTarget {
} }
async loadDevices() { async loadDevices() {
const devices = await this.getDevices(); try {
const devices = await this.getDevices();
this.portCounter = 1; this.portCounter = 1;
this.devices = devices.map((device) => this.createPort(device)); this.devices = devices.map((device) => this.createPort(device));
} catch (error) {
console.error(`${this.logHead} Failed to load devices:`, error);
}
} }
async requestPermissionDevice() { async requestPermissionDevice() {
@ -251,7 +256,11 @@ class WebBluetooth extends EventTarget {
this.readCharacteristic.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this)); this.readCharacteristic.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this));
return await this.readCharacteristic.readValue(); try {
return await this.readCharacteristic.readValue();
} catch (e) {
console.error(`${this.logHead} Failed to read characteristic value:`, e);
}
} }
handleNotification(event) { handleNotification(event) {
@ -261,7 +270,9 @@ class WebBluetooth extends EventTarget {
buffer[i] = event.target.value.getUint8(i); buffer[i] = event.target.value.getUint8(i);
} }
this.dispatchEvent(new CustomEvent("receive", { detail: buffer })); setTimeout(() => {
this.dispatchEvent(new CustomEvent("receive", { detail: buffer }));
}, 0);
} }
startNotifications() { startNotifications() {
@ -342,7 +353,14 @@ class WebBluetooth extends EventTarget {
const dataBuffer = new Uint8Array(data); const dataBuffer = new Uint8Array(data);
await this.writeCharacteristic.writeValue(dataBuffer); try {
if (this.lastWrite) {
await this.lastWrite;
}
} catch (error) {
console.error(error);
}
this.lastWrite = this.writeCharacteristic.writeValue(dataBuffer);
return { return {
bytesSent: data.byteLength, bytesSent: data.byteLength,

View file

@ -1,5 +1,4 @@
import { webSerialDevices, vendorIdNames } from "./devices"; import { webSerialDevices, vendorIdNames } from "./devices";
import { checkBrowserCompatibility } from "../utils/checkBrowserCompatibilty";
import GUI from "../gui"; import GUI from "../gui";
const logHead = "[SERIAL]"; const logHead = "[SERIAL]";
@ -39,8 +38,6 @@ class WebSerial extends EventTarget {
constructor() { constructor() {
super(); super();
checkBrowserCompatibility();
this.connected = false; this.connected = false;
this.openRequested = false; this.openRequested = false;
this.openCanceled = false; this.openCanceled = false;
@ -60,16 +57,20 @@ class WebSerial extends EventTarget {
this.writer = null; this.writer = null;
this.reading = false; this.reading = false;
if (!navigator?.serial) {
console.error(`${logHead} Web Serial API not supported`);
return;
}
this.connect = this.connect.bind(this); this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this); this.disconnect = this.disconnect.bind(this);
this.handleDisconnect = this.handleDisconnect.bind(this); this.handleDisconnect = this.handleDisconnect.bind(this);
this.handleReceiveBytes = this.handleReceiveBytes.bind(this); this.handleReceiveBytes = this.handleReceiveBytes.bind(this);
// Initialize device connection/disconnection listeners // Initialize device connection/disconnection listeners
if (navigator.serial) { navigator.serial.addEventListener("connect", (e) => this.handleNewDevice(e.target));
navigator.serial.addEventListener("connect", (e) => this.handleNewDevice(e.target)); navigator.serial.addEventListener("disconnect", (e) => this.handleRemovedDevice(e.target));
navigator.serial.addEventListener("disconnect", (e) => this.handleRemovedDevice(e.target));
}
this.isNeedBatchWrite = false; this.isNeedBatchWrite = false;
this.loadDevices(); this.loadDevices();
} }
@ -116,11 +117,6 @@ class WebSerial extends EventTarget {
} }
async loadDevices() { async loadDevices() {
if (!navigator.serial) {
console.error(`${logHead} Web Serial API not available`);
return;
}
try { try {
const ports = await navigator.serial.getPorts(); const ports = await navigator.serial.getPorts();
this.portCounter = 1; this.portCounter = 1;
@ -131,11 +127,6 @@ class WebSerial extends EventTarget {
} }
async requestPermissionDevice(showAllSerialDevices = false) { async requestPermissionDevice(showAllSerialDevices = false) {
if (!navigator.serial) {
console.error(`${logHead} Web Serial API not available`);
return null;
}
let newPermissionPort = null; let newPermissionPort = null;
try { try {

View file

@ -77,6 +77,11 @@ class WEBUSBDFU_protocol extends EventTarget {
this.flash_layout = { start_address: 0, total_size: 0, sectors: [] }; this.flash_layout = { start_address: 0, total_size: 0, sectors: [] };
this.transferSize = 2048; // Default USB DFU transfer size for F3,F4 and F7 this.transferSize = 2048; // Default USB DFU transfer size for F3,F4 and F7
if (!navigator?.usb) {
console.error(`${this.logHead} WebUSB API not supported`);
return;
}
navigator.usb.addEventListener("connect", (e) => this.handleNewDevice(e.device)); navigator.usb.addEventListener("connect", (e) => this.handleNewDevice(e.device));
navigator.usb.addEventListener("disconnect", (e) => this.handleNewDevice(e.device)); navigator.usb.addEventListener("disconnect", (e) => this.handleNewDevice(e.device));
} }

View file

@ -28,16 +28,15 @@ export function getOS() {
} }
export function isChromiumBrowser() { export function isChromiumBrowser() {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent if (navigator.userAgentData) {
if (!navigator.userAgentData) { return navigator.userAgentData.brands.some((brand) => {
console.log(navigator.userAgent); return brand.brand == "Chromium";
return false; });
} }
// https://learn.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance // Fallback for older browsers/Android
return navigator.userAgentData.brands.some((brand) => { const ua = navigator.userAgent.toLowerCase();
return brand.brand == "Chromium"; return ua.includes("chrom") || ua.includes("edg");
});
} }
export function isAndroid() { export function isAndroid() {
@ -62,16 +61,21 @@ export function isCapacitorWeb() {
} }
export function checkBrowserCompatibility() { export function checkBrowserCompatibility() {
const webSerial = "serial" in navigator; const isWebSerial = checkWebSerialSupport();
const isNative = Capacitor.isNativePlatform(); const isWebBluetooth = checkWebBluetoothSupport();
const isWebUSB = checkWebUSBSupport();
const isChromium = isChromiumBrowser(); const isChromium = isChromiumBrowser();
const compatible = isNative || (webSerial && isChromium); const isNative = Capacitor.isNativePlatform();
const compatible = isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB));
console.log("User Agent: ", navigator.userAgentData); console.log("User Agent: ", navigator.userAgentData);
console.log("Native: ", isNative); console.log("Native: ", isNative);
console.log("Chromium: ", isChromium); console.log("Chromium: ", isChromium);
console.log("Web Serial: ", webSerial); console.log("Web Serial: ", isWebSerial);
console.log("OS: ", getOS());
console.log("Android: ", isAndroid()); console.log("Android: ", isAndroid());
console.log("iOS: ", isIOS()); console.log("iOS: ", isIOS());
console.log("Capacitor web: ", isCapacitorWeb()); console.log("Capacitor web: ", isCapacitorWeb());
@ -82,11 +86,19 @@ export function checkBrowserCompatibility() {
let errorMessage = ""; let errorMessage = "";
if (!isChromium) { if (!isChromium) {
errorMessage = "Betaflight app requires a Chromium based browser (Chrome, Chromium, Edge)."; errorMessage = "Betaflight app requires a Chromium based browser (Chrome, Chromium, Edge).<br/>";
} }
if (!webSerial) { if (!isWebBluetooth) {
errorMessage += " Web Serial API support is disabled."; errorMessage += "<br/>- Web Bluetooth API support is disabled.";
}
if (!isWebSerial) {
errorMessage += "<br/>- Web Serial API support is disabled.";
}
if (!isWebUSB) {
errorMessage += "<br/>- Web USB API support is disabled.";
} }
const newDiv = document.createElement("div"); const newDiv = document.createElement("div");
@ -114,3 +126,45 @@ export function checkBrowserCompatibility() {
throw new Error("No compatible browser found."); throw new Error("No compatible browser found.");
} }
export function checkWebSerialSupport() {
if (!navigator.serial) {
console.error("Web Serial API is not supported in this browser.");
return false;
}
if (isIOS()) {
console.error("Web Serial API is not supported on iOS.");
return false;
}
return true;
}
export function checkWebBluetoothSupport() {
if (!navigator.bluetooth) {
console.error("Web Bluetooth API is not supported in this browser.");
return false;
}
if (isIOS()) {
console.error("Web Bluetooth API is not supported on iOS.");
return false;
}
return true;
}
export function checkWebUSBSupport() {
if (!navigator.usb) {
console.error("Web USB API is not supported in this browser.");
return false;
}
if (isIOS()) {
console.error("Web USB API is not supported on iOS.");
return false;
}
return true;
}