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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,12 @@ import { EventBus } from "../components/eventBus";
import { serial } from "./serial.js";
import WEBUSBDFU from "./protocols/webusbdfu";
import { reactive } from "vue";
import {
checkBrowserCompatibility,
checkWebBluetoothSupport,
checkWebSerialSupport,
checkWebUSBSupport,
} from "./utils/checkBrowserCompatibility.js";
const DEFAULT_PORT = "noselection";
const DEFAULT_BAUDS = 115200;
@ -27,7 +33,17 @@ const PortHandler = new (function () {
this.bluetoothAvailable = false;
this.dfuAvailable = 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.showManualMode = getConfig("showManualMode", false).showManualMode;
this.showAllSerialDevices = getConfig("showAllSerialDevices", false).showAllSerialDevices;
@ -183,21 +199,21 @@ PortHandler.selectActivePort = function (suggestedDevice = false) {
}
// If there is a connection, return it
// if (selectedPort) {
// console.log(`${this.logHead} Using connected device: ${selectedPort.path}`);
// selectedPort = selectedPort.path;
// return selectedPort;
// }
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;
// }
// }
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) {
@ -289,15 +305,23 @@ PortHandler.updateDeviceList = async function (deviceType) {
try {
switch (deviceType) {
case "bluetooth":
if (this.showBluetoothOption) {
ports = await serial.getDevices("bluetooth");
}
break;
case "usb":
if (this.showUsbOption) {
ports = await WEBUSBDFU.getDevices();
}
break;
case "serial":
default:
if (this.showSerialOption) {
ports = await serial.getDevices("serial");
}
break;
default:
console.warn(`${this.logHead} Unknown device type: ${deviceType}`);
return [];
}
// Sort the ports

View file

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

View file

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

View file

@ -77,6 +77,11 @@ class WEBUSBDFU_protocol extends EventTarget {
this.flash_layout = { start_address: 0, total_size: 0, sectors: [] };
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("disconnect", (e) => this.handleNewDevice(e.device));
}

View file

@ -28,16 +28,15 @@ export function getOS() {
}
export function isChromiumBrowser() {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
if (!navigator.userAgentData) {
console.log(navigator.userAgent);
return false;
}
// https://learn.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance
if (navigator.userAgentData) {
return navigator.userAgentData.brands.some((brand) => {
return brand.brand == "Chromium";
});
}
// Fallback for older browsers/Android
const ua = navigator.userAgent.toLowerCase();
return ua.includes("chrom") || ua.includes("edg");
}
export function isAndroid() {
@ -62,16 +61,21 @@ export function isCapacitorWeb() {
}
export function checkBrowserCompatibility() {
const webSerial = "serial" in navigator;
const isNative = Capacitor.isNativePlatform();
const isWebSerial = checkWebSerialSupport();
const isWebBluetooth = checkWebBluetoothSupport();
const isWebUSB = checkWebUSBSupport();
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("Native: ", isNative);
console.log("Chromium: ", isChromium);
console.log("Web Serial: ", webSerial);
console.log("Web Serial: ", isWebSerial);
console.log("OS: ", getOS());
console.log("Android: ", isAndroid());
console.log("iOS: ", isIOS());
console.log("Capacitor web: ", isCapacitorWeb());
@ -82,11 +86,19 @@ export function checkBrowserCompatibility() {
let errorMessage = "";
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) {
errorMessage += " Web Serial API support is disabled.";
if (!isWebBluetooth) {
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");
@ -114,3 +126,45 @@ export function checkBrowserCompatibility() {
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;
}