diff --git a/android/app/build.gradle b/android/app/build.gradle index d09ae4d9fc..16281d6b5a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,6 +28,9 @@ repositories { flatDir{ dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' } + google() + mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index bbfb44fad1..b65c2dbfda 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,7 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - + implementation project(':capacitor-plugin-usb-serial') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4d7ca38041..2dcc2b1cc0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,14 @@ + + + + + + + + + diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml new file mode 100644 index 0000000000..1dd1503c99 --- /dev/null +++ b/android/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 9a5fa872e7..a5560f042d 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -1,3 +1,6 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capacitor-plugin-usb-serial' +project(':capacitor-plugin-usb-serial').projectDir = new File('../node_modules/capacitor-plugin-usb-serial/android') diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/android/settings.gradle b/android/settings.gradle index 3b4431d772..52d2925a6a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -2,4 +2,12 @@ include ':app' include ':capacitor-cordova-android-plugins' project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} + apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/package.json b/package.json index 995de18faa..1d9da16b3e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "android:release": "vite build && node capacitor.config.generator.mjs && npx cap build android --release", "format": "prettier --write {src,test}/**/*.{js,vue,css,less}", "storybook": "start-storybook -p 6006", - "prepare": "husky install" + "prepare": "husky install", + "postinstall": "patch-package" }, "window": { "icon": "images/bf_icon_128.png", @@ -45,6 +46,7 @@ "@capacitor/core": "^7.0.1", "@fortawesome/fontawesome-free": "^6.5.2", "@vitejs/plugin-vue": "^6.0.1", + "capacitor-plugin-usb-serial": "^0.0.6", "crypto-es": "^2.1.0", "d3": "^7.9.0", "djv": "^2.1.4", @@ -115,7 +117,9 @@ "lint-staged": "^15.4.3", "nw-builder": "^3.8.6", "os": "^0.1.2", + "patch-package": "^8.0.1", "postcss": "^8.5.3", + "postinstall-postinstall": "^2.1.0", "prettier": "^3.5.2", "rollup": "^4.34.9", "rollup-plugin-copy": "^3.5.0", diff --git a/patches/capacitor-plugin-usb-serial+0.0.6.patch b/patches/capacitor-plugin-usb-serial+0.0.6.patch new file mode 100644 index 0000000000..49bf61e005 --- /dev/null +++ b/patches/capacitor-plugin-usb-serial+0.0.6.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/capacitor-plugin-usb-serial/android/src/main/java/com/viewtrak/plugins/usbserial/UsbSerial.java b/node_modules/capacitor-plugin-usb-serial/android/src/main/java/com/viewtrak/plugins/usbserial/UsbSerial.java +index a366b4a..c80e172 100644 +--- a/node_modules/capacitor-plugin-usb-serial/android/src/main/java/com/viewtrak/plugins/usbserial/UsbSerial.java ++++ b/node_modules/capacitor-plugin-usb-serial/android/src/main/java/com/viewtrak/plugins/usbserial/UsbSerial.java +@@ -233,7 +233,8 @@ public class UsbSerial implements SerialInputOutputManager.Listener { + throw new Error("can't send empty string to device", new Throwable("EMPTY_STRING")); + } + try { +- byte[] data = (str + "\r\n").getBytes(); ++ // Convert hex string to bytes (for binary protocols like MSP) ++ byte[] data = HexDump.hexStringToByteArray(str); + usbSerialPort.write(data, WRITE_WAIT_MILLIS); + } catch (Exception e) { + closeSerial(); +@@ -260,25 +261,10 @@ public class UsbSerial implements SerialInputOutputManager.Listener { + + private void updateReceivedData(byte[] data) { + try { +- messageNMEA += new String(data); +- +- while (messageNMEA.indexOf(0x0a) != -1) { +- +- int eol = messageNMEA.indexOf(0x0a); +- if (-1 != eol) { +- String sentence = messageNMEA.substring(0, eol + 1); +- messageNMEA = messageNMEA.substring(eol + 1); +- +- // Boolean allowed = throttle.tryAcquire(); +- // if (!allowed) { +- // return; +- // } +- +- callback.receivedData(sentence); +- } else if (messageNMEA.length() > 128) { +- throw new Exception("invalid NMEA string"); +- } +- } ++ // FIXED: Send all data immediately as hex string (for binary protocols like MSP) ++ // Original code waited for newline characters (NMEA protocol), which never comes with binary data ++ String hexData = HexDump.toHexString(data); ++ callback.receivedData(hexData); + } catch (Exception exception) { + updateReadDataError(exception); + } diff --git a/src/js/gui.js b/src/js/gui.js index b48f97089d..ae5124a3a5 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 tippy from "tippy.js"; import $ from "jquery"; -import { getOS } from "./utils/checkBrowserCompatibility"; +import { getOS } from "./utils/checkCompatibility"; const TABS = {}; diff --git a/src/js/port_handler.js b/src/js/port_handler.js index 3665bce13b..614448a9b9 100644 --- a/src/js/port_handler.js +++ b/src/js/port_handler.js @@ -4,11 +4,11 @@ import { serial } from "./serial.js"; import WEBUSBDFU from "./protocols/webusbdfu"; import { reactive } from "vue"; import { - checkBrowserCompatibility, - checkWebBluetoothSupport, - checkWebSerialSupport, - checkWebUSBSupport, -} from "./utils/checkBrowserCompatibility.js"; + checkCompatibility, + checkBluetoothSupport, + checkSerialSupport, + checkUsbSupport, +} from "./utils/checkCompatibility.js"; const DEFAULT_PORT = "noselection"; const DEFAULT_BAUDS = 115200; @@ -34,11 +34,11 @@ const PortHandler = new (function () { this.dfuAvailable = false; this.portAvailable = false; - checkBrowserCompatibility(); + checkCompatibility(); - this.showBluetoothOption = checkWebBluetoothSupport(); - this.showSerialOption = checkWebSerialSupport(); - this.showUsbOption = checkWebUSBSupport(); + this.showBluetoothOption = checkBluetoothSupport(); + this.showSerialOption = checkSerialSupport(); + this.showUsbOption = checkUsbSupport(); console.log(`${this.logHead} Bluetooth available: ${this.showBluetoothOption}`); console.log(`${this.logHead} Serial available: ${this.showSerialOption}`); @@ -50,8 +50,8 @@ const PortHandler = new (function () { })(); PortHandler.initialize = function () { - EventBus.$on("ports-input:request-permission-bluetooth", () => this.requestDevicePermission("webbluetooth")); - EventBus.$on("ports-input:request-permission-serial", () => this.requestDevicePermission("webserial")); + EventBus.$on("ports-input:request-permission-bluetooth", () => this.requestDevicePermission("bluetooth")); + EventBus.$on("ports-input:request-permission-serial", () => this.requestDevicePermission("serial")); EventBus.$on("ports-input:request-permission-usb", () => this.requestDevicePermission("usb")); EventBus.$on("ports-input:change", this.onChangeSelectedPort.bind(this)); @@ -60,9 +60,9 @@ PortHandler.initialize = function () { const detail = event.detail; if (detail?.path?.startsWith("bluetooth")) { - this.handleDeviceAdded(detail, "webbluetooth"); + this.handleDeviceAdded(detail, "bluetooth"); } else { - this.handleDeviceAdded(detail, "webserial"); + this.handleDeviceAdded(detail, "serial"); } }); @@ -81,8 +81,8 @@ PortHandler.initialize = function () { PortHandler.refreshAllDeviceLists = async function () { // Update all device lists in parallel return Promise.all([ - this.updateDeviceList("webserial"), - this.updateDeviceList("webbluetooth"), + this.updateDeviceList("serial"), + this.updateDeviceList("bluetooth"), this.updateDeviceList("usb"), ]).then(() => { this.selectActivePort(); @@ -112,7 +112,7 @@ PortHandler.removedSerialDevice = function (device) { if (!devicePath) { console.warn(`${this.logHead} Device removal event missing path information`, device); // Still update ports, but don't try to use the undefined path - this.updateDeviceList("webserial").then(() => { + this.updateDeviceList("serial").then(() => { this.selectActivePort(); }); return; @@ -120,8 +120,8 @@ PortHandler.removedSerialDevice = function (device) { // Update the appropriate ports list based on the device type const updatePromise = devicePath.startsWith("bluetooth") - ? this.updateDeviceList("webbluetooth") - : this.updateDeviceList("webserial"); + ? this.updateDeviceList("bluetooth") + : this.updateDeviceList("serial"); const wasSelectedPort = this.portPicker.selectedPort === devicePath; @@ -274,7 +274,7 @@ PortHandler.handleDeviceAdded = function (device, deviceType) { // Update the appropriate device list const updatePromise = - deviceType === "webbluetooth" ? this.updateDeviceList("webbluetooth") : this.updateDeviceList("webserial"); + deviceType === "bluetooth" ? this.updateDeviceList("bluetooth") : this.updateDeviceList("serial"); updatePromise.then(() => { const selectedPort = this.selectActivePort(device); @@ -295,9 +295,9 @@ PortHandler.updateDeviceList = async function (deviceType) { try { switch (deviceType) { - case "webbluetooth": + case "bluetooth": if (this.showBluetoothOption) { - ports = await serial.getDevices("webbluetooth"); + ports = await serial.getDevices("bluetooth"); } break; case "usb": @@ -305,9 +305,9 @@ PortHandler.updateDeviceList = async function (deviceType) { ports = await WEBUSBDFU.getDevices(); } break; - case "webserial": + case "serial": if (this.showSerialOption) { - ports = await serial.getDevices("webserial"); + ports = await serial.getDevices("serial"); } break; default: @@ -320,7 +320,7 @@ PortHandler.updateDeviceList = async function (deviceType) { // Update the appropriate properties based on device type switch (deviceType) { - case "webbluetooth": + case "bluetooth": this.bluetoothAvailable = orderedPorts.length > 0; this.currentBluetoothPorts = [...orderedPorts]; console.log(`${this.logHead} Found bluetooth port(s)`, orderedPorts); @@ -330,7 +330,7 @@ PortHandler.updateDeviceList = async function (deviceType) { this.currentUsbPorts = [...orderedPorts]; console.log(`${this.logHead} Found DFU port(s)`, orderedPorts); break; - case "webserial": + case "serial": this.portAvailable = orderedPorts.length > 0; this.currentSerialPorts = [...orderedPorts]; console.log(`${this.logHead} Found serial port(s)`, orderedPorts); diff --git a/src/js/protocols/CapacitorSerial.js b/src/js/protocols/CapacitorSerial.js new file mode 100644 index 0000000000..8773b606f7 --- /dev/null +++ b/src/js/protocols/CapacitorSerial.js @@ -0,0 +1,287 @@ +import { UsbSerial } from "capacitor-plugin-usb-serial"; +import { serialDevices, vendorIdNames } from "./devices"; + +const logHead = "[Capacitor Serial]"; + +class CapacitorSerialProtocol extends EventTarget { + constructor() { + super(); + + this.connected = false; + this.connectionInfo = null; + + this.bytesSent = 0; + this.bytesReceived = 0; + this.isOpen = false; + this.port = null; + this.ports = []; + + this.connectionId = null; + this.reading = false; + this.selectedDevice = null; + + this.connect = this.connect.bind(this); + this.disconnect = this.disconnect.bind(this); + this.handleDataEvent = this.handleDataEvent.bind(this); + this.handleConnectedEvent = this.handleConnectedEvent.bind(this); + this.handleAttachedEvent = this.handleAttachedEvent.bind(this); + this.handleDetachedEvent = this.handleDetachedEvent.bind(this); + this.handleErrorEvent = this.handleErrorEvent.bind(this); + + // Set up plugin event listeners + this.setupPluginListeners(); + } + + setupPluginListeners() { + // Listen for data from the USB serial plugin + UsbSerial.addListener("data", this.handleDataEvent); + + // Listen for connection events + UsbSerial.addListener("connected", this.handleConnectedEvent); + + // Listen for device attach/detach + UsbSerial.addListener("attached", this.handleAttachedEvent); + UsbSerial.addListener("detached", this.handleDetachedEvent); + + // Listen for errors + UsbSerial.addListener("error", this.handleErrorEvent); + } + + cleanup() { + // Don't remove listeners - they need to persist for reconnection and device attach/detach events + // Only cleanup is handled in disconnect() by resetting state + } + + handleDataEvent(event) { + if (event?.data) { + // Convert hex string from plugin to Uint8Array + const uint8Array = this.hexStringToUint8Array(event.data); + this.bytesReceived += uint8Array.byteLength; + this.dispatchEvent(new CustomEvent("receive", { detail: uint8Array })); + } + } + + handleConnectedEvent(event) { + this.connected = true; + } + + async handleAttachedEvent(event) { + // Reload device list to ensure we have the latest device info with proper permissions + await this.loadDevices(); + + const added = this.handleNewDevice(event); + if (added) { + this.dispatchEvent(new CustomEvent("addedDevice", { detail: added })); + } + } + + handleDetachedEvent(event) { + this.handleDeviceRemoval(event); + } + + handleErrorEvent(event) { + console.error(`${logHead} Error:`, event.error); + this.dispatchEvent(new CustomEvent("error", { detail: event.error })); + } + + hexStringToUint8Array(hexString) { + const length = hexString.length / 2; + const uint8Array = new Uint8Array(length); + + for (let i = 0; i < length; i++) { + uint8Array[i] = Number.parseInt(hexString.slice(i * 2, i * 2 + 2), 16); + } + + return uint8Array; + } + + arrayBufferToHexString(buffer) { + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + } + + getConnectedPort() { + return this.connectionId; + } + + handleNewDevice(device) { + const added = this.createPort(device); + if (added) { + this.ports.push(added); + } + return added; + } + + createPort(device) { + // Avoid duplicates + if (this.ports.some((p) => p.vendorId === device.vendorId && p.productId === device.productId)) { + return null; + } + const displayName = vendorIdNames[device.vendorId] + ? vendorIdNames[device.vendorId] + : `VID:${device.vendorId} PID:${device.productId}`; + + return { + path: "serial", + displayName: `Betaflight ${displayName}`, + vendorId: device.vendorId, + productId: device.productId, + deviceId: device.deviceId, + }; + } + + handleDeviceRemoval(device) { + const index = this.ports.findIndex((p) => p.vendorId === device.vendorId && p.productId === device.productId); + if (index !== -1) { + const removed = this.ports.splice(index, 1)[0]; + this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed })); + } + } + + async loadDevices() { + try { + const result = await UsbSerial.connectedDevices(); + this.ports = []; + + if (result?.devices && Array.isArray(result.devices)) { + for (const device of result.devices) { + const port = this.createPort(device.device); + if (port) { + this.ports.push(port); + } + } + } + } catch (error) { + console.error(`${logHead} Error loading devices:`, error); + } + } + + async getDevices() { + await this.loadDevices(); + return this.ports; + } + + async connect(path, options = { baudRate: 115200 }) { + try { + const device = this.ports.find((device) => device.path === path); + + if (!device) { + console.error(`${logHead} Device not found`); + this.dispatchEvent(new CustomEvent("connect", { detail: false })); + return false; + } + + await UsbSerial.openSerial({ + deviceId: device.deviceId, + portNum: 0, + baudRate: options.baudRate || 115200, + dataBits: 8, + stopBits: 1, + parity: 0, + dtr: false, + rts: false, + }); + + this.isOpen = true; + this.connectionId = path; + this.connected = true; + this.port = device; + + this.dispatchEvent(new CustomEvent("connect", { detail: true })); + + return true; + } catch (error) { + console.error(`${logHead} Error opening serial connection:`, error); + this.dispatchEvent(new CustomEvent("connect", { detail: false })); + return false; + } + } + + async requestPermissionDevice() { + try { + await this.loadDevices(); + + // Try to find a matching device from our known serial devices list + for (const { vendorId, productId } of serialDevices) { + const device = this.ports.find((p) => p.vendorId === vendorId && p.productId === productId); + if (device) { + this.selectedDevice = device; + return device; + } + } + + // If no known device found, select the first available device + if (this.ports.length > 0) { + this.selectedDevice = this.ports[0]; + return this.selectedDevice; + } + + return null; + } catch (error) { + console.error(`${logHead} Error requesting device permission:`, error); + return null; + } + } + + async send(data, callback) { + if (!this.isOpen) { + console.error(`${logHead} Cannot send - not connected`); + callback?.({ bytesSent: 0 }); + return false; + } + + try { + // Handle both Uint8Array and ArrayBuffer + const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data); + + // Convert to hex string for the plugin + const hexString = this.arrayBufferToHexString(uint8Array); + + await UsbSerial.writeSerial({ data: hexString }); + + const bytesSent = uint8Array.byteLength; + this.bytesSent += bytesSent; + + callback?.({ bytesSent }); + return true; + } catch (error) { + console.error(`${logHead} Error sending data:`, error); + callback?.({ bytesSent: 0 }); + return false; + } + } + + async disconnect(callback) { + if (!this.isOpen) { + callback?.(true); + return; + } + + this.reading = false; + let closeError = null; + + try { + await UsbSerial.closeSerial(); + } catch (error) { + console.error(`${logHead} Error closing serial connection:`, error); + closeError = error; + } finally { + // Reset state but keep listeners active for reconnection + this.isOpen = false; + this.connected = false; + this.port = null; + this.connectionId = null; + this.dispatchEvent(new CustomEvent("disconnect", { detail: true })); + } + + callback?.(!closeError); + + if (closeError) { + throw new Error("Failed to close serial port", { cause: closeError }); + } + } +} + +export default CapacitorSerialProtocol; diff --git a/src/js/serial.js b/src/js/serial.js index d9a1a57047..edaf18d136 100644 --- a/src/js/serial.js +++ b/src/js/serial.js @@ -2,6 +2,8 @@ import WebSerial from "./protocols/WebSerial.js"; import WebBluetooth from "./protocols/WebBluetooth.js"; import Websocket from "./protocols/WebSocket.js"; import VirtualSerial from "./protocols/VirtualSerial.js"; +import { isAndroid } from "./utils/checkCompatibility.js"; +import CapacitorSerial from "./protocols/CapacitorSerial.js"; /** * Base Serial class that manages all protocol implementations @@ -16,12 +18,19 @@ class Serial extends EventTarget { this.logHead = "[SERIAL]"; // Initialize protocols with metadata for easier lookup - this._protocols = [ - { name: "webserial", instance: new WebSerial() }, - { name: "webbluetooth", instance: new WebBluetooth() }, - { name: "websocket", instance: new Websocket() }, - { name: "virtual", instance: new VirtualSerial() }, - ]; + + if (isAndroid()) { + this._protocols = [{ name: "serial", instance: new CapacitorSerial() }]; + } else { + this._protocols = [ + { name: "serial", instance: new WebSerial() }, + { name: "bluetooth", instance: new WebBluetooth() }, + { name: "websocket", instance: new Websocket() }, + ]; + } + + // Always add virtual protocol + this._protocols.push({ name: "virtual", instance: new VirtualSerial() }); // Forward events from all protocols to the Serial class this._setupEventForwarding(); @@ -74,7 +83,7 @@ class Serial extends EventTarget { // Determine which protocol to use based on port path const isFn = typeof portPath === "function"; const s = typeof portPath === "string" ? portPath : ""; - // Default to webserial for typical serial device identifiers. + // Default to serial for typical serial device identifiers. if (isFn || s === "virtual") { return this._protocols.find((p) => p.name === "virtual")?.instance; } @@ -82,9 +91,9 @@ class Serial extends EventTarget { return this._protocols.find((p) => p.name === "websocket")?.instance; } if (s.startsWith("bluetooth")) { - return this._protocols.find((p) => p.name === "webbluetooth")?.instance; + return this._protocols.find((p) => p.name === "bluetooth")?.instance; } - return this._protocols.find((p) => p.name === "webserial")?.instance; + return this._protocols.find((p) => p.name === "serial")?.instance; } /** @@ -141,7 +150,7 @@ class Serial extends EventTarget { /** * Get devices from a specific protocol type or current protocol - * @param {string} protocolType - Optional protocol type ('webserial', 'webbluetooth', 'websocket', 'virtual') + * @param {string} protocolType - Optional protocol type ('serial', 'bluetooth', 'websocket', 'virtual') * @returns {Promise} - List of devices */ async getDevices(protocolType = null) { diff --git a/src/js/utils/checkBrowserCompatibility.js b/src/js/utils/checkCompatibility.js similarity index 61% rename from src/js/utils/checkBrowserCompatibility.js rename to src/js/utils/checkCompatibility.js index da3e0fdc20..d0a0a01b3b 100644 --- a/src/js/utils/checkBrowserCompatibility.js +++ b/src/js/utils/checkCompatibility.js @@ -4,8 +4,8 @@ import { Capacitor } from "@capacitor/core"; // Returns standardized OS name string or "unknown" export function getOS() { let os = "unknown"; - const userAgent = window.navigator.userAgent; - const platform = window.navigator?.userAgentData?.platform || window.navigator.platform; + const userAgent = globalThis.navigator.userAgent; + const platform = globalThis.navigator?.userAgentData?.platform; const macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K", "macOS"]; const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; const iosPlatforms = ["iPhone", "iPad", "iPod"]; @@ -53,17 +53,10 @@ export function isIOS() { return false; } -export function isCapacitorWeb() { - if (Capacitor.isNativePlatform()) { - return Capacitor.getPlatform() === "web"; - } - return false; -} - -export function checkBrowserCompatibility() { - const isWebSerial = checkWebSerialSupport(); - const isWebBluetooth = checkWebBluetoothSupport(); - const isWebUSB = checkWebUSBSupport(); +export function checkCompatibility() { + const hasSerialSupport = checkSerialSupport(); + const hasBluetoothSupport = checkBluetoothSupport(); + const hasUsbSupport = checkUsbSupport(); const isChromium = isChromiumBrowser(); const isNative = Capacitor.isNativePlatform(); @@ -72,17 +65,18 @@ export function checkBrowserCompatibility() { const isTestEnvironment = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined); - const compatible = isTestEnvironment || isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB)); + const compatible = + isTestEnvironment || isNative || (isChromium && (hasSerialSupport || hasBluetoothSupport || hasUsbSupport)); console.log("User Agent: ", navigator.userAgentData); console.log("Native: ", isNative); console.log("Chromium: ", isChromium); - console.log("Web Serial: ", isWebSerial); + console.log("Serial: ", hasSerialSupport); + console.log("Bluetooth: ", hasBluetoothSupport); + console.log("USB: ", hasUsbSupport); console.log("OS: ", getOS()); - console.log("Android: ", isAndroid()); console.log("iOS: ", isIOS()); - console.log("Capacitor web: ", isCapacitorWeb()); if (compatible) { return true; @@ -93,16 +87,16 @@ export function checkBrowserCompatibility() { errorMessage = "Betaflight app requires a Chromium based browser (Chrome, Chromium, Edge).
"; } - if (!isWebBluetooth) { - errorMessage += "
- Web Bluetooth API support is disabled."; + if (!hasBluetoothSupport) { + errorMessage += "
- Bluetooth API support is disabled."; } - if (!isWebSerial) { - errorMessage += "
- Web Serial API support is disabled."; + if (!hasSerialSupport) { + errorMessage += "
- Serial API support is disabled."; } - if (!isWebUSB) { - errorMessage += "
- Web USB API support is disabled."; + if (!hasUsbSupport) { + errorMessage += "
- USB API support is disabled."; } const newDiv = document.createElement("div"); @@ -131,44 +125,39 @@ 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; +export function checkSerialSupport() { + let result = false; + if (isAndroid()) { + result = true; + } else if (navigator.serial) { + result = true; + } else if (isIOS()) { + // Not implemented yet } - return true; + return result; } -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; +export function checkBluetoothSupport() { + let result = false; + if (isAndroid()) { + // Not implemented yet + } else if (navigator.bluetooth) { + result = true; + } else if (isIOS()) { + // Not implemented yet } - - return true; + return result; } -export function checkWebUSBSupport() { - if (!navigator.usb) { - console.error("Web USB API is not supported in this browser."); - return false; +export function checkUsbSupport() { + let result = false; + if (isAndroid()) { + // Not implemented yet + } else if (navigator.usb) { + result = true; + } else if (isIOS()) { + // Not implemented yet } - - if (isIOS()) { - console.error("Web USB API is not supported on iOS."); - return false; - } - - return true; + return result; } diff --git a/vite.config.js b/vite.config.js index 1d973f1ca8..5fa4e6898e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -120,6 +120,7 @@ export default defineConfig({ server: { port: 8000, strictPort: true, + host: "0.0.0.0", // Listen on all network interfaces for Android device access }, preview: { port: 8080, diff --git a/yarn.lock b/yarn.lock index ed71ba4025..91d2a6db92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2448,6 +2448,11 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -3203,6 +3208,11 @@ caniuse-lite@^1.0.30001737: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz#67fb92953edc536442f3c9da74320774aa523143" integrity sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw== +capacitor-plugin-usb-serial@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/capacitor-plugin-usb-serial/-/capacitor-plugin-usb-serial-0.0.6.tgz#21bc7dd4a08be81351914f90f116d93b587d101b" + integrity sha512-AHclGGPWsk/+kF2s6CdOCxAkZv8W+zutBJcSN869EHxGN/gYzyLc/AUn0wCNFwOY2M78yXmsQV72cHzZKnvMWQ== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -3250,7 +3260,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3302,6 +3312,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -5082,6 +5097,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + find@^0.2.8: version "0.2.9" resolved "https://registry.yarnpkg.com/find/-/find-0.2.9.tgz#4b73f1ff9e56ad91b76e716407fe5ffe6554bb8c" @@ -5249,6 +5271,15 @@ fs-extra@^1.0.0: jsonfile "^2.1.0" klaw "^1.0.0" +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^11.2.0, fs-extra@^11.3.0: version "11.3.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" @@ -6619,7 +6650,7 @@ is-windows@^1.0.1, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -6823,6 +6854,17 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -6856,6 +6898,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonpointer@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" @@ -6907,6 +6954,13 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -7413,7 +7467,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.8: +micromatch@^4.0.2, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -7917,6 +7971,14 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -8050,6 +8112,26 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== +patch-package@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" + integrity sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^10.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.2.4" + yaml "^2.2.2" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -8283,6 +8365,11 @@ postcss@^8.4.48, postcss@^8.5.3, postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9219,6 +9306,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -9488,7 +9580,7 @@ string-argv@^0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9506,15 +9598,6 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -9620,7 +9703,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9648,13 +9731,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9977,6 +10053,11 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + to-absolute-glob@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" @@ -10927,7 +11008,7 @@ workbox-window@7.3.0, workbox-window@^7.3.0: "@types/trusted-types" "^2.0.2" workbox-core "7.3.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10953,15 +11034,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -11053,6 +11125,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.2.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== + yaml@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"