diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90eabfbc31..8517709552 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,11 +126,11 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - - name: Setup Java 17 + - name: Setup Java 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Setup Android SDK uses: android-actions/setup-android@v3 @@ -151,6 +151,9 @@ jobs: - name: Initialize Tauri Android project run: yarn tauri android init --ci + - name: Patch Android manifest for USB support + run: bash scripts/tauri-patch-android.sh + - name: Build Tauri Android APK uses: tauri-apps/tauri-action@v0 with: diff --git a/ANDROID.md b/ANDROID.md index 40d18a1e64..df2ae4cac6 100644 --- a/ANDROID.md +++ b/ANDROID.md @@ -319,12 +319,6 @@ mkdir -p dist && touch dist/.gitkeep ## Available Scripts -### Emulator Management -- `yarn android:emu:list` - List all AVDs -- `yarn android:emu:check` - Check if emulator is running -- `yarn android:emu:start` - Start emulator with SwiftShader -- `yarn android:emu:start:host` - Start with host GPU - ### Tauri Android - `yarn tauri:dev:android` - Development build with hot reload - `yarn tauri:build:android` - Production release build diff --git a/android-env.sh b/android-env.sh deleted file mode 100755 index 05dea962b7..0000000000 --- a/android-env.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# Android SDK and NDK environment setup script for Tauri Android development - -# Set Android SDK path -export ANDROID_HOME="${ANDROID_SDK_HOME:-$HOME/Android/Sdk}" -export ANDROID_SDK_ROOT="$ANDROID_HOME" - -# Find the NDK version automatically (uses the first one found) -if [[ -d "$ANDROID_HOME/ndk" ]]; then - NDK_VERSION=$(ls -1 "$ANDROID_HOME/ndk" | sort -V | tail -n 1) # Pick highest version - if [[ -n "$NDK_VERSION" ]]; then - export NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION" - echo "Found NDK version: $NDK_VERSION" - else - echo "Warning: No NDK version found in $ANDROID_HOME/ndk" - echo "Please install NDK from Android Studio SDK Manager" - fi -else - echo "Warning: NDK directory not found at $ANDROID_HOME/ndk" - echo "Please install NDK from Android Studio SDK Manager" -fi - -# Add Android tools to PATH -export PATH="$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools" -if [[ -n "$NDK_HOME" ]]; then - export PATH="$PATH:$NDK_HOME" -fi - -# Verify the setup -echo "" -echo "Android environment variables set:" -echo "ANDROID_HOME=$ANDROID_HOME" -echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" -echo "NDK_HOME=$NDK_HOME" -echo "" - -# Check if SDK exists -if [[ ! -d "$ANDROID_HOME" ]]; then - echo "ERROR: Android SDK not found at $ANDROID_HOME" >&2 - echo "Please install Android SDK or update the ANDROID_HOME path in this script" >&2 - return 1 2>/dev/null || exit 1 -fi - -echo "Setup complete! You can now run: yarn tauri:dev:android" diff --git a/package.json b/package.json index 63a846b603..e3f51c663a 100644 --- a/package.json +++ b/package.json @@ -20,33 +20,12 @@ "android:run": "vite build && node capacitor.config.generator.mjs && npx cap run android", "android:sync": "vite build && node capacitor.config.generator.mjs && npx cap sync android", "android:release": "vite build && node capacitor.config.generator.mjs && npx cap build android --release", - "android:emu:list": "run-script-os", - "android:emu:list:win32": "%ANDROID_HOME%\\emulator\\emulator.exe -list-avds", - "android:emu:list:default": "$ANDROID_HOME/emulator/emulator -list-avds", - "android:emu:check": "run-script-os", - "android:emu:check:win32": "%ANDROID_HOME%\\platform-tools\\adb.exe devices", - "android:emu:check:default": "$ANDROID_HOME/platform-tools/adb devices | grep -q emulator && echo 'Emulator running' || echo 'No emulator'", - "android:emu:start": "run-script-os", - "android:emu:start:win32": "start \"\" \"%ANDROID_HOME%\\emulator\\emulator.exe\" -avd Medium_Phone_API_35 -gpu swiftshader_indirect -no-snapshot-load", - "android:emu:start:default": "QT_QPA_PLATFORM=xcb $ANDROID_HOME/emulator/emulator -avd ${AVD:-Medium_Phone_API_35} -gpu swiftshader_indirect -no-snapshot-load &", - "android:emu:start:host": "run-script-os", - "android:emu:start:host:win32": "start \"\" \"%ANDROID_HOME%\\emulator\\emulator.exe\" -avd Medium_Phone_API_35 -gpu host -no-snapshot-load", - "android:emu:start:host:default": "QT_QPA_PLATFORM=xcb $ANDROID_HOME/emulator/emulator -avd ${AVD:-Medium_Phone_API_35} -gpu host -no-snapshot-load &", - "android:adb:wait": "run-script-os", - "android:adb:wait:win32": "%ANDROID_HOME%\\platform-tools\\adb.exe wait-for-device && %ANDROID_HOME%\\platform-tools\\adb.exe shell \"while [ $(getprop sys.boot_completed) != 1 ]; do sleep 1; done\"", - "android:adb:wait:default": "$ANDROID_HOME/platform-tools/adb wait-for-device && until $ANDROID_HOME/platform-tools/adb shell getprop sys.boot_completed 2>/dev/null | grep -q 1; do sleep 1; done", - "android:adb:reverse": "run-script-os", - "android:adb:reverse:win32": "%ANDROID_HOME%\\platform-tools\\adb.exe reverse tcp:8000 tcp:8000", - "android:adb:reverse:default": "$ANDROID_HOME/platform-tools/adb reverse tcp:8000 tcp:8000", "format": "prettier --write {src,test}/**/*.{js,vue,css,less}", "storybook": "start-storybook -p 6006", "prepare": "husky install", "tauri:dev": "tauri dev", "tauri:build": "tauri build", - "tauri:dev:android": "run-script-os", - "tauri:dev:android:win32": "yarn android:emu:start && yarn android:adb:wait && yarn android:adb:reverse && tauri android dev", - "tauri:dev:android:default": "yarn android:emu:start && yarn android:adb:wait && yarn android:adb:reverse && tauri android dev", - "tauri:dev:android:with-dist": "yarn build && yarn tauri:dev:android", + "tauri:dev:android": "tauri android dev", "tauri:build:android": "tauri android build" }, "window": { @@ -94,6 +73,7 @@ "semver-min": "^0.7.2", "short-unique-id": "^5.2.0", "switchery-latest": "^0.8.2", + "tauri-plugin-serialplugin-api": "^2.21.1", "three": "^0.176.0", "tiny-emitter": "^2.1.0", "tippy.js": "^6.3.7", diff --git a/scripts/MainActivity.kt b/scripts/MainActivity.kt new file mode 100644 index 0000000000..c8c6aa4cd9 --- /dev/null +++ b/scripts/MainActivity.kt @@ -0,0 +1,165 @@ +package com.betaflight.app + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbManager +import android.os.Build +import android.os.Bundle +import android.util.Log + +class MainActivity : TauriActivity() { + private val TAG = "BetaflightUSB" + private val ACTION_USB_PERMISSION = "com.betaflight.configurator.USB_PERMISSION" + + private val usbReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + ACTION_USB_PERMISSION -> { + synchronized(this) { + val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + device?.let { + Log.d(TAG, "Permission granted for device ${it.deviceName}") + // Device is ready to use + } + } else { + Log.d(TAG, "Permission denied for device ${device?.deviceName}") + } + } + } + UsbManager.ACTION_USB_DEVICE_ATTACHED -> { + val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + device?.let { + Log.d(TAG, "USB device attached: ${it.deviceName}") + requestUsbPermission(it) + } + } + UsbManager.ACTION_USB_DEVICE_DETACHED -> { + val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + Log.d(TAG, "USB device detached: ${device?.deviceName}") + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Register USB broadcast receiver + val filter = IntentFilter().apply { + addAction(ACTION_USB_PERMISSION) + addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) + addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(usbReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(usbReceiver, filter) + } + + // Check if launched by USB device attachment + if (intent.action == UsbManager.ACTION_USB_DEVICE_ATTACHED) { + val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + device?.let { + Log.d(TAG, "App launched by USB device: ${it.deviceName}") + requestUsbPermission(it) + } + } else { + // Check for already connected devices + checkConnectedDevices() + } + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(usbReceiver) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + + if (intent.action == UsbManager.ACTION_USB_DEVICE_ATTACHED) { + val device: UsbDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + device?.let { + Log.d(TAG, "USB device attached via new intent: ${it.deviceName}") + requestUsbPermission(it) + } + } + } + + private fun checkConnectedDevices() { + val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager + val deviceList = usbManager.deviceList + + Log.d(TAG, "Checking connected USB devices: ${deviceList.size} found") + + deviceList.values.forEach { device -> + Log.d(TAG, "Device: ${device.deviceName}, VID: ${device.vendorId}, PID: ${device.productId}") + if (!usbManager.hasPermission(device)) { + Log.d(TAG, "No permission for ${device.deviceName}, requesting...") + requestUsbPermission(device) + } else { + Log.d(TAG, "Already have permission for ${device.deviceName}") + } + } + } + + private fun requestUsbPermission(device: UsbDevice) { + val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager + + if (usbManager.hasPermission(device)) { + Log.d(TAG, "Already have permission for ${device.deviceName}") + return + } + + // Use FLAG_IMMUTABLE for Android 12+ (API 31+) as required by Android 14+ (API 34+) + // when using implicit intents with PendingIntent + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val permissionIntent = PendingIntent.getBroadcast( + this as Context, + 0, + Intent(ACTION_USB_PERMISSION), + flags + ) + + Log.d(TAG, "Requesting USB permission for ${device.deviceName} (VID: ${device.vendorId}, PID: ${device.productId})") + usbManager.requestPermission(device, permissionIntent) + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..da246bfaec --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,88 @@ +# Android USB Support Scripts + +This directory contains scripts to configure Android USB serial support for the Betaflight Configurator Tauri app. + +## Scripts Overview + +### `tauri-patch-android.sh` (Main Orchestrator) +The main script that coordinates all Android USB configuration. Run this after `cargo tauri android init`. + +**What it does:** +- Calls all individual scripts in the correct order +- Provides overall progress reporting +- Ensures Android project exists before proceeding + +### `patch-android-manifest.sh` +Configures the Android manifest with USB permissions and intent filters. + +**What it does:** +- Adds `android.permission.USB_PERMISSION` permission +- Adds `android.hardware.usb.host` feature +- Adds USB device attach intent filter +- Adds metadata referencing the device filter XML + +### `create-device-filter.sh` +Creates the USB device filter XML file that defines supported USB devices. + +**What it does:** +- Creates `res/xml/device_filter.xml` +- Defines USB device filters for Betaflight-compatible devices: + - FT232R USB UART + - STM32 devices (various modes) + - CP210x devices + - GD32 devices + - AT32 devices + - APM32 devices + - Raspberry Pi Pico devices + +### `patch-gradle-settings.sh` +Configures project-level Gradle repositories in `settings.gradle.kts`. + +**What it does:** +- Adds jitpack.io repository to `dependencyResolutionManagement` +- Adds jitpack.io repository to `pluginManagement` +- Ensures Google Maven and Maven Central are available + +### `patch-app-gradle.sh` +Configures app-level Gradle dependencies and repositories. + +**What it does:** +- Adds jitpack.io repository to app module +- Adds `usb-serial-for-android:3.8.0` dependency +- Ensures dependency resolution works at module level + +## Usage + +After running `cargo tauri android init`, execute: + +```bash +bash scripts/tauri-patch-android.sh +``` + +This will run all configuration steps automatically. + +## Individual Script Usage + +You can also run individual scripts if needed: + +```bash +# Only update manifest permissions +bash scripts/patch-android-manifest.sh + +# Only update device filters +bash scripts/create-device-filter.sh + +# Only update Gradle settings +bash scripts/patch-gradle-settings.sh + +# Only update app dependencies +bash scripts/patch-app-gradle.sh +``` + +## Maintenance Benefits + +- **Separation of Concerns**: Each script handles one specific aspect +- **Independent Testing**: Scripts can be tested and debugged individually +- **Selective Updates**: Only run the scripts that need changes +- **Clear Documentation**: Each script has a focused purpose +- **Easier Maintenance**: Changes to one aspect don't affect others \ No newline at end of file diff --git a/scripts/build-android-local.sh b/scripts/build-android-local.sh new file mode 100755 index 0000000000..e7e13f9cb0 --- /dev/null +++ b/scripts/build-android-local.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +# Build Betaflight Android locally with required manifest/device filter patches +# Supports dev (debuggable) and release (signed) builds +# +# Usage: +# scripts/build-android-local.sh validate # fast checks, no build +# scripts/build-android-local.sh dev # debuggable build, auto-install +# scripts/build-android-local.sh release # release build, signed and install +# scripts/build-android-local.sh release \ +# --keystore /path/to/keystore.jks \ +# --storepass \ +# --keyalias \ +# --keypass +# +# Notes: +# - Ensures Android project is initialized +# - Applies USB permissions + intent filter + device_filter.xml +# - Injects usb-serial-for-android dependency and JitPack repository (fallback) +# - For release: signs APK with provided keystore or auto-generated debug keystore +# - Requires: Node/Yarn, Rust, Android SDK/NDK, apksigner (from build-tools) + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +GEN_ANDROID_DIR="$ROOT_DIR/src-tauri/gen/android" +MANIFEST_PATH="$GEN_ANDROID_DIR/app/src/main/AndroidManifest.xml" +MODE="${1:-dev}" +shift || true + +KEYSTORE="" +STOREPASS="" +KEYALIAS="" +KEYPASS="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --keystore) KEYSTORE="$2"; shift 2;; + --storepass) STOREPASS="$2"; shift 2;; + --keyalias) KEYALIAS="$2"; shift 2;; + --keypass) KEYPASS="$2"; shift 2;; + *) echo "Unknown argument: $1"; exit 1;; + esac +done + +cd "$ROOT_DIR" + +# Helper to run tauri via available toolchain (cargo-tauri preferred) +run_tauri() { + local args=("$@") + # Prefer cargo tauri if available + if command -v cargo >/dev/null 2>&1; then + if cargo tauri --version >/dev/null 2>&1; then + cargo tauri "${args[@]}" + return $? + fi + fi + # Try npx @tauri-apps/cli (no permanent install required) + if command -v npx >/dev/null 2>&1; then + npx --yes @tauri-apps/cli "${args[@]}" + return $? + fi + # Try yarn tauri if project has it + if command -v yarn >/dev/null 2>&1; then + yarn tauri "${args[@]}" + return $? + fi + printf "Error: No Tauri CLI found. Install one of:\n - cargo install tauri-cli (Rust-based)\n - npm i -g @tauri-apps/cli (Node-based)\nOr ensure 'npx' is available.\n" >&2 + return 127 +} + +# Ensure node modules and local vite exist +ensure_node_modules() { + if [[ ! -d "$ROOT_DIR/node_modules" || ! -x "$ROOT_DIR/node_modules/.bin/vite" ]]; then + echo " Installing web dependencies (yarn install)" + yarn install --silent || yarn install + fi +} + +# Fast validation mode (no build, no patch side-effects) +if [[ "$MODE" == "validate" ]]; then + echo "==> VALIDATE mode: running quick checks (no build)" + + echo "- Checking prerequisites" + command -v adb >/dev/null 2>&1 && echo " ✓ adb found" || echo " ✗ adb missing" + command -v keytool >/dev/null 2>&1 && echo " ✓ keytool found" || echo " ⚠ keytool missing (only needed for signing)" + command -v apksigner >/dev/null 2>&1 && echo " ✓ apksigner found" || echo " ⚠ apksigner not on PATH (will try ANDROID_HOME/build-tools)" + command -v yarn >/dev/null 2>&1 && echo " ✓ yarn found" || echo " ✗ yarn missing" + + echo "- Checking Android manifest path" + if [[ -f "$MANIFEST_PATH" ]]; then + echo " ✓ Manifest exists: $MANIFEST_PATH" + else + echo " ✗ Manifest missing (will be created by 'yarn tauri android init --ci')" + fi + + echo "- Dry syntax check for patch script" + bash -n "$ROOT_DIR/scripts/tauri-patch-android.sh" && echo " ✓ tauri-patch-android.sh syntax OK" || echo " ✗ patch script has syntax errors" + + echo "- Listing connected ADB devices" + adb devices + + echo "==> Validate finished. Use 'dev' for debuggable build or 'release' to sign/install." + exit 0 +fi + +# Helper to select Android device for dev builds +select_android_device() { + if ! command -v adb >/dev/null 2>&1; then + echo "adb not found. Please install Android SDK and ensure adb is in PATH." + exit 1 + fi + + echo "==> Checking for connected Android devices..." + local devices + devices=$(adb devices | grep -v "List of devices" | grep -v "^$" | awk '{print $1}') + + if [[ -z "$devices" ]]; then + echo "No devices connected. Please connect your tablet via wireless ADB." + echo "Run: adb connect :" + exit 1 + fi + + local device_count + device_count=$(echo "$devices" | wc -l) + + if [[ $device_count -eq 1 ]]; then + local device_id + device_id=$(echo "$devices" | head -1) + echo "Using device: $device_id" + export ANDROID_SERIAL="$device_id" + else + echo "Multiple devices found. Select one:" + select device_id in $devices; do + if [[ -n "$device_id" ]]; then + echo "Selected: $device_id" + export ANDROID_SERIAL="$device_id" + break + fi + done + fi +} + +echo "==> Checking Android project generation" +if [[ ! -f "$MANIFEST_PATH" ]]; then + echo " Android project not found, initializing..." + run_tauri android init --ci +fi + +# Always patch after init (init may regenerate files) +echo "==> Patching Android manifest and Gradle for USB serial support" +bash "$ROOT_DIR/scripts/tauri-patch-android.sh" + +if [[ "$MODE" == "dev" || "$MODE" == "debug" ]]; then + echo "==> Building debuggable APK (dev)" + echo " This enables WebView debugging so console logs are visible" + # Optional: build web assets so dev fallback exists + if [[ ! -d "$ROOT_DIR/dist" ]]; then + ensure_node_modules + echo " Building web assets (vite)" + yarn build + fi + select_android_device + run_tauri android dev + echo "==> Dev build complete and should be installed on the device." + exit 0 +fi + +if [[ "$MODE" != "release" ]]; then + echo "Error: unknown build mode '$MODE' (use 'dev' or 'release')" >&2 + exit 1 +fi + +echo "==> Building release APK" +run_tauri android build + +# Locate the unsigned universal APK produced by Gradle +UNSIGNED_APK=$(find "$GEN_ANDROID_DIR/app/build/outputs/apk" -type f -name "*-unsigned.apk" | head -1 || true) +if [[ -z "${UNSIGNED_APK}" ]]; then + echo "Error: Could not find unsigned APK under $GEN_ANDROID_DIR/app/build/outputs/apk" >&2 + exit 1 +fi + +echo " Found unsigned APK: $UNSIGNED_APK" +SIGNED_APK="${UNSIGNED_APK/-unsigned/-signed}" + +# Prepare signing +if [[ -z "$KEYSTORE" ]]; then + # Fallback to Android debug keystore (auto-generate if missing) + KEYSTORE="$HOME/.android/debug.keystore" + KEYALIAS="androiddebugkey" + STOREPASS="android" + KEYPASS="android" + + if [[ ! -f "$KEYSTORE" ]]; then + echo "==> Generating debug keystore at $KEYSTORE" + keytool -genkeypair -v \ + -keystore "$KEYSTORE" \ + -storepass "$STOREPASS" \ + -alias "$KEYALIAS" \ + -keypass "$KEYPASS" \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -dname "CN=Android Debug,O=Android,C=US" + fi +else + # Validate custom keystore args + if [[ -z "$STOREPASS" || -z "$KEYALIAS" || -z "$KEYPASS" ]]; then + echo "Error: When using --keystore, you must also specify --storepass, --keyalias and --keypass" >&2 + exit 1 + fi +fi + +# Find apksigner +if command -v apksigner >/dev/null 2>&1; then + APK_SIGNER="apksigner" +else + # Try Android SDK build-tools + if [[ -n "${ANDROID_HOME:-}" ]]; then + APK_SIGNER=$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -1 || true) + fi +fi + +if [[ -z "${APK_SIGNER:-}" ]]; then + echo "Error: apksigner not found. Install Android build-tools and ensure it's on PATH." >&2 + exit 1 +fi + +echo "==> Signing APK" +"$APK_SIGNER" sign \ + --ks "$KEYSTORE" \ + --ks-key-alias "$KEYALIAS" \ + --ks-pass pass:"$STOREPASS" \ + --key-pass pass:"$KEYPASS" \ + --out "$SIGNED_APK" \ + "$UNSIGNED_APK" + +echo "==> Verifying signature" +"$APK_SIGNER" verify -v "$SIGNED_APK" + +echo "==> Installing signed APK" +adb install -r "$SIGNED_APK" || true + +echo "==> Done. Installed: $SIGNED_APK" diff --git a/scripts/create-device-filter.sh b/scripts/create-device-filter.sh new file mode 100755 index 0000000000..b9a1a9ff4c --- /dev/null +++ b/scripts/create-device-filter.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Script to create USB device filter XML for Betaflight-compatible devices +# This should be run after 'cargo tauri android init' or 'cargo tauri android build' + +set -e + +DEVICE_FILTER_PATH="src-tauri/gen/android/app/src/main/res/xml/device_filter.xml" + +echo "Creating USB device filter..." + +mkdir -p "$(dirname "$DEVICE_FILTER_PATH")" +cat > "$DEVICE_FILTER_PATH" << 'EOF' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EOF + +echo "✓ USB device filter created successfully!" \ No newline at end of file diff --git a/scripts/patch-android-manifest.sh b/scripts/patch-android-manifest.sh new file mode 100755 index 0000000000..820c5ea658 --- /dev/null +++ b/scripts/patch-android-manifest.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Script to patch the Android manifest with USB permissions and intent filters +# This should be run after 'cargo tauri android init' or 'cargo tauri android build' + +set -e + +MANIFEST_PATH="src-tauri/gen/android/app/src/main/AndroidManifest.xml" + +if [ ! -f "$MANIFEST_PATH" ]; then + echo "Error: Android manifest not found at $MANIFEST_PATH" + echo "Please run 'cargo tauri android init' first" + exit 1 +fi + +echo "Patching Android manifest for USB serial support..." + +# Backup original manifest +cp "$MANIFEST_PATH" "$MANIFEST_PATH.bak" + +# Check if USB permissions already added +if grep -q "android.permission.USB_PERMISSION" "$MANIFEST_PATH"; then + echo "USB permissions already present in manifest" +else + # Add USB permissions before + # Using awk for portability across macOS and Linux + awk ' + /<\/manifest>/ { + print " " + print " " + print " " + } + { print } + ' "$MANIFEST_PATH" > "$MANIFEST_PATH.tmp" && mv "$MANIFEST_PATH.tmp" "$MANIFEST_PATH" + echo "Added USB permissions to manifest" +fi + +# Check if USB intent filter already added +if grep -q "USB_DEVICE_ATTACHED" "$MANIFEST_PATH"; then + echo "USB intent filter already present in manifest" +else + # Add USB device intent filter and metadata before + # Using awk for portability across macOS and Linux + awk ' + /<\/activity>/ && !found { + print " " + print " " + print " " + print " " + print "" + print " " + print " " + found=1 + } + { print } + ' "$MANIFEST_PATH" > "$MANIFEST_PATH.tmp" && mv "$MANIFEST_PATH.tmp" "$MANIFEST_PATH" + echo "Added USB intent filter to manifest" +fi + +echo "✓ Android manifest patched successfully!" \ No newline at end of file diff --git a/scripts/patch-app-gradle.sh b/scripts/patch-app-gradle.sh new file mode 100755 index 0000000000..509b6f5558 --- /dev/null +++ b/scripts/patch-app-gradle.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Script to patch app/build.gradle.kts with USB serial dependencies and repositories +# This should be run after 'cargo tauri android init' or 'cargo tauri android build' + +set -e + +APP_BUILD_GRADLE="src-tauri/gen/android/app/build.gradle.kts" +echo "Ensuring usb-serial-for-android dependency and repositories in app/build.gradle.kts..." + +if [ -f "$APP_BUILD_GRADLE" ]; then + # Add repositories block to app/build.gradle.kts if missing + if ! grep -q "^repositories {" "$APP_BUILD_GRADLE"; then + echo "Adding repositories block to app/build.gradle.kts..." + # Insert repositories block after plugins block + awk ' + /^plugins \{/ { + print $0 + in_plugins=1 + next + } + in_plugins && /^\}/ { + print $0 + print "" + print "repositories {" + print " maven { url = uri(\"https://jitpack.io\") }" + print "}" + in_plugins=0 + next + } + { print } + ' "$APP_BUILD_GRADLE" > "$APP_BUILD_GRADLE.tmp" && mv "$APP_BUILD_GRADLE.tmp" "$APP_BUILD_GRADLE" + else + # Check if jitpack.io is already in repositories + if ! grep -A 10 "^repositories {" "$APP_BUILD_GRADLE" | grep -q "jitpack.io"; then + echo "Adding jitpack.io repository to existing repositories block..." + # Insert jitpack.io repository into existing repositories block + awk ' + /^repositories \{/ { + print $0 + print " maven { url = uri(\"https://jitpack.io\") }" + found=1 + next + } + { print } + ' "$APP_BUILD_GRADLE" > "$APP_BUILD_GRADLE.tmp" && mv "$APP_BUILD_GRADLE.tmp" "$APP_BUILD_GRADLE" + fi + fi + + if ! grep -q "usb-serial-for-android" "$APP_BUILD_GRADLE"; then + echo "Adding usb-serial-for-android dependency to app module..." + # Find the dependencies block and add the dependency + if grep -q "^dependencies {" "$APP_BUILD_GRADLE"; then + # Insert after the opening dependencies { line + sed -i '/^dependencies {/a\ + implementation("com.github.mik3y:usb-serial-for-android:3.8.0") +' "$APP_BUILD_GRADLE" + else + # Create dependencies block if it doesn't exist + echo "Creating dependencies block with usb-serial-for-android..." + cat >> "$APP_BUILD_GRADLE" << 'EOF' + +dependencies { + implementation("com.github.mik3y:usb-serial-for-android:3.8.0") +} +EOF + fi + else + echo "usb-serial-for-android dependency already present in app module" + fi +else + echo "Warning: app/build.gradle.kts not found, cannot add usb-serial-for-android dependency" +fi + +echo "✓ App Gradle dependencies and repositories configured successfully!" \ No newline at end of file diff --git a/scripts/patch-gradle-settings.sh b/scripts/patch-gradle-settings.sh new file mode 100755 index 0000000000..1496741912 --- /dev/null +++ b/scripts/patch-gradle-settings.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# Script to patch settings.gradle.kts with required repositories +# This should be run after 'cargo tauri android init' or 'cargo tauri android build' + +set -e + +SETTINGS_GRADLE="src-tauri/gen/android/settings.gradle.kts" +echo "Ensuring required repositories in settings.gradle.kts..." + +if [ ! -f "$SETTINGS_GRADLE" ]; then + echo "Creating settings.gradle.kts with required repository blocks..." + cat > "$SETTINGS_GRADLE" << 'EOF' +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +EOF +else + echo "Patching existing settings.gradle.kts..." + + # Add dependencyResolutionManagement repositories if missing + if ! grep -q "dependencyResolutionManagement" "$SETTINGS_GRADLE"; then + echo "Adding dependencyResolutionManagement block..." + cat >> "$SETTINGS_GRADLE" << 'EOF' + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +EOF + else + # Check and add missing repositories in dependencyResolutionManagement + if ! grep -A 10 "dependencyResolutionManagement" "$SETTINGS_GRADLE" | grep -q "google()"; then + sed -i '/dependencyResolutionManagement {/,/}/ { /repositories {/a\ + google() +}' "$SETTINGS_GRADLE" + fi + if ! grep -A 10 "dependencyResolutionManagement" "$SETTINGS_GRADLE" | grep -q "mavenCentral()"; then + sed -i '/dependencyResolutionManagement {/,/}/ { /repositories {/a\ + mavenCentral() +}' "$SETTINGS_GRADLE" + fi + if ! grep -A 10 "dependencyResolutionManagement" "$SETTINGS_GRADLE" | grep -q "jitpack.io"; then + sed -i '/dependencyResolutionManagement {/,/}/ { /repositories {/a\ + maven { url = uri("https://jitpack.io") } +}' "$SETTINGS_GRADLE" + fi + fi + + # Add pluginManagement repositories if missing + if ! grep -q "pluginManagement" "$SETTINGS_GRADLE"; then + echo "Adding pluginManagement block..." + cat >> "$SETTINGS_GRADLE" << 'EOF' + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +EOF + else + # Check and add missing repositories in pluginManagement + if ! grep -A 10 "pluginManagement" "$SETTINGS_GRADLE" | grep -q "gradlePluginPortal()"; then + # Using awk for portability across macOS and Linux + awk ' + /pluginManagement \{/,/\}/ { + if ($0 ~ /repositories \{/ && !found) { + print $0 + print " gradlePluginPortal()" + found=1 + next + } + } + { print } + ' "$SETTINGS_GRADLE" > "$SETTINGS_GRADLE.tmp" && mv "$SETTINGS_GRADLE.tmp" "$SETTINGS_GRADLE" + fi + if ! grep -A 10 "pluginManagement" "$SETTINGS_GRADLE" | grep -q "google()"; then + # Using awk for portability across macOS and Linux + awk ' + /pluginManagement \{/,/\}/ { + if ($0 ~ /repositories \{/ && !found) { + print $0 + print " google()" + found=1 + next + } + } + { print } + ' "$SETTINGS_GRADLE" > "$SETTINGS_GRADLE.tmp" && mv "$SETTINGS_GRADLE.tmp" "$SETTINGS_GRADLE" + fi + if ! grep -A 10 "pluginManagement" "$SETTINGS_GRADLE" | grep -q "mavenCentral()"; then + # Using awk for portability across macOS and Linux + awk ' + /pluginManagement \{/,/\}/ { + if ($0 ~ /repositories \{/ && !found) { + print $0 + print " mavenCentral()" + found=1 + next + } + } + { print } + ' "$SETTINGS_GRADLE" > "$SETTINGS_GRADLE.tmp" && mv "$SETTINGS_GRADLE.tmp" "$SETTINGS_GRADLE" + fi + if ! grep -A 10 "pluginManagement" "$SETTINGS_GRADLE" | grep -q "jitpack.io"; then + # Using awk for portability across macOS and Linux + awk ' + /pluginManagement \{/,/\}/ { + if ($0 ~ /repositories \{/ && !found) { + print $0 + print " maven { url = uri(\"https://jitpack.io\") }" + found=1 + next + } + } + { print } + ' "$SETTINGS_GRADLE" > "$SETTINGS_GRADLE.tmp" && mv "$SETTINGS_GRADLE.tmp" "$SETTINGS_GRADLE" + fi + fi +fi + +echo "✓ Gradle settings repositories configured successfully!" \ No newline at end of file diff --git a/scripts/tauri-patch-android.sh b/scripts/tauri-patch-android.sh new file mode 100755 index 0000000000..f07de2bf5d --- /dev/null +++ b/scripts/tauri-patch-android.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Main script to patch Android project for USB serial support +# This orchestrates separate scripts for different patching aspects +# Run after 'cargo tauri android init' or 'cargo tauri android build' + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +MANIFEST_PATH="src-tauri/gen/android/app/src/main/AndroidManifest.xml" + +if [ ! -f "$MANIFEST_PATH" ]; then + echo "Error: Android manifest not found at $MANIFEST_PATH" + echo "Please run 'cargo tauri android init' first" + exit 1 +fi + +echo "Starting Android USB support configuration..." + +# Run individual patching scripts +echo "1. Patching Android manifest..." +bash "$SCRIPT_DIR/patch-android-manifest.sh" + +echo "2. Creating USB device filter..." +bash "$SCRIPT_DIR/create-device-filter.sh" + +echo "3. Configuring Gradle settings repositories..." +bash "$SCRIPT_DIR/patch-gradle-settings.sh" + +echo "4. Configuring app Gradle dependencies..." +bash "$SCRIPT_DIR/patch-app-gradle.sh" + +echo "5. Applying custom MainActivity (if provided)..." + +# If a custom MainActivity.kt exists in scripts/, copy it into the generated Android project +CUSTOM_MAIN_ACTIVITY_SRC="$SCRIPT_DIR/MainActivity.kt" +CUSTOM_MAIN_ACTIVITY_DST_DIR="src-tauri/gen/android/app/src/main/java/com/betaflight/app" +CUSTOM_MAIN_ACTIVITY_DST="$CUSTOM_MAIN_ACTIVITY_DST_DIR/MainActivity.kt" + +if [ -f "$CUSTOM_MAIN_ACTIVITY_SRC" ]; then + mkdir -p "$CUSTOM_MAIN_ACTIVITY_DST_DIR" + cp "$CUSTOM_MAIN_ACTIVITY_SRC" "$CUSTOM_MAIN_ACTIVITY_DST" + echo " - Custom MainActivity applied to $CUSTOM_MAIN_ACTIVITY_DST" +else + echo " - No custom MainActivity found at $CUSTOM_MAIN_ACTIVITY_SRC; skipping" +fi + +echo "" +echo "✓ Android USB support configuration complete!" +echo "You can now build the Android app with: cargo tauri android build" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ffc2c16029..7793e155b1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -47,6 +47,137 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "atk" version = "0.18.2" @@ -100,11 +231,10 @@ version = "2025.12.0" dependencies = [ "serde", "serde_json", - "serialport", "tauri", "tauri-build", + "tauri-plugin-opener", "tauri-plugin-serialplugin", - "tauri-plugin-shell", ] [[package]] @@ -149,6 +279,19 @@ dependencies = [ "objc2 0.6.3", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "8.0.2" @@ -266,9 +409,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -335,6 +478,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -458,7 +610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -468,7 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -492,7 +644,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -503,7 +655,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -526,7 +678,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -584,7 +736,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -607,7 +759,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -667,12 +819,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "endi" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "cfg-if", + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", ] [[package]] @@ -692,6 +862,43 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -751,7 +958,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -811,6 +1018,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -819,7 +1039,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1069,7 +1289,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1148,7 +1368,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1175,6 +1395,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1314,9 +1540,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1327,9 +1553,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1340,11 +1566,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1355,42 +1580,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1475,9 +1696,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1555,9 +1776,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1684,11 +1905,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -1742,7 +1969,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1861,6 +2088,19 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1901,7 +2141,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2193,13 +2433,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "os_pipe" -version = "1.2.3" +name = "ordered-stream" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ - "libc", - "windows-sys 0.61.2", + "futures-core", + "pin-project-lite", ] [[package]] @@ -2227,6 +2467,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2366,7 +2612,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2408,6 +2654,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2440,11 +2697,25 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -2686,7 +2957,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2762,6 +3033,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2812,9 +3096,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -2831,7 +3115,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2907,7 +3191,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2918,7 +3202,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2942,7 +3226,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2987,7 +3271,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -3003,7 +3287,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3025,7 +3309,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3041,7 +3325,7 @@ dependencies = [ "io-kit-sys", "libudev", "mach2", - "nix", + "nix 0.26.4", "quote", "scopeguard", "unescaper", @@ -3069,44 +3353,12 @@ dependencies = [ "digest", ] -[[package]] -name = "shared_child" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" -dependencies = [ - "libc", - "sigchld", - "windows-sys 0.60.2", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "sigchld" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" -dependencies = [ - "libc", - "os_pipe", - "signal-hook", -] - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -3210,6 +3462,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -3265,9 +3523,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -3291,7 +3549,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3355,7 +3613,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3366,9 +3624,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" dependencies = [ "anyhow", "bytes", @@ -3455,7 +3713,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.108", + "syn 2.0.109", "tauri-utils", "thiserror 2.0.17", "time", @@ -3473,7 +3731,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "tauri-codegen", "tauri-utils", ] @@ -3496,38 +3754,39 @@ dependencies = [ ] [[package]] -name = "tauri-plugin-serialplugin" -version = "2.21.0" +name = "tauri-plugin-opener" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a23ab6d07a643246533ac7ada2921a8d7a858c3245913019ef56d3f4093761" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "open", + "schemars 0.8.22", "serde", "serde_json", - "serialport", "tauri", "tauri-plugin", "thiserror 2.0.17", + "url", + "windows", + "zbus", ] [[package]] -name = "tauri-plugin-shell" -version = "2.3.1" +name = "tauri-plugin-serialplugin" +version = "2.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" +checksum = "8137d7225bfe0a0add4d58eb6417d6f5d1a5755d97e5aa4c41ca6ae10030a754" dependencies = [ - "encoding_rs", - "log", - "open", - "os_pipe", - "regex", - "schemars 0.8.22", "serde", "serde_json", - "shared_child", + "serialport", "tauri", "tauri-plugin", "thiserror 2.0.17", - "tokio", ] [[package]] @@ -3630,6 +3889,19 @@ dependencies = [ "toml 0.9.8", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3667,7 +3939,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3678,7 +3950,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3714,9 +3986,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3738,9 +4010,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -3897,9 +4169,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -3949,6 +4233,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unescaper" version = "0.1.6" @@ -4001,9 +4296,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -4061,9 +4356,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -4133,9 +4428,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -4144,25 +4439,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.108", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -4173,9 +4454,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4183,22 +4464,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", - "wasm-bindgen-backend", + "syn 2.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -4218,9 +4499,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -4292,7 +4573,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4419,7 +4700,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4430,7 +4711,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4776,9 +5057,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" @@ -4848,11 +5129,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4860,16 +5140,77 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -4887,7 +5228,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4907,15 +5248,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -4924,9 +5265,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4935,11 +5276,51 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.109", + "winnow 0.7.13", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9a57e3c3c2..e5e5b8d472 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,25 +8,21 @@ repository = "https://github.com/betaflight/betaflight-configurator" edition = "2024" [lib] -name = "betaflight_app" -path = "src/lib.rs" -crate-type = ["staticlib", "cdylib"] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "betaflight_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2.5", features = [] } [dependencies] tauri = { version = "2.9", features = [] } -tauri-plugin-shell = "2.3" -# Allow newer serial plugin releases (2.21+) which may include Android fixes serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -tauri-plugin-serialplugin = "2.21" - -[target.'cfg(target_os = "android")'.dependencies] -serialport = "4.8" +tauri-plugin-opener = "2.5.2" +tauri-plugin-serialplugin = "2.21.1" [features] default = ["custom-protocol"] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index daf29bb683..69797cb2d3 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,11 +2,11 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Default capability for all windows, allows core and shell open permissions.", - "platforms": ["linux", "windows", "macOS"], + "platforms": ["linux", "windows", "macOS", "android", "iOS"], "windows": ["*"], "permissions": [ "core:default", - "shell:allow-open", + "opener:default", "serialplugin:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 84ca3eba68..34c05bd354 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,8 @@ -#[cfg(any(target_os = "android", target_os = "ios"))] -#[tauri::mobile_entry_point] -fn mobile_entry() { +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_serialplugin::init()) + .plugin(tauri_plugin_opener::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fbecbd774f..28130e3b0b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,28 +1,6 @@ -#![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" -)] +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - let mut builder = tauri::Builder::default() - .plugin(tauri_plugin_shell::init()); - - #[cfg(target_os = "android")] - { - // Local Android serial plugin that mirrors the commands used by the frontend - builder = builder.plugin(crate::serial_android::init_android()); - } - - #[cfg(not(target_os = "android"))] - { - // Desktop: use official plugin for now - builder = builder.plugin(tauri_plugin_serialplugin::init()); - } - - builder - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + betaflight_app_lib::run() } - -#[cfg(target_os = "android")] -mod serial_android; diff --git a/src-tauri/src/serial_android.rs b/src-tauri/src/serial_android.rs deleted file mode 100644 index 629748c607..0000000000 --- a/src-tauri/src/serial_android.rs +++ /dev/null @@ -1,120 +0,0 @@ -#[cfg(target_os = "android")] -use std::{collections::HashMap, io::{Read, Write}, sync::Mutex, time::Duration}; - -#[cfg(target_os = "android")] -use tauri::{plugin::Builder as PluginBuilder, Manager, Runtime, State}; - -#[cfg(target_os = "android")] -use serialport::{SerialPort, SerialPortType}; - -#[cfg(target_os = "android")] -type PortMap = Mutex>; - -#[cfg(target_os = "android")] -struct PortEntry { - port: Box, -} - -#[cfg(target_os = "android")] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct OpenOptions { - path: String, - #[serde(default = "default_baud")] - baudRate: u32, -} - -#[cfg(target_os = "android")] -fn default_baud() -> u32 { 115_200 } - -#[cfg(target_os = "android")] -#[tauri::command] -fn available_ports_android() -> Result { - // Android: serialport enumeration is limited. Probe common device nodes. - let candidates = ["/dev/ttyACM0", "/dev/ttyUSB0"]; - let mut map = serde_json::Map::new(); - for path in candidates { - if std::fs::metadata(path).is_ok() { - // Provide fake but plausible VID/PID so UI filter accepts it. - // STM32 VCP: VID 1155 (0x0483), PID 22336 (0x57C0) - map.insert(path.to_string(), serde_json::json!({ - "vid": 1155, - "pid": 22336, - "serial_number": serde_json::Value::Null - })); - } - } - Ok(serde_json::Value::Object(map)) -} - -#[cfg(target_os = "android")] -#[tauri::command] -fn open_android(state: State<'_, PortMap>, opts: OpenOptions) -> Result { - let port = serialport::new(&opts.path, opts.baudRate) - .timeout(Duration::from_millis(100)) - .open() - .map_err(|e| format!("failed to open: {e}"))?; - let mut map = state.lock().unwrap(); - map.insert(opts.path, PortEntry { port }); - Ok(true) -} - -#[cfg(target_os = "android")] -#[tauri::command] -fn set_timeout_android(state: State<'_, PortMap>, path: String, timeout: u64) -> Result { - let mut map = state.lock().unwrap(); - let entry = map.get_mut(&path).ok_or_else(|| "port not open".to_string())?; - entry.port.set_timeout(Duration::from_millis(timeout)).map_err(|e| e.to_string())?; - Ok(true) -} - -#[cfg(target_os = "android")] -#[tauri::command] -fn read_binary_android(state: State<'_, PortMap>, path: String, size: usize, timeout: Option) -> Result, String> { - let mut map = state.lock().unwrap(); - let entry = map.get_mut(&path).ok_or_else(|| "port not open".to_string())?; - if let Some(ms) = timeout { let _ = entry.port.set_timeout(Duration::from_millis(ms)); } - let mut buf = vec![0u8; size.max(1)]; - match entry.port.read(buf.as_mut_slice()) { - Ok(n) if n > 0 => { buf.truncate(n); Ok(buf) }, - Ok(_n) => Err("no data received".to_string()), - Err(e) => { - let msg = e.to_string(); - if msg.to_lowercase().contains("timed out") { Err("no data received".to_string()) } else { Err(msg) } - } - } -} - -#[cfg(target_os = "android")] -#[tauri::command] -fn write_binary_android(state: State<'_, PortMap>, path: String, value: Vec) -> Result { - let mut map = state.lock().unwrap(); - let entry = map.get_mut(&path).ok_or_else(|| "port not open".to_string())?; - entry.port.write_all(&value).map_err(|e| e.to_string())?; - Ok(value.len()) -} - -#[cfg(target_os = "android")] -#[tauri::command] -fn close_android(state: State<'_, PortMap>, path: String) -> Result { - let mut map = state.lock().unwrap(); - map.remove(&path); - Ok(true) -} - -#[cfg(target_os = "android")] -pub fn init_android() -> tauri::plugin::TauriPlugin { - PluginBuilder::new("serialplugin") - .setup(|app, _api| { - app.manage(Mutex::new(HashMap::::new())); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - available_ports_android, - open_android, - set_timeout_android, - read_binary_android, - write_binary_android, - close_android - ]) - .build() -} diff --git a/src/js/protocols/TauriSerial.js b/src/js/protocols/TauriSerial.js index 322fec2190..b8f810dd99 100644 --- a/src/js/protocols/TauriSerial.js +++ b/src/js/protocols/TauriSerial.js @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { SerialPort } from "tauri-plugin-serialplugin-api"; import { serialDevices, vendorIdNames } from "./devices"; const logHead = "[TAURI SERIAL]"; @@ -39,6 +39,7 @@ class TauriSerial extends EventTarget { this.connect = this.connect.bind(this); this.disconnect = this.disconnect.bind(this); this.handleReceiveBytes = this.handleReceiveBytes.bind(this); + this.handleDisconnect = this.handleDisconnect.bind(this); // Detect if running on macOS with AT32 (needs batch writes) this.isNeedBatchWrite = false; @@ -47,15 +48,24 @@ class TauriSerial extends EventTarget { this.monitoringDevices = false; this.deviceMonitorInterval = null; - this.loadDevices().then(() => this.startDeviceMonitoring()); + // this.loadDevices().then(() => this.startDeviceMonitoring()); + this.loadDevices(); } handleReceiveBytes(info) { this.bytesReceived += info.detail.byteLength; } + handleDisconnect() { + // Handle unexpected disconnections (e.g., device unplugged) + if (this.connected) { + console.log(`${logHead} Unexpected disconnect detected`); + this.disconnect(); + } + } + getConnectedPort() { - return this.connectionId; + return this.port; } handleFatalSerialError() { @@ -116,24 +126,53 @@ class TauriSerial extends EventTarget { }); } + /** + * Request USB permission for a device path (Android only). + * This triggers the permission dialog by attempting a dummy open. + * Usage: await tauriserial.requestPermissionDevice(path) + */ + async requestPermissionDevice() { + try { + console.log(`${logHead} Requesting USB permission for Android device`); + return true; + } catch (error) { + console.error(`${logHead} Error requesting USB permission:`, error); + return false; + } + } + /** * Filter ports to only include known Betaflight-compatible devices. * @private */ _filterToKnownDevices(ports) { - return ports.filter((port) => { - // Only include ports with known vendor IDs (Betaflight-compatible devices) + // Set to true to enable debug logs + const DEBUG = false; + if (DEBUG) { + console.log(`${logHead} Filtering ${ports.length} ports`); + } + const filtered = ports.filter((port) => { if (!port.vendorId || !port.productId) { + if (DEBUG) console.log(`${logHead} FILTERED OUT (no VID/PID): ${port.path}`); return false; } - // Check if this device is in our known devices list - return serialDevices.some((d) => d.vendorId === port.vendorId && d.productId === port.productId); + const isKnown = serialDevices.some((d) => d.vendorId === port.vendorId && d.productId === port.productId); + if (!isKnown && DEBUG) { + console.log( + `${logHead} FILTERED OUT (unknown device): ${port.path} VID:${port.vendorId} PID:${port.productId}`, + ); + } + return isKnown; }); + if (DEBUG) { + console.log(`${logHead} Returning ${filtered.length} filtered ports`); + } + return filtered; } async checkDeviceChanges() { try { - const portsMap = await invoke("plugin:serialplugin|available_ports"); + const portsMap = await SerialPort.available_ports(); // Convert to our format const allPorts = this._convertPortsMapToArray(portsMap); @@ -146,17 +185,17 @@ class TauriSerial extends EventTarget { (oldPort) => !currentPorts.some((newPort) => newPort.path === oldPort.path), ); - // Check for added devices - const addedPorts = currentPorts.filter( - (newPort) => !this.ports.some((oldPort) => oldPort.path === newPort.path), - ); - // Emit events for removed devices for (const removed of removedPorts) { this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed })); console.log(`${logHead} Device removed: ${removed.path}`); } + // Check for added devices + const addedPorts = currentPorts.filter( + (newPort) => !this.ports.some((oldPort) => oldPort.path === newPort.path), + ); + // Emit events for added devices for (const added of addedPorts) { this.dispatchEvent(new CustomEvent("addedDevice", { detail: added })); @@ -164,23 +203,47 @@ class TauriSerial extends EventTarget { } // Update our ports list + console.log(`${logHead} Device check complete. Current ports:`, currentPorts, this.ports); this.ports = currentPorts; } catch (error) { console.warn(`${logHead} Error checking device changes:`, error); } } + createPort(port) { + const displayName = vendorIdNames[port.vendorId] + ? vendorIdNames[port.vendorId] + : `VID:${port.vendorId} PID:${port.productId}`; + return { + path: port.path, + displayName: `Betaflight ${displayName}`, + vendorId: port.vendorId, + productId: port.productId, + port: port, + }; + } + async loadDevices() { try { - const portsMap = await invoke("plugin:serialplugin|available_ports"); - - // Convert the object map to array - const allPorts = this._convertPortsMapToArray(portsMap); + let newPorts = await SerialPort.available_ports(); + console.log(`${logHead} Loaded devices:`, newPorts); - // Filter to only known devices - this.ports = this._filterToKnownDevices(allPorts); + // ANDROID FIX: Check if result is a string (Android deserialization issue) + if (typeof newPorts === "string") { + console.log(`${logHead} Result is a string, attempting to parse...`); + try { + // The Android plugin returns a string like: "{/dev/bus/usb/002/002={type=USB, vid=1155, ...}}" + // We need to convert this to proper JSON + newPorts = this._parseAndroidPortsResponse(newPorts); + console.log(`${logHead} Parsed ports:`, newPorts); + } catch (parseError) { + console.error(`${logHead} Failed to parse string response:`, parseError); + return []; + } + } - console.log(`${logHead} Found ${this.ports.length} serial ports (filtered from ${allPorts.length})`); + const allPorts = this._convertPortsMapToArray(newPorts); + this.ports = allPorts.map((port) => this.createPort(port)); return this.ports; } catch (error) { console.error(`${logHead} Error loading devices:`, error); @@ -208,94 +271,70 @@ class TauriSerial extends EventTarget { this.openRequested = true; - try { - const openOptions = { - path, - baudRate: options.baudRate || 115200, - }; - - console.log(`${logHead} Opening port ${path} at ${openOptions.baudRate} baud`); + const port = { + path: path, + baudRate: options.baudRate || 115200, + }; - // Open the port - const openResult = await invoke("plugin:serialplugin|open", openOptions); - console.log(`${logHead} Open result:`, openResult); + try { + console.log(`${logHead} Connecting to ${path} with options:`, port); + this.port = new SerialPort(port); + const openResult = await this.port.open(); + console.log(`${logHead} Port opened successfully!`, openResult); + } catch (error) { + console.error(`${logHead} Error connecting:`, error); + } - // Set a reasonable timeout for read/write operations (100ms) - try { - await invoke("plugin:serialplugin|set_timeout", { - path, - timeout: 100, - }); - } catch (e) { - console.debug(`${logHead} Could not set timeout:`, e); - } + // Connection successful + this.connected = true; + this.connectionId = path; + this.bitrate = port.baudRate; + this.openRequested = false; - // Connection successful - this.connected = true; - this.connectionId = path; - this.bitrate = openOptions.baudRate; - this.openRequested = false; + this.connectionInfo = { + connectionId: path, + bitrate: this.bitrate, + }; - this.connectionInfo = { - connectionId: path, - bitrate: this.bitrate, - }; + this.addEventListener("receive", this.handleReceiveBytes); + // should we add disconnect handler here ? + this.addEventListener("disconnect", this.handleDisconnect); - this.addEventListener("receive", this.handleReceiveBytes); + // On mobile platforms, listen() events may not work reliably + // Use active polling with read() instead + this.reading = true; + this.readLoop(); - // Start reading - this.reading = true; - this.readLoop(); + this.dispatchEvent(new CustomEvent("connect", { detail: true })); + console.log(`${logHead} Connected to ${path}`); + return true; + } + catch(error) { + console.error(`${logHead} Error connecting:`, error); - this.dispatchEvent(new CustomEvent("connect", { detail: true })); - console.log(`${logHead} Connected to ${path}`); - return true; - } catch (error) { - console.error(`${logHead} Error connecting:`, error); - this.openRequested = false; - this.dispatchEvent(new CustomEvent("connect", { detail: false })); - return false; - } + this.openRequested = false; + this.dispatchEvent(new CustomEvent("connect", { detail: false })); + return false; } async readLoop() { - try { - while (this.reading) { - try { - // Non-blocking read with short timeout - const result = await invoke("plugin:serialplugin|read_binary", { - path: this.connectionId, - size: 256, - timeout: 10, - }); - - if (result && result.length > 0) { - this.dispatchEvent(new CustomEvent("receive", { detail: new Uint8Array(result) })); - } - - // Small delay between polls to avoid overwhelming the system - await new Promise((resolve) => setTimeout(resolve, 5)); - } catch (error) { - const msg = error?.message || (error?.toString ? error.toString() : ""); - // Timeout is expected when no data available - if (msg?.toLowerCase().includes("no data received")) { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - if (isBrokenPipeError(msg)) { - console.error(`${logHead} Fatal poll error (broken pipe) on ${this.connectionId}:`, error); - throw error; - } - console.warn(`${logHead} Poll error:`, error); - await new Promise((resolve) => setTimeout(resolve, 5)); + console.log(`${logHead} Starting read loop`); + while (this.reading) { + try { + const result = await this.port.read({ timeout: 100, size: 1024 }); + + if (result && Array.isArray(result) && result.length > 0) { + console.log(`${logHead} Read ${result.length} bytes`); + this.dispatchEvent(new CustomEvent("receive", { detail: new Uint8Array(result) })); } + + await new Promise((resolve) => setTimeout(resolve, 5)); + } catch (error) { + console.error(`${logHead} Read error:`, error); + await new Promise((resolve) => setTimeout(resolve, 100)); } - } catch (error) { - console.error(`${logHead} Error in read loop:`, error); - this.handleFatalSerialError(error); - } finally { - console.log(`${logHead} Polling stopped for ${this.connectionId || ""}`); } + console.log(`${logHead} Read loop stopped`); } async send(data, callback) { @@ -307,45 +346,16 @@ class TauriSerial extends EventTarget { } try { - // Convert data to Uint8Array - let dataArray; - if (data instanceof ArrayBuffer) { - dataArray = new Uint8Array(data); - } else if (data instanceof Uint8Array) { - dataArray = data; - } else if (Array.isArray(data)) { - dataArray = new Uint8Array(data); - } else { - console.error(`${logHead} Unsupported data type:`, data?.constructor?.name); - const res = { bytesSent: 0 }; - callback?.(res); - return res; - } - this.transmitting = true; - const writeChunk = async (chunk) => { - await invoke("plugin:serialplugin|write_binary", { - path: this.connectionId, - value: Array.from(chunk), - }); - }; - - if (this.isNeedBatchWrite) { - // Batch write for macOS AT32 compatibility - const batchSize = 63; - for (let offset = 0; offset < dataArray.length; offset += batchSize) { - const chunk = dataArray.slice(offset, offset + batchSize); - await writeChunk(chunk); - } - } else { - await writeChunk(dataArray); - } - + const dataArray = data instanceof ArrayBuffer ? new Uint8Array(data) : data; + console.log(`${logHead} Sending ${dataArray.length} bytes:`, Array.from(dataArray.slice(0, 20))); + const bytesWritten = await this.port.writeBinary(dataArray); + this.bytesSent += bytesWritten; this.transmitting = false; - this.bytesSent += dataArray.length; - const res = { bytesSent: dataArray.length }; + console.log(`${logHead} Sent ${bytesWritten} bytes successfully`); + const res = { bytesSent: this.bytesSent }; callback?.(res); return res; } catch (error) { @@ -376,6 +386,7 @@ class TauriSerial extends EventTarget { } this.closeRequested = true; + let result = false; try { this.removeEventListener("receive", this.handleReceiveBytes); @@ -384,32 +395,32 @@ class TauriSerial extends EventTarget { await new Promise((resolve) => setTimeout(resolve, 50)); // Close the port - if (this.connectionId) { + if (this.port) { try { - await invoke("plugin:serialplugin|close", { path: this.connectionId }); + await this.port.close(); console.log(`${logHead} Port closed`); - } catch (error) { - console.warn(`${logHead} Error closing port:`, error); + } catch (closeError) { + // Ignore deserialization errors on close - the port is closed anyway + console.warn(`${logHead} Error during port close (ignored):`, closeError); } } - this.connectionId = null; - this.bitrate = 0; - this.connectionInfo = null; - this.closeRequested = false; - this.dispatchEvent(new CustomEvent("disconnect", { detail: true })); - return true; + result = true; } catch (error) { console.error(`${logHead} Error disconnecting:`, error); this.closeRequested = false; this.dispatchEvent(new CustomEvent("disconnect", { detail: false })); - return false; + result = false; } finally { - if (this.openCanceled) { - this.openCanceled = false; - } + this.connectionId = null; + this.bitrate = 0; + this.connectionInfo = null; + this.closeRequested = false; + this.openCanceled = false; } + + return result; } async getDevices() { diff --git a/src/js/protocols/TauriSerial.js.bck b/src/js/protocols/TauriSerial.js.bck new file mode 100644 index 0000000000..ed532b3512 --- /dev/null +++ b/src/js/protocols/TauriSerial.js.bck @@ -0,0 +1,562 @@ +import { invoke } from "@tauri-apps/api/core"; +import { serialDevices, vendorIdNames } from "./devices"; + +const logHead = "[TAURI SERIAL]"; + +/** + * TauriSerial protocol implementation using tauri-plugin-serialplugin-api + */ +class TauriSerial extends EventTarget { + constructor() { + super(); + + this.connected = false; + this.openRequested = false; + this.openCanceled = false; + this.closeRequested = false; + this.transmitting = false; + this.connectionInfo = null; + + this.bitrate = 0; + this.bytesSent = 0; + this.bytesReceived = 0; + this.failed = 0; + + this.ports = []; + this.connectionId = null; + this.reading = false; + + this.connect = this.connect.bind(this); + this.disconnect = this.disconnect.bind(this); + this.handleReceiveBytes = this.handleReceiveBytes.bind(this); + + // Detect if running on macOS with AT32 (needs batch writes) + this.isNeedBatchWrite = false; + + // Device monitoring + this.monitoringDevices = false; + this.deviceMonitorInterval = null; + + this.loadDevices().then(() => this.startDeviceMonitoring()); + } + + handleReceiveBytes(info) { + this.bytesReceived += info.detail.byteLength; + } + + getConnectedPort() { + return this.connectionId; + } + + handleFatalSerialError() { + // On fatal errors (broken pipe, etc.), just disconnect cleanly + // Device monitoring will automatically detect the removal and emit removedDevice + if (this.connected) { + this.disconnect(); + } + } + + startDeviceMonitoring() { + if (this.monitoringDevices) { + return; + } + + this.monitoringDevices = true; + // Check for device changes every 1 second + this.deviceMonitorInterval = setInterval(async () => { + await this.checkDeviceChanges(); + }, 1000); + + console.log(`${logHead} Device monitoring started`); + } + + stopDeviceMonitoring() { + if (this.deviceMonitorInterval) { + clearInterval(this.deviceMonitorInterval); + this.deviceMonitorInterval = null; + } + this.monitoringDevices = false; + console.log(`${logHead} Device monitoring stopped`); + } + + /** + * Convert the raw portsMap from the plugin into our standardized port objects. + * @private + */ + _convertPortsMapToArray(portsMap) { + return Object.entries(portsMap).map(([path, info]) => { + const vendorId = info.vid + ? typeof info.vid === "number" + ? info.vid + : Number.parseInt(info.vid, 10) + : undefined; + const productId = info.pid + ? typeof info.pid === "number" + ? info.pid + : Number.parseInt(info.pid, 10) + : undefined; + + return { + path, + displayName: this.getDisplayName(path, vendorId, productId), + vendorId, + productId, + serialNumber: info.serial_number, + }; + }); + } + + /** + * Request USB permission for a device path (Android only). + * This triggers the permission dialog by attempting a dummy open. + * Usage: await tauriserial.requestPermissionDevice(path) + */ + async requestPermissionDevice(path) { + try { + console.log(`${logHead} Requesting USB permission for ${path} (Android)`); + // Use a dummy baud rate and catch errors + await invoke("plugin:serialplugin|open", { path, baudRate: 9600 }); + // If permission is granted, this will succeed (or fail for other reasons) + console.log(`${logHead} USB permission granted for ${path}`); + // Immediately close if opened + await invoke("plugin:serialplugin|close", { path }); + return true; + } catch (error) { + const errorStr = error?.toString() || error?.message || ""; + if (errorStr.includes("permission") || errorStr.includes("Permission")) { + console.warn(`${logHead} USB permission denied for ${path}`); + return false; + } + // Other errors + console.error(`${logHead} Error requesting USB permission:`, error); + return false; + } + } + + /** + * Filter ports to only include known Betaflight-compatible devices. + * @private + */ + _filterToKnownDevices(ports) { + // Set to true to enable debug logs + const DEBUG = false; + if (DEBUG) { + console.log(`${logHead} Filtering ${ports.length} ports`); + } + const filtered = ports.filter((port) => { + if (!port.vendorId || !port.productId) { + if (DEBUG) console.log(`${logHead} FILTERED OUT (no VID/PID): ${port.path}`); + return false; + } + const isKnown = serialDevices.some((d) => d.vendorId === port.vendorId && d.productId === port.productId); + if (!isKnown && DEBUG) { + console.log( + `${logHead} FILTERED OUT (unknown device): ${port.path} VID:${port.vendorId} PID:${port.productId}`, + ); + } + return isKnown; + }); + if (DEBUG) { + console.log(`${logHead} Returning ${filtered.length} filtered ports`); + } + return filtered; + } + + async checkDeviceChanges() { + try { + let portsMap = await invoke("plugin:serialplugin|available_ports"); + + // ANDROID FIX: Check if result is a string (Android deserialization issue) + if (typeof portsMap === "string") { + console.log(`${logHead} Result is a string, attempting to parse...`); + try { + // The Android plugin returns a string like: "{/dev/bus/usb/002/002={type=USB, vid=1155, ...}}" + // We need to convert this to proper JSON + portsMap = this._parseAndroidPortsResponse(portsMap); + console.log(`${logHead} Parsed portsMap:`, portsMap); + } catch (parseError) { + console.error(`${logHead} Failed to parse string response:`, parseError); + return; + } + } + + // Convert to our format + const allPorts = this._convertPortsMapToArray(portsMap); + + // Filter to only known devices + const currentPorts = this._filterToKnownDevices(allPorts); + + // Check for removed devices + const removedPorts = this.ports.filter( + (oldPort) => !currentPorts.some((newPort) => newPort.path === oldPort.path), + ); + + // Check for added devices + const addedPorts = currentPorts.filter( + (newPort) => !this.ports.some((oldPort) => oldPort.path === newPort.path), + ); + + // Emit events for removed devices + for (const removed of removedPorts) { + this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed })); + console.log(`${logHead} Device removed: ${removed.path}`); + } + + // Emit events for added devices + for (const added of addedPorts) { + this.dispatchEvent(new CustomEvent("addedDevice", { detail: added })); + console.log(`${logHead} Device added: ${added.path}`); + } + + // Update our ports list + console.log(`${logHead} Device check complete. Current ports:`, currentPorts, portsMap, allPorts); + this.ports = currentPorts; + } catch (error) { + const errorStr = error?.toString() || error?.message || ""; + + // Handle Android USB permission errors specifically + if (errorStr.includes("permission") || errorStr.includes("Permission") || errorStr.includes("not given")) { + console.log(`${logHead} USB permission required for device monitoring. Some devices may not be accessible until permission is granted.`); + // Don't treat this as a fatal error - continue monitoring + return; + } + + console.warn(`${logHead} Error checking device changes:`, error); + } + } + + async loadDevices() { + try { + let portsMap = await invoke("plugin:serialplugin|available_ports"); + + // ANDROID FIX: Check if result is a string (Android deserialization issue) + if (typeof portsMap === "string") { + console.log(`${logHead} Result is a string, attempting to parse...`); + try { + // The Android plugin returns a string like: "{/dev/bus/usb/002/002={type=USB, vid=1155, ...}}" + // We need to convert this to proper JSON + portsMap = this._parseAndroidPortsResponse(portsMap); + console.log(`${logHead} Parsed portsMap:`, portsMap); + } catch (parseError) { + console.error(`${logHead} Failed to parse string response:`, parseError); + return []; + } + } + + // Convert the object map to array + const allPorts = this._convertPortsMapToArray(portsMap); + + // DEBUG: Log all detected ports before filtering + console.log(`${logHead} === DEBUG: All detected ports BEFORE filtering ===`); + console.log(`${logHead} Raw portsMap from plugin:`, portsMap); + console.log(`${logHead} Total ports detected: ${allPorts.length}`); + allPorts.forEach((port, index) => { + console.log( + `${logHead} [${index}] path: ${port.path}, VID: ${port.vendorId}, PID: ${port.productId}, displayName: ${port.displayName}`, + ); + }); + + // Filter to only known devices + this.ports = this._filterToKnownDevices(allPorts); + + console.log(`${logHead} === DEBUG: After filtering ===`); + console.log(`${logHead} Found ${this.ports.length} serial ports (filtered from ${allPorts.length})`); + this.ports.forEach((port, index) => { + console.log(`${logHead} [${index}] KEPT: ${port.path} (${port.displayName})`); + }); + + return this.ports; + } catch (error) { + console.error(`${logHead} Error loading devices:`, error); + return []; + } + } + + /** + * Parse Android plugin's string response to JSON + * Input: "{/dev/bus/usb/002/002={type=USB, vid=1155, pid=22336, manufacturer=Betaflight, ...}}" + * Output: {"/dev/bus/usb/002/002": {type: "USB", vid: "1155", ...}} + * @private + */ + _parseAndroidPortsResponse(responseStr) { + // Remove outer braces + let inner = responseStr.trim(); + if (inner.startsWith("{") && inner.endsWith("}")) { + inner = inner.slice(1, -1); + } + + const ports = {}; + + // Split by port entries (look for pattern: path={...}) + // This regex finds: /dev/bus/usb/XXX/XXX={...} + const portPattern = /(\/dev\/[^=]+)=\{([^}]+)\}/g; + let match; + + while ((match = portPattern.exec(inner)) !== null) { + const path = match[1]; + const propsStr = match[2]; + + // Parse properties: "type=USB, vid=1155, pid=22336, ..." + const props = {}; + const propPairs = propsStr.split(",").map((s) => s.trim()); + + for (const pair of propPairs) { + const [key, value] = pair.split("=").map((s) => s.trim()); + if (key && value) { + props[key] = value; + } + } + + ports[path] = props; + } + + return ports; + } + + getDisplayName(path, vendorId, productId) { + let displayName = path; + + if (vendorId && productId) { + // Use vendor name if available, otherwise show as hex + const vendorName = vendorIdNames[vendorId] || `VID:${vendorId} PID:${productId}`; + displayName = `Betaflight ${vendorName}`; + } + + return displayName; + } + + async connect(path, options) { + if (this.openRequested) { + console.log(`${logHead} Connection already requested`); + return false; + } + + this.openRequested = true; + + try { + const openOptions = { + path, + baudRate: options.baudRate || 115200, + }; + + console.log(`${logHead} Opening port ${path} at ${openOptions.baudRate} baud`); + console.log(`${logHead} Note: On Android, this will trigger a USB permission request dialog`); + + // Open the port - On Android, this automatically requests USB permission + const openResult = await invoke("plugin:serialplugin|open", openOptions); + console.log(`${logHead} Open result:`, openResult); + console.log(`${logHead} USB permission granted and port opened successfully!`); + + // Set a reasonable timeout for read/write operations (100ms) + try { + await invoke("plugin:serialplugin|set_timeout", { + path, + timeout: 100, + }); + } catch (e) { + console.debug(`${logHead} Could not set timeout:`, e); + } + + // Connection successful + this.connected = true; + this.connectionId = path; + this.bitrate = openOptions.baudRate; + this.openRequested = false; + + this.connectionInfo = { + connectionId: path, + bitrate: this.bitrate, + }; + + this.addEventListener("receive", this.handleReceiveBytes); + + // Start reading + this.reading = true; + this.readLoop(); + + this.dispatchEvent(new CustomEvent("connect", { detail: true })); + console.log(`${logHead} Connected to ${path}`); + return true; + } catch (error) { + console.error(`${logHead} Error connecting:`, error); + console.error(`${logHead} Error details:`, { + message: error?.message, + stack: error?.stack, + type: typeof error, + stringValue: error?.toString(), + }); + + // Check if it's a permission error + const errorStr = error?.toString() || error?.message || ""; + if (errorStr.includes("permission") || errorStr.includes("Permission")) { + console.error(`${logHead} USB PERMISSION DENIED! User must grant permission in the Android dialog.`); + console.error(`${logHead} Please check if the permission dialog appeared and was dismissed.`); + } + + this.openRequested = false; + this.dispatchEvent(new CustomEvent("connect", { detail: false })); + return false; + } + } + + async readLoop() { + try { + while (this.reading) { + try { + // Non-blocking read with short timeout + const result = await invoke("plugin:serialplugin|read_binary", { + path: this.connectionId, + size: 256, + timeout: 10, + }); + + if (result && result.length > 0) { + this.dispatchEvent(new CustomEvent("receive", { detail: new Uint8Array(result) })); + } + + // Small delay between polls to avoid overwhelming the system + await new Promise((resolve) => setTimeout(resolve, 5)); + } catch (error) { + const msg = error?.message || (error?.toString ? error.toString() : ""); + // Timeout is expected when no data available + if (msg?.toLowerCase().includes("no data received")) { + await new Promise((resolve) => setTimeout(resolve, 5)); + continue; + } + if (isBrokenPipeError(msg)) { + console.error(`${logHead} Fatal poll error (broken pipe) on ${this.connectionId}:`, error); + throw error; + } + console.warn(`${logHead} Poll error:`, error); + await new Promise((resolve) => setTimeout(resolve, 5)); + } + } + } catch (error) { + console.error(`${logHead} Error in read loop:`, error); + this.handleFatalSerialError(error); + } finally { + console.log(`${logHead} Polling stopped for ${this.connectionId || ""}`); + } + } + + async send(data, callback) { + if (!this.connected) { + console.error(`${logHead} Cannot send: port not connected`); + const res = { bytesSent: 0 }; + callback?.(res); + return res; + } + + try { + // Convert data to Uint8Array + let dataArray; + if (data instanceof ArrayBuffer) { + dataArray = new Uint8Array(data); + } else if (data instanceof Uint8Array) { + dataArray = data; + } else if (Array.isArray(data)) { + dataArray = new Uint8Array(data); + } else { + console.error(`${logHead} Unsupported data type:`, data?.constructor?.name); + const res = { bytesSent: 0 }; + callback?.(res); + return res; + } + + this.transmitting = true; + + const writeChunk = async (chunk) => { + await invoke("plugin:serialplugin|write_binary", { + path: this.connectionId, + value: Array.from(chunk), + }); + }; + + if (this.isNeedBatchWrite) { + // Batch write for macOS AT32 compatibility + const batchSize = 63; + for (let offset = 0; offset < dataArray.length; offset += batchSize) { + const chunk = dataArray.slice(offset, offset + batchSize); + await writeChunk(chunk); + } + } else { + await writeChunk(dataArray); + } + + this.transmitting = false; + this.bytesSent += dataArray.length; + + const res = { bytesSent: dataArray.length }; + callback?.(res); + return res; + } catch (error) { + console.error(`${logHead} Error sending data:`, error); + this.transmitting = false; + if (isBrokenPipeError(error)) { + // Treat as device removal to trigger reconnect flow + this.handleFatalSerialError(error); + } + const res = { bytesSent: 0 }; + callback?.(res); + return res; + } + } + + async disconnect() { + if (!this.connected) { + return true; + } + + // Mark as disconnected immediately + this.connected = false; + this.transmitting = false; + this.reading = false; + + if (this.closeRequested) { + return true; + } + + this.closeRequested = true; + + try { + this.removeEventListener("receive", this.handleReceiveBytes); + + // Small delay to allow read loop to notice state change + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Close the port + if (this.connectionId) { + try { + await invoke("plugin:serialplugin|close", { path: this.connectionId }); + console.log(`${logHead} Port closed`); + } catch (error) { + console.warn(`${logHead} Error closing port:`, error); + } + } + + this.connectionId = null; + this.bitrate = 0; + this.connectionInfo = null; + this.closeRequested = false; + + this.dispatchEvent(new CustomEvent("disconnect", { detail: true })); + return true; + } catch (error) { + console.error(`${logHead} Error disconnecting:`, error); + this.closeRequested = false; + this.dispatchEvent(new CustomEvent("disconnect", { detail: false })); + return false; + } finally { + if (this.openCanceled) { + this.openCanceled = false; + } + } + } + + async getDevices() { + await this.loadDevices(); + return this.ports; + } +} + +export default TauriSerial; diff --git a/src/js/serial.js b/src/js/serial.js index 625aed5250..6ed44222f1 100644 --- a/src/js/serial.js +++ b/src/js/serial.js @@ -2,6 +2,7 @@ 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 TauriSerial from "./protocols/TauriSerial.js"; import { isTauri } from "@tauri-apps/api/core"; /** @@ -17,34 +18,20 @@ 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() }, - ]; - // Forward events from current protocols - this._setupEventForwarding(); - } - - /** - * Perform any asynchronous initialization required by the Serial facade. - * This keeps constructors synchronous and predictable. - */ - async init() { - // Dynamically include the native Tauri serial adapter so web builds don't try to resolve it. if (isTauri()) { - try { - const { default: TauriSerial } = await import("./protocols/TauriSerial.js"); - const inst = new TauriSerial(); - this._protocols.unshift({ name: "tauriserial", instance: inst }); - // Wire event forwarding for this late-added protocol - this._setupEventForwardingFor("tauriserial", inst); - } catch (err) { - console.warn(`${this.logHead} Failed to load TauriSerial adapter:`, err); - } + this._protocols = [{ name: "tauriserial", instance: new TauriSerial() }]; + } else { + this._protocols = [ + { name: "webserial", instance: new WebSerial() }, + { name: "webbluetooth", instance: new WebBluetooth() }, + { name: "websocket", instance: new Websocket() }, + { name: "virtual", instance: new VirtualSerial() }, + ]; } + + // Forward events from current protocols + this._setupEventForwarding(); } /** @@ -58,11 +45,7 @@ class Serial extends EventTarget { } } - _setupEventForwardingFor( - name, - instance, - events = ["addedDevice", "removedDevice", "connect", "disconnect", "receive"], - ) { + _setupEventForwardingFor(name, instance, events) { if (typeof instance?.addEventListener !== "function") { return; } @@ -245,6 +228,3 @@ class Serial extends EventTarget { // Export a singleton instance export const serial = new Serial(); -// Kick off async initialization outside of the constructor. -// Intentionally not awaited to avoid blocking module load. -void serial.init(); diff --git a/yarn.lock b/yarn.lock index f8a3a8b601..b5c35e2813 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2183,7 +2183,7 @@ magic-string "^0.25.0" string.prototype.matchall "^4.0.6" -"@tauri-apps/api@^2.9.0": +"@tauri-apps/api@>=2.0.0-beta.6", "@tauri-apps/api@^2.9.0": version "2.9.0" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.9.0.tgz#047fcbfec05a719b0cec997eee244cee453fb2fc" integrity sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw== @@ -9846,6 +9846,13 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +tauri-plugin-serialplugin-api@^2.21.1: + version "2.21.1" + resolved "https://registry.yarnpkg.com/tauri-plugin-serialplugin-api/-/tauri-plugin-serialplugin-api-2.21.1.tgz#a8ac2ab6ff0024988b552259d98fe22fe21f5cf2" + integrity sha512-V89gkiHmsjRXN000oSP+6Oiatsvt1EGJHjcS2c7+VLiSic+934UYWxKaZ3oG4s3zefajzKFLl3EjankRIaPHRQ== + dependencies: + "@tauri-apps/api" ">=2.0.0-beta.6" + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"