diff --git a/src/components/port-picker/PortPicker.vue b/src/components/port-picker/PortPicker.vue index 0bc32eed..dd22666c 100644 --- a/src/components/port-picker/PortPicker.vue +++ b/src/components/port-picker/PortPicker.vue @@ -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)" /> @@ -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, diff --git a/src/components/port-picker/PortsInput.vue b/src/components/port-picker/PortsInput.vue index ec6016a8..0b73c739 100644 --- a/src/components/port-picker/PortsInput.vue +++ b/src/components/port-picker/PortsInput.vue @@ -19,6 +19,7 @@ {{ $t("portsSelectVirtual") }} - - - @@ -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 }) { diff --git a/src/index.html b/src/index.html index 418df9cf..b3c271a7 100644 --- a/src/index.html +++ b/src/index.html @@ -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" >
diff --git a/src/js/gui.js b/src/js/gui.js index ad19f73a..9244fac8 100644 --- a/src/js/gui.js +++ b/src/js/gui.js @@ -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 = {}; diff --git a/src/js/port_handler.js b/src/js/port_handler.js index b6fd2d2b..0d6499e1 100644 --- a/src/js/port_handler.js +++ b/src/js/port_handler.js @@ -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": - ports = await serial.getDevices("bluetooth"); + if (this.showBluetoothOption) { + ports = await serial.getDevices("bluetooth"); + } break; case "usb": - ports = await WEBUSBDFU.getDevices(); + if (this.showUsbOption) { + ports = await WEBUSBDFU.getDevices(); + } break; case "serial": - default: - ports = await serial.getDevices("serial"); + if (this.showSerialOption) { + ports = await serial.getDevices("serial"); + } break; + default: + console.warn(`${this.logHead} Unknown device type: ${deviceType}`); + return []; } // Sort the ports diff --git a/src/js/protocols/WebBluetooth.js b/src/js/protocols/WebBluetooth.js index a2315820..19751fdf 100644 --- a/src/js/protocols/WebBluetooth.js +++ b/src/js/protocols/WebBluetooth.js @@ -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() { - const devices = await this.getDevices(); + try { + const devices = await this.getDevices(); - this.portCounter = 1; - this.devices = devices.map((device) => this.createPort(device)); + 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)); - 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) { @@ -261,7 +270,9 @@ class WebBluetooth extends EventTarget { buffer[i] = event.target.value.getUint8(i); } - this.dispatchEvent(new CustomEvent("receive", { detail: buffer })); + 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, diff --git a/src/js/protocols/WebSerial.js b/src/js/protocols/WebSerial.js index 22cd14a7..0e7afb0f 100644 --- a/src/js/protocols/WebSerial.js +++ b/src/js/protocols/WebSerial.js @@ -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)); - } + 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 { diff --git a/src/js/protocols/webusbdfu.js b/src/js/protocols/webusbdfu.js index 0dc94d2d..5aaaec35 100644 --- a/src/js/protocols/webusbdfu.js +++ b/src/js/protocols/webusbdfu.js @@ -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)); } diff --git a/src/js/utils/checkBrowserCompatibilty.js b/src/js/utils/checkBrowserCompatibility.js similarity index 59% rename from src/js/utils/checkBrowserCompatibilty.js rename to src/js/utils/checkBrowserCompatibility.js index 3271e7ab..e76e2f6b 100644 --- a/src/js/utils/checkBrowserCompatibilty.js +++ b/src/js/utils/checkBrowserCompatibility.js @@ -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; + if (navigator.userAgentData) { + return navigator.userAgentData.brands.some((brand) => { + return brand.brand == "Chromium"; + }); } - // https://learn.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance - 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).
"; } - if (!webSerial) { - errorMessage += " Web Serial API support is disabled."; + if (!isWebBluetooth) { + errorMessage += "
- Web Bluetooth API support is disabled."; + } + + if (!isWebSerial) { + errorMessage += "
- Web Serial API support is disabled."; + } + + if (!isWebUSB) { + errorMessage += "
- 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; +}