Skip to content

Conversation

@haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented Dec 13, 2025

  • replaces Capacitor bluetooth plugin #4714
  • got it working on second try - but nuked it by accident
  • third try decided to use Nordic Android BLE Library
  • found the kill switch to silence verbose Capacitor logging which would improve other protocols too.
  • added Nordic Android Scanner Compat Library

Tested devices

  • AIRBOTRACINGH7
  • SPEEDYBEEF405AIOV2
  • SPEEDYBEEF405MINI
  • SPEEDYBEEF7V3
  • SDMODELH7V2

Summary by CodeRabbit

  • New Features

    • Android BLE support: native BLE plugin + Capacitor BLE protocol for discovery, pairing, connect/send/receive.
  • Improvements

    • Improved device discovery and fallback: auto-selects first available device when permissions are granted.
    • Android manifest updated with modern Bluetooth permissions and required BLE hardware.
  • Chores

    • Added Android BLE libraries and disabled platform logging on native builds.

✏️ Tip: You can customize this high-level summary in your review settings.

@haslinghuis haslinghuis added this to the 2026.6 milestone Dec 13, 2025
@haslinghuis haslinghuis self-assigned this Dec 13, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 13, 2025

Walkthrough

Adds native Android BLE support: Nordic BLE libraries and permissions, a new Capacitor Android plugin (BetaflightBlePlugin) implementing scanning/GATT, a JS bridge (CapacitorBle) and protocol registration, minor port-handler selection changes, and plugin registration in MainActivity.

Changes

Cohort / File(s) Summary
Android Build Configuration
android/app/build.gradle
Added Nordic Semi BLE dependencies: implementation 'no.nordicsemi.android:ble:2.11.0' and implementation 'no.nordicsemi.android.support.v18:scanner:1.6.0'.
Android Manifest Configuration
android/app/src/main/AndroidManifest.xml
Added Bluetooth permissions for legacy and modern SDKs (BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION with maxSdkVersion="30"; BLUETOOTH_SCAN with minSdkVersion="31" and usesPermissionFlags="neverForLocation"; BLUETOOTH_CONNECT with minSdkVersion="31") and uses-feature android.hardware.bluetooth_le android:required="true".
Android Plugin Implementation
android/app/src/main/java/.../protocols/ble/BetaflightBlePlugin.java
New Capacitor Plugin implementing runtime permission handling, scanning/discovery (service filters + fallback), Known/Discovered device models, Nordic-based BleBridgeManager and GATT callback (MTU, notifications, write handling), base64 I/O, connect/disconnect/send APIs, event emission, and cleanup.
Android Plugin Registration
android/app/src/main/java/betaflight/app/MainActivity.java
Imported and registered BetaflightBlePlugin in onCreate.
JS BLE Protocol Implementation
src/js/protocols/CapacitorBle.js
New CapacitorBle class bridging to native plugin: base64 ⇄ Uint8Array conversion, getDevices/requestPermissionDevice, connect/disconnect, send, receive event handling, CRC-bypass heuristic, and internal state tracking.
JS Protocol Registration
src/js/serial.js
Imported and registered CapacitorBle as the Android "bluetooth" protocol.
Port & Device Management
src/js/port_handler.js
Early return added when permission is cancelled/fails and selected port reset to DEFAULT_PORT; device selection/fallback logic clarified.
Capacitor Core Integration
src/js/main.js
Imported Capacitor from @capacitor/core and conditionally disabled logging on native Capacitor platforms (Capacitor.isLoggingEnabled = false when boolean).
Bluetooth Support Detection
src/js/utils/checkCompatibility.js
On Android, Bluetooth support detection explicitly set to true.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as App/UI
    participant JS as CapacitorBle (JS)
    participant Plugin as BetaflightBlePlugin (Android)
    participant Manager as BleBridgeManager (Nordic)
    participant Device as BLE Device

    User->>UI: trigger scan / connect / send / disconnect
    UI->>JS: getDevices() / requestPermissionDevice() / connect(path) / send(data) / disconnect()
    JS->>Plugin: Capacitor bridge call (getDevices/connect/send/disconnect)
    Plugin->>Manager: startScan() / connect()/ write()/ disconnect()
    Manager->>Device: BLE scanning / GATT connect / write / enable notifications
    Device-->>Manager: adverts / notifications / disconnect
    Manager-->>Plugin: scan results / connection events / notifications
    Plugin-->>JS: callback results / emit events ("connect","receive","disconnect")
    JS-->>UI: propagate events with Uint8Array payloads
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Files needing extra attention:
    • android/app/src/main/java/.../protocols/ble/BetaflightBlePlugin.java — permission handling for Android S+, scan/filter fallback, GATT lifecycle (MTU, notifications), threading and cleanup.
    • src/js/protocols/CapacitorBle.js — data encoding/decoding, event wiring and error propagation across the Capacitor bridge.
    • src/js/port_handler.js — permission-cancel path and selected port reset logic.

Possibly related PRs

Suggested labels

Tested

Suggested reviewers

  • nerdCopter
  • blckmn
  • VitroidFPV

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description lacks the structured format required by the template, missing essential sections like feature description, motivation, and testing details despite mentioning tested devices. Expand the description to follow the template structure: add a clear feature title, detailed motivation, implementation approach, and properly formatted testing section with device list and test results.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Capacitor BLE plugin using Nordic library' clearly and specifically summarizes the main change: implementation of a BLE plugin using the Nordic Android BLE library.
✨ 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.

@haslinghuis haslinghuis moved this to App in 2026.6.0 Dec 13, 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: 0

🧹 Nitpick comments (4)
android/app/src/main/AndroidManifest.xml (1)

69-69: Consider making BLE feature optional for broader device compatibility.

Setting android:required="true" for bluetooth_le will prevent installation on devices without BLE hardware. If the app supports other connection methods (Serial, TCP), consider setting android:required="false" to allow installation on non-BLE devices.

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

125-125: Baud rate storage is unnecessary for BLE.

BLE connections don't use baud rates (it's a packetized protocol, not a serial stream). Storing bitrate from options?.baudRate may be misleading. Consider removing this assignment or documenting why it's kept for compatibility.

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (2)

197-198: Consider canceling pending callbacks when starting fallback scan.

When startFallbackScan is called from line 205, the handler already has a pending callback from line 198. Consider using handler.removeCallbacksAndMessages(null) before posting the new delayed callback to prevent multiple finishScan invocations.

Apply this approach:

 private void startFallbackScan(PluginCall call) {
     if (scanner == null) {
         call.reject("Bluetooth LE scanner unavailable");
         return;
     }
 
+    handler.removeCallbacksAndMessages(null);
     fallbackScan = true;
     scanning = true;

Also applies to: 239-240


324-331: Connection enqueue lacks explicit error handling for queue failures.

While the .fail() handler at lines 327-330 covers connection failures, if the .enqueue() call itself throws an exception (e.g., if the BLE queue is full or the adapter is disabled), the PluginCall may never resolve or reject, leaving the JS layer waiting indefinitely.

Wrap the enqueue in a try-catch:

-        bleManager.connect(device)
+        try {
+            bleManager.connect(device)
             .useAutoConnect(false)
             .timeout(15_000)
             .fail((dev, status) -> {
                 connectedAddress = null;
                 call.reject("Connection failed: " + status);
             })
             .enqueue();
+        } catch (Exception e) {
+            connectedAddress = null;
+            call.reject("Failed to enqueue connection: " + e.getMessage());
+        }
📜 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 4f4522a and e74da72.

📒 Files selected for processing (9)
  • android/app/build.gradle (1 hunks)
  • android/app/src/main/AndroidManifest.xml (1 hunks)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1 hunks)
  • src/js/main.js (1 hunks)
  • src/js/port_handler.js (3 hunks)
  • src/js/protocols/CapacitorBle.js (1 hunks)
  • src/js/serial.js (2 hunks)
  • src/js/utils/checkCompatibility.js (1 hunks)
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.
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.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/build.gradle
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • src/js/main.js
  • src/js/serial.js
  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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/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/js/port_handler.js
  • src/js/serial.js
📚 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/port_handler.js
  • src/js/serial.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/port_handler.js
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • src/js/serial.js
  • src/js/utils/checkCompatibility.js
  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:41:59.374Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.

Applied to files:

  • src/js/port_handler.js
  • src/js/serial.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-11-24T15:07:25.227Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.

Applied to files:

  • src/js/port_handler.js
  • src/js/serial.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-06-09T00:33:22.959Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:33:22.959Z
Learning: In the betaflight-configurator codebase, port paths use counter suffixes for serial and bluetooth ports (e.g., "serial1", "serial2", "bluetooth1", "bluetooth2") instead of using connectionId, which means simple string matching against protocol map keys won't work for protocol selection.

Applied to files:

  • src/js/port_handler.js
📚 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/port_handler.js
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • src/js/serial.js
  • src/js/utils/checkCompatibility.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-22T21:18:08.814Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:11-46
Timestamp: 2025-11-22T21:18:08.814Z
Learning: In betaflight-configurator, protocol implementations (WebSerial, CapacitorSerial, WebBluetooth, etc.) are instantiated once in the Serial class constructor and stored as singletons in the _protocols array for the entire application lifetime. They are never destroyed or recreated, so cleanup methods to remove event listeners are not needed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • src/js/serial.js
  • src/js/protocols/CapacitorBle.js
📚 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:

  • android/app/src/main/AndroidManifest.xml
  • src/js/utils/checkCompatibility.js
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
🧬 Code graph analysis (2)
src/js/port_handler.js (1)
src/js/serial.js (2)
  • serial (232-232)
  • serial (232-232)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1)
src/js/protocols/CapacitorBle.js (2)
  • bytes (11-11)
  • plugin (5-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (18)
src/js/utils/checkCompatibility.js (1)

144-145: LGTM!

Correctly enables Bluetooth support detection for Android native platform, aligning with the new CapacitorBle protocol implementation.

src/js/port_handler.js (3)

162-193: LGTM!

The refactored permission flow correctly:

  1. Updates the device list first
  2. Selects the granted port if available
  3. Falls back to first available device
  4. Resets to DEFAULT_PORT when no devices exist

The use of the IIFE for availablePorts is clean and ensures the correct port array is referenced based on protocol type.


210-211: LGTM!

Adding BLE-specific display names ("SpeedyBee", "Nordic NRF") to the device filter improves automatic selection of Betaflight-compatible BLE devices.


344-365: Good defensive handling for BLE connection stability.

Preserving the existing Bluetooth port list when scans return empty (but a connection or selection is active) prevents losing the selected port mid-connection. This is important because BLE scans can intermittently return empty results while a device remains connected.

src/js/serial.js (2)

7-7: LGTM!

Import follows the existing pattern for protocol implementations.


24-29: LGTM!

The CapacitorBle protocol is correctly registered for Android, following the established pattern. The "bluetooth" name ensures proper protocol selection via portPath.startsWith("bluetooth") in selectProtocol().

Based on learnings, ensure the CapacitorBle implementation uses idempotent listener attachment (attaching native plugin listeners exactly once per app lifetime) to avoid duplicate handlers and MSP queue pressure.

android/app/build.gradle (1)

39-39: LGTM - Nordic BLE library version 2.11.0 is current.

Version 2.11.0 is the latest stable release of the Nordic Semiconductor Android BLE library. The library is well-maintained and widely used for Android BLE development.

src/js/main.js (1)

19-24: LGTM.

The defensive checks ensure robustness, and Capacitor.isLoggingEnabled is a documented property in the official @capacitor/core API. This is a solid developer experience improvement that reduces console noise on native platforms.

android/app/src/main/java/betaflight/configurator/MainActivity.java (1)

6-6: LGTM!

The BLE plugin registration follows the same pattern as the existing Serial and TCP plugins.

Also applies to: 14-14

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

58-65: Version-aware permission structure looks good.

The permission declarations correctly follow Android version requirements: legacy BLUETOOTH/BLUETOOTH_ADMIN with maxSdkVersion="30", and new BLUETOOTH_SCAN/BLUETOOTH_CONNECT for Android 12+. The neverForLocation flag on BLUETOOTH_SCAN properly indicates that location is not the purpose of scanning.

Note: ACCESS_COARSE_LOCATION (line 60) is required for Android 8-11 for BLE scanning, and the Java code at lines 111-120 of BetaflightBlePlugin.java properly checks permissions based on Android version, which aligns with the learnings on version-aware permission handling.

src/js/protocols/CapacitorBle.js (3)

27-54: Event listener setup follows the singleton pattern correctly.

The listeners are attached in the constructor, which is appropriate since protocol implementations are instantiated once and never destroyed. This matches the learning that native plugin listeners should be attached exactly once per app lifetime.


166-186: LGTM!

The send method properly handles the disconnected state, base64 encoding, error handling, and callback invocation.


70-70: Path naming convention bluetooth-${d.address} is intentional and consistent with the codebase.

The address-specific naming (using Bluetooth MAC addresses) is the correct approach for this implementation. It provides unique, persistent device identification across connections, similar to how webusbdfu.js uses usb_${serialNumber} and CapacitorSerial.js uses capacitor-${deviceId}. The path is properly used throughout the class for device lookup and connection management.

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (5)

107-121: LGTM! Version-aware permission checking implemented correctly.

The permission checking properly differentiates between Android 12+ (API 31+) requiring BLUETOOTH_SCAN and BLUETOOTH_CONNECT, versus Android 8-11 requiring BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION. This aligns with the learnings on version-aware BLE permission handling.

Based on learnings about Android BLE permission requirements across API levels.


281-322: Connection observer setup is comprehensive.

The ConnectionObserver properly handles all lifecycle states (connecting, connected, failed, ready, disconnecting, disconnected) and correctly resolves/rejects the PluginCall based on the outcome. The onDeviceReady callback (lines 303-307) is the appropriate place to resolve the connection, as it signals that services are discovered and notifications are enabled.

This aligns with the learning that the connection should only be announced after GATT services are resolved and notifications are started.


584-595: LGTM! Write characteristic caching follows best practices.

The write characteristic is resolved and cached in isRequiredServiceSupported() (line 584), the write type is determined and set based on PROPERTY_WRITE_NO_RESPONSE (lines 591-593), and the cached characteristic is used in the send() method (line 616). Characteristics are properly cleared in onServicesInvalidated() (lines 607-608).

Based on learnings about caching write characteristics once in onServicesDiscovered and using cached values in write operations.


612-617: Send method correctly uses cached write characteristic.

The send() method is strictly "write-only" - it uses the cached writeCharacteristic without performing any UUID lookups or characteristic resolution. The .split() call enables packet splitting for large payloads, which is appropriate for BLE's MTU constraints.

This matches the learning that write methods should be "write-only" and use cached characteristics.


143-199: The timeout coordination is safe—callbacks execute sequentially, not concurrently.

When the primary scan timeout fires at 5 seconds, finishScan() executes, calls stopScan(), and if no devices are found, immediately returns after posting the fallback timeout. The fallback timeout is not posted until the primary timeout has already fired and completed. Subsequent finishScan() callbacks fire at 9 seconds (5s + 4s), after the primary one has fully executed. There is no race condition with multiple pending callbacks.

@github-actions
Copy link
Contributor

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: 1

🧹 Nitpick comments (4)
src/js/port_handler.js (1)

162-174: updatedPorts variable is unused in practice.

The updatedPorts return value is only referenced in the default case (line 172), which handles unknown protocols that wouldn't have valid ports anyway. Since updateDeviceList already populates this.current*Ports arrays before the IIFE executes, the variable serves no practical purpose.

Consider simplifying:

-        const updatedPorts = await this.updateDeviceList(protocol);
-        const availablePorts = (() => {
-            switch (protocol) {
-                case "bluetooth":
-                    return this.currentBluetoothPorts;
-                case "usb":
-                    return this.currentUsbPorts;
-                case "serial":
-                    return this.currentSerialPorts;
-                default:
-                    return updatedPorts ?? [];
-            }
-        })();
+        await this.updateDeviceList(protocol);
+        const availablePorts =
+            protocol === "bluetooth" ? this.currentBluetoothPorts :
+            protocol === "usb" ? this.currentUsbPorts :
+            protocol === "serial" ? this.currentSerialPorts : [];
src/js/protocols/CapacitorBle.js (1)

8-8: Add braces to single-line if statements.

SonarCloud flags missing braces on single-line if statements. While functionally correct, adding braces improves consistency and prevents errors during future modifications.

Apply this diff to add braces:

 const base64ToUint8Array = (b64) => {
-    if (!b64) return new Uint8Array(0);
+    if (!b64) {
+        return new Uint8Array(0);
+    }
     const binary = atob(b64);
 async getDevices() {
-    if (!plugin) return [];
+    if (!plugin) {
+        return [];
+    }
 async connect(path, options) {
-    if (!plugin) return false;
+    if (!plugin) {
+        return false;
+    }
 async disconnect() {
-    if (!plugin) return false;
-    if (!this.connected) return true;
+    if (!plugin) {
+        return false;
+    }
+    if (!this.connected) {
+        return true;
+    }
 shouldBypassCrc(expectedChecksum) {
     const deviceDescription = this.deviceDescription;
-    if (!deviceDescription) return false;
+    if (!deviceDescription) {
+        return false;
+    }

Also applies to: 61-61, 96-96, 139-140, 160-160

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (2)

182-186: Consider logging empty service UUIDs for debugging.

When requestedServices is empty (either from no input or all invalid UUIDs), the code silently falls back to KNOWN_DEVICES. Consider logging this fallback to help diagnose issues where specific service UUIDs are expected but not received.

Apply this diff to add debug logging:

 if (requestedServices.isEmpty()) {
+    Log.d(TAG, "No valid service UUIDs requested, using all known devices");
     for (String service : KNOWN_DEVICES.keySet()) {
         requestedServices.add(UUID.fromString(service));
     }
 }

68-88: Document the fallback device name aliases.

Lines 86-87 add name-based fallback aliases for SpeedyBee devices. Consider adding a comment explaining that these aliases are used during fallback scans when devices don't advertise service UUIDs, to help future maintainers understand this mapping.

Apply this diff to add clarifying comments:

         addDevice("DroneBridge", "0000db32-0000-1000-8000-00805f9b34fb",
             "0000db33-0000-1000-8000-00805f9b34fb", "0000db34-0000-1000-8000-00805f9b34fb");
-        // Extra name aliases that map to known profiles (helps when service UUIDs aren't advertised)
+        // Fallback name aliases for devices that don't always advertise service UUIDs
+        // Used during unfiltered scan when service-filtered scan returns no results
         KNOWN_DEVICES_BY_NAME.put("speedybee f7v3", KNOWN_DEVICES.get("0000abf0-0000-1000-8000-00805f9b34fb"));
         KNOWN_DEVICES_BY_NAME.put("speedybee", KNOWN_DEVICES.get("0000abf0-0000-1000-8000-00805f9b34fb"));
📜 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 e74da72 and 6de4e92.

📒 Files selected for processing (9)
  • android/app/build.gradle (1 hunks)
  • android/app/src/main/AndroidManifest.xml (1 hunks)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1 hunks)
  • src/js/main.js (1 hunks)
  • src/js/port_handler.js (3 hunks)
  • src/js/protocols/CapacitorBle.js (1 hunks)
  • src/js/serial.js (2 hunks)
  • src/js/utils/checkCompatibility.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/js/main.js
  • src/js/serial.js
  • android/app/src/main/AndroidManifest.xml
  • android/app/build.gradle
  • src/js/utils/checkCompatibility.js
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
📚 Learning: 2025-11-22T21:18:08.814Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:11-46
Timestamp: 2025-11-22T21:18:08.814Z
Learning: In betaflight-configurator, protocol implementations (WebSerial, CapacitorSerial, WebBluetooth, etc.) are instantiated once in the Serial class constructor and stored as singletons in the _protocols array for the entire application lifetime. They are never destroyed or recreated, so cleanup methods to remove event listeners are not needed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-11-28T22:41:59.374Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
📚 Learning: 2025-11-24T15:07:25.227Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
📚 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/protocols/CapacitorBle.js
  • src/js/port_handler.js
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • src/js/protocols/CapacitorBle.js
📚 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/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/js/port_handler.js
📚 Learning: 2025-06-09T00:33:22.959Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:33:22.959Z
Learning: In the betaflight-configurator codebase, port paths use counter suffixes for serial and bluetooth ports (e.g., "serial1", "serial2", "bluetooth1", "bluetooth2") instead of using connectionId, which means simple string matching against protocol map keys won't work for protocol selection.

Applied to files:

  • src/js/port_handler.js
🧬 Code graph analysis (1)
src/js/port_handler.js (1)
src/js/serial.js (2)
  • serial (232-232)
  • serial (232-232)
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorBle.js

[failure] 61-61: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjR&open=AZsZRuMthFfV5pEjIjjR&pullRequest=4735


[failure] 160-160: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjV&open=AZsZRuMthFfV5pEjIjjV&pullRequest=4735


[warning] 13-13: Prefer String#codePointAt() over String#charCodeAt().

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjP&open=AZsZRuMthFfV5pEjIjjP&pullRequest=4735


[failure] 96-96: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjS&open=AZsZRuMthFfV5pEjIjjS&pullRequest=4735


[failure] 139-139: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjT&open=AZsZRuMthFfV5pEjIjjT&pullRequest=4735


[failure] 8-8: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjO&open=AZsZRuMthFfV5pEjIjjO&pullRequest=4735


[failure] 140-140: Expected { after 'if' condition.

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjU&open=AZsZRuMthFfV5pEjIjjU&pullRequest=4735


[warning] 21-21: Prefer String.fromCodePoint() over String.fromCharCode().

See more on https://sonarcloud.io/project/issues?id=betaflight_betaflight-configurator&issues=AZsZRuMthFfV5pEjIjjQ&open=AZsZRuMthFfV5pEjIjjQ&pullRequest=4735

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
src/js/port_handler.js (3)

176-193: Clean permission flow with proper fallback handling.

The early return pattern and fallback chain (explicit port → first available → default) handles edge cases well. The flow correctly resets to DEFAULT_PORT when no devices are found after permission request.


211-211: Good addition of BLE device identifiers.

Adding "SpeedyBee" and "Nordic NRF" to the device filter enables automatic port selection for common BLE devices, which aligns with the Capacitor BLE plugin feature.


344-365: Good defensive logic to preserve Bluetooth ports during empty scans.

This correctly addresses the scenario where BLE scans return empty while an active connection or user selection exists. The condition properly checks for both connected state and selected port prefix, preventing loss of the active Bluetooth port from the UI.

Based on learnings, this pattern is important for maintaining stable Bluetooth connections in the configurator.

src/js/protocols/CapacitorBle.js (3)

43-53: LGTM! Event listeners attached correctly.

The constructor attaches native plugin listeners exactly once, which is correct since protocol implementations are singletons in this codebase. The event names "dataReceived" and "disconnected" match the native plugin's notifyListeners calls.

Based on learnings, this pattern prevents duplicate handlers and MSP queue pressure issues.


7-16: Base64 encoding is correct; ignore SonarCloud warning.

SonarCloud suggests using codePointAt() instead of charCodeAt(), but this is a false positive. Base64 encoding requires Latin-1 byte-to-character mapping (0-255 range), and charCodeAt() is the correct choice here. Using codePointAt() would break the encoding for non-ASCII bytes.


95-136: Verify connection timing aligns with GATT readiness.

The connect method emits the "connect" event immediately after plugin.connect resolves. Based on learnings, this should only happen after GATT services are resolved and notifications are started to prevent "Unable to write characteristic" errors.

Reviewing the native code (BetaflightBlePlugin.java lines 303-307), the plugin resolves the call in onDeviceReady, which is called after service discovery and notification setup complete. This timing is correct.

Verify that the first MSP write after connection succeeds consistently. If you observe "Unable to write characteristic" errors, it may indicate the native plugin is resolving too early.

Based on learnings about connection timing and GATT service readiness.

android/app/src/main/java/betaflight/configurator/MainActivity.java (1)

6-6: LGTM!

The BLE plugin registration follows the same pattern as existing plugins (Serial, TCP). The import and registration are correct.

Also applies to: 14-14

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (3)

107-121: LGTM! Version-aware permission handling.

The permission check correctly handles Android version differences:

  • Android 12+ (API 31+): Checks BLUETOOTH_SCAN and BLUETOOTH_CONNECT
  • Android 8-11 (API 26-30): Checks BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION

This implementation matches the learnings about version-aware permission handling and prevents requesting non-existent permissions on older Android versions.

Based on learnings about Android Bluetooth permission handling across API levels.


576-609: LGTM! Characteristic caching follows best practices.

The ManagerGattCallback correctly implements the characteristic caching pattern:

  • Caches writeCharacteristic and notifyCharacteristic in isRequiredServiceSupported
  • Sets write type to WRITE_TYPE_NO_RESPONSE when supported (preferred for throughput)
  • Enables notifications in initialize()
  • Clears cached characteristics in onServicesInvalidated()

This follows the learnings about caching characteristics once during service discovery and clearing them on disconnect.

Based on learnings about write-only pattern and characteristic caching.


303-307: LGTM! Connection resolves at the correct time.

The connect call is resolved in onDeviceReady, which is called after service discovery and notification setup are complete. This ensures the JavaScript layer only receives the "connect" event after the GATT connection is fully ready for data transfer, preventing "Unable to write characteristic" errors.

Based on learnings about connection timing and GATT service readiness.

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: 1

♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1)

603-606: LGTM: MTU negotiation properly implemented.

The code correctly requests MTU (247 bytes) during connection initialization and stores the negotiated value for use in data transmission. This addresses the concern from previous reviews about ensuring efficient BLE writes by negotiating a larger MTU than the default 23 bytes.

📜 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 6de4e92 and b27256a.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T16:39:04.171Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/FileSystem.js:101-107
Timestamp: 2025-09-28T16:39:04.171Z
Learning: In the Betaflight Configurator dataflash download implementation, removing await from FileSystem.writeChunk() and closeFile() is intentional to prevent MSP queue overwhelm. Awaiting file I/O operations slows the download loop enough to cause MSP timeouts and queue growth, leading to stalls. The non-blocking approach prioritizes download throughput over immediate file write error detection.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
🪛 GitHub Actions: Deployment (PR and Push)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

[error] 625-625: no suitable method found for split(int). The call to split(chunkSize) uses an int, but WriteRequest.split(...) requires DataSplitter or WriteProgressCallback.

🔇 Additional comments (9)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (9)

47-59: LGTM: Plugin permissions properly declared.

The permission annotation correctly includes all necessary BLE permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT, ACCESS_COARSE_LOCATION) under a single "bluetooth" alias, which aligns with the version-aware permission handling implemented in the permission check methods.


65-88: LGTM: Known device registry enables flexible discovery.

The dual-map approach (by service UUID and by name) allows matching devices both by advertised service UUIDs and by device names during fallback scans, which is essential for devices that don't advertise service UUIDs properly.


109-142: LGTM: Version-aware permission handling implemented correctly.

The implementation properly checks different permissions based on Android version (BLUETOOTH_SCAN/BLUETOOTH_CONNECT for API 31+, BLUETOOTH/BLUETOOTH_ADMIN/ACCESS_COARSE_LOCATION for earlier versions), which aligns with Android's BLE permission model changes.

Based on learnings, this matches the required pattern for Android BLE permission handling.


144-243: LGTM: Two-phase scanning strategy handles devices with and without advertised service UUIDs.

The implementation uses filtered scanning (by service UUID) as the primary method, falling back to unfiltered scanning with name-based device matching if no results are found. This balances power efficiency with device discoverability, which is appropriate for native Android BLE (unlike Web Bluetooth's acceptAllDevices pattern).


250-334: LGTM: Connection lifecycle properly managed with Nordic BLE library.

The implementation creates a new BleBridgeManager instance per connection, sets up connection observers to emit events to the JS layer, and uses Nordic BLE library's fluent API with appropriate timeout (15s) and error handling.


360-389: LGTM: Send method follows write-only pattern.

The implementation correctly delegates to the pre-configured bleManager.send() without performing per-call UUID parsing or characteristic lookups, and accurately reports the original payload length as bytesSent (the internal fragmentation is transparent to the caller).

Based on learnings, the write-only pattern is preferred for the public send method.


391-402: LGTM: Cleanup properly stops scans and closes BLE manager.

The handleOnDestroy() method ensures that active scans are stopped and the BLE manager is closed when the plugin is destroyed, preventing resource leaks.


581-599: LGTM: Characteristics cached and write type determined once during service discovery.

The implementation correctly caches writeCharacteristic and notifyCharacteristic references in isRequiredServiceSupported(), sets up notification callbacks, and configures the write type based on characteristic properties. This matches the pattern from learnings where characteristic resolution happens once during service discovery rather than per-write operation.

Based on learnings, caching characteristics and write type in onServicesDiscovered (isRequiredServiceSupported) is the preferred pattern.


517-525: LGTM: Notification handler correctly bridges native BLE data to JS layer.

The method receives binary data from BLE notifications, encodes it as base64, and emits it to JavaScript listeners via the "dataReceived" event, which is the appropriate pattern for binary data transfer between native and JavaScript.

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

♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1)

620-626: Remove the problematic split() call and unused chunkSize calculation.

As identified in the previous review, line 625 is missing the problematic .split(chunkSize) call that causes a compilation error because WriteRequest.split() does not accept an int parameter. The Nordic BLE library automatically handles fragmentation based on the negotiated MTU when WRITE_TYPE_NO_RESPONSE is set (line 595).

Additionally, the chunkSize calculation at line 624 is unused and should be removed for clarity.

The current code at line 625 is:

return writeCharacteristic(writeCharacteristic, data);

If the problematic split(chunkSize) call is present, apply this diff:

 	WriteRequest send(byte[] data) {
 		if (writeCharacteristic == null) {
 			return null;
 		}
-		int chunkSize = Math.max(negotiatedMtu - 3, 20); // 3 bytes ATT overhead
-		return writeCharacteristic(writeCharacteristic, data).split(chunkSize);
+		return writeCharacteristic(writeCharacteristic, data);
 	}

The library will automatically split packets based on the negotiatedMtu when using WRITE_TYPE_NO_RESPONSE.

📜 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 b27256a and dbd4114.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T16:39:04.171Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/FileSystem.js:101-107
Timestamp: 2025-09-28T16:39:04.171Z
Learning: In the Betaflight Configurator dataflash download implementation, removing await from FileSystem.writeChunk() and closeFile() is intentional to prevent MSP queue overwhelm. Awaiting file I/O operations slows the download loop enough to cause MSP timeouts and queue growth, leading to stalls. The non-blocking approach prioritizes download throughput over immediate file write error detection.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy
🔇 Additional comments (3)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (3)

144-243: LGTM! Well-designed two-phase scanning approach.

The scanning logic correctly implements a two-phase approach: first scanning with service UUID filters (lines 190-200), then falling back to an unfiltered scan with name matching (lines 228-242) if no devices are found. This balances efficiency with device discovery completeness. The exception handling for malformed service UUID arrays (lines 169-181) and permission/adapter checks are appropriate.


391-452: LGTM! Proper lifecycle management and cleanup.

The lifecycle management is well-implemented with appropriate cleanup in handleOnDestroy() (lines 391-402) and exception handling for both BLE manager closure (line 398) and scan stopping (line 409). The dual scan callback structure correctly delegates to handleResult().


454-515: LGTM! Robust profile matching logic.

The result handling correctly prioritizes service UUID matching over name-based matching, updates RSSI for duplicate discoveries (line 468), and implements a fallback hierarchy that aligns with the two-phase scan strategy.

Comment on lines +134 to +153
@PermissionCallback
private void onBlePermissionResult(PluginCall call) {
if (hasBlePermissions()) {
// Continue the original request now that permission is granted
getDevices(call);
} else {
call.reject("Bluetooth permission denied");
}
}
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

Fix permission callback to resume the correct original method.

The onBlePermissionResult callback always calls getDevices(call), but permission requests can originate from connect() via line 252. When a user calls connect() without permissions, the callback incorrectly invokes getDevices() instead of resuming connect(), breaking the connection flow.

Apply this diff to use a saved call method or save the original method name:

+	private String pendingCallMethod = null;
+
 	@PermissionCallback
 	private void onBlePermissionResult(PluginCall call) {
 		if (hasBlePermissions()) {
-			// Continue the original request now that permission is granted
-			getDevices(call);
+			if ("connect".equals(pendingCallMethod)) {
+				connect(call);
+			} else {
+				getDevices(call);
+			}
+			pendingCallMethod = null;
 		} else {
 			call.reject("Bluetooth permission denied");
+			pendingCallMethod = null;
 		}
 	}

Then update ensurePermissions() to accept and save the method name:

-	private boolean ensurePermissions(PluginCall call) {
+	private boolean ensurePermissions(PluginCall call, String methodName) {
 		if (hasBlePermissions()) {
 			return true;
 		}
-
+		pendingCallMethod = methodName;
 		requestPermissionForAlias("bluetooth", call, "onBlePermissionResult");
 		return false;
 	}

And update callers:

-		if (!ensurePermissions(call)) {
+		if (!ensurePermissions(call, "getDevices")) {
 			return;
 		}
-		if (!ensurePermissions(call)) {
+		if (!ensurePermissions(call, "connect")) {
 			return;
 		}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
around lines 134-142, the permission callback always resumes getDevices(call)
which breaks flows that requested permission from different methods (e.g.,
connect). Modify the class to save the original PluginCall and the originating
method name when requesting permissions (add fields like pendingCall and
pendingMethod), change ensurePermissions(...) to accept and store the method
name and call, update callers (getDevices and connect) to pass their method name
when calling ensurePermissions, and in onBlePermissionResult resume the correct
flow by checking pendingMethod (call getDevices(pendingCall) or
connect(pendingCall) accordingly); finally clear pendingCall/pendingMethod after
handling.

Comment on lines +283 to +344
bleManager.setConnectionObserver(new ConnectionObserver() {
@Override
public void onDeviceConnecting(@NonNull BluetoothDevice device) {
Log.d(TAG, "Connecting to " + device.getAddress());
}

@Override
public void onDeviceConnected(@NonNull BluetoothDevice device) {
Log.d(TAG, "Connected to " + device.getAddress());
connectedAddress = device.getAddress();
JSObject evt = new JSObject();
evt.put("address", connectedAddress);
notifyListeners("connected", evt);
}

@Override
public void onDeviceFailedToConnect(@NonNull BluetoothDevice device, int reason) {
connectedAddress = null;
call.reject("Connection failed: " + reason);
}

@Override
public void onDeviceReady(@NonNull BluetoothDevice device) {
JSObject res = new JSObject();
res.put("success", true);
call.resolve(res);
}

@Override
public void onDeviceDisconnecting(@NonNull BluetoothDevice device) {
Log.d(TAG, "Disconnecting " + device.getAddress());
}

@Override
public void onDeviceDisconnected(@NonNull BluetoothDevice device, int reason) {
connectedAddress = null;
JSObject evt = new JSObject();
evt.put("address", device.getAddress());
evt.put("reason", reason);
notifyListeners("disconnected", evt);
}
});

bleManager.connect(device)
.useAutoConnect(false)
.timeout(15_000)
.fail((dev, status) -> {
connectedAddress = null;
call.reject("Connection failed: " + status);
})
.enqueue();
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

Prevent duplicate call resolution in connection callbacks.

The connection flow has multiple code paths that can resolve or reject the same PluginCall: onDeviceFailedToConnect() (line 301), onDeviceReady() (line 308), and connect().fail() (line 331). If onDeviceFailedToConnect() fires, then connect().fail() may also trigger, causing a duplicate rejection and potential runtime exception.

Apply this diff to guard against duplicate resolution:

+		final boolean[] callResolved = {false};
+
 		bleManager.setConnectionObserver(new ConnectionObserver() {
 			@Override
 			public void onDeviceConnecting(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Connecting to " + device.getAddress());
 			}

 			@Override
 			public void onDeviceConnected(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Connected to " + device.getAddress());
 				connectedAddress = device.getAddress();
 				JSObject evt = new JSObject();
 				evt.put("address", connectedAddress);
 				notifyListeners("connected", evt);
 			}

 			@Override
 			public void onDeviceFailedToConnect(@NonNull BluetoothDevice device, int reason) {
 				connectedAddress = null;
+				if (!callResolved[0]) {
+					callResolved[0] = true;
 					call.reject("Connection failed: " + reason);
+				}
 			}

 			@Override
 			public void onDeviceReady(@NonNull BluetoothDevice device) {
+				if (!callResolved[0]) {
+					callResolved[0] = true;
 					JSObject res = new JSObject();
 					res.put("success", true);
 					call.resolve(res);
+				}
 			}

 			@Override
 			public void onDeviceDisconnecting(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Disconnecting " + device.getAddress());
 			}

 			@Override
 			public void onDeviceDisconnected(@NonNull BluetoothDevice device, int reason) {
 				connectedAddress = null;
 				JSObject evt = new JSObject();
 				evt.put("address", device.getAddress());
 				evt.put("reason", reason);
 				notifyListeners("disconnected", evt);
 			}
 		});

 		bleManager.connect(device)
 			.useAutoConnect(false)
 			.timeout(15_000)
 			.fail((dev, status) -> {
 				connectedAddress = null;
+				if (!callResolved[0]) {
+					callResolved[0] = true;
 					call.reject("Connection failed: " + status);
+				}
 			})
 			.enqueue();
🤖 Prompt for AI Agents
In
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
around lines 283-333, multiple callbacks can resolve/reject the same PluginCall
causing duplicate resolution exceptions; introduce a single atomic guard (e.g.,
an AtomicBoolean called callHandled) or equivalent synchronized boolean and
check it before calling call.resolve()/call.reject(), and set it when you first
resolve or reject so subsequent callbacks (onDeviceFailedToConnect,
onDeviceReady, and connect().fail()) skip acting on the call; update every place
that currently calls call.resolve/call.reject to first test-and-set the guard
and avoid double-resolution.

Comment on lines +360 to +400
@PluginMethod
public void send(PluginCall call) {
if (bleManager == null || !bleManager.isConnected()) {
call.reject("Not connected");
return;
}

String b64 = call.getString("data");
if (b64 == null || b64.isEmpty()) {
call.reject("data is required");
return;
}

byte[] payload = Base64.decode(b64, Base64.NO_WRAP);
WriteRequest request = bleManager.send(payload);

if (request == null) {
call.reject("Not ready to send data");
return;
}

request
.done(device -> {
JSObject res = new JSObject();
res.put("bytesSent", payload.length);
call.resolve(res);
})
.fail((device, status) -> call.reject("Send failed: " + status))
.enqueue();
}
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

Add error handling for Base64 decoding.

The Base64.decode() call at line 373 can throw IllegalArgumentException if the input is malformed, causing an unhandled exception. While invalid input is a client error, gracefully rejecting the call improves robustness.

Apply this diff to add error handling:

 	@PluginMethod
 	public void send(PluginCall call) {
 		if (bleManager == null || !bleManager.isConnected()) {
 			call.reject("Not connected");
 			return;
 		}

 		String b64 = call.getString("data");
 		if (b64 == null || b64.isEmpty()) {
 			call.reject("data is required");
 			return;
 		}

-		byte[] payload = Base64.decode(b64, Base64.NO_WRAP);
+		byte[] payload;
+		try {
+			payload = Base64.decode(b64, Base64.NO_WRAP);
+		} catch (IllegalArgumentException e) {
+			call.reject("Invalid base64 data: " + e.getMessage());
+			return;
+		}
+
 		WriteRequest request = bleManager.send(payload);

 		if (request == null) {
 			call.reject("Not ready to send data");
 			return;
 		}

 		request
 			.done(device -> {
 				JSObject res = new JSObject();
 				res.put("bytesSent", payload.length);
 				call.resolve(res);
 			})
 			.fail((device, status) -> call.reject("Send failed: " + status))
 			.enqueue();
 	}
🤖 Prompt for AI Agents
In
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
around lines 360 to 389, the Base64.decode(b64, Base64.NO_WRAP) call can throw
IllegalArgumentException for malformed input; wrap the decode in a try/catch
catching IllegalArgumentException, call.call.reject with a clear message
(include the exception message) when decoding fails, and return immediately to
avoid proceeding with a null payload or throwing; ensure subsequent code only
runs if decoding succeeded.

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: 1

♻️ Duplicate comments (3)
android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (3)

145-153: Fix permission callback to resume the correct originating method.

The onBlePermissionResult callback always invokes getDevices(call), but permissions can be requested from both getDevices() (line 157) and connect() (line 263). When a user calls connect() without permissions, the callback incorrectly resumes getDevices() instead of connect(), breaking the connection flow.

Store the originating method name when requesting permissions:

+	private String pendingCallMethod = null;
+
 	@PermissionCallback
 	private void onBlePermissionResult(PluginCall call) {
 		if (hasBlePermissions()) {
-			// Continue the original request now that permission is granted
-			getDevices(call);
+			if ("connect".equals(pendingCallMethod)) {
+				connect(call);
+			} else {
+				getDevices(call);
+			}
+			pendingCallMethod = null;
 		} else {
 			call.reject("Bluetooth permission denied");
+			pendingCallMethod = null;
 		}
 	}

Update ensurePermissions() to save the method name:

-	private boolean ensurePermissions(PluginCall call) {
+	private boolean ensurePermissions(PluginCall call, String methodName) {
 		if (hasBlePermissions()) {
 			return true;
 		}
+		pendingCallMethod = methodName;
 
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 			requestPermissionForAlias("bluetooth", call, "onBlePermissionResult");
 		} else {
 			requestPermissionForAlias("bluetoothLegacy", call, "onBlePermissionResult");
 		}
 		return false;
 	}

Update callers to pass method names:

-		if (!ensurePermissions(call)) {
+		if (!ensurePermissions(call, "getDevices")) {
 			return;
 		}
-		if (!ensurePermissions(call)) {
+		if (!ensurePermissions(call, "connect")) {
 			return;
 		}

294-344: Prevent duplicate call resolution in connection callbacks.

Multiple callbacks can resolve or reject the same PluginCall: onDeviceFailedToConnect() (line 310), onDeviceReady() (line 316), and connect().fail() (line 340). If onDeviceFailedToConnect() fires and then connect().fail() also triggers, a duplicate rejection occurs, causing a runtime exception.

Guard against duplicate resolution with an atomic flag:

+		final AtomicBoolean callResolved = new AtomicBoolean(false);
+
 		bleManager.setConnectionObserver(new ConnectionObserver() {
 			@Override
 			public void onDeviceConnecting(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Connecting to " + device.getAddress());
 			}

 			@Override
 			public void onDeviceConnected(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Connected to " + device.getAddress());
 				connectedAddress = device.getAddress();
 				JSObject evt = new JSObject();
 				evt.put("address", connectedAddress);
 				notifyListeners("connected", evt);
 			}

 			@Override
 			public void onDeviceFailedToConnect(@NonNull BluetoothDevice device, int reason) {
 				connectedAddress = null;
+				if (callResolved.compareAndSet(false, true)) {
 					call.reject("Connection failed: " + reason);
+				}
 			}

 			@Override
 			public void onDeviceReady(@NonNull BluetoothDevice device) {
+				if (callResolved.compareAndSet(false, true)) {
 					JSObject res = new JSObject();
 					res.put("success", true);
 					call.resolve(res);
+				}
 			}

 			@Override
 			public void onDeviceDisconnecting(@NonNull BluetoothDevice device) {
 				Log.d(TAG, "Disconnecting " + device.getAddress());
 			}

 			@Override
 			public void onDeviceDisconnected(@NonNull BluetoothDevice device, int reason) {
 				connectedAddress = null;
 				JSObject evt = new JSObject();
 				evt.put("address", device.getAddress());
 				evt.put("reason", reason);
 				notifyListeners("disconnected", evt);
 			}
 		});

 		bleManager.connect(device)
 			.useAutoConnect(false)
 			.timeout(15_000)
 			.fail((dev, status) -> {
 				connectedAddress = null;
+				if (callResolved.compareAndSet(false, true)) {
 					call.reject("Connection failed: " + status);
+				}
 			})
 			.enqueue();

Import java.util.concurrent.atomic.AtomicBoolean at the top of the file.


371-400: Add error handling for Base64 decoding.

The Base64.decode() call at line 384 can throw IllegalArgumentException if the input is malformed. While invalid input is a client error, gracefully rejecting the call improves robustness.

Apply this diff:

 	@PluginMethod
 	public void send(PluginCall call) {
 		if (bleManager == null || !bleManager.isConnected()) {
 			call.reject("Not connected");
 			return;
 		}

 		String b64 = call.getString("data");
 		if (b64 == null || b64.isEmpty()) {
 			call.reject("data is required");
 			return;
 		}

-		byte[] payload = Base64.decode(b64, Base64.NO_WRAP);
+		byte[] payload;
+		try {
+			payload = Base64.decode(b64, Base64.NO_WRAP);
+		} catch (IllegalArgumentException e) {
+			call.reject("Invalid base64 data: " + e.getMessage());
+			return;
+		}
+
 		WriteRequest request = bleManager.send(payload);

 		if (request == null) {
 			call.reject("Not ready to send data");
 			return;
 		}

 		request
 			.done(device -> {
 				JSObject res = new JSObject();
 				res.put("bytesSent", payload.length);
 				call.resolve(res);
 			})
 			.fail((device, status) -> call.reject("Send failed: " + status))
 			.enqueue();
 	}
🧹 Nitpick comments (2)
src/js/protocols/CapacitorBle.js (1)

43-53: Consider adding idempotent listener guard.

The native plugin listeners are attached in the constructor without a guard to prevent duplicate registration. While protocol implementations are singletons, explicitly guarding against re-attachment (e.g., with a static flag or _attachNativeListenersOnce()) would make this more robust.

Based on learnings, duplicate listener attachment in similar BLE implementations can lead to duplicate handlers and increased MSP queue pressure.

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1)

631-637: Remove unused chunkSize calculation.

The chunkSize variable is calculated at line 635 but never used. The Nordic BLE library automatically handles payload fragmentation based on the negotiated MTU when using WRITE_TYPE_NO_RESPONSE.

Apply this diff:

 	WriteRequest send(byte[] data) {
 		if (writeCharacteristic == null) {
 			return null;
 		}
-		int chunkSize = Math.max(negotiatedMtu - 3, 20); // 3 bytes ATT overhead
 		return writeCharacteristic(writeCharacteristic, data);
 	}
📜 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 dbd4114 and 230f144.

📒 Files selected for processing (9)
  • android/app/build.gradle (1 hunks)
  • android/app/src/main/AndroidManifest.xml (1 hunks)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1 hunks)
  • src/js/main.js (1 hunks)
  • src/js/port_handler.js (3 hunks)
  • src/js/protocols/CapacitorBle.js (1 hunks)
  • src/js/serial.js (2 hunks)
  • src/js/utils/checkCompatibility.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • android/app/build.gradle
  • src/js/utils/checkCompatibility.js
  • src/js/main.js
  • src/js/serial.js
🧰 Additional context used
🧠 Learnings (24)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 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/js/port_handler.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 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/js/port_handler.js
📚 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/port_handler.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/port_handler.js
  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:41:59.374Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.

Applied to files:

  • src/js/port_handler.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • src/js/port_handler.js
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-24T15:07:25.227Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.

Applied to files:

  • src/js/port_handler.js
  • src/js/protocols/CapacitorBle.js
📚 Learning: 2025-06-09T00:33:22.959Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:33:22.959Z
Learning: In the betaflight-configurator codebase, port paths use counter suffixes for serial and bluetooth ports (e.g., "serial1", "serial2", "bluetooth1", "bluetooth2") instead of using connectionId, which means simple string matching against protocol map keys won't work for protocol selection.

Applied to files:

  • src/js/port_handler.js
📚 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/port_handler.js
  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-22T21:18:08.814Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:11-46
Timestamp: 2025-11-22T21:18:08.814Z
Learning: In betaflight-configurator, protocol implementations (WebSerial, CapacitorSerial, WebBluetooth, etc.) are instantiated once in the Serial class constructor and stored as singletons in the _protocols array for the entire application lifetime. They are never destroyed or recreated, so cleanup methods to remove event listeners are not needed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/MainActivity.java
📚 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:

  • android/app/src/main/AndroidManifest.xml
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T16:39:04.171Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/FileSystem.js:101-107
Timestamp: 2025-09-28T16:39:04.171Z
Learning: In the Betaflight Configurator dataflash download implementation, removing await from FileSystem.writeChunk() and closeFile() is intentional to prevent MSP queue overwhelm. Awaiting file I/O operations slows the download loop enough to cause MSP timeouts and queue growth, leading to stalls. The non-blocking approach prioritizes download throughput over immediate file write error detection.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java
🧬 Code graph analysis (1)
src/js/port_handler.js (1)
src/js/serial.js (2)
  • serial (232-232)
  • serial (232-232)
🔇 Additional comments (5)
android/app/src/main/java/betaflight/configurator/MainActivity.java (1)

6-6: LGTM!

The BLE plugin registration follows the established pattern for serial and TCP plugins.

Also applies to: 14-14

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

58-71: LGTM!

The BLE permissions and feature declarations correctly follow version-aware patterns: legacy permissions (BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION) are scoped to API 26-30, while modern permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT) target API 31+.

Based on learnings, Android BLE permission handling must be version-aware to avoid requesting non-existent permissions on older versions.

src/js/port_handler.js (2)

211-211: LGTM!

Expanding the device filter to include "SpeedyBee" and "Nordic NRF" correctly supports BLE device auto-selection alongside existing serial identifiers.


344-365: LGTM!

Preserving the Bluetooth port list when a scan returns empty but a connection or selection exists prevents losing the active port. This defensive logic correctly handles the case where scanning doesn't always detect already-connected devices.

android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java (1)

613-622: LGTM!

The initialization sequence correctly requests MTU negotiation before enabling notifications, and the Nordic BLE library ensures onDeviceReady() is called only after initialize() completes. This guarantees services are discovered and notifications are active before the JS layer receives the connect event.

Based on learnings, announcing connection before service discovery causes MSP write failures.

Comment on lines +112 to +128
try {
const result = await plugin.connect({
address: device.address,
serviceUuid: device.serviceUuid,
writeCharacteristic: device.writeCharacteristic,
notifyCharacteristic: device.notifyCharacteristic,
});

const success = !!result?.success;
this.connected = success;
this.connectionId = success ? device.path : null;
this.bytesSent = 0;
this.bytesReceived = 0;
this.bitrate = options?.baudRate ?? 115200;

this.dispatchEvent(new CustomEvent("connect", { detail: success }));
return success;
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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "BetaflightBluetoothPlugin.java" -o -name "*BlePlugin.java" | head -20

Repository: betaflight/betaflight-configurator

Length of output: 168


🏁 Script executed:

cat -n android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java | head -100

Repository: betaflight/betaflight-configurator

Length of output: 4870


🏁 Script executed:

wc -l android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

Repository: betaflight/betaflight-configurator

Length of output: 170


🏁 Script executed:

rg -n "connect|onServicesDiscovered|onNotification" android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java | head -30

Repository: betaflight/betaflight-configurator

Length of output: 1106


🏁 Script executed:

sed -n '262,347p' android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

Repository: betaflight/betaflight-configurator

Length of output: 2709


🏁 Script executed:

find . -type f -name "BleBridgeManager.java"

Repository: betaflight/betaflight-configurator

Length of output: 60


🏁 Script executed:

find android -type f -name "*.java" | xargs grep -l "BleBridgeManager\|onServicesDiscovered" | head -10

Repository: betaflight/betaflight-configurator

Length of output: 166


🏁 Script executed:

rg -n "class BleBridgeManager" android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

Repository: betaflight/betaflight-configurator

Length of output: 141


🏁 Script executed:

sed -n '566,639p' android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

Repository: betaflight/betaflight-configurator

Length of output: 2558


🏁 Script executed:

rg -n "onDeviceReady|ManagerGattCallback" android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java -B2 -A2

Repository: betaflight/betaflight-configurator

Length of output: 560


🏁 Script executed:

rg -n "enqueue\|onDeviceReady" android/app/src/main/java/betaflight/configurator/protocols/ble/BetaflightBlePlugin.java

Repository: betaflight/betaflight-configurator

Length of output: 60


Wait for notification enablement to complete before resolving the connect promise.

In BetaflightBlePlugin.java, the initialize() method enqueues notification and MTU operations without waiting for them to complete. This causes onDeviceReady() to be called while notifications are still being enabled, leading to race conditions where early MSP writes fail with "Unable to write characteristic". Refactor initialize() to explicitly wait for enableNotifications() to complete before the promise resolves—either by chaining the operations or by deferring the onDeviceReady() callback until all initialization tasks complete.

🤖 Prompt for AI Agents
In src/js/protocols/CapacitorBle.js around lines 112-128, the connect promise is
being resolved before the BLE peripheral has finished enabling notifications
which causes race conditions; update the connect flow so you explicitly wait for
notification enablement (or the plugin's initialize sequence that enables
MTU/notifications) to complete before setting this.connected, this.connectionId,
bytes counters, dispatching the "connect" event, and returning. Concretely:
ensure the plugin.connect call (or a subsequent
plugin.enableNotifications/plugin.initialize call) is awaited and only after
that promise resolves set state and resolve the connect promise; if the native
plugin exposes a separate notification-ready signal, wait for that signal (or
chain promises) before calling dispatchEvent and returning success.

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: 1

♻️ Duplicate comments (4)
src/js/protocols/CapacitorBle.js (1)

112-128: Race condition: connect resolves before notifications are ready.

The connect promise resolves as soon as plugin.connect() returns success, but the native side's notification enablement is asynchronous (enqueued in initialize()). Early MSP writes may fail with "Unable to write characteristic" because notifications aren't ready yet.

Consider adding a delay or waiting for a "ready" event from the native plugin before resolving:

 try {
     const result = await plugin.connect({
         address: device.address,
         serviceUuid: device.serviceUuid,
         writeCharacteristic: device.writeCharacteristic,
         notifyCharacteristic: device.notifyCharacteristic,
     });

     const success = !!result?.success;
+    // Small delay to allow notification enablement to complete
+    if (success) {
+        await new Promise((resolve) => setTimeout(resolve, 500));
+    }
     this.connected = success;

Alternatively, modify the native plugin to only resolve after enableNotifications completes.

android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (3)

145-153: Permission callback always resumes getDevices, breaking connect flow.

When permissions are requested from connect() (line 263), the callback incorrectly invokes getDevices() instead of resuming the connection. This breaks the user flow when connecting to a device without prior permission grant.

Consider tracking the originating method:

+    private String pendingCallMethod = null;
+
     @PermissionCallback
     private void onBlePermissionResult(PluginCall call) {
         if (hasBlePermissions()) {
-            getDevices(call);
+            if ("connect".equals(pendingCallMethod)) {
+                connect(call);
+            } else {
+                getDevices(call);
+            }
+            pendingCallMethod = null;
         } else {
             call.reject("Bluetooth permission denied");
+            pendingCallMethod = null;
         }
     }

And update ensurePermissions() to track the method name.


309-319: Multiple code paths can resolve the same PluginCall causing duplicate resolution.

onDeviceFailedToConnect() (line 312), onDeviceReady() (line 319), and connect().fail() (line 342) can all attempt to resolve/reject the same PluginCall. If onDeviceFailedToConnect fires before fail(), a duplicate rejection occurs.

Add a guard to prevent double resolution:

+        final boolean[] callResolved = {false};
+
         bleManager.setConnectionObserver(new ConnectionObserver() {
             // ... other callbacks ...

             @Override
             public void onDeviceFailedToConnect(@NonNull BluetoothDevice device, int reason) {
                 connectedAddress = null;
-                call.reject("Connection failed: " + reason);
+                if (!callResolved[0]) {
+                    callResolved[0] = true;
+                    call.reject("Connection failed: " + reason);
+                }
             }

             @Override
             public void onDeviceReady(@NonNull BluetoothDevice device) {
-                JSObject res = new JSObject();
-                res.put("success", true);
-                call.resolve(res);
+                if (!callResolved[0]) {
+                    callResolved[0] = true;
+                    JSObject res = new JSObject();
+                    res.put("success", true);
+                    call.resolve(res);
+                }
             }
         });

         bleManager.connect(device)
             // ...
             .fail((dev, status) -> {
                 connectedAddress = null;
-                call.reject("Connection failed: " + status);
+                if (!callResolved[0]) {
+                    callResolved[0] = true;
+                    call.reject("Connection failed: " + status);
+                }
             })

Also applies to: 337-344


384-384: Add error handling for Base64 decoding.

Base64.decode() throws IllegalArgumentException for malformed input. While unlikely with internal data, gracefully rejecting improves robustness:

-        byte[] payload = Base64.decode(b64, Base64.NO_WRAP);
+        byte[] payload;
+        try {
+            payload = Base64.decode(b64, Base64.NO_WRAP);
+        } catch (IllegalArgumentException e) {
+            call.reject("Invalid base64 data: " + e.getMessage());
+            return;
+        }
🧹 Nitpick comments (2)
android/app/src/main/AndroidManifest.xml (1)

71-71: Consider making BLE feature optional to avoid excluding devices.

Setting android:required="true" for the BLE feature will exclude devices without BLE hardware from installing the app via Play Store. Since the app has other connectivity options (USB serial, TCP), consider making this optional:

-    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
+    <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />

With required="false", devices without BLE can still install the app, and BLE features can be disabled at runtime when unavailable.

android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1)

612-621: MTU and notification operations are enqueued without sequencing.

The requestMtu() and enableNotifications() are both enqueued independently. If notifications are critical before data transmission, consider chaining them to ensure proper sequencing:

     @Override
     protected void initialize() {
         requestMtu(DESIRED_MTU)
             .with((device, mtu) -> negotiatedMtu = mtu)
             .fail((device, status) -> Log.w(TAG, "MTU request failed with status " + status))
+            .done(device -> {
+                if (notifyCharacteristic != null) {
+                    enableNotifications(notifyCharacteristic).enqueue();
+                }
+            })
             .enqueue();
-
-        if (notifyCharacteristic != null) {
-            enableNotifications(notifyCharacteristic).enqueue();
-        }
     }

This ensures notifications are enabled after MTU negotiation completes.

📜 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 230f144 and 4bcdae3.

📒 Files selected for processing (9)
  • android/app/build.gradle (1 hunks)
  • android/app/src/main/AndroidManifest.xml (1 hunks)
  • android/app/src/main/java/betaflight/app/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1 hunks)
  • src/js/main.js (1 hunks)
  • src/js/port_handler.js (3 hunks)
  • src/js/protocols/CapacitorBle.js (1 hunks)
  • src/js/serial.js (2 hunks)
  • src/js/utils/checkCompatibility.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/main.js
🧰 Additional context used
🧠 Learnings (25)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
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.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/build.gradle
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/app/MainActivity.java
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-14T20:22:03.568Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".

Applied to files:

  • src/js/utils/checkCompatibility.js
  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • src/js/serial.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/utils/checkCompatibility.js
  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • android/app/src/main/AndroidManifest.xml
  • src/js/serial.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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/utils/checkCompatibility.js
  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/betaflight/app/MainActivity.java
  • src/js/serial.js
📚 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:

  • src/js/utils/checkCompatibility.js
  • android/app/src/main/AndroidManifest.xml
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/MainActivity.java
  • src/js/serial.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:41:59.374Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • src/js/serial.js
📚 Learning: 2025-11-24T15:07:25.227Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • src/js/serial.js
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/MainActivity.java
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • src/js/protocols/CapacitorBle.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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/js/protocols/CapacitorBle.js
  • src/js/port_handler.js
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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/js/port_handler.js
  • src/js/serial.js
📚 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/port_handler.js
  • src/js/serial.js
📚 Learning: 2025-06-09T00:33:22.959Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:33:22.959Z
Learning: In the betaflight-configurator codebase, port paths use counter suffixes for serial and bluetooth ports (e.g., "serial1", "serial2", "bluetooth1", "bluetooth2") instead of using connectionId, which means simple string matching against protocol map keys won't work for protocol selection.

Applied to files:

  • src/js/port_handler.js
📚 Learning: 2025-11-22T21:18:08.814Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:11-46
Timestamp: 2025-11-22T21:18:08.814Z
Learning: In betaflight-configurator, protocol implementations (WebSerial, CapacitorSerial, WebBluetooth, etc.) are instantiated once in the Serial class constructor and stored as singletons in the _protocols array for the entire application lifetime. They are never destroyed or recreated, so cleanup methods to remove event listeners are not needed.

Applied to files:

  • android/app/src/main/java/betaflight/app/MainActivity.java
  • src/js/serial.js
📚 Learning: 2025-12-14T20:22:03.568Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator, the serial.protocol getter should return unified protocol types ("serial", "ble", "tcp", "virtual") rather than implementation names ("webserial", "capacitorserial", "webbluetooth"). A separate protocolImplementation property should be added to expose the specific adapter implementation for debugging purposes.

Applied to files:

  • src/js/serial.js
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
🧬 Code graph analysis (2)
src/js/port_handler.js (1)
src/js/serial.js (2)
  • serial (232-232)
  • serial (232-232)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1)
src/js/protocols/CapacitorBle.js (1)
  • plugin (5-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build / build
🔇 Additional comments (9)
src/js/utils/checkCompatibility.js (1)

144-145: LGTM!

Correctly enables Bluetooth support detection for Android, aligning with the new Capacitor BLE plugin integration.

src/js/serial.js (1)

7-7: LGTM!

Clean integration of the CapacitorBle protocol for Android, following the established protocol registration pattern.

Also applies to: 27-27

android/app/src/main/java/betaflight/app/MainActivity.java (1)

8-8: LGTM!

Plugin registration follows the established pattern, correctly placed before super.onCreate().

Also applies to: 14-14

src/js/port_handler.js (2)

344-365: Good defensive logic for preserving Bluetooth ports.

The preservation logic correctly prevents losing the selected Bluetooth device when a scan returns empty but a connection or selection is active. This avoids user frustration from disappearing devices.


211-211: LGTM!

Adding "SpeedyBee" and "Nordic NRF" to the device filter enables automatic selection of common BLE devices.

src/js/protocols/CapacitorBle.js (2)

43-53: LGTM - listeners attached idempotently in constructor.

Per learnings, native plugin listeners must be attached exactly once per app lifetime. The constructor-based attachment is correct since protocol instances are singletons.


90-93: requestPermissionDevice returns first device without user selection.

Unlike WebBluetooth which shows a picker dialog, this implementation auto-selects the first discovered device. This may be intentional for Capacitor's scan-based approach, but verify this matches expected UX.

android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1)

76-95: LGTM - comprehensive known device profiles.

The static device profiles cover common BLE serial modules used with Betaflight flight controllers, with appropriate name aliases for fallback matching.

android/app/build.gradle (1)

39-39: The Nordic BLE library version 2.11.0 is the current stable release (Sep 11, 2025) with no known security advisories. No action needed.

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: 1

♻️ Duplicate comments (3)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (3)

145-153: Fix permission callback to resume the correct original method.

This issue was already identified in a previous review: the callback always invokes getDevices(call), but permission requests can originate from connect() (line 263). When connect() requests permissions, the callback incorrectly calls getDevices() instead of resuming connect(), breaking the connection flow.


294-344: Prevent duplicate call resolution in connection callbacks.

This issue was already identified in a previous review: multiple code paths can resolve or reject the same PluginCall. Both onDeviceFailedToConnect() (line 310) and connect().fail() (line 340) can reject the call, while onDeviceReady() (line 316) can resolve it, causing potential duplicate resolution exceptions.


371-400: Add error handling for Base64 decoding.

This issue was already identified in a previous review: Base64.decode() at line 384 can throw IllegalArgumentException for malformed input, causing an unhandled exception. The call should be wrapped in try-catch to gracefully reject with a clear error message.

🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1)

256-259: Consider clarifying requestPermission behavior.

The method delegates to getDevices(), which requests permissions but also starts a device scan. While functional, the method name suggests a permission-only operation. Consider either renaming to reflect the dual behavior (e.g., requestPermissionAndScan) or implementing pure permission request logic.

📜 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 4bcdae3 and 6e9cf1f.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-14T20:22:03.568Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build / build
🔇 Additional comments (2)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (2)

566-637: Strong BLE implementation using Nordic library.

The BleBridgeManager and ManagerGattCallback implementation is well-structured:

  • Characteristic caching follows maintainer preferences (write and notify characteristics resolved once in isRequiredServiceSupported)
  • MTU negotiation properly implemented (requesting 247 bytes)
  • Write type correctly determined from characteristic properties
  • Notification callbacks wired appropriately

The integration with the Nordic library follows best practices.


156-212: Well-designed dual-scan strategy.

The two-phase scanning approach is effective:

  1. Primary scan (lines 156-212) with service UUID filters efficiently finds devices advertising known services
  2. Fallback scan (lines 239-254) without filters catches devices that don't advertise service UUIDs in scan records
  3. Profile matching (lines 492-526) uses both service UUIDs and device names, supporting common BLE modules

This handles real-world BLE device behavior where service UUIDs aren't always advertised during discovery.

Also applies to: 239-254, 465-526

Comment on lines 210 to 211
scanner.startScan(filters, settings, scanCallback);
handler.postDelayed(() -> finishScan(call, false), SCAN_DURATION_MS);
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

Remove pending handler callbacks in handleOnDestroy.

Handler callbacks scheduled at lines 211 and 253 are not removed when the plugin is destroyed. If handleOnDestroy() is called while a scan is in progress, the pending callback will still fire after cleanup, potentially calling finishScan() on invalid state or attempting to resolve a stale PluginCall.

Apply this diff to remove pending callbacks during cleanup:

 	@Override
 	protected void handleOnDestroy() {
+		handler.removeCallbacksAndMessages(null);
 		stopScan();
 		try {
 			if (bleManager != null) {
 				bleManager.close();
 			}
 		} catch (Exception e) {
 			Log.e(TAG, "Error closing BLE manager", e);
 		}
 		super.handleOnDestroy();
 	}

Also applies to: 252-253, 402-413

🤖 Prompt for AI Agents
In
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
around lines 210-211, 252-253 and 402-413, scheduled handler runnables that call
finishScan() (and other timeout/cleanup runnables) are not removed when the
plugin is destroyed; update handleOnDestroy() to cancel those pending callbacks.
Specifically, keep references to the Runnables you post (the scan finish
Runnable(s) and any connection timeout Runnables), and in handleOnDestroy() call
handler.removeCallbacks(scanFinishRunnable) and
handler.removeCallbacks(connectionTimeoutRunnable) (or
handler.removeCallbacksAndMessages(null) if you prefer to remove all) before
performing other cleanup so no delayed finishScan() or other callbacks run
against torn-down state or stale PluginCall.

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: 1

♻️ Duplicate comments (4)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (4)

175-183: Fix permission callback to resume the correct original method (still unresolved).

The permission callback still always calls getDevices(call), but permission requests can originate from either getDevices() (line 187) or connect() (line 295). When connect() requests permissions, the callback incorrectly resumes getDevices() instead, breaking the connection flow.

This issue was previously flagged and remains unaddressed. Please track the originating method and resume the correct flow in onBlePermissionResult.


326-376: Prevent duplicate call resolution in connection callbacks (still unresolved).

Multiple callbacks can still resolve or reject the same PluginCall:

  • onDeviceFailedToConnect() at line 344
  • onDeviceReady() at line 351
  • connect().fail() at line 374

If a connection fails, both onDeviceFailedToConnect and connect().fail() may fire, causing a duplicate rejection and potential runtime exception.

This issue was previously flagged and remains unaddressed. Please add a guard (e.g., AtomicBoolean or synchronized flag) to ensure the call is resolved/rejected only once.


403-432: Add error handling for Base64 decoding (still unresolved).

Line 416's Base64.decode() can throw IllegalArgumentException if the input is malformed, causing an unhandled exception. While this is a client error, gracefully rejecting the call improves robustness.

This issue was previously flagged and remains unaddressed. Please wrap the decode in a try-catch and reject the call with a clear error message.


434-445: Remove pending handler callbacks in handleOnDestroy (still unresolved).

Handler callbacks scheduled at lines 234 and 281 are not removed when the plugin is destroyed. If handleOnDestroy() is called while a scan is in progress, the pending callback will still fire after cleanup, potentially calling finishScan() on invalid state or attempting to resolve a stale PluginCall.

This issue was previously flagged and remains unaddressed. Add handler.removeCallbacksAndMessages(null); at the beginning of handleOnDestroy() to cancel all pending callbacks before cleanup.

📜 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 abb3c08 and a4bf700.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-14T20:22:03.568Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy
🔇 Additional comments (1)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1)

721-729: MTU negotiation implemented correctly.

The implementation properly requests MTU negotiation at line 722 with the desired MTU of 247, which significantly improves BLE throughput by reducing packet fragmentation. This addresses the concern raised in previous discussions about the 64-byte limitation.

As noted in the earlier investigation, while typical MSP messages are small, MTU negotiation still provides substantial benefits:

  • Default MTU 23 = ~17 bytes per packet (after ATT overhead)
  • Negotiated MTU 247 = ~244 bytes per packet
  • A 60-byte message: 4 packets → 1 packet
  • A 200-byte dataflash read: 12+ packets → 1 packet

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: 1

♻️ Duplicate comments (4)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (4)

175-183: Permission callback routing issue still present.

The onBlePermissionResult callback always invokes getDevices(call) (line 179), but permission requests can originate from either getDevices() or connect() (line 295). When a user calls connect() without permissions and then grants them, the callback incorrectly invokes getDevices() instead of resuming connect(), breaking the connection flow.

This issue was flagged in a previous review and remains unresolved.

Based on learnings, permission handling must properly route callbacks to the originating method.


326-376: Duplicate call resolution issue still present.

Multiple code paths can resolve or reject the same PluginCall: onDeviceFailedToConnect() (line 344), onDeviceReady() (line 351), and connect().fail() (line 374). If onDeviceFailedToConnect() fires, then connect().fail() may also trigger, causing a duplicate rejection and potential runtime exception.

This issue was flagged in a previous review and remains unresolved. An atomic guard (e.g., AtomicBoolean) should test-and-set before any call.resolve() or call.reject() to prevent double-resolution.


403-432: Base64 decoding error handling still missing.

The Base64.decode() call at line 416 can throw IllegalArgumentException if the input is malformed, causing an unhandled exception. While invalid input indicates a client error, gracefully rejecting the call improves robustness.

This issue was flagged in a previous review and remains unresolved.

Apply this diff to add error handling:

 	String b64 = call.getString("data");
 	if (b64 == null || b64.isEmpty()) {
 		call.reject("data is required");
 		return;
 	}

-	byte[] payload = Base64.decode(b64, Base64.NO_WRAP);
+	byte[] payload;
+	try {
+		payload = Base64.decode(b64, Base64.NO_WRAP);
+	} catch (IllegalArgumentException e) {
+		call.reject("Invalid base64 data: " + e.getMessage());
+		return;
+	}
+
 	WriteRequest request = bleManager.send(payload);

434-457: Handler callbacks not removed during destroy.

Handler callbacks scheduled at lines 234 and 281 are not cancelled when the plugin is destroyed. If handleOnDestroy() is called while a scan is in progress, the pending callbacks will still fire after cleanup, potentially calling finishScan() on invalid state or attempting to resolve a stale PluginCall.

This issue was flagged in a previous review and remains unresolved.

Apply this diff to remove pending callbacks:

 @Override
 protected void handleOnDestroy() {
+	handler.removeCallbacksAndMessages(null);
 	stopScan();
 	try {
🧹 Nitpick comments (3)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (3)

185-239: Consider guarding against concurrent scan requests.

If getDevices() is called while a scan is already in progress, the method clears discoveredDevices (line 200) and starts a new scan without canceling the previous handler callback (line 234). This can lead to multiple finishScan() invocations with overlapping state, potentially causing confusing results.

Consider checking the scanning flag at the method entry and either rejecting concurrent requests or explicitly stopping the previous scan first.


379-401: disconnect() doesn't handle connecting state.

Lines 381-386 check if bleManager is null or not connected and immediately resolve with success. However, if disconnect() is called while the device is still connecting (not yet fully connected), this check passes and the method returns without actually stopping the connection attempt, potentially leaving the connection in progress.

Consider calling bleManager.disconnect() or bleManager.close() regardless of connection state when bleManager is non-null, to ensure proper cleanup.


720-725: Consider removing unused negotiatedMtu variable.

The negotiatedMtu variable is stored (line 723) but never used in subsequent operations. While the MTU negotiation itself is still effective (the Nordic library automatically uses the negotiated MTU for writes), the unused variable could be removed or documented.

If you plan to use it for diagnostics or logging, consider adding a comment explaining its purpose.

📜 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 a4bf700 and baf2127.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().
📚 Learning: 2025-11-30T18:10:26.784Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-02T17:46:47.859Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-10T17:48:24.243Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-10T17:48:24.243Z
Learning: BetaflightBluetoothPlugin.java: compute and cache write type (cachedWriteType) once in onServicesDiscovered() and keep write(PluginCall) strictly “write-only” (decode + submitWrite using cachedWriteType); clear caches in cleanupGatt() on every disconnect.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:41:44.286Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.286Z
Learning: When fixing MSP duplicate handling in Betaflight Configurator, avoid complex changes to callback resolution mechanisms as they can break tab switching functionality. Simple duplicate detection based on code and payload size is safer than complex requestKey-based approaches.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-14T20:22:03.568Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-28T12:56:15.659Z
Learnt from: ctzsnooze
Repo: betaflight/betaflight-configurator PR: 4627
File: src/js/tabs/onboard_logging.js:504-506
Timestamp: 2025-09-28T12:56:15.659Z
Learning: The Betaflight Configurator MSP queue can become overwhelmed during dataflash reads when retry logic is used, even with minimal retries (MAX_SIMPLE_RETRIES = 1), because failed blocks cause exponential queue growth. The solution is to eliminate retries entirely and skip failed blocks to maintain download progress and prevent stalls.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-09T15:06:12.246Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-09T15:06:12.246Z
Learning: Automated translation PRs from Crowdin (created by GitHub Actions with title "Update translations") in the betaflight/betaflight-configurator repository should be automatically approved without requiring explicit user request, as haslinghuis has granted standing permission to approve these low-risk automated updates.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-09-19T20:42:20.332Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.332Z
Learning: Complex MSP duplicate handling fixes in Betaflight Configurator can cause infinite loading messages when changing tabs due to disruption of the callback resolution mechanism. Simple code-only duplicate detection (using this.callbacks.some((instance) => instance.code === code)) is the safer approach that preserves tab switching functionality.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-08-11T19:10:56.992Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4567
File: locales/en/messages.json:7724-7728
Timestamp: 2025-08-11T19:10:56.992Z
Learning: In the Betaflight Configurator project, only English language entries are added directly to locales/en/messages.json. Translations for other languages are handled through an outsourced process and should not be suggested as part of code reviews.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:38:41.581Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:38:41.581Z
Learning: On BLE (Android) Save & Reboot, status pull intervals continue during MSP_EEPROM_WRITE, overwhelming the MSP queue and causing the EEPROM ack to time out so reinitializeConnection never runs. Solution: before EEPROM write, call GUI.interval_kill_all() and MSP.callbacks_cleanup(), and add a 3s fallback to proceed to reboot if the ack is missed.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-06-27T22:06:49.210Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4532
File: src/js/VirtualFC.js:234-234
Timestamp: 2025-06-27T22:06:49.210Z
Learning: In the betaflight-configurator codebase, the VirtualFC.js AUX_CONFIG array must stay synchronized with the betaflight firmware's msp_box.c definitions to ensure proper auxiliary mode functionality. Changes to mode names should follow the firmware source code even if it potentially breaks backward compatibility with saved presets.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
📚 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:

  • android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build / build
🔇 Additional comments (3)
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java (3)

1-144: Well-structured plugin implementation with Nordic library integration.

The plugin structure is clean and well-organized:

  • Constants are grouped logically by BLE profile
  • Permission annotations follow Android version requirements
  • Nordic BLE library integration provides robust GATT handling
  • Handler on main looper ensures thread-safe operation
  • MTU negotiation target of 247 is appropriate for BLE throughput

459-608: Scan result handling is well-designed.

The two-stage discovery approach is effective:

  • Primary scan matches by advertised service UUIDs (lines 531-549)
  • Fallback scan additionally matches by device name (lines 552-563)
  • Unknown devices are logged once per address with detailed advertisement data (lines 568-608)

This strategy balances fast discovery for standard devices with compatibility for devices that don't advertise service UUIDs correctly.


648-745: BleBridgeManager implementation follows best practices.

The Nordic BLE library integration is well-implemented:

  • Write characteristic cached in isRequiredServiceSupported() per learnings
  • MTU negotiation (line 722) enables optimal throughput
  • Characteristics properly cleared in onServicesInvalidated() (lines 733-736)
  • SpeedyBee fallback profile switching (lines 675-704) provides excellent device compatibility
  • Nordic library's writeCharacteristic() automatically handles data splitting based on negotiated MTU

Based on learnings, write characteristics should be cached once and the Nordic library handles MTU-aware data splitting.

Comment on lines +293 to +377
@PluginMethod
public void connect(PluginCall call) {
if (!ensurePermissions(call)) {
return;
}

String address = call.getString("address");
String serviceUuid = call.getString("serviceUuid");
String writeUuid = call.getString("writeCharacteristic");
String notifyUuid = call.getString("notifyCharacteristic");

if (address == null || serviceUuid == null || writeUuid == null || notifyUuid == null) {
call.reject("address, serviceUuid, writeCharacteristic, and notifyCharacteristic are required");
return;
}

BluetoothManager manager = (BluetoothManager) getContext().getSystemService(Context.BLUETOOTH_SERVICE);
adapter = manager.getAdapter();
if (adapter == null) {
call.reject("Bluetooth adapter unavailable");
return;
}

BluetoothDevice device = adapter.getRemoteDevice(address);
if (device == null) {
call.reject("Device not found: " + address);
return;
}

KnownDevice profile = KNOWN_DEVICES.getOrDefault(serviceUuid.toLowerCase(),
new KnownDevice("Unknown", serviceUuid, writeUuid, notifyUuid));

bleManager = new BleBridgeManager(getContext(), this, profile);
bleManager.setConnectionObserver(new ConnectionObserver() {
@Override
public void onDeviceConnecting(@NonNull BluetoothDevice device) {
Log.d(TAG, "Connecting to " + device.getAddress());
}

@Override
public void onDeviceConnected(@NonNull BluetoothDevice device) {
Log.d(TAG, "Connected to " + device.getAddress());
connectedAddress = device.getAddress();
JSObject evt = new JSObject();
evt.put("address", connectedAddress);
notifyListeners("connected", evt);
}

@Override
public void onDeviceFailedToConnect(@NonNull BluetoothDevice device, int reason) {
connectedAddress = null;
call.reject("Connection failed: " + reason);
}

@Override
public void onDeviceReady(@NonNull BluetoothDevice device) {
JSObject res = new JSObject();
res.put("success", true);
call.resolve(res);
}

@Override
public void onDeviceDisconnecting(@NonNull BluetoothDevice device) {
Log.d(TAG, "Disconnecting " + device.getAddress());
}

@Override
public void onDeviceDisconnected(@NonNull BluetoothDevice device, int reason) {
connectedAddress = null;
JSObject evt = new JSObject();
evt.put("address", device.getAddress());
evt.put("reason", reason);
notifyListeners("disconnected", evt);
}
});

bleManager.connect(device)
.useAutoConnect(false)
.timeout(15_000)
.fail((dev, status) -> {
connectedAddress = null;
call.reject("Connection failed: " + status);
})
.enqueue();
}
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

Guard against concurrent connection attempts.

If connect() is called while a connection is already in progress, line 325 creates a new BleBridgeManager instance and replaces the previous bleManager reference without cleanup. The old BleBridgeManager may have an open GATT connection and active ConnectionObserver callbacks that continue firing, leading to resource leaks and potential state corruption.

Check if bleManager is already non-null and either reject the new connection request or explicitly close the existing manager before creating a new one.

Apply this diff to add state checking:

 @PluginMethod
 public void connect(PluginCall call) {
 	if (!ensurePermissions(call)) {
 		return;
 	}
+	
+	if (bleManager != null) {
+		call.reject("Connection already in progress or established");
+		return;
+	}

 	String address = call.getString("address");
🤖 Prompt for AI Agents
In
android/app/src/main/java/betaflight/app/protocols/ble/BetaflightBlePlugin.java
around lines 293-377, the connect() method can overwrite an existing bleManager
and its observer when called concurrently, causing leaks and stray callbacks;
add a guard at the start of connect() to detect an existing active
connection/connection-in-progress and either reject the new call or cleanly
close the existing manager before creating a new one. Concretely: after
permissions and before creating a new BleBridgeManager, check if bleManager !=
null (or a boolean connecting flag / connectedAddress != null) and if so either
call call.reject("Connection already in progress") and return, or invoke the
existing manager's disconnect/close and remove its observer (wait for it to
finish) before instantiating a new BleBridgeManager; ensure you clear bleManager
and connectedAddress when tearing down so future connects are allowed.

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: 0

🧹 Nitpick comments (2)
src/js/port_handler.js (2)

162-174: Approve improved device list refresh with optional simplification.

The logic correctly refreshes the device list after permission and computes available ports. However, since updateDeviceList(protocol) both updates the corresponding current*Ports array and returns the same data, the switch statement to compute availablePorts is functionally redundant—you could simplify by using updatedPorts directly.

Optional simplification:

 const updatedPorts = await this.updateDeviceList(protocol);
-const availablePorts = (() => {
-    switch (protocol) {
-        case "bluetooth":
-            return this.currentBluetoothPorts;
-        case "usb":
-            return this.currentUsbPorts;
-        case "serial":
-            return this.currentSerialPorts;
-        default:
-            return updatedPorts ?? [];
-    }
-})();
+const availablePorts = updatedPorts ?? [];

344-349: Minor style inconsistency with other cases.

The bluetooth case now has braces while the usb and serial cases don't, creating a minor style inconsistency. Consider either removing the braces here or adding them to the other cases for uniformity.

📜 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 baf2127 and 410a360.

📒 Files selected for processing (1)
  • src/js/port_handler.js (2 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-14T20:22:03.568Z
Learning: In betaflight-configurator's msp.js, the bluetooth CRC bypass check at line 266 uses serial.protocol === "bluetooth". When protocol names are unified to "ble", this check must be updated to serial.protocol === "ble".
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: In betaflight-configurator’s CapacitorBluetooth.js, native plugin listeners (notification, services, connectionState) must be attached exactly once per app lifetime. Re-attaching them during writes/connect cycles leads to duplicate handlers, increased JS workload, MSP queue pressure, and can prevent the MSP_EEPROM_WRITE callback from firing (so reinitializeConnection is not reached). Implement an idempotent _attachNativeListenersOnce() guard.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.859Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.784Z
Learning: In betaflight-configurator's Android Bluetooth implementation, permission handling must be version-aware: Android 8-11 (API 26-30) requires BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION permissions, while Android 12+ (API 31+) requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The AndroidManifest.xml should use android:maxSdkVersion="30" on legacy permissions and android:minSdkVersion="31" on new permissions. The Java code must conditionally request permissions using Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check to avoid requesting non-existent permissions on older Android versions.
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: 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.
📚 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/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/js/port_handler.js
📚 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/port_handler.js
📚 Learning: 2025-11-28T22:41:59.374Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:135-143
Timestamp: 2025-11-28T22:41:59.374Z
Learning: In betaflight-configurator, WebSerial.js uses a constant path "serial" for all devices, and CapacitorBluetooth.js uses constant path "bluetooth", consistent with the single-device-at-a-time workflow. WebBluetooth.js uses counter-based paths (bluetooth_${counter}), but that's specific to its implementation. The path strategy varies by protocol based on their specific requirements.

Applied to files:

  • src/js/port_handler.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/port_handler.js
📚 Learning: 2025-11-24T15:07:25.227Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4706
File: src/js/protocols/CapacitorSerial.js:47-47
Timestamp: 2025-11-24T15:07:25.227Z
Learning: In betaflight-configurator's CapacitorSerial protocol (src/js/protocols/CapacitorSerial.js), the fire-and-forget this.loadDevices() call in the constructor is intentional background pre-loading. The actual critical device list loading for UI population is orchestrated by port_handler.js via serial.getDevices("serial"), which properly awaits CapacitorSerial.getDevices() → loadDevices() in a complete async chain. The constructor's async call does not cause race conditions.

Applied to files:

  • src/js/port_handler.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build / build
🔇 Additional comments (1)
src/js/port_handler.js (1)

176-193: LGTM! Improved permission flow with robust fallback handling.

The changes properly handle all permission outcomes:

  • If a specific port is granted, it's selected immediately (line 179 early return)
  • If permission is granted but no specific port returned, the first available device is selected as fallback (lines 182-190)
  • If no devices exist post-permission, it logs and resets to DEFAULT_PORT (lines 192-193)

This is a significant improvement over previous behavior and handles edge cases more gracefully.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@github-actions
Copy link
Contributor

🎉 Do you want to test this code? 🎉

⚠️ CAUTION: The build may be unstable and result in corrupted configurations or data loss. Use only for testing! ⚠️

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

Projects

Status: App

Development

Successfully merging this pull request may close these issues.

2 participants