-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Capacitor bluetooth plugin #4714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Capacitor bluetooth plugin #4714
Conversation
WalkthroughAdds end-to-end Android BLE support: manifest permissions/features, a new Capacitor Android plugin and its registration, a JS CapacitorBluetooth protocol adapter integrated into protocol selection, plus related serial backend and MSP EEPROM timing/cleanup adjustments. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as Configurator UI
participant JS as CapacitorBluetooth (JS)
participant Bridge as Capacitor Bridge
participant Plugin as BetaflightBluetoothPlugin (Android)
participant BLE as Android BLE Stack
UI->>JS: requestDevice()
JS->>Bridge: requestDevice()
Bridge->>Plugin: startScan(filters)
BLE-->>Plugin: onScanResult(device)
Plugin-->>Bridge: emit deviceDiscovered
Bridge-->>JS: deviceDiscovered
JS->>UI: show devices
UI->>JS: connect(deviceId)
JS->>Bridge: connect(deviceId)
Bridge->>Plugin: connect -> connectGatt()
BLE-->>Plugin: onConnectionStateChange(CONNECTED)
Plugin->>BLE: discoverServices()
BLE-->>Plugin: onServicesDiscovered
Plugin-->>Bridge: emit services, connectionState
Bridge-->>JS: services, connectionState
JS->>UI: connection ready
UI->>JS: send(data)
JS->>Bridge: write(service,char,payload)
Bridge->>Plugin: write()
Plugin->>BLE: writeCharacteristic()
BLE-->>Plugin: onCharacteristicWrite
Plugin-->>Bridge: write result
UI->>JS: startNotifications(char)
JS->>Bridge: startNotifications()
Bridge->>Plugin: startNotifications()
Plugin->>BLE: enable notifications (CCCD)
BLE-->>Plugin: onCharacteristicChanged(data)
Plugin-->>Bridge: emit notification
Bridge-->>JS: notification event
JS->>UI: deliver data
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45–60 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
🧹 Nitpick comments (3)
android/app/src/main/AndroidManifest.xml (1)
78-81: Bothbluetooth_leandusb.hostare marked as required, limiting device compatibility.Setting
android:required="true"for both features means the app will only be installable on devices supporting both USB Host and Bluetooth LE. Consider whetherandroid:required="false"would be more appropriate for one or both features, allowing the app to work on devices with only one capability while gracefully degrading functionality.- <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> + <uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/> <!-- USB Host feature --> - <uses-feature android:name="android.hardware.usb.host" android:required="true" /> + <uses-feature android:name="android.hardware.usb.host" android:required="false" />src/js/protocols/CapacitorBluetooth.js (1)
36-60: Base64 encoding/decoding implementation is correct for byte data.The static analysis suggests
String.fromCodePoint()andcodePointAt(), but for base64 encoding of binary data (byte values 0-255),fromCharCode()/charCodeAt()are equivalent and commonly used. This is a false positive since BLE data is always byte-based.The
for-ofloop suggestion is a minor style preference. Consider usingfor...ofif you want to align with project conventions:const toBase64 = (buffer) => { if (typeof Buffer !== "undefined") { return Buffer.from(buffer).toString("base64"); } let binary = ""; - for (let idx = 0; idx < buffer.length; idx += 1) { - binary += String.fromCharCode(buffer[idx]); + for (const byte of buffer) { + binary += String.fromCharCode(byte); } return btoa(binary); };android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (1)
919-970: Consider adding lifecycle guards to GATT callback.While
handleOnDestroy()callscleanupGatt(), GATT callbacks can potentially be invoked after plugin destruction. Consider adding defensive checks at the start of each callback method to verify the plugin is still in a valid state.Example pattern:
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (bluetoothAdapter == null) { + return; // Plugin has been destroyed + } if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { // existing implementationApply similar guards to
onServicesDiscoveredandonCharacteristicChanged.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
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/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)src/js/protocols/CapacitorBluetooth.js(1 hunks)src/js/serial.js(2 hunks)src/js/utils/checkCompatibility.js(1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-06-09T00:32:21.385Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:32:21.385Z
Learning: In the betaflight-configurator codebase, port paths use counter prefixes (e.g., "bluetooth1", "bluetooth2", "serial1") rather than direct protocol identifiers. The protocol selection logic correctly uses `portPath.startsWith("bluetooth")` to detect bluetooth ports regardless of the counter suffix, rather than direct string matching against protocol map keys.
Applied to files:
src/js/serial.jssrc/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/java/betaflight/configurator/MainActivity.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:
src/js/serial.jssrc/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/MainActivity.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/serial.jssrc/js/protocols/CapacitorBluetooth.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/serial.jssrc/js/utils/checkCompatibility.jssrc/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/MainActivity.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/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/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
🧬 Code graph analysis (1)
src/js/protocols/CapacitorBluetooth.js (1)
src/js/protocols/devices.js (2)
bluetoothDevices(1-51)bluetoothDevices(1-51)
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorBluetooth.js
[warning] 178-178: Remove this useless assignment to variable "uuids".
[warning] 57-57: Prefer String#codePointAt() over String#charCodeAt().
[warning] 42-42: Prefer String.fromCodePoint() over String.fromCharCode().
[warning] 192-196: Remove this commented out code.
[warning] 41-43: Expected a for-of loop instead of a for loop with this simple iteration.
[failure] 87-87: Refactor this asynchronous operation outside of the constructor.
[warning] 178-178: Remove the declaration of the unused 'uuids' variable.
🔇 Additional comments (19)
src/js/utils/checkCompatibility.js (1)
141-151: LGTM! Android Bluetooth support correctly enabled.This change aligns with the retrieved learnings that Android Chrome browsers support Bluetooth API, and the new CapacitorBluetooth protocol will handle the native implementation.
android/app/src/main/java/betaflight/configurator/MainActivity.java (1)
5-14: LGTM! Bluetooth plugin registration follows established pattern.The plugin registration is correctly placed before
super.onCreate()and follows the same pattern as the existingBetaflightSerialPlugin.src/js/serial.js (1)
23-34: LGTM! Android protocols correctly include Bluetooth.The CapacitorBluetooth protocol is properly added alongside CapacitorSerial for Android. Based on learnings, these protocol instances are singletons maintained for the application lifetime, which is the intended pattern.
Note: The Android path doesn't include the
tcp(Websocket) protocol that's available on non-Android platforms. Verify this is intentional.src/js/protocols/CapacitorBluetooth.js (5)
62-101: Constructor initialization is appropriate.The static analysis warning about "asynchronous operation" at line 87 is a false positive.
Promise.resolve()is synchronous initialization of a resolved promise for the write queue pattern. TheattachNativeListeners()call is also synchronous - it registers event listeners without awaiting. Based on learnings, this fire-and-forget pattern for listener setup in protocol constructors is the established pattern in this codebase.
399-422: Write queue error handling is correct but could be clearer.The pattern of using
.catch(() => {})on line 410 to prevent the queue from breaking while handling errors in the outer try/catch is valid. The queue continues processing even if individual writes fail, and errors are properly logged and returned.
631-642: Notification handler correctly filters by device ID.The filtering logic on lines 635-637 ensures notifications from other devices are ignored. This is important when multiple devices might be paired. The null checks are appropriate.
571-588: LGTM! Service discovery and characteristic resolution logic is well-structured.The dynamic service discovery pattern properly caches services, resolves pending promises, and updates the device description for the connected device. The merge logic correctly combines static profiles with discovered characteristics.
361-367: Based on my investigation, I've encountered limitations accessing the repository directly. However, I have found critical information about the API being used.Web search results confirm that the
@capacitor-community/bluetooth-leplugin (the standard Capacitor Bluetooth LE API) uses a consistent string-based parameter format for all operations:await BleClient.disconnect(device.deviceId); // deviceId is a STRINGThis is documented in the official plugin README and npm package docs.
Given this API documentation, the review comment's concern about passing
deviceIdas a string todisconnect()appears to align with the correct API specification. However, I cannot definitively verify whetherconnect()andwrite()actually use an object format{ deviceId: ... }as claimed, since I cannot access the repository code directly.
Verify the parameter formats in connect() and write() method calls at lines 243 and 401 to confirm whether they truly use object syntax or also pass string parameters consistently with the standard plugin API. Based on standard Capacitor Bluetooth LE documentation, all methods should accept plain string deviceId parameters.
android/app/src/main/AndroidManifest.xml (1)
67-76:ACCESS_FINE_LOCATIONis required for backward compatibility with Android 11 (API ≤30); there is no conflict withneverForLocation.The
neverForLocationflag is valid for Android 12+ (API 31+) to assert that your app never derives physical location from BLE scan results. However,ACCESS_FINE_LOCATIONis still necessary for older devices running Android 11 or below, where it is required to perform Bluetooth scanning regardless of theneverForLocationflag. These permissions coexist appropriately for backward compatibility. If your app targets only Android 12 and above, you could removeACCESS_FINE_LOCATION; otherwise, keep it. Do not useACCESS_COARSE_LOCATIONas a substitute—official Android documentation specifiesACCESS_FINE_LOCATIONfor BLE scanning on older API levels.android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (10)
50-73: LGTM! Plugin configuration is correct.The permission declarations properly handle both legacy (BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION) and modern Android 12+ permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT). The timeout constants and CCCD UUID are appropriate for BLE operations.
110-172: LGTM! Lifecycle and basic methods are well-implemented.Proper initialization in
load(), cleanup inhandleOnDestroy(), and permission handling follows Capacitor best practices.
253-338: LGTM! Excellent write implementation with robust fallbacks.The method includes comprehensive validation, smart characteristic lookup with service fallback (lines 287-296), intelligent write type selection based on supported properties (lines 319-327), and thorough error handling. The fallback logic at lines 290-295 is particularly well-designed for cases where the service UUID might not match exactly.
340-441: LGTM! Notification handling is well-designed.The queue mechanism for pending notifications until services are discovered (line 347) is a good pattern. The characteristic lookup includes the same robust fallback logic as the write method, and cleanup is handled properly.
443-526: LGTM! Scanning implementation is correct.The scan flow includes proper filter building, timeout handling, and cleanup. The use of
SCAN_MODE_LOW_LATENCYis appropriate for user-initiated device discovery.
528-636: LGTM! Connection management is robust.Proper API level handling for GATT connection (lines 536-540), comprehensive cleanup with exception guards (lines 617-623), and correct state management throughout.
750-762: LGTM! Correct API level handling.Properly handles the API change in Android 13 (Tiramisu) where
writeCharacteristicsignature changed.
726-748: LGTM! Notification enabling is correctly implemented.Properly distinguishes between notifications and indications (lines 735-737) and correctly writes the CCCD descriptor.
854-869: LGTM! Hex decoding is correctly implemented.Proper validation of input length and hex digits with clear error messages.
972-1021: LGTM! Scan filtering logic is well-implemented.The
matches()method correctly handles name, prefix, and service UUID filtering with appropriate case-insensitive comparisons and null checks.
...app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
Outdated
Show resolved
Hide resolved
...app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
Show resolved
Hide resolved
There was a problem hiding this 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)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (2)
213-253: Consider explicitly rejecting a pending connect on manual disconnectIf
disconnect()is called while a connection is still in progress (stateCONNECTINGandpendingConnectCallnon‑null), the plugin relies on the Bluetooth stack to deliver a subsequentSTATE_DISCONNECTEDcallback so thatfailConnect()will reject the pending call. If, for any reason, no callback arrives aftercleanupGatt()(e.g., stack quirks), the JSconnect()promise could remain unresolved.A small defensive improvement is to explicitly reject any outstanding
pendingConnectCallinsidedisconnectInternal(false)aftercleanupGatt():private void disconnectInternal(boolean fromDestroy) { ConnectionState current = connectionState.getAndSet(fromDestroy ? ConnectionState.DISCONNECTED : ConnectionState.DISCONNECTING); if (current == ConnectionState.DISCONNECTED && !fromDestroy) { cleanupGatt(); return; } if (connectTimeoutRunnable != null) { mainHandler.removeCallbacks(connectTimeoutRunnable); connectTimeoutRunnable = null; } String lastDeviceId = connectedDeviceId; cleanupGatt(); activeNotifications.clear(); + + // Defensively fail any in‑flight connect() so JS does not hang if no callback arrives + if (!fromDestroy && pendingConnectCall != null) { + PluginCall call = pendingConnectCall; + pendingConnectCall = null; + call.setKeepAlive(false); + call.reject("Disconnected"); + } + connectionState.set(ConnectionState.DISCONNECTED); notifyConnectionState(false, "disconnected", lastDeviceId); rejectPendingStartNotifications("Device disconnected"); }
445-529: Scanning helpers are well‑encapsulated; minor concurrency improvement possible
startLeScan,handleScanResult, andstopScanInternalform a clean unit: they refresh the scanner, build filters, wire callbacks, push discoveries intodiscoveredDevices, emitdeviceDiscoveredevents, and ensure timeouts and callbacks are cleaned up in all paths.One minor concurrency refinement:
pendingScanCallis written on the plugin thread and read from the scan callback thread and timeout runnable. Declaring itvolatilewould make this intent explicit and avoid any subtle visibility races:- private PluginCall pendingScanCall; + private volatile PluginCall pendingScanCall;Similarly, you might consider
volatileforpendingConnectCallsince it’s read/written across the plugin, timeout, and GATT callback threads.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-05-14T21:51:09.253Z
Learning: On Android Chrome browsers, Web Bluetooth API is supported for BLE devices and Web USB API is supported, but Web Serial API is not supported (except limited Bluetooth serial support added in 2025). The Betaflight Configurator should detect and use available APIs on Android rather than requiring all three.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 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-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/bluetooth/BetaflightBluetoothPlugin.java
🔇 Additional comments (15)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (15)
56-111: Solid plugin annotation and concurrency‑safe core stateUse of a single permission alias,
AtomicReference<ConnectionState>,ConcurrentHashMapfor device/notification maps, andConcurrentLinkedQueuefor pending notification calls is appropriate here and fixes the earlier race onpendingStartNotificationCalls. No issues in this block.
112-131: Lifecycle handling looks correct
load()initializes the adapter/scanner once, andhandleOnDestroy()performs a best‑effort scan stop and disconnect before delegating tosuper.handleOnDestroy(). This is a clean shutdown pattern for the plugin.
132-175: Permission / status / device list API design is coherent
checkStatus,requestPermissions/onPermissionResult, andgetDevicesexpose a clear, minimal surface and correctly gate behavior on the Capacitor permission alias. ReturningdeviceId,available,enabled, andconnectedgives JS everything it needs without leaking extra state. No changes needed.
176-210: Request/stop scan flow is well structured
requestDeviceenforces single active scan viapendingScanCall, merges required/optional services into a unified filter, uses keep‑alive correctly, andstopScansimply delegates to the internal stop helper. Behavior here matches expectations for a Web‑Bluetooth‑stylerequestDevice.
255-341: Write path is robust and MSP‑friendlyArgument validation, UUID parsing with a service fallback search, property checks for write vs write‑without‑response, flexible payload decoding (
base64/hex/utf8), and API‑level‑awarewriteCharacteristichandling are all well done. ResolvingbytesSentfrom the payload length is straightforward and sufficient given you’re not tracking the async write result. No issues spotted here.
342-443: Notification start/stop flow and queuing behavior look correct
startNotificationscorrectly queues calls untilservicesDiscoveredis true, thenstartNotificationsInternalperforms validation, characteristic lookup with service fallback, enables notifications, and tracks active subscriptions.stopNotificationshandles both key‑based and characteristic‑based removal and gracefully returns success when nothing is active. The queuing viapendingStartNotificationCallsand clearing on reconnect/disconnect is clean.
530-577: GATT open and failure handling are thorough
openGattcorrectly chooses the transport overload based on API level and catchesSecurityExceptionto surface a clear error when permissions are missing.failConnectcentralizes state reset, GATT cleanup, timeout teardown, connect‑call rejection, connection state event emission, and pending notification rejection, which keeps the error paths consistent. This section looks solid.
578-629: Connection lifecycle and cleanup logic are coherent
connectedSuccessfullycancels the timeout, resolves the pending connect call once, and emits aconnectionStateevent.disconnectInternaldrives the state machine throughDISCONNECTING→DISCONNECTED, cancels timeouts, clears notifications, and sends a final connection event, whilecleanupGattdefensively disconnects/closes and resets fields. Overall, the lifecycle management is clear and consistent with the rest of the plugin.
640-685: Readiness checks and device resolution are appropriate
ensureBluetoothReadyandensureConnectedprovide clear, early rejections with actionable error messages, andresolveDevicefirst consults the discovery cache before falling back togetRemoteDevicewith error logging for invalid IDs. This is a good separation of concerns.
686-707: Device payload construction is comprehensive
createDevicePayloadincludes ID, name, bond state, RSSI, and advertised service UUIDs, which matches what the JS side typically needs for device selection UIs. Conditional handling of missingScanRecord/UUIDs is safe. No issues here.
709-727: Scan rejection and main‑thread utility function are clean
rejectPendingScansafely logs, rejects the pending call (if any), and then delegates tostopScanInternalfor cleanup, avoiding duplicate logic.runOnMainThreadcorrectly short‑circuits when already on the main looper. This block looks good.
728-765: Notification enable/disable and write submission are implemented correctly
enableNotificationsusessetCharacteristicNotificationplus the standard 0x2902 (client config) descriptor with indicate vs notify selection, andsubmitWritecleanly switches between the Android 13+writeCharacteristicoverload and the legacysetValue+writeCharacteristiccall. Exceptions for permission issues are logged and surfaced asfalse. No further changes needed.
766-903: Helper utilities are robust and defensive
decodePayload,hexToBytes,parseUuidArray,parseUuid,notificationKey,buildScanFilters, andsafeDeviceIdare all straightforward, validate inputs, and handle malformed data gracefully (e.g., hex length/characters, UUID parsing errors). UsingCollections.emptyList()defaults keeps null‑handling simple. This section is in good shape.
919-977: Gatt callback logic and compatibility handling are solid
onConnectionStateChangeadvances the connection state, kicks off service discovery on successful connect, and funnels all disconnects throughfailConnect()to unify error and teardown handling.onServicesDiscoveredmarksservicesDiscovered, flushes queued notifications, logs the layout, and emits a structuredservicespayload. The addition of the deprecated two‑argumentonCharacteristicChangedthat forwards to the three‑argument Android 13+ overload resolves the earlier compatibility gap for Android 12 and below. Notification events include device, service, characteristic, and base64 payload as expected.
979-1028: ScanCriteria matching logic is clear and flexibleThe
ScanCriteriahelper cleanly encodesacceptAll, exact name, prefix (case‑insensitive), and advertised service UUID constraints, short‑circuiting early where possible. TreatingacceptAllas an override that bypasses other filters is a reasonable design choice as long as it matches the JS expectations. No functional problems found.
4d85241 to
4af5c2e
Compare
There was a problem hiding this 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
♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (1)
961-975: Null safety issue confirmed—add guard against nullvalueinonCharacteristicChangedThe Android Bluetooth API documentation confirms that
BluetoothGattCharacteristic.getValue()can return null due to GATT edge cases, uninitialized cache, or stack-specific behavior (especially on pre-API 33). When the deprecated 2-argonCharacteristicChangedforwardscharacteristic.getValue()to the 3-arg override, a null value reachesBase64.encodeToString(value, ...), causing aNullPointerExceptionand dropping the notification silently.Add a null-safety guard to handle this gracefully:
@Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + if (value == null || characteristic == null || characteristic.getService() == null) { + Log.w(TAG, "Notification with null value or characteristic, ignoring"); + return; + } JSObject payload = new JSObject(); payload.put("deviceId", connectedDeviceId); payload.put("service", characteristic.getService().getUuid().toString()); payload.put("characteristic", characteristic.getUuid().toString()); payload.put("value", Base64.encodeToString(value, Base64.NO_WRAP)); notifyListeners("notification", payload); }This prevents crashes on documented Bluetooth stack quirks while keeping normal notifications unchanged.
🧹 Nitpick comments (3)
android/app/src/main/AndroidManifest.xml (1)
58-79: Confirm that hard‑requiring BLE is intendedThe Bluetooth permission set looks aligned with the new plugin, but declaring
android.hardware.bluetooth_lewithandroid:required="true"means the app won’t be installable on devices without BLE, even if USB would still work. If you want “USB‑only but no BLE” devices to remain supported, considerandroid:required="false"here.android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (2)
176-202: Scan filters still restrict results whenacceptAllDevicesis true
requestDevicemergesservicesandoptionalServicesintocombinedFilter, andstartLeScanalways feedscriteria.serviceUuidsintobuildScanFilters, so Android still filters by service UUID even whenacceptAllDevicesis true. This diverges from the WebBluetooth / PWA pattern where{ acceptAllDevices: true, optionalServices: [...] }shows all devices and does not filter by services at scan time.Since
ScanCriteria.matches()already short‑circuits onacceptAll, a minimal way to align behavior is to skip platform scan filters whenacceptAllis true:- List<ScanFilter> filters = buildScanFilters(criteria.serviceUuids); + // When acceptAll is true, don't restrict by service UUIDs at scan time. + List<ScanFilter> filters = buildScanFilters( + criteria.acceptAll ? Collections.emptyList() : criteria.serviceUuids + );This preserves service‑based filtering when explicitly requested (
acceptAllDevices == false && services.length > 0), while letting the Capacitor path behave like WebBluetooth.js when usingacceptAllDevices+optionalServices.Based on learnings, this would align the CapacitorBluetooth behavior with the existing WebBluetooth.js flow.
Also applies to: 445-484, 904-913, 979-1027
557-576: Disconnect handling can emit duplicate/ambiguousconnectionStateevents
disconnectInternal(false)already callscleanupGatt(), setsconnectionStatetoDISCONNECTED, and sends a"disconnected"connectionStateevent with the last non‑nulldeviceId. Later,onConnectionStateChange(... STATE_DISCONNECTED ...)also callscleanupGatt()andfailConnect(...), which in turn sends anotherconnectionStateevent, but now withdeviceIdcoming fromconnectedDeviceIdafter it has been nulled bycleanupGatt().Result: for an explicit disconnect you can see two events (one “disconnected” with a deviceId, one “Connection failed: Disconnected” with null deviceId), and for remote disconnects the event may carry a null
deviceId.Consider distinguishing “expected disconnect” from “connect failure/remote drop”, and capturing the deviceId before calling
cleanupGatt()in the callback path so listeners always get a consistentdeviceIdfor the last connection. For example, gate thefailConnect(...)call onconnectionState != DISCONNECTING, or split out a dedicated “remoteDisconnect” helper that snapshots the deviceId before cleanup.Also applies to: 596-614, 919-931
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
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/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)src/js/protocols/CapacitorBluetooth.js(1 hunks)src/js/serial.js(2 hunks)src/js/serial_backend.js(2 hunks)src/js/utils/checkCompatibility.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/js/serial_backend.js
- src/js/serial.js
- src/js/protocols/CapacitorBluetooth.js
- src/js/utils/checkCompatibility.js
🧰 Additional context used
🧠 Learnings (13)
📓 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: 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-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-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/MainActivity.javaandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/MainActivity.javaandroid/app/src/main/AndroidManifest.xmlandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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
📚 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-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/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-09T20:02:33.475Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4597
File: src/js/utils/common.js:95-127
Timestamp: 2025-09-09T20:02:33.475Z
Learning: In the Betaflight Configurator codebase, the Chromium v140 compatibility issue with sortSelect was resolved by rewriting the implementation to use native DOM APIs (Array.from, select.options, select.remove, select.add) instead of jQuery DOM manipulation methods (this.children, this.empty().append). The fix still performs DOM operations but avoids the specific jQuery methods that were causing issues in Chromium v140.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-08-27T22:07:26.353Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/data_storage.js:9-9
Timestamp: 2025-08-27T22:07:26.353Z
Learning: In the betaflight-configurator CalVer refactoring, the generateVirtualApiVersions function in src/js/utils/common.js works correctly despite the apparent mathematical issue with bounds logic where compareVersions.minor() returns different ranges for API_VERSION_MAX_SUPPORTED (25.12.0) vs API_VERSION_ACCEPTED (1.44.0).
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: In betaflight-configurator, dependency updates are handled incrementally - Vue dependencies are updated separately from Vite dependencies for better isolation and maintainability.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-11-19T17:55:51.714Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4708
File: src/js/tabs/firmware_flasher.js:0-0
Timestamp: 2025-11-19T17:55:51.714Z
Learning: In the betaflight-configurator project, only current Chrome and Edge-based browsers are officially supported, and users are expected to keep their browsers up-to-date for security reasons. This means ES2023 features like Array.prototype.toSorted() and Object.groupBy() can be safely used without polyfills or transpilation.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
🔇 Additional comments (3)
android/app/src/main/java/betaflight/configurator/MainActivity.java (1)
5-13: Bluetooth plugin registration is correct and consistentRegistering
BetaflightBluetoothPluginalongsideBetaflightSerialPluginbeforesuper.onCreatefollows the existing pattern and should integrate cleanly with Capacitor’s bridge lifecycle. No changes needed here.android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (2)
94-111: Overall plugin structure and thread‑safety look solidUsing
AtomicReferenceforconnectionState,ConcurrentHashMap/ConcurrentLinkedQueuefor shared collections, and a main‑threadHandlerfor GATT entry points gives a good concurrency baseline. Error handling inwrite, notification setup/teardown, and UUID/payload parsing are defensive and clear. Aside from the specific points noted in other comments, the class is well‑structured and fits the existing Capacitor plugin patterns.Also applies to: 167-175, 283-340, 373-443, 771-774, 871-886
204-210: Based on my research into Capacitor plugin architecture, I can now assess the review comment.The web search confirms that Capacitor uses a
setKeepAlive/saveCallpattern for async operations. When aPluginCallis marked as keep-alive and saved, it must be explicitlyresolve()d orreject()d to settle the JavaScript promise. If neither occurs, the promise will hang indefinitely.The review comment correctly identifies this architectural concern: if
requestDevice()keeps the call alive and saves it, butstopScan()only callsstopScanInternal()without settling the promise, the JavaScript promise will indeed remain unresolved.However, since the repository clone failed, I cannot verify:
- The actual implementation of
stopScan()andstopScanInternal()- Whether
pendingScanCallandrejectPendingScan()actually exist- How
requestDevice()manages the call lifecycle- What occurs at lines 515-528 ("Also applies to")
The concern raised is architecturally valid and follows documented Capacitor best practices. The suggested fix (routing through
rejectPendingScan()) is the correct pattern for settling an outstanding async call.
stopScanmay leave the originalrequestDevicecall unresolved if the promise settlement pattern is not followedIn Capacitor's async callback model, when a
PluginCallis kept alive (viasetKeepAlive(true)), it must be explicitly resolved or rejected to settle the JavaScript promise. IfrequestDevice()saves the call andstopScan()callsstopScanInternal()without settling it, the JS promise will hang indefinitely.Route user-initiated stops through
rejectPendingScan(...)to ensure the promise is settled:@PluginMethod public void stopScan(PluginCall call) { - stopScanInternal(); + if (pendingScanCall != null) { + // Cancel the outstanding requestDevice call as well. + rejectPendingScan("Scan stopped"); + } else { + stopScanInternal(); + } JSObject result = new JSObject(); result.put("stopped", true); call.resolve(result); }This keeps internal error/timeout paths unchanged while making
stopScan()behavior predictable for callers.
4af5c2e to
619ba7f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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 (1)
src/js/protocols/CapacitorBluetooth.js (1)
178-178: Remove unused variable.The
uuidsvariable is computed but never used. Based on past review discussions, theacceptAllDevices: trueworkaround (line 189) is intentional due to UUID mismatch issues on Android, so the uuids computation is no longer needed.Apply this diff:
async requestPermissionDevice() { let newPermissionDevice = null; - const uuids = bluetoothDevices.map((device) => device.serviceUuid).filter(Boolean); try { await BetaflightBluetooth.requestPermissions(); // prompt onceBased on learnings, acceptAllDevices pattern is used because Android devices advertise different service UUIDs than what's in the bluetoothDevices catalog.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
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/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)src/js/protocols/CapacitorBluetooth.js(1 hunks)src/js/serial.js(2 hunks)src/js/serial_backend.js(2 hunks)src/js/utils/checkCompatibility.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- android/app/src/main/java/betaflight/configurator/MainActivity.java
- src/js/serial_backend.js
- src/js/utils/checkCompatibility.js
- src/js/serial.js
🧰 Additional context used
🧠 Learnings (21)
📓 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: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.760Z
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: 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.
📚 Learning: 2025-11-30T18:10:26.760Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.760Z
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/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/AndroidManifest.xmlsrc/js/protocols/CapacitorBluetooth.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/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/AndroidManifest.xmlsrc/js/protocols/CapacitorBluetooth.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:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/protocols/CapacitorBluetooth.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:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/protocols/CapacitorBluetooth.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:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/AndroidManifest.xmlsrc/js/protocols/CapacitorBluetooth.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/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/protocols/CapacitorBluetooth.js
📚 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/bluetooth/BetaflightBluetoothPlugin.javasrc/js/protocols/CapacitorBluetooth.js
📚 Learning: 2025-09-09T20:02:33.475Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4597
File: src/js/utils/common.js:95-127
Timestamp: 2025-09-09T20:02:33.475Z
Learning: In the Betaflight Configurator codebase, the Chromium v140 compatibility issue with sortSelect was resolved by rewriting the implementation to use native DOM APIs (Array.from, select.options, select.remove, select.add) instead of jQuery DOM manipulation methods (this.children, this.empty().append). The fix still performs DOM operations but avoids the specific jQuery methods that were causing issues in Chromium v140.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-08-27T22:07:26.353Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/data_storage.js:9-9
Timestamp: 2025-08-27T22:07:26.353Z
Learning: In the betaflight-configurator CalVer refactoring, the generateVirtualApiVersions function in src/js/utils/common.js works correctly despite the apparent mathematical issue with bounds logic where compareVersions.minor() returns different ranges for API_VERSION_MAX_SUPPORTED (25.12.0) vs API_VERSION_ACCEPTED (1.44.0).
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: In betaflight-configurator, dependency updates are handled incrementally - Vue dependencies are updated separately from Vite dependencies for better isolation and maintainability.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-11-19T17:55:51.714Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4708
File: src/js/tabs/firmware_flasher.js:0-0
Timestamp: 2025-11-19T17:55:51.714Z
Learning: In the betaflight-configurator project, only current Chrome and Edge-based browsers are officially supported, and users are expected to keep their browsers up-to-date for security reasons. This means ES2023 features like Array.prototype.toSorted() and Object.groupBy() can be safely used without polyfills or transpilation.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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.xmlsrc/js/protocols/CapacitorBluetooth.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/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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:
src/js/protocols/CapacitorBluetooth.js
📚 Learning: 2025-11-06T17:26:50.201Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4687
File: src/js/protocols/TauriSerial.js:299-310
Timestamp: 2025-11-06T17:26:50.201Z
Learning: In tauri-plugin-serialplugin Android development, the Android project must be initialized via `tauri android init` before the USB support patches (AndroidManifest.xml permissions, device_filter.xml, MainActivity.kt) can be applied. Without initialization, the plugin returns empty results from available_ports_direct() because the generated Android project structure and USB permissions don't exist yet.
Applied to files:
src/js/protocols/CapacitorBluetooth.js
🧬 Code graph analysis (1)
src/js/protocols/CapacitorBluetooth.js (2)
src/js/protocols/devices.js (2)
bluetoothDevices(1-51)bluetoothDevices(1-51)src/js/gui_log.js (1)
gui_log(7-21)
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorBluetooth.js
[warning] 178-178: Remove this useless assignment to variable "uuids".
[warning] 41-43: Expected a for-of loop instead of a for loop with this simple iteration.
[warning] 57-57: Prefer String#codePointAt() over String#charCodeAt().
[failure] 87-87: Refactor this asynchronous operation outside of the constructor.
[warning] 42-42: Prefer String.fromCodePoint() over String.fromCharCode().
[warning] 178-178: Remove the declaration of the unused 'uuids' variable.
⏰ 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 (12)
src/js/protocols/CapacitorBluetooth.js (3)
62-101: LGTM: Constructor follows established patterns.The constructor properly initializes state, attaches native listeners, and follows the singleton pattern established in the codebase. The fire-and-forget listener attachment is appropriate since protocol implementations live for the entire application lifetime.
Based on learnings, protocol implementations are instantiated once and never destroyed, so cleanup methods for event listeners are not needed.
204-276: LGTM: Connection flow with robust service resolution.The connection flow properly:
- Manages connection state flags
- Waits for device characteristics with timeout-based fallback
- Validates complete service/characteristic availability before proceeding
- Starts notifications after successful connection
- Handles errors and cleanup appropriately
The
waitForDeviceCharacteristicspattern with pending promises and timeouts provides robust coordination with the native service discovery events.
375-414: LGTM: Write operation with proper queuing and error handling.The write queue pattern using promise chaining ensures sequential writes while avoiding race conditions. The base64 encoding, payload conversion, and error handling are all appropriate for BLE transport.
android/app/src/main/AndroidManifest.xml (2)
59-77: LGTM: Version-aware Bluetooth permissions properly configured.The permission declarations correctly implement version-specific Bluetooth permission handling:
- Legacy permissions (BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION) are scoped to Android 8-11 with
maxSdkVersion="30"- New permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT) are scoped to Android 12+ with
minSdkVersion="31"- The
neverForLocationflag on BLUETOOTH_SCAN is a good privacy practiceBased on learnings, this structure is necessary because Android 8-11 requires legacy permissions while Android 12+ requires the new runtime permissions, and the Java code must conditionally request permissions using SDK version checks.
68-69: Verify if ACCESS_FINE_LOCATION is necessary for BLE scanning.
ACCESS_FINE_LOCATIONis declared but may not be required. According to Android permission requirements for BLE,ACCESS_COARSE_LOCATIONis typically sufficient for BLE scanning on Android 6-11. Review the Bluetooth implementation code to confirm whether fine location precision is actually needed for your use case, or if coarse location (withmaxSdkVersion="30") would be adequate.android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (7)
159-242: LGTM: Version-aware permission handling implemented correctly.The permission request implementation properly handles Android version differences:
- Uses
getRequiredPermissions()to determine version-specific permissions- Android 12+ requests BLUETOOTH_SCAN and BLUETOOTH_CONNECT
- Android 8-11 requests legacy BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_COARSE_LOCATION
- Uses native
ActivityCompat.requestPermissionswith proper callback handling- Stores pending call for later resolution
This matches the learning about version-aware permission handling and avoids requesting non-existent permissions on older Android versions.
115-116: LGTM: Thread-safe collection for pending notification calls.The switch from
ArrayListtoConcurrentLinkedQueueaddresses the race condition identified in previous reviews. The volatileservicesDiscoveredflag combined with the thread-safe queue ensures safe access from multiple threads.
536-604: LGTM: Robust BLE scanning with timeout and criteria matching.The scanning implementation properly:
- Builds scan filters from service UUIDs
- Uses low-latency scan mode for responsive discovery
- Implements scan timeout with cleanup
- Matches devices against name/prefix/service criteria
- Emits device discovery events
- Resolves pending scan call on first match
- Handles scan failures gracefully
The batch result handling and criteria matching logic are well-designed.
622-646: LGTM: GATT connection with proper API level handling.The
openGattmethod correctly:
- Uses
TRANSPORT_LEparameter on Android 6+ (API 23+)- Falls back to two-parameter signature on older versions
- Handles
SecurityExceptionfor missing permissions- Sets up connection timeout with cleanup
- Uses main thread for GATT operations
843-855: LGTM: Write operation with API level compatibility.The write submission handles Android API differences correctly:
- Android 13+ (Tiramisu): Uses new three-parameter
writeCharacteristicAPI- Earlier versions: Uses deprecated two-parameter API with
setValue/setWriteType- Properly handles
SecurityException
1024-1050: LGTM: Service discovery with comprehensive event emission.The
onServicesDiscoveredcallback:
- Sets
servicesDiscoveredflag to unblock pending notification calls- Flushes queued
startNotificationscalls- Builds complete service/characteristic hierarchy with properties
- Emits detailed
servicesevent for JavaScript layerThis provides the JavaScript side with all information needed for dynamic service resolution.
918-943: LGTM: Pending notification call queue with proper lifecycle.The queue management correctly:
- Queues
startNotificationscalls when services aren't discovered yet- Flushes the queue once services are available
- Rejects pending calls on disconnect or connection failure
- Uses thread-safe
ConcurrentLinkedQueueThis ensures notifications can be started immediately after connection without race conditions.
...app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
Show resolved
Hide resolved
There was a problem hiding this 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 (2)
src/js/protocols/CapacitorBluetooth.js (1)
36-59: Optional: simplify base64 helpers with modern iteration APIs
toBase64/fromBase64manually build strings with an indexedforloop andString.fromCharCode/charCodeAt. Functionally fine, but you could switch tofor...ofon theUint8ArrayandcodePointAt()for slightly clearer, more idiomatic code if you touch this again. Given the Buffer paths handle most environments, this is purely a readability/style tweak, not a bug.android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (1)
61-76: Align permission checks with the new runtime permission flow and clean up unused callbackYou’ve moved to explicit runtime permission handling via
getRequiredPermissions()+ActivityCompat.requestPermissions, but:
- The plugin annotation still defines a
bluetoothalias withstrings = {}, andensureBluetoothReady()still checksgetPermissionState("bluetooth"), while@PermissionCallback onPermissionResultis no longer used (you don’t callrequestPermissionForAliasanymore).This means the Capacitor alias state is effectively decoupled from the real OS permission state, and the early “Bluetooth permission not granted” guard may never trigger even if Android permissions are denied.
Consider simplifying and making these consistent by:
- Dropping the alias-based permission path (or leaving the alias empty but not relying on it), and
- Basing readiness purely on your version-aware permission helper:
- private boolean ensureBluetoothReady(PluginCall call) { - if (getPermissionState("bluetooth") != PermissionState.GRANTED) { - call.reject("Bluetooth permission not granted"); - return false; - } + private boolean ensureBluetoothReady(PluginCall call) { + // Check OS permissions using the same logic as requestPermissions() + String[] requiredPermissions = getRequiredPermissions(); + for (String permission : requiredPermissions) { + if (ContextCompat.checkSelfPermission(getContext(), permission) + != PackageManager.PERMISSION_GRANTED) { + call.reject("Bluetooth permission not granted"); + return false; + } + } if (bluetoothAdapter == null) { call.reject("Bluetooth adapter unavailable"); return false; } @@ return true; }And, if you keep this approach, you can remove the now-unused
onPermissionResult@PermissionCallbackmethod and the commented-outrequestPermissionsimplementation to avoid confusion.This will make the permission story clearer and ensure
ensureBluetoothReady()reflects the actual runtime permission state on both Android 8–11 and 12+.Also applies to: 159-210, 244-254, 731-745
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)src/js/protocols/CapacitorBluetooth.js(1 hunks)
🧰 Additional context used
🧠 Learnings (21)
📓 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: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.760Z
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: 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.
📚 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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/protocols/CapacitorBluetooth.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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:
src/js/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.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:
src/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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:
src/js/protocols/CapacitorBluetooth.js
📚 Learning: 2025-11-30T18:10:26.760Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-30T18:10:26.760Z
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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-11-06T17:26:50.201Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4687
File: src/js/protocols/TauriSerial.js:299-310
Timestamp: 2025-11-06T17:26:50.201Z
Learning: In tauri-plugin-serialplugin Android development, the Android project must be initialized via `tauri android init` before the USB support patches (AndroidManifest.xml permissions, device_filter.xml, MainActivity.kt) can be applied. Without initialization, the plugin returns empty results from available_ports_direct() because the generated Android project structure and USB permissions don't exist yet.
Applied to files:
src/js/protocols/CapacitorBluetooth.js
📚 Learning: 2025-09-09T20:02:33.475Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4597
File: src/js/utils/common.js:95-127
Timestamp: 2025-09-09T20:02:33.475Z
Learning: In the Betaflight Configurator codebase, the Chromium v140 compatibility issue with sortSelect was resolved by rewriting the implementation to use native DOM APIs (Array.from, select.options, select.remove, select.add) instead of jQuery DOM manipulation methods (this.children, this.empty().append). The fix still performs DOM operations but avoids the specific jQuery methods that were causing issues in Chromium v140.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-08-27T22:07:26.353Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/data_storage.js:9-9
Timestamp: 2025-08-27T22:07:26.353Z
Learning: In the betaflight-configurator CalVer refactoring, the generateVirtualApiVersions function in src/js/utils/common.js works correctly despite the apparent mathematical issue with bounds logic where compareVersions.minor() returns different ranges for API_VERSION_MAX_SUPPORTED (25.12.0) vs API_VERSION_ACCEPTED (1.44.0).
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: In betaflight-configurator, dependency updates are handled incrementally - Vue dependencies are updated separately from Vite dependencies for better isolation and maintainability.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-11-19T17:55:51.714Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4708
File: src/js/tabs/firmware_flasher.js:0-0
Timestamp: 2025-11-19T17:55:51.714Z
Learning: In the betaflight-configurator project, only current Chrome and Edge-based browsers are officially supported, and users are expected to keep their browsers up-to-date for security reasons. This means ES2023 features like Array.prototype.toSorted() and Object.groupBy() can be safely used without polyfills or transpilation.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
🧬 Code graph analysis (1)
src/js/protocols/CapacitorBluetooth.js (2)
src/js/gui_log.js (1)
gui_log(7-21)src/js/protocols/devices.js (2)
bluetoothDevices(1-51)bluetoothDevices(1-51)
🪛 GitHub Actions: Deployment (PR and Push)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
[error] 1055-1055: Compilation failed: cannot find symbol 'value' referenced in 'if (characteristic == null || value == null) {'. Likely 'value' is undefined or out of scope in this context (BetaflightBluetoothPlugin.java:1055).
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorBluetooth.js
[failure] 87-87: Refactor this asynchronous operation outside of the constructor.
[warning] 42-42: Prefer String.fromCodePoint() over String.fromCharCode().
[warning] 41-43: Expected a for-of loop instead of a for loop with this simple iteration.
[warning] 57-57: Prefer String#codePointAt() over String#charCodeAt().
🔇 Additional comments (1)
src/js/protocols/CapacitorBluetooth.js (1)
62-647: Capacitor Bluetooth adapter design looks solid and consistent with existing protocolsConnection lifecycle, dynamic service/characteristic resolution, CRC workaround, serialized write queue, and native listener wiring all look coherent and in line with the existing Serial/WebBluetooth patterns. No blocking issues from this side.
...app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
Show resolved
Hide resolved
|
@coderabbitai - on any tab when using save and reboot it does not fire reinitializeConnection (MSP_REBOOT) in serial_backend.js - it does not even reach the function |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 6146 🏁 Script executed: Length of output: 2537 🏁 Script executed: Length of output: 3125 🏁 Script executed: Length of output: 5808 🏁 Script executed: Length of output: 7362 🏁 Script executed: Length of output: 2699 🏁 Script executed: Length of output: 4587 🏁 Script executed: Length of output: 1884 🏁 Script executed: Length of output: 2471 🏁 Script executed: Length of output: 2118 🏁 Script executed: Length of output: 976 🏁 Script executed: Length of output: 4334 🏁 Script executed: Length of output: 71 🏁 Script executed: Length of output: 1297 🏁 Script executed: Length of output: 2638 🏁 Script executed: Length of output: 4273 🏁 Script executed: Length of output: 5551 @haslinghuis I found the issue! The problem is that several tabs have Here's the flow:
Tabs with missing callback invocations:
power.cleanup = function (callback) {
// Empty - callback never called!
};
setup_osd.cleanup = function (callback) {
// Empty - callback never called!
};
transponder.cleanup = function (callback) {
// Empty - callback never called!
};
Solution: All tab cleanup functions must invoke the callback: // Add this to each problematic cleanup function
if (callback) callback();This is not specific to the Capacitor Bluetooth implementation - it would affect save and reboot on any connection type when on these tabs. 🧠 Learnings used |
182da54 to
b888cab
Compare
There was a problem hiding this 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
♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (1)
1029-1041: Add null safety check forcharacteristic.getService().Line 1037 calls
characteristic.getService().getUuid()without verifying thatgetService()returns non-null. If the service is unavailable, this will throw NPE.Apply this diff:
@Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { - if (characteristic == null || value == null) { + if (characteristic == null || characteristic.getService() == null || value == null) { Log.w(TAG, "Received notification with null characteristic or value"); return; } JSObject payload = new JSObject(); payload.put("deviceId", connectedDeviceId); payload.put("service", characteristic.getService().getUuid().toString()); payload.put("characteristic", characteristic.getUuid().toString()); payload.put("value", Base64.encodeToString(value, Base64.NO_WRAP)); notifyListeners("notification", payload); }
🧹 Nitpick comments (1)
src/js/serial_backend.js (1)
106-165: Connection lock usage is sound; consider guarding against synchronousserial.connectthrowsThe new
GUI.connect_lockguard aroundserial.connect(...)correctly prevents overlapping connect attempts (including across different protocols) and the.finallyensures the lock is cleared on both resolve and reject. Because JS is single-threaded, theif (!GUI.connect_lock ...)check at Line 109 remains safe even though the lock is set slightly later.One small defensive improvement: if
serial.connectwere ever to throw synchronously before returning a Promise,GUI.connect_lockwould staytrueand block further connects. Wrapping the call in atry { ... } finally { GUI.connect_lock = false; }(or atry/catchthat resets the lock before re‑throwing) would make the guard robust to that scenario, while keeping the current behavior for normal Promise-based errors.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
android/app/src/main/AndroidManifest.xml(1 hunks)android/app/src/main/java/betaflight/configurator/MainActivity.java(2 hunks)android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java(1 hunks)src/js/msp/MSPHelper.js(1 hunks)src/js/protocols/CapacitorBluetooth.js(1 hunks)src/js/serial.js(2 hunks)src/js/serial_backend.js(1 hunks)src/js/utils/checkCompatibility.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/js/serial.js
- android/app/src/main/java/betaflight/configurator/MainActivity.java
- src/js/utils/checkCompatibility.js
🧰 Additional context used
🧠 Learnings (30)
📓 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-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-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.
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.
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: 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: 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.
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-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-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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/msp/MSPHelper.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/protocols/CapacitorBluetooth.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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 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/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.jssrc/js/serial_backend.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/protocols/CapacitorBluetooth.jssrc/js/serial_backend.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:
src/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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:
src/js/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/msp/MSPHelper.js
📚 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/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javasrc/js/msp/MSPHelper.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:
src/js/protocols/CapacitorBluetooth.js
📚 Learning: 2025-08-22T16:43:20.901Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4576
File: src/js/port_usage.js:17-23
Timestamp: 2025-08-22T16:43:20.901Z
Learning: In betaflight-configurator, the serial facade architecture requires accessing metrics like bitrate, bytesReceived, and bytesSent from serial._protocol rather than the top-level serial object. This change maintains compatibility with existing port utilization calculations that have been stable for over 11 years.
Applied to files:
src/js/protocols/CapacitorBluetooth.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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/AndroidManifest.xml
📚 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/protocols/CapacitorBluetooth.jsandroid/app/src/main/AndroidManifest.xml
📚 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/protocols/CapacitorBluetooth.jsandroid/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.javaandroid/app/src/main/AndroidManifest.xml
📚 Learning: 2025-11-06T17:26:50.201Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4687
File: src/js/protocols/TauriSerial.js:299-310
Timestamp: 2025-11-06T17:26:50.201Z
Learning: In tauri-plugin-serialplugin Android development, the Android project must be initialized via `tauri android init` before the USB support patches (AndroidManifest.xml permissions, device_filter.xml, MainActivity.kt) can be applied. Without initialization, the plugin returns empty results from available_ports_direct() because the generated Android project structure and USB permissions don't exist yet.
Applied to files:
src/js/protocols/CapacitorBluetooth.js
📚 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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-09T20:02:33.475Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4597
File: src/js/utils/common.js:95-127
Timestamp: 2025-09-09T20:02:33.475Z
Learning: In the Betaflight Configurator codebase, the Chromium v140 compatibility issue with sortSelect was resolved by rewriting the implementation to use native DOM APIs (Array.from, select.options, select.remove, select.add) instead of jQuery DOM manipulation methods (this.children, this.empty().append). The fix still performs DOM operations but avoids the specific jQuery methods that were causing issues in Chromium v140.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-08-27T22:07:26.353Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/data_storage.js:9-9
Timestamp: 2025-08-27T22:07:26.353Z
Learning: In the betaflight-configurator CalVer refactoring, the generateVirtualApiVersions function in src/js/utils/common.js works correctly despite the apparent mathematical issue with bounds logic where compareVersions.minor() returns different ranges for API_VERSION_MAX_SUPPORTED (25.12.0) vs API_VERSION_ACCEPTED (1.44.0).
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-09-10T18:26:10.136Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4601
File: package.json:125-125
Timestamp: 2025-09-10T18:26:10.136Z
Learning: In betaflight-configurator, dependency updates are handled incrementally - Vue dependencies are updated separately from Vite dependencies for better isolation and maintainability.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java
📚 Learning: 2025-11-19T17:55:51.714Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4708
File: src/js/tabs/firmware_flasher.js:0-0
Timestamp: 2025-11-19T17:55:51.714Z
Learning: In the betaflight-configurator project, only current Chrome and Edge-based browsers are officially supported, and users are expected to keep their browsers up-to-date for security reasons. This means ES2023 features like Array.prototype.toSorted() and Object.groupBy() can be safely used without polyfills or transpilation.
Applied to files:
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.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/msp/MSPHelper.js
📚 Learning: 2025-08-27T19:36:13.733Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/tabs/pid_tuning.js:80-82
Timestamp: 2025-08-27T19:36:13.733Z
Learning: MSP_STATUS_EX calls in serial_backend.js and setup.js are intentionally unguarded because they are used for status fetching on page load to initialize profiles and are safe to call on older firmware versions. Only conditional/optional MSP_STATUS_EX usage (like in pid_tuning.js) needs version gating to API_VERSION_25_12.
Applied to files:
src/js/msp/MSPHelper.js
📚 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:
src/js/msp/MSPHelper.js
📚 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:
src/js/msp/MSPHelper.js
🧬 Code graph analysis (2)
src/js/protocols/CapacitorBluetooth.js (1)
src/js/protocols/devices.js (2)
bluetoothDevices(1-51)bluetoothDevices(1-51)
src/js/serial_backend.js (3)
src/js/gui.js (1)
GUI(473-473)src/js/serial.js (2)
serial(232-232)serial(232-232)src/js/port_handler.js (4)
PortHandler(16-50)selectedPort(140-140)selectedPort(190-190)selectedPort(279-279)
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorBluetooth.js
[failure] 87-87: Refactor this asynchronous operation outside of the constructor.
[warning] 41-43: Expected a for-of loop instead of a for loop with this simple iteration.
[warning] 57-57: Prefer String#codePointAt() over String#charCodeAt().
[warning] 42-42: Prefer String.fromCodePoint() over String.fromCharCode().
src/js/msp/MSPHelper.js
[warning] 2973-2973: Unexpected negated condition.
[failure] 2955-2955: Expected { after 'if' condition.
[failure] 2947-2947: Expected { after 'if' condition.
⏰ 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 (12)
src/js/msp/MSPHelper.js (1)
2935-2969: BLE save/reboot flow: quieting + fallback look correct and should resolve missed MSP_EEPROM_WRITE acksThe new
writeConfigurationflow does the right things for the BLE issues we discussed:
- Pre‑save
GUI.interval_kill_all()andMSP.callbacks_cleanup()to stop background status pulls and stale callbacks beforeMSP_EEPROM_WRITE, which directly targets the queue‑starvation problem on Android BLE.sendEepromuses afinishedguard plus a 3s fallback timer, so either the EEPROM ack or the timeout will callfinish()exactly once, ensuringGUI.tab_switch_cleanup(() => reinitializeConnection())andcallbackare always reached whenrebootis true.- The fallback path still allows a late MSP ack to arrive safely: the timer is cleared when the ack handler runs (if it’s still pending), and the
finishedflag prevents duplicate reboot/callback execution.- Arming safety is preserved: when the FC is currently armed, you still send
MSP_ARMING_DISABLEbefore scheduling the EEPROM write, but you no longer block on its callback, which avoids deadlock under BLE load.This is aligned with the prior root‑cause analysis of BLE queue overload and missed EEPROM callbacks. Based on learnings, this structure is appropriate and low risk.
It would still be good to sanity‑check one thing in real usage: on “save without reboot” flows,
GUI.interval_kill_all()andMSP.callbacks_cleanup()will also run. Please verify that the relevant tabs re‑establish any needed status intervals after a plain save (or that it’s acceptable for them to stay quiet until the user navigates away/back).android/app/src/main/AndroidManifest.xml (1)
59-75: LGTM! Version-aware Bluetooth permissions correctly implemented.The manifest properly declares Bluetooth permissions with SDK version guards to support both Android 8-11 and Android 12+, matching the required pattern. The
neverForLocationflag onBLUETOOTH_SCANand therequired="true"BLE feature are appropriate.Based on learnings, this permission structure aligns with Android's version-specific requirements.
android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java (6)
61-115: LGTM! Plugin structure correctly implements version-aware permission handling.The empty
@Permissionannotation is correct since permissions are handled via native Android API with version-specific branching. The cached write characteristic fields (writeCharacteristic,writeNoResponseSupported,cachedWriteType) align with the learning that the write characteristic should be resolved once inonServicesDiscovered()and reused.Based on learnings, this structure properly supports the version-aware permission workflow.
147-228: LGTM! Version-aware permission handling correctly implemented.The permission flow properly branches based on Android version:
- Android 12+ requests
BLUETOOTH_SCANandBLUETOOTH_CONNECT- Android 8-11 requests
BLUETOOTH,BLUETOOTH_ADMIN, andACCESS_COARSE_LOCATIONThis matches the solution discussed in past comments and properly uses native Android permission APIs with Capacitor's
handleRequestPermissionsResult()callback.Based on learnings, this implementation resolves the Android 8 "Permissions denied" issue.
460-508: LGTM! Scan start correctly guarded against SecurityException.The
try-catchblock aroundbluetoothLeScanner.startScan()prevents app crashes when Bluetooth permissions are missing or revoked at runtime. The scan timeout is only scheduled after successful scan start, which is the correct pattern.Per past comments, this change was critical for preventing crashes when permissions aren't in the expected state.
332-355: LGTM! Write method correctly uses cached characteristic.The
write()method reads payload params from the call, uses the cachedwriteCharacteristic, picks the cached write type, and callssubmitWritewithout per-call UUID lookups. This matches the maintainer preference from learnings for a "write-only" implementation.Based on learnings, the characteristic is cached once in
onServicesDiscovered()and reused here.
957-1011: LGTM! Service discovery correctly caches write characteristic and delays connection success.Lines 962-987 resolve and cache the write characteristic once, preferring
WRITE_NO_RESPONSEoverWRITE, which matches the maintainer preference from learnings. Line 1010 callsconnectedSuccessfully()only after services are discovered, ensuring the JS 'open' event is delayed until GATT services and notifications are ready.Based on learnings, this addresses the early "Unable to write characteristic" issue and supports the write-only
write()method.
640-652: LGTM! Cached characteristic correctly reset on disconnect.Lines 647-649 reset
writeCharacteristic,writeNoResponseSupported, andcachedWriteTypeon all disconnect paths. This ensures the cache doesn't hold stale characteristics after disconnect or reboot.Based on learnings, this addresses the issue where writes could occur using a stale cached characteristic after reboot.
src/js/protocols/CapacitorBluetooth.js (4)
599-649: LGTM! Native listeners correctly attached once per app lifetime.Lines 600-604 implement an idempotent guard using the
nativeListenersReadyflag. TheattachNativeListeners()method returns early if already called, preventing duplicate handlers that could cause MSP queue pressure and prevent MSP_EEPROM_WRITE callbacks from firing.Based on learnings, this addresses the issue where re-attaching listeners during write cycles led to increased JS workload and prevented reinitializeConnection from being reached.
208-280: LGTM! Connection flow correctly delays 'open' event until services and notifications are ready.Lines 243-246 wait for device characteristics (including GATT services resolution), line 255 starts notifications, and line 270 dispatches the 'connect' event only after both are complete. This ensures the first MSP write doesn't fail with "Unable to write characteristic."
Based on learnings, delaying the 'open' event until after GATT services are resolved and notifications are started prevents early queue load and write failures.
464-597: LGTM! Service resolution implements robust async handling with caching and timeout fallback.The service resolution flow properly waits for GATT services to be discovered:
- Checks cached services first (lines 469-472)
- Creates a pending promise if characteristics incomplete (line 474)
- Resolves when native 'services' event arrives (lines 567-584)
- Falls back to provided description if timeout occurs (line 519)
This ensures the connection flow waits for service discovery before allowing writes, preventing early "Unable to write characteristic" errors.
379-418: LGTM! Send method implements correct write queue pattern.The write queue (lines 395-406) ensures writes are serialized and the queue continues even if a write fails (line 406). The callback pattern provides backward compatibility with the existing serial API. The early checks (lines 380-389) prevent writes when disconnected or characteristic unavailable.
|
|
Preview URL: https://pr4714.betaflight-app-preview.pages.dev |
|
@coderabbitai checked implementation CapacitorSerial and CapacitorTcp in https://github.com/betaflight/betaflight-configurator/ to compare with CapicitorBluetooth. Issues to resolve
Gemini Answer Here is a breakdown of the key points for your BLE implementation and advice on optimization. I. Review of Custom Plugins Your implementation of CapacitorSerial and CapacitorTcp aligns with the best practices for Capacitor binary communication:
II. BLE Implementation Feedback (PR #4714) The issues you've identified with the BLE implementation are common pain points in cross-platform mobile Bluetooth development.
Recommendation:
The performance issues are likely due to the inherent nature of how BLE transactions are queued on the native side and the low default throughput. Refactoring Strategy (Read/Write Separation):
That's a fantastic update on the connection plugins! I've reviewed the general approach to your CapacitorSerial and CapacitorTcp plugins and your new BLE implementation. The custom plugin approach is definitely the right choice for the level of control Betaflight requires. Here is a breakdown of the key points for your BLE implementation and advice on optimization. Your implementation of CapacitorSerial and CapacitorTcp aligns with the best practices for Capacitor binary communication: II. BLE Implementation Feedback (PR #4714) The issues you've identified with the BLE implementation are common pain points in cross-platform mobile Bluetooth development.
The performance issues are likely due to the inherent nature of how BLE transactions are queued on the native side and the low default throughput.
The device discovery and selection dialog must be handled explicitly in your plugin to give the user control.
For more details on why your read/write speed may be limited and how to implement flow control, you can review this video: Android Bluetooth Low Energy Sequential Write Performance. This video discusses the typical write limitations in the Android BLE stack. @coderabbitai perhaps take a deep dive and suggest changes for each step. |
|
Tip For best results, initiate chat on the files or code changes. @haslinghuis here’s a focused, step-by-step plan with minimal patches for each item. I’ve kept write() strictly write-only, moved all decisions to connect-time, and avoided adding unrelated helpers. A) Logging is too verbose (Android + JS) Android (BetaflightBluetoothPlugin.java)
// Fields near other members
private static final String TAG = "BetaflightBluetooth";
private static volatile int LOG_LEVEL = android.util.Log.WARN; // VERBOSE<DEBUG<INFO<WARN<ERROR
@PluginMethod
public void setLogLevel(PluginCall call) {
String level = call.getString("level", "warn").toLowerCase(Locale.ROOT);
switch (level) {
case "verbose": LOG_LEVEL = Log.VERBOSE; break;
case "debug": LOG_LEVEL = Log.DEBUG; break;
case "info": LOG_LEVEL = Log.INFO; break;
case "error": LOG_LEVEL = Log.ERROR; break;
default: LOG_LEVEL = Log.WARN;
}
JSObject ret = new JSObject(); ret.put("level", level); call.resolve(ret);
}
private static void log(int lvl, String msg) {
if (lvl >= LOG_LEVEL) Log.println(lvl, TAG, msg);
}
JS (src/js/protocols/CapacitorBluetooth.js)
// Top of file
const DBG_BT = !!(window?.BF_DEBUG_BLE); // set via console or dev toggle
function dbg(...args) { if (DBG_BT) console.log("[BLE]", ...args); }
// Replace verbose console.log(...) calls with dbg(...)Optionally set Android log level at app start: try { BetaflightBluetooth.setLogLevel({ level: "warn" }); } catch {}B) Read/write is slow — refactor to “write-only” and do all setup once on connect Android (BetaflightBluetoothPlugin.java)
// Cached per connection
private BluetoothGattCharacteristic writeCharacteristic = null;
private boolean writeNoResponseSupported = false;
private int cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int newState, int status) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// Optional throughput boosters (safe no-ops on older APIs)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try { gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M, BluetoothDevice.PHY_LE_2M,
BluetoothDevice.PHY_OPTION_NO_PREFERRED); } catch (Exception ignored) {}
}
try { gatt.requestMtu(247); } catch (Exception ignored) {} // 247 is broadly supported
gatt.discoverServices();
return;
}
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
cleanupGatt();
notifyConnectionState("disconnected", null);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) return;
servicesDiscovered = true;
writeCharacteristic = null;
writeNoResponseSupported = false;
cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
for (BluetoothGattService svc : gatt.getServices()) {
BluetoothGattCharacteristic preferred = null, fallback = null;
for (BluetoothGattCharacteristic ch : svc.getCharacteristics()) {
int props = ch.getProperties();
if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) { preferred = ch; break; }
if (fallback == null && (props & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) fallback = ch;
}
BluetoothGattCharacteristic chosen = (preferred != null) ? preferred : fallback;
if (chosen != null) {
writeCharacteristic = chosen;
writeNoResponseSupported =
(chosen.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0;
cachedWriteType = writeNoResponseSupported
? BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
: BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
break;
}
}
flushPendingStartNotificationCalls();
// ... existing “services” event notification stays as-is ...
}
@PluginMethod
public void write(PluginCall call) {
if (!ensureConnected(call)) return;
final BluetoothGatt gatt = bluetoothGatt;
if (gatt == null || !servicesDiscovered) { call.reject("Not connected"); return; }
final BluetoothGattCharacteristic target = writeCharacteristic;
if (target == null) { call.reject("Write characteristic not available"); return; }
final String value = call.getString("value", call.getString("data"));
final String encoding = call.getString("encoding", "base64");
final byte[] payload;
try { payload = decodePayload(value, encoding); }
catch (IllegalArgumentException ex) { call.reject("Failed to decode payload: " + ex.getMessage()); return; }
final boolean ok = submitWrite(gatt, target, payload, cachedWriteType);
if (ok) {
JSObject res = new JSObject(); res.put("bytesSent", payload.length); call.resolve(res);
} else {
call.reject("Unable to write characteristic");
}
}
private void cleanupGatt() {
if (bluetoothGatt != null) {
try { bluetoothGatt.disconnect(); } catch (Exception ignored) {}
try { bluetoothGatt.close(); } catch (Exception ignored) {}
}
bluetoothGatt = null;
writeCharacteristic = null;
writeNoResponseSupported = false;
cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
connectedDeviceId = null;
servicesDiscovered = false;
}JS (src/js/protocols/CapacitorBluetooth.js)
// Wait for the native 'services' event for this device before announcing 'open'
_waitForServices(deviceId, timeoutMs = 4000) {
if (this._servicesReady) return Promise.resolve();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Services discovery timeout")), timeoutMs);
const sub = BetaflightBluetooth.addListener("services", (payload) => {
if (!payload || payload.deviceId !== deviceId) return;
clearTimeout(timer); this._servicesReady = true; sub.remove(); resolve();
});
});
}
async connect(device, options = {}) {
// ... existing connect ...
await this._waitForServices(device.deviceId, 4000);
await BetaflightBluetooth.startNotifications({ deviceId: device.deviceId });
this.dispatchEvent(new CustomEvent("open", {
detail: { path: "bluetooth", baudRate: options.baudRate ?? 115200 }
}));
}Optional one-shot retry in send(): async send(buffer) {
const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
try {
await BetaflightBluetooth.write({ value: arrayBufferToBase64(data), encoding: "base64" });
this.bytesSent += data.length;
} catch (err) {
if (!this._writeRetried && String(err?.message || err).includes("Unable to write characteristic")) {
this._writeRetried = true;
await new Promise(r => setTimeout(r, 60));
try { await this._waitForServices(this.device?.device?.deviceId ?? this.device?.deviceId, 2000); } catch {}
return this.send(data);
}
this._writeRetried = false;
throw err;
}
this._writeRetried = false;
}C) No dialog when permissions are required to choose a device (native picker in requestDevice)
Skeleton (drop-in for your @PluginMethod requestDevice): @PluginMethod
public void requestDevice(PluginCall call) {
Activity activity = getActivity();
if (activity == null) { call.reject("No activity"); return; }
// 1) Ensure permissions by API level (per your manifest split)
if (!ensurePermissionsForScan(call)) return; // implement using your existing permission helpers
activity.runOnUiThread(() -> {
// 2) Start scanning and collect results (accept all; fill discoveredDevices list)
startLeScan(/* accept-all, no filters */);
// 3) Build dialog list from current discovered devices
final List<BluetoothDevice> list = new ArrayList<>(discoveredDevices.values());
final ArrayAdapter<String> adapter = new ArrayAdapter<>(activity, android.R.layout.select_dialog_singlechoice);
for (BluetoothDevice d : list) adapter.add(d.getName() != null ? d.getName() : d.getAddress());
new AlertDialog.Builder(activity)
.setTitle("Select Bluetooth device")
.setSingleChoiceItems(adapter, -1, null)
.setPositiveButton("Select", (dlg, which) -> {
ListView lv = ((AlertDialog) dlg).getListView();
int idx = lv.getCheckedItemPosition();
if (idx >= 0 && idx < list.size()) {
BluetoothDevice dev = list.get(idx);
stopLeScan();
JSObject ret = new JSObject();
ret.put("deviceId", dev.getAddress());
ret.put("name", dev.getName());
// If you maintain uuids in discoveredDevices, include them:
// ret.put("uuids", toJsArray(knownUuidsFor(dev)));
call.resolve(ret);
} else {
stopLeScan(); call.reject("No device selected");
}
})
.setNegativeButton("Cancel", (dlg, w) -> { stopLeScan(); call.reject("Cancelled"); })
.setOnCancelListener(dlg -> { stopLeScan(); call.reject("Cancelled"); })
.show();
});
}Notes
D) Ensure Save & Reboot always reaches reinitializeConnection (BLE-safe)
// src/js/msp/MSPHelper.js — before EEPROM write:
try { GUI.interval_kill_all(); MSP.callbacks_cleanup(); } catch {}
let finished = false;
const finish = () => { if (finished) return; finished = true;
if (reboot) GUI.tab_switch_cleanup(() => reinitializeConnection());
if (callback) callback();
};
const fb = setTimeout(() => { console.warn("EEPROM ack timeout; proceeding to reboot"); finish(); }, 3000);
MSP.send_message(MSPCodes.MSP_EEPROM_WRITE, false, false, () => {
clearTimeout(fb); gui_log(i18n.getMessage("configurationEepromSaved")); finish();
});Summary of what changes where
If you want, I can turn this into a small follow-up patch that only touches:
Which parts would you like me to prepare first for a CI build? 🧠 Learnings used |
|
Tip For best results, initiate chat on the files or code changes. @haslinghuis here’s a focused, minimal set of changes addressing the three items (logging, read/write speed, device-selection dialog), keeping write() strictly “write-only” and moving all decisions to connect-time. I’ve aligned with your latest write() structure and avoided adding unrelated helpers. A) Tone down logging (default WARN; switchable at runtime)
// near top
private static final String TAG = "BetaflightBluetooth";
private static volatile int LOG_LEVEL = android.util.Log.WARN; // VERBOSE<DEBUG<INFO<WARN<ERROR
private static void log(int lvl, String msg) { if (lvl >= LOG_LEVEL) android.util.Log.println(lvl, TAG, msg); }
// Optional: expose to JS so QA can turn up logging when needed
@PluginMethod
public void setLogLevel(PluginCall call) {
String level = String.valueOf(call.getString("level", "warn")).toLowerCase(Locale.ROOT);
switch (level) {
case "verbose": LOG_LEVEL = Log.VERBOSE; break;
case "debug": LOG_LEVEL = Log.DEBUG; break;
case "info": LOG_LEVEL = Log.INFO; break;
case "error": LOG_LEVEL = Log.ERROR; break;
default: LOG_LEVEL = Log.WARN;
}
JSObject ret = new JSObject(); ret.put("level", level); call.resolve(ret);
}JS usage (optional, once on app init): try { BetaflightBluetooth.setLogLevel({ level: "warn" }); } catch {}B) Make write fast: resolve everything once; keep write “decode → submit”
BetaflightBluetoothPlugin.java // class fields (near other members)
private BluetoothGattCharacteristic writeCharacteristic = null;
private boolean writeNoResponseSupported = false;
private int cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
// onConnectionStateChange (optional throughput boosts; safe no-ops on some stacks)
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int newState, int status) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
try { gatt.requestMtu(247); } catch (Exception ignored) {}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try { gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M, BluetoothDevice.PHY_LE_2M,
BluetoothDevice.PHY_OPTION_NO_PREFERRED); } catch (Exception ignored) {}
}
gatt.discoverServices();
return;
}
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
cleanupGatt();
notifyConnectionState("disconnected", null);
}
}
// onServicesDiscovered: resolve once, prefer WRITE_NO_RESPONSE
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) return;
servicesDiscovered = true;
writeCharacteristic = null;
writeNoResponseSupported = false;
cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
for (BluetoothGattService svc : gatt.getServices()) {
BluetoothGattCharacteristic preferred = null, fallback = null;
for (BluetoothGattCharacteristic ch : svc.getCharacteristics()) {
int props = ch.getProperties();
if ((props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) {
preferred = ch; break;
}
if (fallback == null && (props & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) {
fallback = ch;
}
}
final BluetoothGattCharacteristic chosen = (preferred != null) ? preferred : fallback;
if (chosen != null) {
writeCharacteristic = chosen;
writeNoResponseSupported =
(chosen.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0;
cachedWriteType = writeNoResponseSupported
? BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
: BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
break;
}
}
flushPendingStartNotificationCalls(); // keep your existing call
// ... existing “services” payload emission remains unchanged ...
}
// write() — decode + submit only (no per-call lookups, no property reads)
@PluginMethod
public void write(PluginCall call) {
if (!ensureConnected(call)) return;
final BluetoothGatt gatt = bluetoothGatt;
if (gatt == null || !servicesDiscovered) {
call.reject("Not connected");
return;
}
final BluetoothGattCharacteristic target = writeCharacteristic;
if (target == null) {
call.reject("Write characteristic not available");
return;
}
final String value = call.getString("value", call.getString("data"));
final String encoding = call.getString("encoding", "base64");
final byte[] payload;
try {
payload = decodePayload(value, encoding); // reuse your existing helper
} catch (IllegalArgumentException ex) {
call.reject("Failed to decode payload: " + ex.getMessage());
return;
}
final boolean ok = submitWrite(gatt, target, payload, cachedWriteType);
if (ok) {
JSObject result = new JSObject();
result.put("bytesSent", payload.length);
call.resolve(result);
} else {
call.reject("Unable to write characteristic");
}
}
// cleanup — must clear caches on any disconnect path
private void cleanupGatt() {
if (bluetoothGatt != null) {
try { bluetoothGatt.disconnect(); } catch (Exception ignored) {}
try { bluetoothGatt.close(); } catch (Exception ignored) {}
}
bluetoothGatt = null;
writeCharacteristic = null;
writeNoResponseSupported = false;
cachedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
connectedDeviceId = null;
servicesDiscovered = false;
}Small JS guard to prevent “Unable to write characteristic” around reboot
src/js/protocols/CapacitorBluetooth.js // helper
_waitForServices(deviceId, timeoutMs = 4000) {
if (this._servicesReady) return Promise.resolve();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Services discovery timeout")), timeoutMs);
const sub = BetaflightBluetooth.addListener("services", (payload) => {
if (!payload || payload.deviceId !== deviceId) return;
clearTimeout(timer);
this._servicesReady = true;
sub.remove();
resolve();
});
});
}
async connect(device, options = {}) {
// ... your existing connect() up to native connect(...)
await this._waitForServices(device.deviceId, 4000);
await BetaflightBluetooth.startNotifications({ deviceId: device.deviceId });
// now emit 'open'
this.dispatchEvent(new CustomEvent("open", {
detail: { path: "bluetooth", baudRate: options.baudRate ?? 115200 }
}));
}
// optional, very small one-shot retry
async send(buffer) {
const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
try {
await BetaflightBluetooth.write({ value: arrayBufferToBase64(data), encoding: "base64" });
this.bytesSent += data.length;
} catch (err) {
const msg = String(err?.message || err);
if (!this._writeRetried && msg.includes("Unable to write characteristic")) {
this._writeRetried = true;
await new Promise(r => setTimeout(r, 60));
try { await this._waitForServices(this.device?.device?.deviceId ?? this.device?.deviceId, 2000); } catch {}
return this.send(data);
}
this._writeRetried = false;
throw err;
}
this._writeRetried = false;
}C) Device-selection dialog (native requestDevice) BetaflightBluetoothPlugin.java (skeleton that plugs into your existing scan map) @PluginMethod
public void requestDevice(PluginCall call) {
Activity activity = getActivity();
if (activity == null) { call.reject("No activity"); return; }
// Ensure permissions per API level (use your existing helpers)
if (!ensurePermissionsForScan(call)) return;
activity.runOnUiThread(() -> {
// 1) start accept-all scan (reuse your current startScan mechanism)
try { startScanAcceptAll(); } catch (Exception e) { call.reject("Scan failed: " + e.getMessage()); return; }
// 2) build a list from your discoveredDevices map
final List<BluetoothDevice> devices = new ArrayList<>(discoveredDevices.values());
final ArrayAdapter<String> adapter =
new ArrayAdapter<>(activity, android.R.layout.select_dialog_singlechoice);
for (BluetoothDevice d : devices) {
String name = (d.getName() != null) ? d.getName() : d.getAddress();
adapter.add(name);
}
// 3) dialog
AlertDialog dlg = new AlertDialog.Builder(activity)
.setTitle("Select Bluetooth device")
.setSingleChoiceItems(adapter, -1, null)
.setPositiveButton("Select", (dialog, whichBtn) -> {
ListView lv = ((AlertDialog) dialog).getListView();
int idx = lv.getCheckedItemPosition();
stopScanSafe();
if (idx >= 0 && idx < devices.size()) {
BluetoothDevice dev = devices.get(idx);
JSObject ret = new JSObject();
ret.put("deviceId", dev.getAddress());
ret.put("name", dev.getName());
// If you keep uuids per device, add them as a JSArray here.
call.resolve(ret);
} else {
call.reject("No device selected");
}
})
.setNegativeButton("Cancel", (d, w) -> { stopScanSafe(); call.reject("Cancelled"); })
.setOnCancelListener(d -> { stopScanSafe(); call.reject("Cancelled"); })
.create();
dlg.show();
});
}
// helpers using your existing scan API
private void startScanAcceptAll() { /* reuse your scanner with no filters */ }
private void stopScanSafe() { try { stopScanInternal(); } catch (Exception ignored) {} }JS side (use the native picker when we don’t already have a permitted device) // When user clicks “Connect via Bluetooth”
let dev = null;
try {
dev = await BetaflightBluetooth.requestDevice(); // opens native picker
} catch (e) {
// user cancelled or permissions missing
// optionally fall back to getDevices() or show a UI message
}
if (dev) {
await serial.connect("bluetooth", { device: dev, baudRate: 115200 });
}Sequence (end-to-end) sequenceDiagram
autonumber
participant UI as UI
participant JS as CapacitorBluetooth.js
participant Plugin as BetaflightBluetoothPlugin
participant GATT as Android BLE Stack
UI->>JS: requestDevice()
JS->>Plugin: requestDevice()
Plugin->>GATT: startScan (accept all)
GATT-->>Plugin: onScanResult(...)
Plugin-->>JS: (dialog) resolve { deviceId, name }
JS->>Plugin: connect(deviceId)
Plugin->>GATT: connect + requestMtu + discoverServices
GATT-->>Plugin: onServicesDiscovered
Plugin-->>JS: services event
JS->>Plugin: startNotifications
JS-->>UI: dispatch 'open'
UI->>JS: MSP writes
JS->>Plugin: write(value)
Plugin->>GATT: writeCharacteristic (cachedWriteType)
GATT-->>Plugin: onCharacteristicChanged (notifications)
Plugin-->>JS: notification event
Why this resolves your three issues
Sanity checks to run while testing on Android 8 and 15
If you want, I can turn this into a tiny follow-up patch (only these deltas in BetaflightBluetoothPlugin.java and CapacitorBluetooth.js) so you can grab a CI artifact and confirm on real hardware. ✏️ Learnings added
🧠 Learnings used |
|
Closing in favor of #4735 |



issues remaining
Summary by CodeRabbit
New Features
Chores / Improvements
✏️ Tip: You can customize this high-level summary in your review settings.