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:
parent
77a719dd2d
commit
9baef76904
9 changed files with 188 additions and 63 deletions
|
@ -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,
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -28,18 +28,17 @@ 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() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
return Capacitor.getPlatform() === "android";
|
||||
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue