Skip to content

Conversation

@haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented Nov 17, 2025

Summary by CodeRabbit

  • New Features

    • Native Android USB‑serial support via a bundled Capacitor plugin, integrated as a new serial protocol with automatic device discovery, permission flow, connect/disconnect and data I/O
    • Port picker/UI updated to surface connected Android USB devices and request permissions
  • Documentation

    • Added comprehensive implementation docs and a Quick Start guide with setup, testing and troubleshooting
  • Chores

    • Android build/config updated to enable USB host, device filters and include the plugin; dev server now listens on all interfaces

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

Walkthrough

Adds a self-contained Capacitor USB-serial plugin for Android (native Java + Capacitor wrapper), a TypeScript protocol adapter, UI and port-handler integration, Android manifest/build updates, and comprehensive documentation and quick-start guides to enable MSP over USB on Android devices.

Changes

Cohort / File(s) Summary
Documentation
CAPACITOR_SERIAL_IMPLEMENTATION.md, QUICK_START.md, README_IMPLEMENTATION.md
New implementation, quick-start, and README docs describing plugin design, installation, testing, migration notes, and troubleshooting.
Plugin: TypeScript API & Web Stub
capacitor-plugin-betaflight-serial/src/definitions.ts, capacitor-plugin-betaflight-serial/src/index.ts, capacitor-plugin-betaflight-serial/src/web.ts
Adds plugin type definitions, registers the Capacitor plugin, and provides web fallback methods (not implemented stubs).
Plugin: Packaging & Build
capacitor-plugin-betaflight-serial/package.json, capacitor-plugin-betaflight-serial/tsconfig.json, capacitor-plugin-betaflight-serial/rollup.config.js, capacitor-plugin-betaflight-serial/.gitignore
Adds package metadata, build scripts, TS config, Rollup bundling config, and gitignore for the plugin package.
Plugin: Android Native
capacitor-plugin-betaflight-serial/android/build.gradle, capacitor-plugin-betaflight-serial/android/proguard-rules.pro, capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
New Android library module implementing USB permission handling, device enumeration, serial port open/configure/read/write, attach/detach events, and lifecycle cleanup.
Plugin: Documentation (package)
capacitor-plugin-betaflight-serial/README.md
Plugin README documenting API, usage, supported chipsets, and configuration (device_filter, manifest notes).
Android Build Integration
android/app/capacitor.build.gradle, android/build.gradle, android/capacitor.settings.gradle, android/app/src/main/AndroidManifest.xml, android/app/src/main/res/xml/device_filter.xml
Adds plugin module to project, JitPack repo, Gradle dependency, USB host feature, device attach intent/filter and device_filter.xml entries for supported VID/PID pairs.
Protocol Adapter
src/js/protocols/CapacitorSerial.js
New JS protocol implementing WebSerial-like API, hex↔Uint8Array conversion, device discovery, permission flow, connect/disconnect, send/receive, and event propagation.
Serial System Integration
src/js/serial.js, src/js/port_handler.js
Registers CapacitorSerial protocol, routes capacitor- prefixed ports, adds currentCapacitorPorts, capacitorAvailable, showCapacitorOption, and integrates capacitor device discovery/selection into existing flows.
UI Components
src/components/port-picker/PortPicker.vue, src/components/port-picker/PortsInput.vue, src/index.html
Adds connectedCapacitorDevices and showCapacitorOption props; renders capacitor device group and permission option in port picker and wires permission request events.
Compatibility & Config
src/js/utils/checkBrowserCompatibility.js, package.json, vite.config.js
Adds checkCapacitorSerialSupport() (Android-only), adds local plugin dependency reference, and sets Vite dev server host to 0.0.0.0.

Sequence Diagram(s)

sequenceDiagram
    participant App as App (CapacitorSerial.js)
    participant Native as BetaflightSerialPlugin
    participant USB as USB Device

    rect rgb(220,235,255)
    Note over App,Native: Init & discovery
    App->>Native: setupNativeListeners()
    App->>Native: getDevices()
    Native->>USB: probe devices
    USB-->>Native: device list
    Native-->>App: devices array (event/response)
    end

    rect rgb(220,255,230)
    Note over App,Native: Connect flow
    App->>Native: requestPermission(deviceId)
    Native-->>App: permission granted (event)
    App->>Native: connect(deviceId, params)
    Native->>USB: open/configure port
    Native-->>App: connected
    end

    rect rgb(255,235,230)
    Note over App,Native: Data exchange
    App->>App: send(Uint8Array) -> hex
    App->>Native: write(hexString)
    Native->>USB: write bytes
    USB-->>Native: bytes written
    USB-->>Native: incoming bytes
    Native->>App: dataReceived(hexString) (event)
    App->>App: hex -> Uint8Array -> dispatch to consumer
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Review focus:
    • capacitor-plugin-betaflight-serial/android/src/main/java/.../BetaflightSerialPlugin.java — USB permission/attach/detach, SerialInputOutputManager lifecycle, threading and resource cleanup.
    • src/js/protocols/CapacitorSerial.js and src/js/port_handler.js — hex/byte conversions, event wiring, error handling, and integration into port selection.
    • Android build/manifest changes and device_filter.xml coverage and Gradle module wiring.

Possibly related PRs

Suggested labels

RN: IMPROVEMENT, RN: BUILD SYSTEM

Suggested reviewers

  • nerdCopter
  • VitroidFPV
  • blckmn
  • SupaflyFPV

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request has no description provided by the author, but the template is present in the repository. A description is required to explain the changes and their rationale. Add a comprehensive pull request description explaining the purpose, implementation details, testing performed, and any relevant issue references following the repository template.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Custom android serial plugin' is concise and directly describes the main change: adding a custom Android serial plugin implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…l Android APK; publish artifacts to draft release
…2gtk-4.1, gtk3, ayatana-appindicator, librsvg2, patchelf)
…tifact path (src/dist/) to fix missing assets
@haslinghuis haslinghuis force-pushed the custom-android-serial-plugin branch from 131c168 to 9a8bf7f Compare November 17, 2025 21:27
@sonarqubecloud
Copy link

@haslinghuis haslinghuis self-assigned this Nov 17, 2025
@haslinghuis haslinghuis moved this to App in 2026.6.0 Nov 17, 2025
@haslinghuis haslinghuis added this to the 2026.6 milestone Nov 17, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/js/port_handler.js (2)

88-99: Remove the constant || true so capacitor devices are only queried when actually supported

In updateDeviceList you have:

case "capacitorserial":
    if (this.showCapacitorOption || true) {
        ports = await serial.getDevices("capacitorserial");
    }
    break;

This makes the condition always true, so serial.getDevices("capacitorserial") is called on all platforms, even when Capacitor Serial isn’t available. Combined with the web stub that throws unimplemented, this results in unnecessary errors (caught, but noisy) and is also what Biome is flagging.

Suggest:

-        case "capacitorserial":
-            if (this.showCapacitorOption || true) {
-                ports = await serial.getDevices("capacitorserial");
-            }
-            break;
+        case "capacitorserial":
+            if (this.showCapacitorOption) {
+                ports = await serial.getDevices("capacitorserial");
+            }
+            break;

This keeps capacitor discovery behind the same capability gate as the UI, and avoids calling the native-only plugin on web builds.

Also applies to: 318-333


115-145: Align capacitor device removal and active-port selection with the new protocol

There are a couple of consistency gaps around Capacitor Serial:

  • removedSerialDevice only distinguishes bluetooth vs “everything else” (webserial). When a removedDevice event comes in with a path starting with "capacitor-", you never call updateDeviceList("capacitorserial"), so currentCapacitorPorts can remain stale after a detach.
  • In selectActivePort, the “already connected” branch looks only in currentSerialPorts:
    if (serial.connected) {
        selectedPort = this.currentSerialPorts.find(
            (device) => device === serial.getConnectedPort(),
        );
    }
    If the active connection is via Capacitor Serial, this won’t match any entry and the logic will ignore the active port.
  • Capacitor ports are considered last in the auto-selection pipeline, which is fine, but they should be treated consistently wherever other serial backends are.

To make behavior consistent, consider:

  • Updating removedSerialDevice to detect capacitor paths:
  • const updatePromise = devicePath.startsWith("bluetooth")
  •    ? this.updateDeviceList("webbluetooth")
    
  •    : this.updateDeviceList("webserial");
    
  • const updatePromise = devicePath.startsWith("bluetooth")
  •    ? this.updateDeviceList("webbluetooth")
    
  •    : devicePath.startsWith("capacitor-")
    
  •        ? this.updateDeviceList("capacitorserial")
    
  •        : this.updateDeviceList("webserial");
    
  • Including currentCapacitorPorts when resolving the already-connected port, or comparing by .path against serial.getConnectedPort().path, so capacitor connections are respected in the “use the connected device” branch.

That will keep the port lists and auto-selection behavior in sync with the new protocol.

Also applies to: 198-243, 254-262, 285-303, 352-366

🧹 Nitpick comments (5)
vite.config.js (1)

123-123: Network binding looks good for Android development workflow.

Binding to 0.0.0.0 is appropriate for testing the Capacitor plugin on Android devices. Since this affects only the dev server, the security implications are limited to development environments.

Consider making this configurable via an environment variable (e.g., VITE_HOST) to give developers control over network exposure, especially when working on shared or untrusted networks.

Optional refactor: Make host configurable

     server: {
         port: 8000,
         strictPort: true,
-        host: "0.0.0.0", // Listen on all network interfaces for Android device access
+        host: process.env.VITE_HOST || "0.0.0.0", // Listen on all network interfaces for Android device access
     },
package.json (1)

48-48: Consider the long-term dependency strategy.

The plugin is currently referenced as a local file dependency. While this works for development and self-contained distributions, consider whether the plugin should eventually be published to npm for better package management and easier updates across installations.

Local file dependencies can have implications:

  • Lock files may include absolute paths in some environments
  • Different behavior between development and production
  • Harder to version independently

If the plugin is intended to remain tightly coupled to the configurator, the current approach is acceptable. Otherwise, consider publishing to npm (public or private registry).

README_IMPLEMENTATION.md (1)

68-132: Add languages to fenced code blocks to satisfy markdownlint

Several fenced blocks (architecture tree, file lists, checklists) are missing language identifiers, which triggers MD040. Suggest adding appropriate languages, for example:

  • ```text for directory trees / ASCII diagrams
  • ```bash for shell commands
  • ```javascript for JS examples

This will keep linters quiet and improve rendering in some viewers.

Also applies to: 188-206

src/js/protocols/CapacitorSerial.js (1)

315-332: Consider making hex conversion slightly more defensive

Right now hexStringToUint8Array assumes an even-length hex string and silently truncates the last nibble if length is odd. If there’s any chance of malformed input from native, consider:

  • Early-returning or logging when hexString.length % 2 !== 0
  • Guarding parseInt failures (e.g. with Number.isNaN)

This would make failures easier to diagnose if native ever emits bad data.

CAPACITOR_SERIAL_IMPLEMENTATION.md (1)

19-38: Specify languages on fenced code blocks for better linting and readability

A few code/diagram blocks (architecture tree, serial usage examples, USB library URLs) don’t specify a language. Adding one (e.g. text, bash, javascript, xml) will satisfy markdownlint and improve syntax highlighting.

Also applies to: 81-93, 151-166, 290-306

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 399afa3 and 9a8bf7f.

⛔ Files ignored due to path filters (2)
  • capacitor-plugin-betaflight-serial/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (28)
  • CAPACITOR_SERIAL_IMPLEMENTATION.md (1 hunks)
  • QUICK_START.md (1 hunks)
  • README_IMPLEMENTATION.md (1 hunks)
  • android/app/capacitor.build.gradle (1 hunks)
  • android/app/src/main/AndroidManifest.xml (2 hunks)
  • android/app/src/main/res/xml/device_filter.xml (1 hunks)
  • android/build.gradle (1 hunks)
  • android/capacitor.settings.gradle (1 hunks)
  • capacitor-plugin-betaflight-serial/.gitignore (1 hunks)
  • capacitor-plugin-betaflight-serial/README.md (1 hunks)
  • capacitor-plugin-betaflight-serial/android/build.gradle (1 hunks)
  • capacitor-plugin-betaflight-serial/android/proguard-rules.pro (1 hunks)
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java (1 hunks)
  • capacitor-plugin-betaflight-serial/package.json (1 hunks)
  • capacitor-plugin-betaflight-serial/rollup.config.js (1 hunks)
  • capacitor-plugin-betaflight-serial/src/definitions.ts (1 hunks)
  • capacitor-plugin-betaflight-serial/src/index.ts (1 hunks)
  • capacitor-plugin-betaflight-serial/src/web.ts (1 hunks)
  • capacitor-plugin-betaflight-serial/tsconfig.json (1 hunks)
  • package.json (1 hunks)
  • src/components/port-picker/PortPicker.vue (3 hunks)
  • src/components/port-picker/PortsInput.vue (5 hunks)
  • src/index.html (1 hunks)
  • src/js/port_handler.js (10 hunks)
  • src/js/protocols/CapacitorSerial.js (1 hunks)
  • src/js/serial.js (3 hunks)
  • src/js/utils/checkBrowserCompatibility.js (2 hunks)
  • vite.config.js (1 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-05-14T21:51:09.253Z
Learning: On Android Chrome browsers, Web Bluetooth API is supported for BLE devices and Web USB API is supported, but Web Serial API is not supported (except limited Bluetooth serial support added in 2025). The Betaflight Configurator should detect and use available APIs on Android rather than requiring all three.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4576
File: src/js/port_usage.js:17-23
Timestamp: 2025-08-22T16:43:20.901Z
Learning: In betaflight-configurator, the serial facade architecture requires accessing metrics like bitrate, bytesReceived, and bytesSent from serial._protocol rather than the top-level serial object. This change maintains compatibility with existing port utilization calculations that have been stable for over 11 years.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4687
File: src/js/protocols/TauriSerial.js:299-310
Timestamp: 2025-11-06T17:26:50.201Z
Learning: In tauri-plugin-serialplugin Android development, the Android project must be initialized via `tauri android init` before the USB support patches (AndroidManifest.xml permissions, device_filter.xml, MainActivity.kt) can be applied. Without initialization, the plugin returns empty results from available_ports_direct() because the generated Android project structure and USB permissions don't exist yet.
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
Repo: betaflight/betaflight-configurator PR: 4521
File: src/js/protocols/WebSerial.js:148-151
Timestamp: 2025-06-19T22:13:09.136Z
Learning: In WebSerial.js, there's a timing issue where the cached `this.ports` array doesn't immediately reflect newly permitted devices after `requestPermissionDevice()` completes. The `getDevices()` method needs to refresh the device list from the browser API to return accurate data immediately following a permission request and user acceptance.

Applied to files:

  • src/components/port-picker/PortsInput.vue
  • src/js/serial.js
  • src/js/protocols/CapacitorSerial.js
  • capacitor-plugin-betaflight-serial/src/web.ts
  • src/js/port_handler.js
📚 Learning: 2025-10-25T21:16:32.474Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4379
File: src/js/protocols/TauriSerial.js:203-259
Timestamp: 2025-10-25T21:16:32.474Z
Learning: In TauriSerial (src/js/protocols/TauriSerial.js), the requestPermissionDevice() method is not needed and not invoked. Tauri automatically discovers serial devices through the constructor's loadDevices() and startDeviceMonitoring() calls, bypassing the browser permission model that WebSerial requires. Devices are auto-detected via a 1-second polling interval without user permission prompts.

Applied to files:

  • src/components/port-picker/PortsInput.vue
  • src/js/serial.js
  • src/js/protocols/CapacitorSerial.js
  • capacitor-plugin-betaflight-serial/src/web.ts
  • src/js/port_handler.js
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
  • capacitor-plugin-betaflight-serial/src/definitions.ts
  • capacitor-plugin-betaflight-serial/README.md
📚 Learning: 2025-06-09T00:32:21.385Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:32:21.385Z
Learning: In the betaflight-configurator codebase, port paths use counter prefixes (e.g., "bluetooth1", "bluetooth2", "serial1") rather than direct protocol identifiers. The protocol selection logic correctly uses `portPath.startsWith("bluetooth")` to detect bluetooth ports regardless of the counter suffix, rather than direct string matching against protocol map keys.

Applied to files:

  • src/js/serial.js
  • src/js/port_handler.js
📚 Learning: 2025-08-22T16:43:20.901Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4576
File: src/js/port_usage.js:17-23
Timestamp: 2025-08-22T16:43:20.901Z
Learning: In betaflight-configurator, the serial facade architecture requires accessing metrics like bitrate, bytesReceived, and bytesSent from serial._protocol rather than the top-level serial object. This change maintains compatibility with existing port utilization calculations that have been stable for over 11 years.

Applied to files:

  • src/js/serial.js
  • CAPACITOR_SERIAL_IMPLEMENTATION.md
  • src/js/protocols/CapacitorSerial.js
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
  • capacitor-plugin-betaflight-serial/README.md
📚 Learning: 2025-05-14T21:51:09.253Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-05-14T21:51:09.253Z
Learning: On Android Chrome browsers, Web Bluetooth API is supported for BLE devices and Web USB API is supported, but Web Serial API is not supported (except limited Bluetooth serial support added in 2025). The Betaflight Configurator should detect and use available APIs on Android rather than requiring all three.

Applied to files:

  • src/js/serial.js
  • CAPACITOR_SERIAL_IMPLEMENTATION.md
  • src/js/protocols/CapacitorSerial.js
  • src/js/utils/checkBrowserCompatibility.js
  • capacitor-plugin-betaflight-serial/src/web.ts
  • src/js/port_handler.js
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
  • capacitor-plugin-betaflight-serial/src/definitions.ts
  • capacitor-plugin-betaflight-serial/README.md
  • QUICK_START.md
📚 Learning: 2025-11-06T17:26:50.201Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4687
File: src/js/protocols/TauriSerial.js:299-310
Timestamp: 2025-11-06T17:26:50.201Z
Learning: In tauri-plugin-serialplugin Android development, the Android project must be initialized via `tauri android init` before the USB support patches (AndroidManifest.xml permissions, device_filter.xml, MainActivity.kt) can be applied. Without initialization, the plugin returns empty results from available_ports_direct() because the generated Android project structure and USB permissions don't exist yet.

Applied to files:

  • capacitor-plugin-betaflight-serial/android/build.gradle
  • CAPACITOR_SERIAL_IMPLEMENTATION.md
  • src/js/protocols/CapacitorSerial.js
  • android/app/src/main/AndroidManifest.xml
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
  • QUICK_START.md
📚 Learning: 2025-05-14T21:39:16.388Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-05-14T21:39:16.388Z
Learning: On Android Chrome browsers, Web Bluetooth API is supported from version 133+ (requires Android 6.0+) and Web USB API is supported from version 136+, but Web Serial API is not supported. The Betaflight Configurator should detect and use available APIs on Android rather than requiring all three.

Applied to files:

  • CAPACITOR_SERIAL_IMPLEMENTATION.md
  • src/js/protocols/CapacitorSerial.js
  • src/js/utils/checkBrowserCompatibility.js
  • capacitor-plugin-betaflight-serial/src/web.ts
  • src/js/port_handler.js
  • capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
  • capacitor-plugin-betaflight-serial/README.md
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: The betaflight-configurator project uses Node 20.19.4, which satisfies Vite 7's minimum Node requirement of 20.19+.

Applied to files:

  • package.json
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: In betaflight-configurator, dependency updates are handled incrementally - Vue dependencies are updated separately from Vite dependencies for better isolation and maintainability.

Applied to files:

  • package.json
📚 Learning: 2025-06-20T12:35:49.283Z
Learnt from: mituritsyn
Repo: betaflight/betaflight-configurator PR: 4526
File: src/js/gui.js:43-43
Timestamp: 2025-06-20T12:35:49.283Z
Learning: In the Betaflight Configurator codebase, tabs in `defaultCloudBuildTabOptions` are conditionally displayed based on firmware build options. The logic in `serial_backend.js` checks `FC.CONFIG.buildOptions` and only adds tabs to `GUI.allowedTabs` if the firmware was built with support for that feature.

Applied to files:

  • src/js/port_handler.js
🧬 Code graph analysis (4)
capacitor-plugin-betaflight-serial/src/index.ts (1)
capacitor-plugin-betaflight-serial/src/definitions.ts (1)
  • BetaflightSerialPlugin (1-90)
capacitor-plugin-betaflight-serial/src/web.ts (1)
capacitor-plugin-betaflight-serial/src/definitions.ts (3)
  • SerialDevice (95-135)
  • ConnectOptions (140-175)
  • WriteOptions (180-185)
src/js/port_handler.js (2)
src/js/utils/checkBrowserCompatibility.js (5)
  • checkBrowserCompatibility (72-147)
  • checkWebBluetoothSupport (163-175)
  • checkWebSerialSupport (149-161)
  • checkWebUSBSupport (177-189)
  • checkCapacitorSerialSupport (63-70)
src/js/serial.js (2)
  • serial (221-221)
  • serial (221-221)
capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java (2)
capacitor-plugin-betaflight-serial/src/definitions.ts (1)
  • BetaflightSerialPlugin (1-90)
capacitor-plugin-betaflight-serial/src/web.ts (6)
  • requestPermission (6-8)
  • getDevices (10-12)
  • connect (14-16)
  • disconnect (18-20)
  • write (22-24)
  • read (26-28)
🪛 Biome (2.1.2)
src/js/port_handler.js

[error] 329-329: Unexpected constant condition.

(lint/correctness/noConstantCondition)

🪛 LanguageTool
CAPACITOR_SERIAL_IMPLEMENTATION.md

[style] ~275-~275: Consider an alternative adverb to strengthen your wording.
Context: ... 3. Verify flight controller is powered properly 4. Check for conflicting apps using USB...

(PROPERLY_THOROUGHLY)


[style] ~313-~313: Consider using a different verb for a more formal wording.
Context: ...Extensible* - Easy to add features or fix issues 6. Documented - Comprehensiv...

(FIX_RESOLVE)

README_IMPLEMENTATION.md

[style] ~167-~167: The double modal “required Designed” is nonstandard (only accepted in certain dialects). Consider “to be Designed”.
Context: ...oding built-in - No patching required - Designed for MSP from the start ### ✅ Comprehen...

(NEEDS_FIXED)


[uncategorized] ~305-~305: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... the Android app 4. Test on device ### Short Term 1. Test with various devices 2. Gather ...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~311-~311: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...x any issues 4. Performance tuning ### Long Term 1. User documentation 2. Beta release 3...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[style] ~351-~351: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...rly! 🎯 --- Created: November 17, 2025 Status: ✅ Ready for Testing **L...

(MISSING_COMMA_AFTER_YEAR)

QUICK_START.md

[style] ~19-~19: Consider shortening or rephrasing this to strengthen your wording.
Context: ...d the Plugin (Optional) If you want to make changes to the plugin TypeScript code: ```bash cd...

(MAKE_CHANGES)


[style] ~220-~220: Consider shortening or rephrasing this to strengthen your wording.
Context: ...ified ``` ## Development Workflow ### Making Changes to Native Code 1. Edit `capacitor-plugin-...

(MAKE_CHANGES)


[style] ~226-~226: Consider shortening or rephrasing this to strengthen your wording.
Context: ...3. Rebuild app: yarn android:run ### Making Changes to TypeScript 1. Edit files in `capacitor...

(MAKE_CHANGES)


[style] ~233-~233: Consider shortening or rephrasing this to strengthen your wording.
Context: ...4. Rebuild app: yarn android:run ### Making Changes to Protocol Adapter 1. Edit `src/js/proto...

(MAKE_CHANGES)

🪛 markdownlint-cli2 (0.18.1)
CAPACITOR_SERIAL_IMPLEMENTATION.md

45-45: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


70-70: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


192-192: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

README_IMPLEMENTATION.md

68-68: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


84-84: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


99-99: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


116-116: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (30)
capacitor-plugin-betaflight-serial/.gitignore (1)

1-17: Clean and comprehensive .gitignore for Capacitor plugin with Android development.

The .gitignore appropriately covers build artifacts, npm dependencies, IDE/editor files, and Android-specific build outputs. The entries follow standard patterns for Capacitor plugins and align well with the Android native code integration described in the PR.

capacitor-plugin-betaflight-serial/tsconfig.json (1)

1-22: LGTM! Standard Capacitor plugin TypeScript configuration.

The TypeScript configuration is appropriate for a Capacitor plugin, with sensible compiler options, strict checks enabled, and proper module/target settings for modern environments.

capacitor-plugin-betaflight-serial/android/proguard-rules.pro (1)

1-1: LGTM! Necessary ProGuard rule for Capacitor plugin.

The rule correctly preserves the plugin classes from obfuscation, which is required for Capacitor's reflection-based plugin invocation mechanism.

android/app/capacitor.build.gradle (1)

12-12: LGTM! Standard Capacitor plugin dependency.

The plugin dependency is correctly wired into the Android app module using Gradle's project reference syntax.

android/build.gradle (1)

24-24: LGTM! JitPack repository for USB serial library.

Adding JitPack is the standard approach for resolving GitHub-hosted Android libraries like usb-serial-for-android.

android/capacitor.settings.gradle (1)

5-6: LGTM! Standard Capacitor plugin module registration.

The plugin module is correctly included and pointed to the node_modules location where it will be installed.

android/app/src/main/res/xml/device_filter.xml (1)

1-34: LGTM! Comprehensive USB device filter coverage.

The device filters correctly enumerate common Betaflight-compatible USB-to-serial devices with accurate vendor and product IDs (verified in decimal format). This includes:

  • FTDI adapters (FT232R)
  • STM32 boards and bootloaders
  • CP210x Silicon Labs adapters
  • GD32, AT32, and APM32 microcontroller boards
  • Raspberry Pi Pico variants

The filter integrates with AndroidManifest.xml to enable USB device attachment detection for the plugin.

src/js/serial.js (3)

5-5: LGTM! CapacitorSerial import added.

The import follows the same pattern as other protocol implementations.


23-23: LGTM! CapacitorSerial protocol registration.

The protocol is correctly registered in the protocols array with a name and instance, consistent with other protocol registrations.


89-91: LGTM! Capacitor port routing logic.

The routing logic correctly detects "capacitor-" prefixed port paths and selects the CapacitorSerial protocol instance. This follows the same pattern as the bluetooth protocol routing and aligns with the port path convention used throughout the configurator.

Based on learnings

src/components/port-picker/PortPicker.vue (1)

18-18: LGTM! Clean integration following established patterns.

The Capacitor device props and bindings mirror the existing bluetooth/serial/usb patterns perfectly. The implementation is consistent with the component's architecture.

Also applies to: 25-25, 62-65, 86-89

src/index.html (1)

34-34: LGTM! Bindings are consistent with existing port types.

The Capacitor device bindings follow the established pattern and correctly reference the PortHandler properties.

Also applies to: 40-40

src/js/utils/checkBrowserCompatibility.js (1)

63-70: LGTM! Capacitor Serial support correctly extends compatibility checks for Android.

The new checkCapacitorSerialSupport() function properly detects Android platform and integrates into the compatibility logic. This allows Android devices to pass compatibility checks using Capacitor Serial even when Web Serial API is unavailable.

Based on learnings.

Also applies to: 76-76, 85-89, 95-95

capacitor-plugin-betaflight-serial/package.json (1)

1-71: Package structure looks good overall.

The package.json follows standard Capacitor plugin conventions with appropriate scripts, dependencies, and metadata. The build workflow and tooling configuration are well-structured.

android/app/src/main/AndroidManifest.xml (1)

25-31: LGTM! Standard Android USB host configuration.

The manifest changes correctly implement USB device support with proper intent filters, metadata references, and feature declarations. This follows Android USB host best practices.

Also applies to: 51-51

capacitor-plugin-betaflight-serial/README.md (1)

1-248: LGTM! Excellent comprehensive documentation.

The README provides thorough documentation covering features, installation, Android setup, API usage, and examples. The step-by-step setup instructions and TypeScript examples are clear and helpful.

src/components/port-picker/PortsInput.vue (1)

45-52: LGTM! UI extension follows established patterns perfectly.

The Capacitor device integration in PortsInput mirrors the existing serial/bluetooth/usb implementation patterns. The event handling and prop definitions are consistent with the component's architecture.

Also applies to: 62-64, 122-125, 154-157, 201-202

capacitor-plugin-betaflight-serial/src/index.ts (1)

1-10: LGTM! Standard Capacitor plugin entry point.

The plugin registration and exports follow Capacitor conventions with proper lazy loading of the web implementation and TypeScript type exports.

capacitor-plugin-betaflight-serial/rollup.config.js (1)

1-22: Rollup config looks consistent for a Capacitor plugin bundle

Input/output formats and externals match the expected Capacitor plugin pattern; I don’t see any issues here.

src/js/protocols/CapacitorSerial.js (1)

21-27: Protocol metrics and event wiring align with the serial facade

Keeping bitrate, bytesSent, bytesReceived, and failed on the protocol instance and incrementing them in handleDataReceived/send matches how port utilization expects to read metrics from serial._protocol, so this should slot cleanly into the existing facade.

Also applies to: 64-71, 188-205

capacitor-plugin-betaflight-serial/src/web.ts (1)

5-28: Explicit web stub behavior is clear and appropriate

Throwing unimplemented with a clear message for all methods makes it obvious this plugin is native-only and should not be used in the browser; no changes needed here.

capacitor-plugin-betaflight-serial/src/definitions.ts (1)

1-202: Type definitions for the plugin API look solid

The interfaces capture the plugin surface and event contracts cleanly (including listener overloads and hex-string data), which should make both native and JS sides easier to keep in sync.

capacitor-plugin-betaflight-serial/android/build.gradle (1)

1-62: Android plugin Gradle configuration looks consistent

Namespace, sdk levels, Java 17, capacitor-android, and usb-serial-for-android dependencies are wired in a standard way for a library module; I don’t see issues in this build file itself.

capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java (4)

81-99: LGTM! Proper API level handling for broadcast receiver registration.

The code correctly handles the Android 13+ requirement for RECEIVER_NOT_EXPORTED flag when registering broadcast receivers, with appropriate fallback for older API levels.


175-247: LGTM! Robust connection handling with proper error recovery.

The connect method correctly:

  • Validates required parameters
  • Closes existing connections before opening new ones
  • Checks permissions before attempting connection
  • Returns structured success/error responses
  • Cleans up on failure

265-274: Good handling of empty write payloads.

Treating empty hex strings as a no-op (returning 0 bytes sent) is a sensible design choice that prevents errors while maintaining API consistency.


453-475: LGTM! Comprehensive resource cleanup.

The cleanup method correctly handles all resources in reverse order of acquisition and is null-safe, preventing resource leaks even in error scenarios.

QUICK_START.md (3)

80-126: LGTM! Comprehensive Android testing guide.

The testing instructions cover all essential aspects:

  • Prerequisites clearly listed
  • Step-by-step setup for USB debugging
  • Expected behavior with clear success criteria
  • Debugging commands with adb filtering

This will help users quickly validate the plugin functionality.


145-189: LGTM! Excellent troubleshooting section.

The common issues section anticipates real problems users will encounter (permission dialogs, device detection, build errors) and provides actionable solutions. The graduated approach from simple to comprehensive fixes is well thought out.


240-259: Helpful comparison table with PR #4698.

The comparison clearly articulates the key differences between this custom implementation and the external plugin approach, helping reviewers and users understand the trade-offs and benefits of this design choice.

Comment on lines +117 to +161
public void requestPermission(PluginCall call) {
try {
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager);

if (availableDrivers.isEmpty()) {
JSObject result = new JSObject();
result.put("devices", new JSArray());
call.resolve(result);
return;
}

pendingPermissionCall = call;
permissionRequestedDevices.clear();

// Request permission for each device
for (UsbSerialDriver driver : availableDrivers) {
UsbDevice device = driver.getDevice();

if (!usbManager.hasPermission(device)) {
String deviceKey = getDeviceKey(device);
permissionRequestedDevices.put(deviceKey, device);

PendingIntent permissionIntent = PendingIntent.getBroadcast(
getContext(),
0,
new Intent(ACTION_USB_PERMISSION),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
: PendingIntent.FLAG_UPDATE_CURRENT
);

usbManager.requestPermission(device, permissionIntent);
Log.d(TAG, "Requested permission for device: " + deviceKey);
}
}

// If all devices already have permission, resolve immediately
if (permissionRequestedDevices.isEmpty()) {
resolveWithDeviceList(call);
}
} catch (Exception e) {
Log.e(TAG, "Error requesting permission", e);
call.reject("Failed to request permission: " + e.getMessage());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Concurrency issue: requestPermission lacks protection against overlapping calls.

If requestPermission is called multiple times before permission dialogs are resolved, the following race conditions occur:

  1. pendingPermissionCall gets overwritten (line 128), causing earlier calls to never resolve
  2. permissionRequestedDevices.clear() (line 129) disrupts tracking for in-flight permission requests
  3. The first call may resolve with incomplete device lists or hang indefinitely

Consider one of these solutions:

Solution 1: Reject overlapping calls

 @PluginMethod
 public void requestPermission(PluginCall call) {
     try {
+        if (pendingPermissionCall != null) {
+            call.reject("Permission request already in progress");
+            return;
+        }
+
         List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager);

Solution 2: Queue calls (more complex)
Maintain a queue of pending calls and process them sequentially.

🤖 Prompt for AI Agents
In
capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
around lines 117 to 161, requestPermission currently allows overlapping calls
which overwrites pendingPermissionCall and clears permissionRequestedDevices
causing lost/blocked responses; fix by rejecting overlapping calls at the start
(if pendingPermissionCall != null then call.reject("Permission request already
in progress") and return), only call permissionRequestedDevices.clear() when you
have established that you own the new request (i.e., after checking no active
pending call), and ensure you always clear pendingPermissionCall and
permissionRequestedDevices when the flow completes or errors so subsequent
requests can proceed.

Comment on lines +498 to +506
private byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: hexStringToByteArray lacks input validation.

The method will crash or behave incorrectly with:

  1. Odd-length hex strings (e.g., "abc" → ArrayIndexOutOfBoundsException)
  2. Invalid hex characters (e.g., "zz" → NumberFormatException with return value -1)

This is a direct crash risk since write() accepts user-provided hex strings.

Apply this diff to add validation:

 private byte[] hexStringToByteArray(String hexString) {
+    if (hexString.length() % 2 != 0) {
+        throw new IllegalArgumentException("Hex string must have even length");
+    }
+    
     int len = hexString.length();
     byte[] data = new byte[len / 2];
     for (int i = 0; i < len; i += 2) {
-        data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
-                + Character.digit(hexString.charAt(i + 1), 16));
+        int highNibble = Character.digit(hexString.charAt(i), 16);
+        int lowNibble = Character.digit(hexString.charAt(i + 1), 16);
+        
+        if (highNibble == -1 || lowNibble == -1) {
+            throw new IllegalArgumentException("Invalid hex string: contains non-hexadecimal characters");
+        }
+        
+        data[i / 2] = (byte) ((highNibble << 4) + lowNibble);
     }
     return data;
 }
🤖 Prompt for AI Agents
In
capacitor-plugin-betaflight-serial/android/src/main/java/com/betaflight/plugin/serial/BetaflightSerialPlugin.java
around lines 498 to 506, the hexStringToByteArray method lacks input validation
and can crash on odd-length strings or invalid hex characters; update the method
to validate input: check for null/empty, optionally strip a leading "0x"/"0X"
and whitespace, verify the remaining length is even, and ensure every character
is a valid hex digit (0-9, a-f, A-F) before parsing; if validation fails, throw
an IllegalArgumentException with a clear message rather than letting it produce
ArrayIndexOutOfBoundsException or negative digit values.

Comment on lines +17 to +20
"repository": {
"type": "git",
"url": "github.com/betaflight/betaflight-configurator"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the repository URL format.

The repository URL is missing the protocol prefix. It should be either "https://github.com/betaflight/betaflight-configurator" or "git+https://github.com/betaflight/betaflight-configurator.git".

Apply this diff:

   "repository": {
     "type": "git",
-    "url": "github.com/betaflight/betaflight-configurator"
+    "url": "https://github.com/betaflight/betaflight-configurator"
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"repository": {
"type": "git",
"url": "github.com/betaflight/betaflight-configurator"
},
"repository": {
"type": "git",
"url": "https://github.com/betaflight/betaflight-configurator"
},
🤖 Prompt for AI Agents
In capacitor-plugin-betaflight-serial/package.json around lines 17 to 20 the
repository.url is missing the protocol and proper git suffix; update the value
to a proper URL such as "https://github.com/betaflight/betaflight-configurator"
or "git+https://github.com/betaflight/betaflight-configurator.git" so the
repository field conforms to npm/package.json expectations.

Comment on lines +7 to +11
```bash
cd /home/mark/dev/betaflight/betaflight-configurator/capacitor-plugin-serial/betaflight-configurator

# Install the custom plugin (local package)
yarn install
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove hardcoded user-specific path.

Line 8 contains a hardcoded path specific to a developer's local environment (/home/mark/dev/betaflight/...). This should be replaced with a generic placeholder or removed entirely.

Apply this diff:

 ```bash
-cd /home/mark/dev/betaflight/betaflight-configurator/capacitor-plugin-serial/betaflight-configurator
+# Navigate to your configurator directory
+cd betaflight-configurator
 
 # Install the custom plugin (local package)
 yarn install
🤖 Prompt for AI Agents
In QUICK_START.md around lines 7 to 11, remove the hardcoded user-specific path
and replace it with a generic instruction: change the explicit "/home/mark/..."
line to a neutral navigational note such as "Navigate to your configurator
directory" followed by the command "cd betaflight-configurator" (or simply
provide the single "cd betaflight-configurator" command), leaving the following
"yarn install" step unchanged so the guide works for all users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: App

Development

Successfully merging this pull request may close these issues.

1 participant