Skip to content

Conversation

@haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented May 14, 2025

Status

  • Change Data Stream: Replace BufferedReader/BufferedWriter with raw InputStream/OutputStream.
  • Continuous Reading: Replace the single-call receive method with a background thread that continually reads data and uses this.notifyListeners (or bridge.trigger...) to push events (dataReceived) asynchronously to the JavaScript side.
  • Data Format: The data payload sent back to JavaScript should be a Base64-encoded string of the raw binary data, as this is the standard way to transfer binary from native to JS in a Capacitor plugin.
  • Rebase after Add USB serial support for Android (Capacitor) #4698
  • Add Android permissions to manifest
  • Add protocol to serial facade.
  • Update compatibility checks

Summary by CodeRabbit

  • New Features
    • Added native TCP connectivity on Android alongside serial and Bluetooth, allowing direct TCP connections to devices.
    • New Capacitor-backed TCP client with bidirectional data transfer, real-time connection state, and robust connection lifecycle (connect, disconnect, status).
    • TCP support initialized at app startup so the new protocol is available immediately.

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

@haslinghuis haslinghuis added this to the 11.0 milestone May 14, 2025
@haslinghuis haslinghuis self-assigned this May 14, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 14, 2025

Walkthrough

Adds Android TCP transport: new Capacitor Android plugin (BetaflightTcpPlugin), registers it in MainActivity, and exposes TCP via a new JS wrapper (CapacitorTcp) and updated protocol selection so Android supports TCP alongside serial/virtual transports.

Changes

Cohort / File(s) Summary
Android MainActivity
android/app/src/main/java/betaflight/configurator/MainActivity.java
Changed onCreate visibility to public, imported and registered BetaflightTcpPlugin alongside BetaflightSerialPlugin, and moved plugin registrations before super.onCreate(...).
Android TCP Plugin
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java
New Capacitor plugin implementing TCP: connect/send/receive(disabled)/disconnect/getStatus, thread-safe state (AtomicReference, ReentrantLock), reader thread emitting events, centralized error handling, and resource cleanup including onDestroy.
JS Capacitor TCP wrapper
src/js/protocols/CapacitorTcp.js
New JS module wrapping native plugin: Base64 helpers, connection lifecycle (connect/disconnect), send (base64 payload), event translation (dataReceived/connectionClosed), and internal counters/state.
JS protocol selection
src/js/serial.js
Import and include CapacitorTcp in Android protocol options so Android initializes both CapacitorSerial and CapacitorTcp (plus virtual) as protocol choices.

Sequence Diagram

sequenceDiagram
    participant Client as JS Client
    participant Serial as serial.js
    participant CapacitorTcp as CapacitorTcp (JS)
    participant Plugin as BetaflightTcpPlugin
    participant Socket as TCP Socket
    participant Reader as Reader Thread

    Client->>Serial: request connect via TCP
    Serial->>CapacitorTcp: createPort/connect(path)
    CapacitorTcp->>Plugin: connect(host, port)
    Plugin->>Plugin: validate, set CONNECTING
    Plugin->>Socket: open & connect (timeout)
    Socket-->>Plugin: connected
    Plugin->>Plugin: init streams, set CONNECTED, start Reader
    Plugin-->>CapacitorTcp: resolve connect
    CapacitorTcp-->>Serial: emit connect
    Serial-->>Client: connected

    Client->>Serial: send(data)
    Serial->>CapacitorTcp: send(data)
    CapacitorTcp->>Plugin: send(base64)
    Plugin->>Plugin: decode, writer lock, write to Socket
    Plugin-->>CapacitorTcp: send success
    CapacitorTcp-->>Serial: notify send complete

    Reader->>Socket: read bytes
    Socket-->>Reader: data chunk
    Reader->>Plugin: emit dataReceived(base64)
    Plugin->>CapacitorTcp: event dataReceived
    CapacitorTcp-->>Serial: forward data
    Serial-->>Client: data available

    Client->>Serial: disconnect()
    Serial->>CapacitorTcp: disconnect
    CapacitorTcp->>Plugin: disconnect()
    Plugin->>Plugin: set DISCONNECTING, close resources
    Plugin->>Plugin: set DISCONNECTED, emit connectionClosed
    Plugin-->>CapacitorTcp: resolved disconnect
    CapacitorTcp-->>Serial: emit disconnect
    Serial-->>Client: disconnected
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Focus areas:
    • Thread-safety and locking in BetaflightTcpPlugin.java
    • Reader thread lifecycle, socket close semantics, and resource cleanup (onDestroy)
    • Base64 encoding/decoding parity between Java and JS
    • Plugin registration order in MainActivity.java

Possibly related PRs

Suggested reviewers

  • nerdCopter

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: adding a custom Capacitor TCP plugin for raw TCP socket support, which aligns with all significant file additions (BetaflightTcpPlugin.java, CapacitorTcp.js) and modifications.
Description check ✅ Passed The PR description includes a detailed checklist tracking the implementation status of key objectives, with most items marked complete; however, it lacks a standard narrative explanation of the changes, their rationale, and impact.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@haslinghuis haslinghuis moved this to App in 2025.12.0 May 14, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (6)
capacitor-plugin-socket/src/definitions.ts (2)

1-6: Consider supporting binary data payloads.
Raw TCP sockets often exchange binary data. Currently send/receive use string for data. You may want to use ArrayBuffer or Uint8Array (or overloads) in the interface to handle binary payloads.


1-6: Add JSDoc comments for improved DX.
Annotating each method (connect, send, receive, disconnect) with JSDoc will surface signatures and parameter docs in editors.

capacitor-plugin-socket/src/index.ts (2)

1-1: Specify the plugin interface generic.
Use the generic on registerPlugin for full TypeScript type inference:

const SocketPlugin = registerPlugin<SocketPlugin>('SocketPlugin', {});

3-5: Add explicit iOS fallback.
Currently only web is defined. To avoid runtime errors on unsupported platforms, add an ios stub to import your web implementation:

const SocketPlugin = registerPlugin<SocketPlugin>('SocketPlugin', {
  web: () => import('./web').then(m => new m.SocketPluginWeb()),
  ios: () => import('./web').then(m => new m.SocketPluginWeb()),
});
capacitor-plugin-socket/src/web.ts (1)

5-23: Use the WebPlugin’s unimplemented helper.
Rather than console.log + default returns, call this.unimplemented() for a standardized unimplemented error:

 async connect(...) {
-  console.log('Web implementation does not support raw TCP sockets.', options);
-  return { success: false };
+  return this.unimplemented('Raw TCP sockets are not supported on web.');
 }

Apply similarly for send, receive, and disconnect.

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

1-72: Add thread safety and consider using AsyncTask or ExecutorService.

The current implementation performs network operations on the main thread and lacks thread safety:

  1. Network operations on the main UI thread can cause ANR issues
  2. No proper handling of concurrent method calls
  3. Consider using background threads for socket operations

While Capacitor's executeAsync helps with background execution, you may want to implement a more robust threading strategy. Consider:

  1. Using Android's ExecutorService for managing a thread pool
  2. Implementing proper synchronization for shared resources
  3. Adding cancellation support for long-running operations

Additionally, consider implementing timeout handling for all socket operations and add proper documentation for users of this plugin, explaining:

  • When to use SSL vs. raw sockets
  • How to handle timeouts and connection failures
  • Best practices for error handling
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cecebc and 2881743.

📒 Files selected for processing (6)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
  • capacitor-plugin-socket/package.json (1 hunks)
  • capacitor-plugin-socket/src/definitions.ts (1 hunks)
  • capacitor-plugin-socket/src/index.ts (1 hunks)
  • capacitor-plugin-socket/src/web.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
android/app/src/main/java/betaflight/configurator/MainActivity.java (1)
capacitor-plugin-socket/src/definitions.ts (1)
  • SocketPlugin (1-6)
capacitor-plugin-socket/src/index.ts (1)
capacitor-plugin-socket/src/definitions.ts (1)
  • SocketPlugin (1-6)
capacitor-plugin-socket/src/web.ts (1)
capacitor-plugin-socket/src/definitions.ts (1)
  • SocketPlugin (1-6)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)
capacitor-plugin-socket/src/definitions.ts (1)
  • SocketPlugin (1-6)
capacitor-plugin-socket/src/web.ts (4)
  • connect (5-8)
  • send (10-13)
  • receive (15-18)
  • disconnect (20-23)
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

@haslinghuis haslinghuis force-pushed the capacitor-plugin-socket branch from 2881743 to 507f32c Compare May 15, 2025 18:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (7)

19-33: Improve security and robustness in connect method.

The connect method has several issues that could lead to application crashes or security vulnerabilities:

  1. No input validation
  2. No check for existing connections
  3. Using an unencrypted socket (security concern)
  4. Operations run on the main thread (may freeze UI)
  5. No socket timeout
  6. No resource cleanup on failure
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


35-47: Add state validation to send method.

The send method should check if there's an active connection before attempting to write data. It also runs on the main thread and lacks input validation.


49-59: Potential blocking operation in receive method.

The readLine() method blocks indefinitely until data with a line terminator is available or the connection is closed, which can cause the app to freeze.


61-72: Improve resource cleanup in disconnect method.

The disconnect method only closes the socket but doesn't properly clean up all resources:

  1. Reader and writer aren't closed
  2. Class fields aren't reset
  3. No state tracking

1-72: 🛠️ Refactor suggestion

Implement a helper method for resource cleanup.

The class needs a helper method for closing resources consistently across all methods to avoid resource leaks and maintain proper state.

@PluginMethod
public void disconnect(PluginCall call) {
    try {
-        if (socket != null) socket.close();
+        closeResources();
+        isConnected = false;
        JSObject ret = new JSObject();
        ret.put("success", true);
        call.resolve(ret);
    } catch (Exception e) {
        call.reject("Disconnect failed: " + e.getMessage());
    }
}

+/**
+ * Helper method to close all resources and clean up state
+ */
+private void closeResources() {
+    try {
+        if (reader != null) {
+            reader.close();
+            reader = null;
+        }
+        if (writer != null) {
+            writer.close();
+            writer = null;
+        }
+        if (socket != null) {
+            socket.close();
+            socket = null;
+        }
+    } catch (IOException e) {
+        // Log but continue cleanup
+        Log.e("SocketPlugin", "Error closing resources", e);
+    }
+}
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


13-18: 🛠️ Refactor suggestion

Add connection state tracking.

The class lacks state management to track when a socket is connected. This is important for preventing operations on closed sockets and proper cleanup.

@CapacitorPlugin(name = "SocketPlugin")
public class SocketPlugin extends Plugin {
    private Socket socket;
    private BufferedReader reader;
    private BufferedWriter writer;
+    private boolean isConnected = false;

51-52: 🛠️ Refactor suggestion

Move network I/O off the main thread.

Reading from the socket on the main thread can cause UI freezes (ANR - Application Not Responding). Move this operation to a background thread.

@PluginMethod
public void receive(PluginCall call) {
+    // Verify connection state first
+    if (socket == null || !isConnected) {
+        call.reject("Not connected to any server");
+        return;
+    }
+    
+    // Use executeAsync to move this work off the main thread
+    executeAsync(() -> {
        try {
            String data = reader.readLine();
+            if (data == null) {
+                // Connection closed by server
+                closeResources();
+                isConnected = false;
+                call.reject("Connection closed by server");
+                return;
+            }
            JSObject ret = new JSObject();
            ret.put("data", data);
            call.resolve(ret);
        } catch (Exception e) {
+            closeResources();
+            isConnected = false;
            call.reject("Receive failed: " + e.getMessage());
        }
+    });
}
🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

1-12: Remove unused import and add class documentation.

The android.util.Log import on line 3 is not used in the code. Also, the class would benefit from proper documentation explaining its purpose and usage.

package betaflight.configurator.plugin;

-import android.util.Log;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.annotation.CapacitorPlugin;

import java.io.*;
import java.net.Socket;

+/**
+ * Capacitor plugin that provides raw TCP socket functionality.
+ * Implements methods to connect, send, receive, and disconnect from TCP servers.
+ */
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2881743 and 507f32c.

📒 Files selected for processing (6)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
  • capacitor-plugin-socket/package.json (1 hunks)
  • capacitor-plugin-socket/src/definitions.ts (1 hunks)
  • capacitor-plugin-socket/src/index.ts (1 hunks)
  • capacitor-plugin-socket/src/web.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • capacitor-plugin-socket/src/web.ts
  • capacitor-plugin-socket/package.json
  • capacitor-plugin-socket/src/index.ts
  • capacitor-plugin-socket/src/definitions.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)
capacitor-plugin-socket/src/definitions.ts (1)
  • SocketPlugin (1-6)
capacitor-plugin-socket/src/web.ts (4)
  • connect (5-8)
  • send (10-13)
  • receive (15-18)
  • disconnect (20-23)
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

23-23:

❓ Verification inconclusive

Consider using encrypted sockets for sensitive data.

The current implementation uses unencrypted sockets which could expose transmitted data to network eavesdropping. For sensitive information, consider using SSL/TLS sockets.


🏁 Script executed:

#!/bin/bash
# Check if the application is transmitting sensitive information through this socket
rg -i "password|credential|token|secret|key" --type java -C 3 android/app/src/main | grep -i "socket"

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Inspect SocketPlugin.java for socket initialization patterns
echo "=== new Socket usage ==="
grep -R "new Socket" -n android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java || echo "none found"
echo "=== SSLSocket usage ==="
grep -R "SSLSocket" -n android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java || echo "none found"
echo "=== SSLContext usage ==="
grep -R "SSLContext" -n android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java || echo "none found"

Length of output: 580


Evaluate encryption for SocketPlugin.java

The SocketPlugin currently opens an unencrypted TCP socket (java.net.Socket) on line 24 without any SSL/TLS layers. If this channel ever carries sensitive data (credentials, tokens, personal info), it would be exposed to eavesdropping.

• File: android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
– Line 24: socket = new Socket(ip, port);
– No use of SSLSocket or SSLContext detected

Please verify whether any sensitive information is sent over this socket. If so, replace Socket with an SSLSocket (or configure an SSLContext) to enable TLS encryption.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 23-23: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

@haslinghuis haslinghuis force-pushed the capacitor-plugin-socket branch from 1864ae5 to b94e7a6 Compare May 15, 2025 18:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

🧹 Nitpick comments (3)
capacitor-plugin-socket/package.json (3)

12-13: Replace placeholder author information before publishing
The "author": "Your Name" placeholder will be published to the registry as-is.

-  "author": "Your Name",
+  "author": "Betaflight Configurator Team <dev@betaflight.com>",

1-20: Add repository / bugs URLs for better traceability
Users of the plugin will not see where to report issues or view the source.

   "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/betaflight-configurator/capacitor-plugin-socket"
+  },
+  "bugs": {
+    "url": "https://github.com/betaflight-configurator/capacitor-plugin-socket/issues"
+  },

8-10: Include a clean step and ensure output goes to dist/
tsc will emit files next to sources unless outDir is configured. Consider:

   "scripts": {
-    "build": "tsc"
+    "clean": "rimraf dist",
+    "build": "npm run clean && tsc -p tsconfig.json"
   },

Also add rimraf to devDependencies if you adopt this.

🛑 Comments failed to post (5)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (5)

88-96: 🛠️ Refactor suggestion

receive() performs a blocking read on the UI thread
reader.readLine() can block indefinitely, risking ANR. Off-load to a background thread:

executeAsync(() -> {
  try {
      String data = reader.readLine();
      …
  } catch (…) { … }
});

Also verify connection state (socket == null || !isConnected), and handle null results (server closed).

🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 88 to 96, the receive() method performs a blocking read on the UI
thread using reader.readLine(), which can cause ANR issues. To fix this, wrap
the readLine() call and subsequent logic inside an executeAsync() runnable to
offload it to a background thread. Additionally, before reading, check if the
socket is null or not connected and reject the call early if so. Also handle the
case where readLine() returns null, indicating the server closed the connection,
and reject the call accordingly.

68-72: 🛠️ Refactor suggestion

send() still executed on main thread
Large writes or network stalls will freeze the UI. Similar to receive(), wrap the write in executeAsync(() -> { … }).

🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 68 to 72, the send() method is currently executed on the main
thread, which can cause UI freezes during large writes or network delays. To fix
this, wrap the send operation inside an executeAsync(() -> { ... }) block to run
it asynchronously off the main thread, similar to how receive() is handled. This
will prevent UI blocking during socket writes.

44-53: ⚠️ Potential issue

isConnected never set to true after successful connect
Because the flag remains false, every send() call will immediately reject.

             writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
+            isConnected = true;
📝 Committable suggestion

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

        try {
            socket = new Socket(ip, port);
            socket.setSoTimeout(30_000); // 30s timeout
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            isConnected = true;
            JSObject ret = new JSObject();
            ret.put("success", true);
            call.resolve(ret);
        } catch (Exception e) {
            closeResources();
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 44 to 53, the isConnected flag is not set to true after a
successful socket connection, causing all send() calls to reject immediately. To
fix this, set isConnected to true right after the socket and streams are
successfully initialized and before resolving the call, ensuring the flag
accurately reflects the connection state.

24-35: 🛠️ Refactor suggestion

Potential NPE from auto-unboxing Integer port value
call.getInt("port") returns Integer; if absent it becomes null and unboxing into int throws.

-        int port = call.getInt("port");
+        Integer port = call.getInt("port");

Then adjust subsequent comparisons (port == null || port < 1 …).

📝 Committable suggestion

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

        String ip = call.getString("ip");
-       int port = call.getInt("port");
+       Integer port = call.getInt("port");

        // Validate inputs
        if (ip == null || ip.isEmpty()) {
            call.reject("IP address is required");
            return;
        }

        if (port <= 0 || port > 65535) {
            call.reject("Invalid port number");
            return;
        }
🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 24 to 35, the code uses call.getInt("port") which returns an
Integer that may be null, causing a NullPointerException when auto-unboxed to
int. Change the port variable to Integer and check if it is null before
performing numeric comparisons. Update the validation condition to verify port
is not null and within the valid range before proceeding.

129-133: ⚠️ Potential issue

Missing Log import – compilation will fail
Log.e is referenced but android.util.Log is not imported.

+import android.util.Log;
📝 Committable suggestion

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

// at the top of android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

package betaflight.configurator.plugin;

import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.annotation.CapacitorPlugin;
// … rest of imports and class definition
🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 129 to 133, the code uses Log.e for error logging but does not
import android.util.Log, causing a compilation error. Add the import statement
for android.util.Log at the top of the file with the other imports to fix this
issue.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

44-56: 🛠️ Refactor suggestion

Blocking network I/O is still executed on the UI thread

new Socket(...) and stream construction are potentially slow and will freeze the UI, causing ANRs on older devices.
A previous review already pointed this out; consider wrapping the whole block in executeAsync(() -> { … }).

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


87-97: ⚠️ Potential issue

receive is blocking & lacks state checks

All earlier concerns remain:

  • No socket != null && isConnected check.
  • readLine() blocks indefinitely on the UI thread.
  • data == null (server closed) is not handled.

Please apply the earlier suggestion: guard state, use executeAsync, and handle EOF.

🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

112-134: UI-thread hopping for a simple log is unnecessary & getContext() may not exist

getContext().getActivity() is not part of the public Plugin API; use getActivity() directly, or just:

-        // Log but continue cleanup
-        getContext().getActivity().runOnUiThread(() -> 
-            Log.e("SocketPlugin", "Error closing resources", e));
+        // Log but continue cleanup
+        Log.e("SocketPlugin", "Error closing resources", e);

This also eliminates the need to obtain the main-thread handler.


44-46: Consider encrypted transport (SSL/TLS) when handling credentials or PII

Static analysis flags the clear-text Socket; if the plugin will ever carry sensitive data, plan an SSLSocket option or document the security trade-offs.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 1864ae5 and b94e7a6.

📒 Files selected for processing (6)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
  • capacitor-plugin-socket/package.json (1 hunks)
  • capacitor-plugin-socket/src/definitions.ts (1 hunks)
  • capacitor-plugin-socket/src/index.ts (1 hunks)
  • capacitor-plugin-socket/src/web.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • capacitor-plugin-socket/src/definitions.ts
  • capacitor-plugin-socket/src/web.ts
  • capacitor-plugin-socket/src/index.ts
  • capacitor-plugin-socket/package.json
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

@haslinghuis haslinghuis force-pushed the capacitor-plugin-socket branch 2 times, most recently from 84f4673 to acfa06d Compare May 15, 2025 19:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

38-56: 🛠️ Refactor suggestion

⚠️ Potential issue

connect never sets isConnected and still blocks the main thread

  1. isConnected is never flipped to true; every later send/receive call will be rejected.
  2. All network I/O is executed on the UI thread → risk of ANR.
-    try {
-        socket = new Socket(ip, port);
+    executeAsync(() -> {
+        try {
+            socket = new Socket(ip, port);
             socket.setSoTimeout(30_000); // 30 s timeout
             reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
+            isConnected = true;                   // 🟢 mark connected
             JSObject ret = new JSObject();
             ret.put("success", true);
-            call.resolve(ret);
-        } catch (Exception e) {
-            closeResources();
-            call.reject("Connection failed: " + e.getMessage());
-        }
+            call.resolve(ret);
+        } catch (Exception e) {
+            closeResources();
+            call.reject("Connection failed: " + e.getMessage());
+        }
+    });
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


58-85: 🛠️ Refactor suggestion

send still runs on UI thread and lacks framing / synchronization

Large writes can stall the UI and concurrent calls may corrupt the stream.

Suggested adjustments:

  1. Wrap the logic in executeAsync.
  2. synchronized(writer) to prevent concurrent writes.
  3. Append a newline or convert to bytes if the protocol is line-oriented/binary.
-    try {
-        writer.write(data);
-        writer.flush();
+    executeAsync(() -> {
+        try {
+            synchronized (writer) {
+                writer.write(data);
+                writer.flush();
+            }
+            JSObject ret = new JSObject();
+            ret.put("success", true);
+            call.resolve(ret);
+        } catch (Exception e) {
+            closeResources();
+            isConnected = false;
+            call.reject("Send failed: " + e.getMessage());
+        }
+    });
-        JSObject ret = new JSObject();
-        ret.put("success", true);
-        call.resolve(ret);
-    } catch (Exception e) {
-        closeResources();
-        isConnected = false;
-        call.reject("Send failed: " + e.getMessage());
-    }
🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

116-134: Potential NPE when getActivity() is null & missing error logging strategy

getContext().getActivity() may return null when the plugin is used without an Activity (e.g. in background). Consider using Log directly or gating the UI-thread call:

if (getContext() != null && getContext().getActivity() != null) {
    getContext().getActivity().runOnUiThread(
        () -> Log.e("SocketPlugin", "Error closing resources", e));
} else {
    Log.e("SocketPlugin", "Error closing resources", e);
}

Also consider switching wildcard java.io.* import to explicit classes for clarity.


44-46: Unencrypted socket — evaluate need for TLS

Static analysis flags clear-text transmission (new Socket(ip, port)). If the data is sensitive, migrate to SSLSocketFactory or document why plain TCP is acceptable.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🛑 Comments failed to post (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

87-97: 🛠️ Refactor suggestion

⚠️ Potential issue

receive can freeze the UI and lacks connection/state checks

readLine() blocks indefinitely on the main thread and there is no guard for disconnected sockets; on failure resources stay open.

Key fixes:

  1. Check socket != null && !socket.isClosed() && isConnected.
  2. Run the blocking read inside executeAsync.
  3. Handle null return (peer closed) and call closeResources().
🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 87 to 97, the receive method blocks the main thread by calling
readLine() directly and does not check socket connection state, risking UI
freeze and resource leaks. Fix this by first verifying socket is not null, not
closed, and connected before reading. Move the blocking readLine() call inside
an executeAsync block to avoid blocking the UI thread. Also, handle the case
where readLine() returns null (indicating the peer closed the connection) by
calling closeResources() to clean up properly.

3-10: ⚠️ Potential issue

Missing imports lead to compilation failure

@PluginMethod and Log are referenced but not imported, so the file will not compile.

 import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.PluginMethod;   // annotation in use
+
+import android.util.Log;                          // used in closeResources
📝 Committable suggestion

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

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.PluginMethod;

import android.util.Log;

import java.io.*;
import java.net.Socket;
🤖 Prompt for AI Agents
In android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
around lines 3 to 10, the imports for @PluginMethod and Log are missing, causing
compilation errors. Add the import statements for com.getcapacitor.PluginMethod
and android.util.Log to resolve the missing references and ensure the file
compiles successfully.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (6)

1-23: Refine imports and class‐level documentation
The wildcard import java.io.* can be replaced with explicit imports (BufferedReader, InputStreamReader, BufferedWriter, OutputStreamWriter, IOException) to improve readability and avoid accidental namespace collisions. Also, consider moving import android.util.Log; next to other Android imports for consistency.


24-34: Enhance parameter validation feedback
Current checks reject on missing/empty IP or invalid port, but don’t include the invalid value in the error. Consider:

-    if (ip == null || ip.isEmpty()) {
-        call.reject("IP address is required");
+    if (ip == null || ip.isEmpty()) {
+        call.reject("IP address is required: received '" + ip + "'");
         return;
     }
-    if (port <= 0 || port > 65535) {
-        call.reject("Invalid port number");
+    if (port <= 0 || port > 65535) {
+        call.reject("Invalid port number: " + port);
         return;
     }

40-47: Prevent double‐registration race
The duplicate‐connection guard checks socket.isClosed(), but doesn’t account for a connection in progress. Consider synchronizing access to socket or using an explicit lock to avoid a race condition when two connect calls happen in quick succession.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 46-46: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


46-55: Handle encrypted connections or document trust boundary
Static analysis flagged an unencrypted Socket. If this plugin may transport sensitive data, either switch to SSLSocketFactory (SSLSocket) or clearly document that it’s intended for plain‐text debug use only.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 46-46: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


61-69: Early‐return input validation is good—align formatting
The @PluginMethod annotation on send is flush‐left; indent it to match other methods for consistency.


115-137: Simplify resource cleanup logging
You post the Log.e call back on the UI thread; logging can occur on the current thread safely. You can simplify:

-    getContext().getActivity().runOnUiThread(() -> 
-        Log.e("SocketPlugin", "Error closing resources", e));
+    Log.e("SocketPlugin", "Error closing resources", e);

This avoids unnecessary UI thread marshaling.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 84f4673 and acfa06d.

📒 Files selected for processing (3)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
  • capacitor-plugin-socket/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java
  • capacitor-plugin-socket/package.json
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 46-46: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

102-113: Safe disconnect & state reset is correct
The disconnect method cleanly closes resources and flips isConnected. Good use of the helper.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

100-115: Consider non-blocking read alternatives.

The readLine() call will block indefinitely if no newline character is received, which could lead to resource leaks if the connection stays idle.

Consider implementing a timeout mechanism or alternative read approach:

 getBridge().getExecutor().execute(() -> {
     try {
-        String data = reader.readLine();
+        // Option 1: Use socket timeout already set
+        String data = reader.readLine();
+        if (data == null) {
+            call.reject("Connection closed by server");
+            closeResources();
+            isConnected = false;
+            return;
+        }
         
         // Option 2: Alternative - read with fixed buffer size
         // char[] buffer = new char[8192];
         // int length = reader.read(buffer, 0, buffer.length);
         // String data = new String(buffer, 0, length);
         
         JSObject ret = new JSObject();
         ret.put("data", data);
         call.resolve(ret);
     } catch (Exception e) {
         closeResources();
         isConnected = false;
         call.reject("Receive failed: " + e.getMessage());
     }
 });
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between acfa06d and e337188.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: deploy
🔇 Additional comments (6)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (6)

1-16: Well-structured package and imports with clear documentation.

The package structure and imports are appropriate for a Capacitor plugin. The class documentation clearly explains the plugin's purpose, which is good practice.


17-22: Good class structure with proper field initialization.

The class is correctly annotated with @CapacitorPlugin and extends Plugin as required. The instance variables for socket, reader, writer, and connection state are appropriately defined.


64-98: Well-designed send method with thread safety.

The send method includes proper input validation, connection state checking, and runs write operations on a background thread. The synchronization on the writer prevents concurrent modification issues, and appending a newline for framing is appropriate for text-based protocols.


117-128: Well-implemented disconnect method.

The disconnect method properly cleans up resources and maintains state consistency. The error handling is appropriate.


130-152: Well-designed resource cleanup helper.

The closeResources method thoroughly cleans up all open resources in the correct order, nullifies references, and handles exceptions properly. Using the UI thread for logging is appropriate.


24-62:

❓ Verification inconclusive

Well-implemented connect method with security considerations.

The connect method has proper input validation, duplicate connection prevention, and runs network operations on a background thread. Setting a timeout is a good practice.

However, the socket communication is not encrypted, which could be a security concern if sensitive data is transmitted.

Consider using an SSLSocket for secure communication if sensitive data will be transmitted. If encryption is not required for your specific use case, add a comment explaining why.


🏁 Script executed:

#!/bin/bash
# Check if this app handles sensitive data elsewhere that might require encryption
rg -A 2 "password|token|credential|api_key" --type java

Length of output: 57


Verify encryption requirements for socket communication

No references to passwords, tokens, credentials, or API keys were found in the Java codebase. If you are confident that only non-sensitive data is sent over this socket, please add a comment in SocketPlugin.java explaining why unencrypted communication is acceptable. If there is any chance you’ll transmit credentials or other sensitive information in the future, switch to an SSLSocket to ensure data is encrypted in transit.

• File to update:

  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (in the connect method)
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

110-118: 🛠️ Refactor suggestion

Inconsistent error handling in receive method.

Unlike the send method, the receive method doesn't clean up resources or update connection state when an error occurs. This could lead to resource leaks and inconsistent state.

            } catch (Exception e) {
+                closeResources();
+                isConnected = false;
                call.reject("Receive failed: " + e.getMessage());
            }
🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

3-3: Unused import that should be removed.

The android.util.Log import is currently only used in the closeResources method for error logging. If that's the only usage, it's fine to keep, but if you're not going to use it elsewhere, consider using a more Capacitor-specific logging approach.

-import android.util.Log;

122-132: Consider running disconnect on a background thread.

Although disconnect operations are typically quick, they can occasionally block. For consistency with other methods and to avoid potential UI freezes, consider running the disconnect operation on a background thread.

    @PluginMethod
    public void disconnect(PluginCall call) {
-        try {
-            closeResources();
-            isConnected = false;
-            JSObject ret = new JSObject();
-            ret.put("success", true);
-            call.resolve(ret);
-        } catch (Exception e) {
-            call.reject("Disconnect failed: " + e.getMessage());
-        }
+        getBridge().getExecutor().execute(() -> {
+            try {
+                closeResources();
+                isConnected = false;
+                JSObject ret = new JSObject();
+                ret.put("success", true);
+                call.resolve(ret);
+            } catch (Exception e) {
+                call.reject("Disconnect failed: " + e.getMessage());
+            }
+        });
    }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between e337188 and bf1924a.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

1-157: Overall implementation looks solid with good error handling and background threading.

The plugin follows Capacitor's conventions correctly and implements proper validation, background threading, and resource management. The synchronization for writes is a good practice to prevent race conditions. The code has addressed previous review comments effectively.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


46-61:

❓ Verification inconclusive

Consider using encrypted socket for sensitive communications.

The current implementation uses a plain Socket which transmits data in cleartext. If this plugin will handle sensitive information, consider using SSLSocket instead.


🏁 Script executed:

#!/bin/bash
# Check if this app transmits sensitive information that would require encryption
# Look for any existing SSL/TLS usage in the codebase
rg -A 2 "SSLSocket|SSLSocketFactory|TLS|SSL" --type java

Length of output: 58


Evaluate data sensitivity and add TLS if needed

I ran a search across the Java code and didn’t find any existing use of SSLSocket, SSLSocketFactory, TLS, or SSL. The current implementation in SocketPlugin.java (lines 46–61) uses a plain Socket, which sends everything in cleartext.

If this plugin ever handles sensitive or security-critical data (e.g. credentials, configuration parameters that must be protected, or commands over untrusted networks), please switch to an encrypted socket (for example, via SSLSocketFactory/SSLContext) and validate certificates to ensure confidentiality and integrity.

• Affected file:
• android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (lines 46–61)

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

122-124: 🛠️ Refactor suggestion

Ensure resources are cleaned up on receive failure
Mirror the send error path by closing resources and resetting isConnected when receive throws an exception:

- } catch (Exception e) {
-     call.reject("Receive failed: " + e.getMessage());
- }
+ } catch (Exception e) {
+     closeResources();
+     isConnected = false;
+     call.reject("Receive failed: " + e.getMessage());
+ }
🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

49-50: Consider supporting encrypted connections
The plugin currently uses an unencrypted Socket for raw TCP. For production or sensitive data, consider allowing an SSLSocket via SSLSocketFactory, or clearly document that this is intended only for trusted networks.


159-163: Use a static TAG constant for logging and guard against null activity
Extract the log tag into private static final String TAG = "SocketPlugin" and check getContext()/getActivity() before runOnUiThread to avoid possible NPEs:

+ private static final String TAG = "SocketPlugin";
...
- getContext().getActivity().runOnUiThread(() ->
-     Log.e("SocketPlugin", "Error closing resources", e));
+ if (getContext() != null && getContext().getActivity() != null) {
+     getContext().getActivity().runOnUiThread(() ->
+         Log.e(TAG, "Error closing resources", e));
+ }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between bf1924a and cfefb6a.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 48-48: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (4)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (4)

53-60: Verify thread‐safety of call.resolve()/call.reject()
Resolving or rejecting PluginCall instances from a background executor may have threading implications within the Capacitor runtime. Confirm these calls are safe off the main thread, or alternatively dispatch them back to the UI thread:

getBridge().getExecutor().execute(() -> {
    // ...
    call.resolve(ret);
    // or
    getActivity().runOnUiThread(() -> call.resolve(ret));
});

65-72: Solid input validation in send
The null-check for data correctly guards against missing payloads before attempting to write. Nice work.


80-88: Good use of background thread and synchronization in send
Offloading the write and flush to the executor and synchronizing on writer prevents UI blocking and race conditions.


129-136: Effective cleanup and state reset in disconnect
Calling closeResources() and resetting the isConnected flag ensures no sockets or streams linger after disconnect.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

1-166: 💡 Verification agent

🧩 Analysis chain

Consider using encrypted socket communication for sensitive data.

The current implementation uses an unencrypted Socket, which could allow traffic interception by malicious actors.


🌐 Web query:

When should I use SSLSocket instead of Socket in Java?

💡 Result:

You should use SSLSocket instead of Socket in Java when transferring sensitive data over insecure communication channels. The SSLSocket class provides critical security protections that regular Socket does not offer, making it essential for secure communications.

Security Protections Provided by SSLSocket

SSLSocket extends the basic Socket class but adds several important security layers through protocols like Secure Sockets Layer (SSL) and Transport Layer Security (TLS)[1][2]. These additional protections include:

Integrity Protection: SSL prevents modification of messages by active attackers, ensuring that data cannot be tampered with during transmission[1].

Authentication: SSLSocket provides peer authentication capabilities. In most implementations, servers are always authenticated, and clients can be authenticated when requested by servers[1][4].

Confidentiality: SSLSocket encrypts data being sent between client and server, protecting sensitive information such as financial data or personal information from eavesdropping by passive wiretappers[1].

Specific Use Cases

You should use SSLSocket rather than Socket in the following scenarios:

  1. When handling sensitive data: Any application that deals with personal information, financial data, login credentials, or other confidential information[1].

  2. For secure Remote Method Invocation (RMI): When using Java RMI, which depends on object serialization, serialized data must be protected in transit[1].

  3. When communicating across untrusted networks: Any data transmitted over public networks like the internet should use secure sockets[1][5].

  4. For compliance requirements: Many regulatory standards require secure communications for sensitive data transfers[1].

Implementation Comparison

Implementing SSLSocket involves a few more steps than using a regular Socket, but the security benefits are substantial. A basic SSLSocket implementation requires:

  1. Initializing an SSLContext
  2. Creating an SSLSocketFactory from the context
  3. Using the factory to create an SSLSocket
  4. Starting the SSL handshake process[5][6]

For example, client-side implementation requires:

// Initialize SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

// Create SSLSocket
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket("localhost", 8443);

// Start SSL handshake
sslSocket.startHandshake();

While SSLSocket adds security, it's worth noting that it may introduce some performance overhead compared to plain Socket connections[3]. However, this trade-off is necessary and justified when security is a requirement.

Citations:


Switch to SSLSocket for secure communications

The plugin currently opens a plain Socket, which sends all data—including potentially sensitive payloads—in clear text over the network. To prevent eavesdropping, tampering, and meet common compliance requirements, you should use Java’s SSLSocket (TLS) instead:

• File: android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
• Method: connect(PluginCall)

Example refactor for connect(...):

- socket = new Socket(ip, port);
+ // Initialize SSL context (e.g. TLSv1.2+)
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, /* custom TrustManagers if needed */ null, null);
+ SSLSocketFactory factory = sslContext.getSocketFactory();
+ SSLSocket sslSocket = (SSLSocket) factory.createSocket(ip, port);
+ sslSocket.startHandshake();
+ socket = sslSocket;

Next steps:

  • Configure a TrustManager/KeyManager if you need custom certificates or mutual auth.
  • Verify hostname on the server certificate to avoid MITM attacks.
  • Adjust timeouts and cipher suites as required by your security policy.
🧰 Tools
🪛 ast-grep (0.31.1)

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

86-88: Consider making newline behavior configurable.

The current implementation always appends a newline character after the data, which assumes a line-based protocol. This might not be appropriate for all TCP protocols.

                synchronized (writer) {
-                    // Append newline for framing; adjust as needed for your protocol
-                    writer.write(data);
-                    writer.newLine();
+                    // Allow configurable newline behavior
+                    boolean appendNewline = call.getBoolean("appendNewline", true);
+                    writer.write(data);
+                    if (appendNewline) {
+                        writer.newLine();
+                    }
                    writer.flush();
                }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between cfefb6a and 39715ab.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: deploy

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (3)

131-142: Consider executing disconnect on a background thread.

While the disconnect implementation correctly cleans up resources, it runs on the calling thread which is likely the UI thread. Socket closure can be blocking.

 @PluginMethod
 public void disconnect(PluginCall call) {
-    try {
-        closeResources();
-        isConnected = false;
-        JSObject ret = new JSObject();
-        ret.put("success", true);
-        call.resolve(ret);
-    } catch (Exception e) {
-        call.reject("Disconnect failed: " + e.getMessage());
-    }
+    getBridge().getExecutor().execute(() -> {
+        try {
+            closeResources();
+            isConnected = false;
+            JSObject ret = new JSObject();
+            ret.put("success", true);
+            call.resolve(ret);
+        } catch (Exception e) {
+            call.reject("Disconnect failed: " + e.getMessage());
+        }
+    });
 }

144-167: Thorough resource cleanup implementation.

The closeResources method properly:

  • Closes all resources in the correct order
  • Nullifies references to prevent memory leaks
  • Updates connection state on failures
  • Handles exceptions gracefully with logging
  • Runs logging on the UI thread

Consider adding a writer.flush() before closing to ensure all buffered data is sent.

         if (writer != null) {
+            writer.flush(); // Ensure all buffered data is sent
             writer.close();
             writer = null;
         }

24-63:

❓ Verification inconclusive

Consider using encrypted socket for sensitive data.

The implementation correctly validates inputs, prevents duplicate connections, and moves network operations to a background thread. The timeout setting is a good practice.

However, the code uses an unencrypted socket which could expose transmitted data to interception.

If your application transmits sensitive data, consider using SSLSocket instead:

-                socket = new Socket(ip, port);
+                SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+                socket = (SSLSocket) factory.createSocket(ip, port);

This would require adding these imports:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

🏁 Script executed:

#!/bin/bash
# Description: Check if secure communications are required elsewhere in the app

# Check for SSL/TLS usage
rg -A 2 "SSLSocket|SSLSocketFactory|TrustManager|KeyStore" --type java

# Check for sensitive data patterns
rg -A 2 "password|credential|token|key|secret|auth" --type java

Length of output: 138


Verify encryption requirements for Socket connections

File: android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java
Lines: 24-63

Ripgrep across the Java codebase didn’t detect any existing SSL/TLS usage (SSLSocketFactory, TrustManager, KeyStore, etc.) or common sensitive-data patterns (password, token, key, secret). Absence of evidence isn’t evidence of absence—if this plugin ever carries credentials or other sensitive information, you should consider switching to an SSLSocket:

-                socket = new Socket(ip, port);
+                SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+                socket = (SSLSocket) factory.createSocket(ip, port);

And add these imports:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

Please verify whether encrypted communication is required for your use case.

🧰 Tools
🪛 ast-grep (0.31.1)

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 39715ab and e777360.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.31.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (4)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (4)

1-16: Well-structured package declarations and class documentation.

The class is properly organized with appropriate imports and clear documentation that explains the plugin's purpose. The Capacitor plugin annotation is correctly set up.


19-22: Good field declarations with proper connection state tracking.

The class fields are well-defined, including the boolean flag to track connection state. This will help prevent operations on closed sockets.


65-99: Thread-safe send implementation with proper validation.

The send method implementation is robust with:

  • Input validation
  • Connection state verification
  • Background thread execution
  • Synchronized writer access
  • Protocol framing with newline
  • Proper error handling and resource cleanup

This addresses all the concerns from previous reviews.


101-129: Excellent non-blocking receive implementation with peer disconnection handling.

The receive method correctly:

  • Validates connection state before proceeding
  • Executes read operations on a background thread
  • Handles the case when a peer closes the connection (null readLine return)
  • Properly manages resources on exceptions
  • Returns data in the correct format

This addresses all previous concerns about blocking reads and error handling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (3)

49-54: Raw TCP is sent in cleartext – evaluate TLS or document threat model

Static analysis flags the plain new Socket(ip, port) instantiation. If credentials or other sensitive data traverse this channel, consider upgrading to SSLSocketFactory or at least documenting why encryption is unnecessary in your context (e.g. LAN-only diagnostics).

🧰 Tools
🪛 ast-grep (0.38.1)

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


150-160: flush() in cleanup can block – usually unnecessary before close()

BufferedWriter.close() already performs an implicit flush. Calling flush() first can add latency or block during error paths (e.g. lost network). You can safely drop it and rely on close().


86-88: Assumed protocol framing – maybe make configurable

Appending newLine() hard-codes a text-based framing scheme. If binary protocols or CRLF expectations arise, consider exposing a boolean/enum option (e.g. terminator) so callers can pick the delimiter, or document the newline requirement clearly.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between e777360 and 30e43e2.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.38.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

30-40: Input validation looks solid

The guard clauses for IP emptiness and port range prevent common misuse and avoid NullPointerException from unboxing. Nicely done.

@haslinghuis haslinghuis force-pushed the capacitor-plugin-socket branch from 30e43e2 to 6e8146c Compare May 16, 2025 15:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (3)

25-63: Consider encrypted transport or document security implications
The connect method creates a raw Socket, which is unencrypted by default (static analysis flagged CWE-319). If you intend to transmit sensitive information, integrate SSLSocketFactory to enable TLS or clearly document that this plugin provides unauthenticated plain-text TCP. Providing an option to configure SSL/TLS will make this plugin more versatile and secure.

🧰 Tools
🪛 ast-grep (0.38.1)

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


105-133: Capture reader locally to avoid race conditions
You check reader != null before scheduling the background task, but you don't capture it inside the executor. If disconnect races in before the task runs, reader may be null and trigger an NPE. Mirror the send approach by doing:

BufferedReader localReader = reader;
if (localReader == null) {
  call.reject("Connection lost");
  return;
}
String data = localReader.readLine();

153-174: closeResources cleanup looks robust
The helper neatly closes streams, nullifies references, logs errors on the UI thread, and resets isConnected. As a minor nitpick, you can remove the explicit writer.flush()close() will flush internally—or separate its exception handling if you want to log flush-specific failures.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 30e43e2 and 6e8146c.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.38.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 49-49: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket(ip, port)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: deploy
🔇 Additional comments (2)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (2)

65-103: send method correctly offloads work and guards state
Great use of getBridge().getExecutor() to avoid ANRs, input validation, capture of writer, synchronized writes, and cleanup on error. Just verify in your Capacitor version that invoking call.resolve/call.reject from a background thread is safe.


135-147: Verify background-thread context for disconnect callbacks
disconnect offloads cleanup to the executor, which is good, but now the call.resolve fires off the background thread. Ensure that downstream JavaScript consumers don’t rely on callbacks coming from the UI thread or expect immediate synchronous cleanup.

@haslinghuis
Copy link
Member Author

@coderabbitio We are stuck in an endless review loop here. All changes made are made on your requests, but keep getting more requests after that for the same file. Please provide a full refactor and working solution for android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 16, 2025

Tip

For best results, initiate chat on the files or code changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 16, 2025

A summary of the edits CodeRabbit can apply:

We will fully refactor SocketPlugin.java to improve thread safety, resource management, error handling, and overall code structure.

A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):

✏️ android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

Replace the entire contents of SocketPlugin.java with the following improved implementation:

package betaflight.configurator.plugin;

import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Capacitor plugin that provides raw TCP socket functionality with thread safety,
 * robust resource management, and comprehensive error handling.
 */
@CapacitorPlugin(name = "SocketPlugin")
public class SocketPlugin extends Plugin {
    private static final String TAG = "SocketPlugin";

    // Error messages
    private static final String ERROR_IP_REQUIRED = "IP address is required";
    private static final String ERROR_INVALID_PORT = "Invalid port number";
    private static final String ERROR_ALREADY_CONNECTED = "Already connected; please disconnect first";
    private static final String ERROR_NOT_CONNECTED = "Not connected to any server";
    private static final String ERROR_DATA_REQUIRED = "Data is required";
    private static final String ERROR_CONNECTION_LOST = "Connection lost";
    private static final String ERROR_CONNECTION_CLOSED = "Connection closed by peer";

    // Connection settings
    private static final int DEFAULT_TIMEOUT_MS = 30_000;
    private static final int MIN_PORT = 1;
    private static final int MAX_PORT = 65535;

    private enum ConnectionState {
        DISCONNECTED,
        CONNECTING,
        CONNECTED,
        DISCONNECTING,
        ERROR
    }

    // Thread-safe state and locks
    private final AtomicReference<ConnectionState> state = new AtomicReference<>(ConnectionState.DISCONNECTED);
    private final ReentrantLock socketLock = new ReentrantLock();
    private final ReentrantLock writerLock = new ReentrantLock();

    private Socket socket;
    private BufferedReader reader;
    private BufferedWriter writer;

    @PluginMethod
    public void connect(final PluginCall call) {
        call.setKeepAlive(true);
        String ip = call.getString("ip");
        int port = call.getInt("port", -1);

        if (ip == null || ip.isEmpty()) {
            call.reject(ERROR_IP_REQUIRED);
            call.setKeepAlive(false);
            return;
        }
        if (port < MIN_PORT || port > MAX_PORT) {
            call.reject(ERROR_INVALID_PORT);
            call.setKeepAlive(false);
            return;
        }
        if (!compareAndSetState(ConnectionState.DISCONNECTED, ConnectionState.CONNECTING)) {
            call.reject(ERROR_ALREADY_CONNECTED);
            call.setKeepAlive(false);
            return;
        }

        getBridge().getExecutor().execute(() -> {
            socketLock.lock();
            try {
                socket = new Socket();
                socket.connect(new InetSocketAddress(ip, port), DEFAULT_TIMEOUT_MS);
                socket.setSoTimeout(DEFAULT_TIMEOUT_MS);

                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

                state.set(ConnectionState.CONNECTED);
                JSObject result = new JSObject();
                result.put("success", true);
                call.resolve(result);
                Log.d(TAG, "Connected to " + ip + ":" + port);
            } catch (Exception e) {
                state.set(ConnectionState.ERROR);
                closeResourcesInternal();
                call.reject("Connection failed: " + e.getMessage());
                Log.e(TAG, "Connection failed", e);
            } finally {
                socketLock.unlock();
                call.setKeepAlive(false);
            }
        });
    }

    @PluginMethod
    public void send(final PluginCall call) {
        String data = call.getString("data");
        if (data == null || data.isEmpty()) {
            call.reject(ERROR_DATA_REQUIRED);
            return;
        }
        if (state.get() != ConnectionState.CONNECTED) {
            call.reject(ERROR_NOT_CONNECTED);
            return;
        }
        call.setKeepAlive(true);

        getBridge().getExecutor().execute(() -> {
            writerLock.lock();
            try {
                if (writer == null || state.get() != ConnectionState.CONNECTED) {
                    call.reject(ERROR_CONNECTION_LOST);
                    return;
                }
                writer.write(data);
                writer.newLine();
                writer.flush();

                JSObject result = new JSObject();
                result.put("success", true);
                call.resolve(result);
                Log.d(TAG, "Sent data: " + truncateForLog(data));
            } catch (Exception e) {
                handleCommunicationError(e, "Send failed", call);
            } finally {
                writerLock.unlock();
                call.setKeepAlive(false);
            }
        });
    }

    @PluginMethod
    public void receive(final PluginCall call) {
        if (state.get() != ConnectionState.CONNECTED || reader == null) {
            call.reject(ERROR_NOT_CONNECTED);
            return;
        }
        call.setKeepAlive(true);

        getBridge().getExecutor().execute(() -> {
            try {
                String data = reader.readLine();
                if (data == null) {
                    handleCommunicationError(new IOException("End of stream"), ERROR_CONNECTION_CLOSED, call);
                    return;
                }
                JSObject result = new JSObject();
                result.put("data", data);
                call.resolve(result);
                Log.d(TAG, "Received data: " + truncateForLog(data));
            } catch (Exception e) {
                handleCommunicationError(e, "Receive failed", call);
            } finally {
                call.setKeepAlive(false);
            }
        });
    }

    @PluginMethod
    public void disconnect(final PluginCall call) {
        ConnectionState current = state.get();
        if (current == ConnectionState.DISCONNECTED) {
            JSObject result = new JSObject();
            result.put("success", true);
            call.resolve(result);
            return;
        }
        if (!compareAndSetState(current, ConnectionState.DISCONNECTING)) {
            call.reject("Invalid state for disconnect: " + current);
            return;
        }
        call.setKeepAlive(true);

        getBridge().getExecutor().execute(() -> {
            socketLock.lock();
            try {
                closeResourcesInternal();
                state.set(ConnectionState.DISCONNECTED);
                JSObject result = new JSObject();
                result.put("success", true);
                call.resolve(result);
                Log.d(TAG, "Disconnected successfully");
            } catch (Exception e) {
                state.set(ConnectionState.ERROR);
                call.reject("Disconnect failed: " + e.getMessage());
                Log.e(TAG, "Disconnect failed", e);
            } finally {
                socketLock.unlock();
                call.setKeepAlive(false);
            }
        });
    }

    @Override
    protected void handleOnDestroy() {
        socketLock.lock();
        try {
            state.set(ConnectionState.DISCONNECTING);
            closeResourcesInternal();
            state.set(ConnectionState.DISCONNECTED);
        } catch (Exception e) {
            Log.e(TAG, "Error cleaning up resources on destroy", e);
        } finally {
            socketLock.unlock();
        }
        super.handleOnDestroy();
    }

    private void closeResourcesInternal() {
        if (reader != null) {
            try { reader.close(); } catch (IOException e) { Log.e(TAG, "Error closing reader", e);} finally { reader = null; }
        }
        if (writer != null) {
            try { writer.flush(); writer.close(); } catch (IOException e) { Log.e(TAG, "Error closing writer", e);} finally { writer = null; }
        }
        if (socket != null) {
            try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e);} finally { socket = null; }
        }
    }

    private void handleCommunicationError(Exception error, String message, PluginCall call) {
        socketLock.lock();
        try {
            state.set(ConnectionState.ERROR);
            closeResourcesInternal();
            state.set(ConnectionState.DISCONNECTED);
            call.reject(message + ": " + error.getMessage());
            Log.e(TAG, message, error);
        } finally {
            socketLock.unlock();
        }
    }

    private boolean compareAndSetState(ConnectionState expected, ConnectionState newState) {
        return state.compareAndSet(expected, newState);
    }

    private String truncateForLog(String data) {
        if (data == null) return "null";
        final int maxLen = 100;
        if (data.length() <= maxLen) return data;
        return data.substring(0, maxLen) + "... (" + data.length() + " chars)";
    }
}

Build the Android project, deploy the Capacitor app to a device or emulator, and exercise the SocketPlugin by calling its connect, send, receive, and disconnect methods. Verify that operations succeed, errors are handled gracefully, logs are produced as expected, and resources are cleaned up on disconnect or app termination.

A summary of the context that CodeRabbit is considering across the codebase:

Ensure that java.util.concurrent.atomic.AtomicReference and java.util.concurrent.locks.ReentrantLock are available for thread-safe state management.

  • Created local commit - (🔄 Check again to try again)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 16, 2025

Caution

CodeRabbit has failed to create a local commit with your edits.

1 similar comment
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 16, 2025

Caution

CodeRabbit has failed to create a local commit with your edits.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1)

221-231: Consider separating resource closing into individual methods.

While the current implementation works well, having individual methods for closing each resource type (reader, writer, socket) could improve readability and maintainability.

- private void closeResourcesInternal() {
-     if (reader != null) {
-         try { reader.close(); } catch (IOException e) { Log.e(TAG, "Error closing reader", e);} finally { reader = null; }
-     }
-     if (writer != null) {
-         try { writer.flush(); writer.close(); } catch (IOException e) { Log.e(TAG, "Error closing writer", e);} finally { writer = null; }
-     }
-     if (socket != null) {
-         try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e);} finally { socket = null; }
-     }
- }
+ private void closeResourcesInternal() {
+     closeReader();
+     closeWriter();
+     closeSocket();
+ }
+
+ private void closeReader() {
+     if (reader != null) {
+         try { 
+             reader.close(); 
+         } catch (IOException e) { 
+             Log.e(TAG, "Error closing reader", e);
+         } finally { 
+             reader = null; 
+         }
+     }
+ }
+
+ private void closeWriter() {
+     if (writer != null) {
+         try { 
+             writer.flush(); 
+             writer.close(); 
+         } catch (IOException e) { 
+             Log.e(TAG, "Error closing writer", e);
+         } finally { 
+             writer = null; 
+         }
+     }
+ }
+
+ private void closeSocket() {
+     if (socket != null) {
+         try { 
+             socket.close(); 
+         } catch (IOException e) { 
+             Log.e(TAG, "Error closing socket", e);
+         } finally { 
+             socket = null; 
+         }
+     }
+ }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6e8146c and 7c25def.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (1 hunks)
🧰 Additional context used
🪛 ast-grep (0.38.1)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java

[info] 82-82: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (9)
android/app/src/main/java/betaflight/configurator/plugin/SocketPlugin.java (9)

19-24: Well-designed Capacitor plugin structure with clear documentation.

The class is properly annotated with @CapacitorPlugin and has clear documentation explaining its purpose and features. The plugin name matches the class name, making it consistent and easy to understand.


27-40: Excellent use of constants for error messages and settings.

Using string constants for error messages enhances maintainability and consistency throughout the codebase. The connection settings constants are also well-defined with appropriate names and values.


41-56: Good use of enum and thread-safe fields.

The ConnectionState enum provides a clear state model, and the thread-safety approach using AtomicReference and ReentrantLock is appropriate for a multi-threaded environment where socket operations run on background threads.


107-142: Good implementation of asynchronous send operation with thread safety.

The method correctly validates input, checks connection state, and performs I/O operations asynchronously on a background thread with proper synchronization and error handling. The use of truncateForLog is a nice touch to prevent log pollution.


144-169: Appropriately handling null response in receive method.

The receive method correctly detects when the connection is closed by the peer (when readLine() returns null) and handles this case properly by transitioning to an error state and cleaning up resources.


171-204: Robust disconnect implementation with proper state management.

The disconnect method is well-implemented with state validation, thread safety, and proper resource cleanup. It also handles the case where the socket is already disconnected, avoiding unnecessary operations.


206-219: Excellent resource cleanup in handleOnDestroy.

The override of handleOnDestroy ensures that resources are properly cleaned up when the plugin is destroyed, preventing resource leaks. The use of locks ensures that cleanup operations don't conflict with ongoing operations.


233-244: Good centralized error handling approach.

The handleCommunicationError method centralizes error handling logic, making the code more maintainable and consistent. It appropriately transitions state, cleans up resources, and logs errors.


246-255: Well-implemented helper methods.

The helper methods for state comparison and log truncation are well-implemented. The truncateForLog method is a particularly nice touch that prevents log pollution when dealing with large data payloads.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

91-97: createUnresolved with port 0 will fail on socket.connect().

When port == -1, you create an unresolved address with port 0. However, InetSocketAddress.createUnresolved() creates an address that cannot be resolved, and calling socket.connect() on it will throw an exception. If port is truly optional, provide a sensible default or reject the call.

-                if (port != -1) {
-                    address = new InetSocketAddress(ip, port);
-                } else {
-                    address = InetSocketAddress.createUnresolved(ip, 0);
-                }
+                if (port == -1) {
+                    throw new IllegalArgumentException("Port is required for TCP connection");
+                }
+                address = new InetSocketAddress(ip, port);
🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (2)

10-10: Unused import.

ByteArrayOutputStream is imported but never used in the code.

-import java.io.ByteArrayOutputStream;

159-165: Unused JSObject creation.

The result object is created and populated but never used since the method always calls reject(). Remove the dead code.

 @PluginMethod
 public void receive(final PluginCall call) {
-    // Deprecated by continuous reader (Task 2)
-    JSObject result = new JSObject();
-    result.put("data", "");
     call.reject("Continuous read active. Listen for 'dataReceived' events instead.");
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 999fd45 and a308c1b.

📒 Files selected for processing (2)
  • android/app/src/main/java/betaflight/configurator/MainActivity.java (1 hunks)
  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
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.
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: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: blckmn
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-09-01T11:01:38.695Z
Learning: The Betaflight Configurator implements analytics through src/js/Analytics.js with the URL "https://analytics.betaflight.com" at line 68, which serves as a proxy frontend for the actual telemetry provider (currently Telemetry Deck), allowing vendor flexibility without code changes.
Learnt from: blckmn
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-09-01T11:01:38.695Z
Learning: In the Betaflight Configurator project, telemetry analytics (including Telemetry Deck) is implemented through a proxy server at analytics.betaflight.com, which allows the team to swap out telemetry vendors without changing application code.
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: 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: blckmn
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-09-01T11:02:00.555Z
Learning: In Betaflight Configurator, Telemetry Deck analytics is implemented through the analytics.betaflight.com proxy server, giving the team control over data anonymization and the ability to swap telemetry vendors without changing application code.
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-05-14T21:39:16.388Z
Learning: On Android Chrome browsers, Web Bluetooth API is supported from version 133+ (requires Android 6.0+) and Web USB API is supported from version 136+, but Web Serial API is not supported. The Betaflight Configurator should detect and use available APIs on Android rather than requiring all three.
📚 Learning: 2025-12-02T17:46:47.828Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.828Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

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

Applied to files:

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

Applied to files:

  • android/app/src/main/java/betaflight/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
🪛 ast-grep (0.40.0)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java

[info] 89-89: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

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

10-15: LGTM!

Plugin registration is correctly implemented. Registering plugins before super.onCreate() is the proper pattern for Capacitor, and both BetaflightSerialPlugin and BetaflightTcpPlugin are now properly registered.

android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (6)

43-60: Solid thread-safety design.

Good use of AtomicReference<ConnectionState> for lock-free state transitions, ReentrantLock for protecting socket/writer operations, and volatile for the reader control flag. This provides proper synchronization for the multi-threaded socket operations.


122-157: LGTM!

The send() method has proper input validation, state checking, thread-safe locking, and delegates error handling to the centralized handleCommunicationError() method.


167-208: LGTM!

The disconnect() method properly handles the already-disconnected case, uses atomic state transitions, and correctly resets state to DISCONNECTED after error handling (line 202).


210-231: LGTM!

getStatus() provides useful connection state information, and handleOnDestroy() ensures proper resource cleanup with appropriate locking.


233-267: LGTM!

The reader thread implementation is well-structured with proper loop conditions, Base64 encoding for binary data, and appropriate event notifications. The named thread ("SocketReaderThread") aids debugging.


302-324: LGTM!

handleCommunicationError() properly handles the null PluginCall case (from background reader thread) while still performing cleanup and logging. The centralized error handling ensures consistent state management.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (1)

287-292: Consider splitting resource cleanup for readability.

The cleanup logic is correct, but the single-line format makes it harder to read and debug. This is a minor style suggestion.

     private void closeResourcesInternal() {
         stopReaderThread();
-        if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, "Error closing input stream", e); } finally { input = null; } }
-        if (output != null) { try { output.close(); } catch (IOException e) { Log.e(TAG, "Error closing output stream", e); } finally { output = null; } }
-        if (socket != null) { try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e); } finally { socket = null; } }
+        if (input != null) {
+            try { input.close(); } catch (IOException e) { Log.e(TAG, "Error closing input stream", e); }
+            input = null;
+        }
+        if (output != null) {
+            try { output.close(); } catch (IOException e) { Log.e(TAG, "Error closing output stream", e); }
+            output = null;
+        }
+        if (socket != null) {
+            try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e); }
+            socket = null;
+        }
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a308c1b and 8fa9a5b.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-12-02T17:46:47.828Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.828Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

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

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java
📚 Learning: 2025-06-09T00:32:21.385Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:32:21.385Z
Learning: In the betaflight-configurator codebase, port paths use counter prefixes (e.g., "bluetooth1", "bluetooth2", "serial1") rather than direct protocol identifiers. The protocol selection logic correctly uses `portPath.startsWith("bluetooth")` to detect bluetooth ports regardless of the counter suffix, rather than direct string matching against protocol map keys.

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.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/tcp/BetaflightTcpPlugin.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/tcp/BetaflightTcpPlugin.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/tcp/BetaflightTcpPlugin.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/tcp/BetaflightTcpPlugin.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/tcp/BetaflightTcpPlugin.java
🪛 ast-grep (0.40.0)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java

[info] 85-85: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

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

21-60: Well-structured class design with proper thread-safety primitives.

The use of AtomicReference<ConnectionState> for state management combined with separate ReentrantLock instances for socket and writer operations provides good concurrency control. The constants for error messages and settings enhance maintainability.


62-112: Solid connect() implementation with proper state management.

The atomic state transition, timeout configuration, and error recovery flow (including the ERROR → DISCONNECTED reset after failure) are correctly implemented. The background thread execution via new Thread(...).start() aligns with the Capacitor 7 API requirements noted in the PR discussion.


151-157: Clear deprecation of synchronous receive in favor of event-based approach.

The receive() method correctly informs callers to use the dataReceived event listener pattern, consistent with the continuous reading architecture.


159-200: Robust disconnect() with proper cleanup and state recovery.

The idempotent handling for already-disconnected state, nested try-catch for cleanup during errors, and guaranteed reset to DISCONNECTED state ensure reliable disconnection. The past review issues are properly addressed.


202-223: Proper status reporting and lifecycle cleanup.

getStatus() provides useful connection information. handleOnDestroy() correctly acquires the lock, performs cleanup, and calls super.handleOnDestroy() to complete the lifecycle.


225-274: Well-implemented continuous reader with proper event emission.

The reader thread correctly:

  • Guards against duplicate thread creation
  • Reads data continuously and encodes to Base64 for JS consumption
  • Detects peer disconnection (EOF) and notifies listeners
  • Handles errors with both event emission and centralized error handling

294-316: Comprehensive error handling with null-safe PluginCall guard.

The centralized error handler correctly:

  • Acquires lock for thread-safe state mutation
  • Performs cleanup and state reset
  • Handles null PluginCall (from reader thread) gracefully

The past review issue regarding null PluginCall is properly addressed.


318-327: Clean utility methods.

compareAndSetState() provides a readable wrapper for atomic state transitions, and truncateForLog() safely handles log output for potentially large data payloads.


127-149: Verify data encoding: should send() decode Base64?

The review comments that the reader thread (line 240) encodes received bytes to Base64 before sending to JS, but send() treats input as UTF-8 text rather than decoding Base64. If the JS layer encodes binary data as Base64 before calling send(), this would transmit the Base64 string bytes instead of the decoded binary data, corrupting MSP protocol packets.

To confirm this issue, examine:

  1. How CapacitorSocket.js (or equivalent) encodes data before calling the native send() method
  2. Whether Base64 encoding is used symmetrically with the receiver (which encodes to Base64 at line 240)

If Base64 encoding is confirmed, apply the suggested fix to decode the input:

-                byte[] payload = data.getBytes(StandardCharsets.UTF_8);
+                byte[] payload = Base64.decode(data, Base64.NO_WRAP);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (2)

286-291: Consider reformatting long resource cleanup lines.

The resource cleanup logic is correct (individual try-catch-finally blocks, null assignments), but lines 288-290 are very long (>100 characters each), making them harder to read and maintain.

Consider reformatting for clarity:

 private void closeResourcesInternal() {
     stopReaderThread();
-    if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, "Error closing input stream", e); } finally { input = null; } }
-    if (output != null) { try { output.close(); } catch (IOException e) { Log.e(TAG, "Error closing output stream", e); } finally { output = null; } }
-    if (socket != null) { try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e); } finally { socket = null; } }
+    if (input != null) {
+        try {
+            input.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Error closing input stream", e);
+        } finally {
+            input = null;
+        }
+    }
+    if (output != null) {
+        try {
+            output.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Error closing output stream", e);
+        } finally {
+            output = null;
+        }
+    }
+    if (socket != null) {
+        try {
+            socket.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Error closing socket", e);
+        } finally {
+            socket = null;
+        }
+    }
 }

321-326: Unused utility method.

The truncateForLog method is defined but never called in the code. Consider either using it in your log statements (e.g., line 97, 140) or removing it.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa9a5b and ad6e0d5.

📒 Files selected for processing (1)
  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-12-02T17:46:47.828Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-02T17:46:47.828Z
Learning: BetaflightBluetoothPlugin.java: maintainers prefer write(PluginCall) to be “write-only” — no per-call UUID parsing or characteristic lookups. Resolve and cache the write characteristic once in onServicesDiscovered() (prefer PROPERTY_WRITE_NO_RESPONSE; else PROPERTY_WRITE). write() should read payload params from call, use the cached writeCharacteristic, pick write type, and submitWrite; clear the cache in cleanupGatt().

Applied to files:

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

Applied to files:

  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java
📚 Learning: 2025-06-09T00:32:21.385Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-06-09T00:32:21.385Z
Learning: In the betaflight-configurator codebase, port paths use counter prefixes (e.g., "bluetooth1", "bluetooth2", "serial1") rather than direct protocol identifiers. The protocol selection logic correctly uses `portPath.startsWith("bluetooth")` to detect bluetooth ports regardless of the counter suffix, rather than direct string matching against protocol map keys.

Applied to files:

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

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

  • android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java
🪛 ast-grep (0.40.0)
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java

[info] 84-84: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

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

20-60: LGTM: Well-structured plugin foundation.

The class design demonstrates solid engineering practices:

  • Clear ConnectionState enum for state transitions
  • Constants for error messages and configuration
  • Thread-safe primitives (AtomicReference, ReentrantLock)
  • Comprehensive instance fields for connection management

61-111: LGTM: Connect implementation is robust.

The connection logic correctly implements all requirements:

  • Port handling allows optional values without artificial defaults (per maintainer clarification)
  • State transition includes the ERROR → DISCONNECTED reset (line 103), addressing the past issue
  • Uses new Thread().start() pattern correctly for Capacitor 7 compatibility
  • Comprehensive error handling with proper resource cleanup

113-148: LGTM: Send implementation matches protocol requirements.

The send method correctly:

  • Decodes Base64 payload to raw bytes (line 133), matching serial/BLE implementations as required
  • Uses thread-safe writerLock synchronization
  • Double-checks connection state after lock acquisition (line 129)
  • Follows the correct threading pattern with new Thread().start()

150-207: LGTM: Connection management methods are well-implemented.

All three methods demonstrate proper design:

  • receive() appropriately directs users to event-based continuous reading
  • disconnect() includes the ERROR → DISCONNECTED transition (line 193) with robust cleanup
  • getStatus() provides clear state reporting for UI components

209-258: LGTM: Thread lifecycle and continuous reading are properly implemented.

The reader thread implementation demonstrates excellent practices:

  • Proper thread lifecycle management with guards and cleanup
  • Base64 encoding of received data (line 239) matches the protocol
  • EOF detection and peer disconnect handling (lines 233-235)
  • Null-safe error handling when called from background thread (line 251), checked at line 301

The blocking read on line 232 is safe because setSoTimeout was configured in connect().


293-315: LGTM: Error handling correctly addresses NPE risk.

The handleCommunicationError method properly handles null PluginCall parameters (lines 301-310), which prevents NPE when called from the background reader thread (line 251). This correctly implements the fix from past reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

197-245: Fix missing return statement causing incorrect bytesSent on success.

After successfully sending data (line 219), the method updates counters and invokes the callback with bytesSent: bytes.byteLength (line 224), but does not return. Execution falls through to line 242, which returns { bytesSent: 0 }, incorrectly reporting zero bytes sent despite the successful send. This breaks port usage statistics and contradicts the callback value.

Apply this diff to return the correct value:

                 if (res.success) {
                     this.bytesSent += bytes.byteLength;
                     if (cb) {
                         cb({
                             error: null,
                             bytesSent: bytes.byteLength,
                         });
                     }
+                    return {
+                        bytesSent: bytes.byteLength,
+                    };
                 } else {
                     throw new Error("Send failed");
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to send data e: ${e}`);

                 if (cb) {
                     cb({
                         error: e,
                         bytesSent: 0,
                     });
                 }
+                return {
+                    bytesSent: 0,
+                };
             }
         }

         return {
             bytesSent: 0,
         };
     }
🧹 Nitpick comments (2)
src/js/protocols/CapacitorSocket.js (2)

121-165: Resolve TODO or clarify the required check.

The TODO comment on line 160 states "need to check result detail," but the code already validates result?.success on line 154 and dispatches appropriate event details (address on success, false on failure). Consider removing the TODO if no additional check is needed, or specify what validation is still required.

Additionally, SonarCloud suggests using TypeError instead of Error on line 147 for the invalid port case, which would be more semantically accurate for a type/validation error.

Apply this diff to address both points:

             const fallbackPort = Number.isNaN(parsedPort) ? Number.parseInt(options?.port, 10) : parsedPort;
             if (Number.isNaN(fallbackPort)) {
-                throw new Error(`Invalid port in path: ${path}`);
+                throw new TypeError(`Invalid port in path: ${path}`);
             }
             port = fallbackPort;

             console.log(`${this.logHead} Connecting to ${host}:${port}`);

             const result = await this.plugin.connect({ ip: host, port });
             if (result?.success) {
                 this.address = `${host}:${port}`;
                 this.connected = true;
             } else {
                 throw new Error("Connect failed");
             }
-            this.dispatchEvent(new CustomEvent("connect", { detail: this.address })); // TODO need to check result detail
+            this.dispatchEvent(new CustomEvent("connect", { detail: this.address }));
         } catch (e) {

167-195: Simplify disconnect by removing redundant state reset.

Line 192 unconditionally sets this.connected = false after line 185 already sets it on successful disconnect. While harmless, this is redundant. Consider moving the counter reset inside the success block or clarifying that the force-cleanup behavior (resetting counters even when disconnect wasn't attempted) is intentional.

Apply this diff to remove redundancy:

         if (this.connected) {
             try {
                 const res = await this.plugin.disconnect();
                 if (res.success) {
                     this.connected = false;
+                    this.bytesReceived = 0;
+                    this.bytesSent = 0;
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to close socket: ${e}`);
             }
         }
-
-        this.connected = false;
-        this.bytesReceived = 0;
-        this.bytesSent = 0;
     }

Alternatively, if the intent is to always reset state regardless of disconnect success, add a comment explaining the force-cleanup behavior.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad6e0d5 and 190e166.

📒 Files selected for processing (1)
  • src/js/protocols/CapacitorSocket.js (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📚 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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/protocols/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 Learning: 2025-08-27T22:08:55.679Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/utils/compareVersions.js:3-17
Timestamp: 2025-08-27T22:08:55.679Z
Learning: In betaflight-configurator's compareVersions utility (src/js/utils/compareVersions.js), the behavior of treating non-numeric segments as NaN -> 0 is intentional by design, not a bug that needs fixing.

Applied to files:

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

Applied to files:

  • src/js/protocols/CapacitorSocket.js
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorSocket.js

[warning] 160-160: Complete the task associated to this "TODO" comment.

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


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

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


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

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


[warning] 147-147: new Error() is too unspecific for a type check. Use new TypeError() instead.

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

⏰ 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 (4)
src/js/protocols/CapacitorSocket.js (4)

5-24: Binary conversion utilities are correctly implemented; ignore SonarCloud warnings.

The use of charCodeAt (line 13) and fromCharCode (line 21) is correct for byte-level Base64 encoding/decoding. atob() returns a binary string where each character represents a single byte (0–255), and these methods safely extract/create byte values. SonarCloud's suggestions to use codePointAt/fromCodePoint are false positives—those methods handle Unicode code points that can exceed 255, which would corrupt binary data. The inline comment (lines 10–12) already documents this reasoning.


26-40: LGTM!

The normalizeToUint8Array function correctly handles all expected input types (Uint8Array, ArrayBuffer views, ArrayBuffer, Array) and appropriately throws TypeError for unsupported data, ensuring type safety for the TCP send path.


42-85: Native listener setup is correct for singleton protocol usage.

The constructor attaches native event listeners (dataReceived, dataReceivedError, connectionClosed) once, which is appropriate given that protocol instances are singletons in the serial facade. The dataReceived handler correctly decodes Base64, updates bytesReceived, and dispatches the receive event. Based on learnings, this pattern avoids the duplicate listener issues that can occur with re-attachment during connection cycles.


87-119: LGTM!

Helper methods are straightforward and correct. handleReceiveBytes properly updates counters, handleDisconnect triggers cleanup, and the port/device management methods (createPort, getConnectedPort, getDevices) follow established protocol patterns. Returning an empty array from getDevices is appropriate since TCP connections don't enumerate devices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

198-246: Fix missing return statements: send() always returns bytesSent: 0.

The method has a critical control flow bug. After a successful send (line 220-227) or a caught error (line 231-239), execution falls through to line 243 and always returns { bytesSent: 0 }, even when bytes were successfully sent. Callers relying on the return value will see zero bytes sent regardless of the outcome; only the optional callback receives correct values.

Apply this diff to add early returns in both success and error paths:

         if (this.connected) {
             const bytes = normalizeToUint8Array(data);
             try {
                 const payload = uint8ArrayToBase64(bytes);
                 const res = await this.plugin.send({ data: payload });

                 if (res.success) {
                     this.bytesSent += bytes.byteLength;
                     if (cb) {
                         cb({
                             error: null,
                             bytesSent: bytes.byteLength,
                         });
                     }
+                    return {
+                        bytesSent: bytes.byteLength,
+                    };
                 } else {
                     throw new Error("Send failed");
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to send data e: ${e}`);

                 if (cb) {
                     cb({
                         error: e,
                         bytesSent: 0,
                     });
                 }
+                return {
+                    bytesSent: 0,
+                };
             }
         }

         return {
             bytesSent: 0,
         };
🧹 Nitpick comments (1)
src/js/protocols/CapacitorSocket.js (1)

120-164: Consider using TypeError for invalid input and removing the resolved TODO.

The connect logic is solid, but two small improvements:

  1. Line 146 throws a generic Error for an invalid port parameter; TypeError would better convey that the input is malformed.
  2. The TODO at line 159 can be removed—the code already checks result?.success and dispatches appropriate event details (address on success, false on failure).

Apply this diff:

             if (Number.isNaN(fallbackPort)) {
-                throw new Error(`Invalid port in path: ${path}`);
+                throw new TypeError(`Invalid port in path: ${path}`);
             }
             port = fallbackPort;

             console.log(`${this.logHead} Connecting to ${host}:${port}`);

             const result = await this.plugin.connect({ ip: host, port });
             if (result?.success) {
                 this.address = `${host}:${port}`;
                 this.connected = true;
             } else {
                 throw new Error("Connect failed");
             }
-            this.dispatchEvent(new CustomEvent("connect", { detail: this.address })); // TODO need to check result detail
+            this.dispatchEvent(new CustomEvent("connect", { detail: this.address }));
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 190e166 and 89e0a98.

📒 Files selected for processing (1)
  • src/js/protocols/CapacitorSocket.js (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📚 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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/protocols/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 Learning: 2025-08-27T22:08:55.679Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/utils/compareVersions.js:3-17
Timestamp: 2025-08-27T22:08:55.679Z
Learning: In betaflight-configurator's compareVersions utility (src/js/utils/compareVersions.js), the behavior of treating non-numeric segments as NaN -> 0 is intentional by design, not a bug that needs fixing.

Applied to files:

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

Applied to files:

  • src/js/protocols/CapacitorSocket.js
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorSocket.js

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

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


[warning] 159-159: Complete the task associated to this "TODO" comment.

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


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

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


[warning] 146-146: new Error() is too unspecific for a type check. Use new TypeError() instead.

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

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

5-40: LGTM! Binary utility functions are correctly implemented.

The base64 conversion functions properly use charCodeAt and fromCharCode for byte-level operations. The inline comments (lines 10-13) correctly explain why charCodeAt is appropriate here—atob() returns a binary string where each character represents a single byte (0–255), and codePointAt would allow values >255 that corrupt the Uint8Array.

SonarCloud's suggestions to use codePointAt/fromCodePoint are false positives; those methods are for Unicode code points, not byte-level binary data.


42-85: LGTM! Constructor and event listeners are correctly wired.

The plugin event handlers properly integrate with the protocol lifecycle:

  • dataReceived decodes Base64, updates bytesReceived, and dispatches the receive event with raw bytes
  • dataReceivedError triggers disconnect recovery
  • connectionClosed updates state and notifies listeners

Based on learnings, these plugin listeners are attached once during construction, which is appropriate for the TCP transport lifecycle.


87-118: LGTM! Helper methods are correctly implemented.

  • handleReceiveBytes properly tracks incoming byte counts for port usage statistics
  • Port metadata methods return appropriate structures for TCP connections
  • getDevices() correctly returns an empty array, as TCP connections are manual (user-provided host:port) rather than enumerated

Based on learnings, the path strategy is appropriate for the TCP protocol's single-connection workflow.


166-196: LGTM! Disconnect logic is correct.

The method properly:

  • Checks plugin availability before attempting disconnect
  • Only calls plugin.disconnect() when actually connected
  • Dispatches events with appropriate success/failure details
  • Unconditionally resets state (lines 193-195) to ensure cleanup even if the disconnect call fails

This defensive state reset is intentional and prevents stale connection flags.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

167-200: Critical: send() always returns { bytesSent: 0 } even on success.

The success path at lines 174-181 and error path at lines 185-193 both fall through to line 197, which unconditionally returns { bytesSent: 0 }. Successful sends increment this.bytesSent and call the callback correctly, but the return value is always 0. This was flagged in previous reviews but the fix was not applied.

Apply this diff to add early returns:

     async send(data, cb) {
         if (this.connected) {
             const bytes = normalizeToUint8Array(data);
             try {
                 const payload = uint8ArrayToBase64(bytes);
                 const res = await this.plugin.send({ data: payload });

                 if (res.success) {
                     this.bytesSent += bytes.byteLength;
                     if (cb) {
                         cb({
                             error: null,
                             bytesSent: bytes.byteLength,
                         });
                     }
+                    return {
+                        bytesSent: bytes.byteLength,
+                    };
                 } else {
                     throw new Error("Send failed");
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to send data e: ${e}`);

                 if (cb) {
                     cb({
                         error: e,
                         bytesSent: 0,
                     });
                 }
+                return {
+                    bytesSent: 0,
+                };
             }
         }

         return {
             bytesSent: 0,
         };
     }
🧹 Nitpick comments (2)
src/js/protocols/CapacitorSocket.js (2)

143-143: Address or remove the TODO comment.

The connect event is now properly dispatched with this.address on success and false on failure, matching the pattern used in other protocols. The TODO appears resolved.

Apply this diff to remove the TODO:

-            this.dispatchEvent(new CustomEvent("connect", { detail: this.address })); // TODO need to check result detail
+            this.dispatchEvent(new CustomEvent("connect", { detail: this.address }));

130-130: Use TypeError for type/format validation errors.

The invalid port error is a type/format validation issue rather than a generic runtime error.

Apply this diff:

             if (Number.isNaN(fallbackPort)) {
-                throw new Error(`Invalid port in path: ${path}`);
+                throw new TypeError(`Invalid port in path: ${path}`);
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89e0a98 and 9d1daca.

📒 Files selected for processing (1)
  • src/js/protocols/CapacitorSocket.js (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📚 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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.js
📚 Learning: 2025-11-28T22:51:11.691Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4714
File: src/js/protocols/CapacitorBluetooth.js:176-206
Timestamp: 2025-11-28T22:51:11.691Z
Learning: In betaflight-configurator, Web Bluetooth API's `services` option filters scan results to only devices advertising those UUIDs, while `optionalServices` requests permission to access services after connection without filtering. PWA (WebBluetooth.js) uses `{ acceptAllDevices: true, optionalServices: uuids }` to show all devices while requesting post-connection service access. CapacitorBluetooth.js should match this pattern rather than using `services` as a scan filter.

Applied to files:

  • src/js/protocols/CapacitorSocket.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/CapacitorSocket.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/CapacitorSocket.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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 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:

  • src/js/protocols/CapacitorSocket.js
📚 Learning: 2025-08-27T22:08:55.679Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 4579
File: src/js/utils/compareVersions.js:3-17
Timestamp: 2025-08-27T22:08:55.679Z
Learning: In betaflight-configurator's compareVersions utility (src/js/utils/compareVersions.js), the behavior of treating non-numeric segments as NaN -> 0 is intentional by design, not a bug that needs fixing.

Applied to files:

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

Applied to files:

  • src/js/protocols/CapacitorSocket.js
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorSocket.js

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

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


[warning] 143-143: Complete the task associated to this "TODO" comment.

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


[warning] 130-130: new Error() is too unspecific for a type check. Use new TypeError() instead.

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


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

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

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

5-24: Binary conversion utilities are correctly implemented.

The use of charCodeAt and fromCharCode for byte-level base64 encoding/decoding is appropriate. The static analysis warnings suggesting codePointAt/fromCodePoint are false positives—those methods are for Unicode code points and can return values >255, which would corrupt binary data.


42-84: Constructor and plugin listener setup look good.

The plugin listeners are properly registered in the constructor, ensuring they're attached exactly once per instance. The dataReceived handler correctly updates byte counters and forwards data via the receive event, addressing previous review feedback.

Based on learnings, native plugin listeners must be attached exactly once per app lifetime to avoid duplicate handlers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/js/protocols/CapacitorTcp.js (3)

130-130: Optional: Consider using TypeError for consistency.

While new Error() is acceptable for invalid port parsing, using new TypeError() would be more semantically accurate for type/format validation errors. This is a stylistic preference and not critical.


150-165: Remove redundant state assignment.

Line 154 sets this.connected = false, and line 162 sets it again unconditionally. The duplicate assignment is unnecessary.

Apply this diff to consolidate state updates:

     async disconnect() {
         try {
             const res = await this.plugin.disconnect();
-            if (res.success) {
-                this.connected = false;
-            }
             this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
         } catch (e) {
             console.error(`${this.logHead}Failed to close connection: ${e}`);
             this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
         }
 
         this.connected = false;
         this.bytesReceived = 0;
         this.bytesSent = 0;
     }

197-199: Clarify or remove the inconsistent return value.

The method returns { bytesSent: 0 } after both successful sends (where bytes were actually sent) and when not connected. This is inconsistent with the callback-based reporting (lines 177-180) and may confuse callers.

If the return value is unused (callers rely on the callback), consider removing it or documenting the behavior. Otherwise, return accurate values:

     async send(data, cb) {
         if (this.connected) {
             const bytes = normalizeToUint8Array(data);
+            let sentBytes = 0;
             try {
                 const payload = uint8ArrayToBase64(bytes);
                 const res = await this.plugin.send({ data: payload });
 
                 if (res.success) {
-                    this.bytesSent += bytes.byteLength;
+                    sentBytes = bytes.byteLength;
+                    this.bytesSent += sentBytes;
                     if (cb) {
                         cb({
                             error: null,
-                            bytesSent: bytes.byteLength,
+                            bytesSent: sentBytes,
                         });
                     }
                 } else {
                     throw new Error("Send failed");
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to send data e: ${e}`);
 
                 if (cb) {
                     cb({
                         error: e,
                         bytesSent: 0,
                     });
                 }
             }
+            return { bytesSent: sentBytes };
         }
 
         return {
             bytesSent: 0,
         };
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f72bab4 and d6ffbf1.

📒 Files selected for processing (1)
  • src/js/protocols/CapacitorTcp.js (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/CapacitorTcp.js
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorTcp.js

[warning] 143-143: Complete the task associated to this "TODO" comment.

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


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

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


[warning] 130-130: new Error() is too unspecific for a type check. Use new TypeError() instead.

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


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

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

⏰ 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 (6)
src/js/protocols/CapacitorTcp.js (6)

1-3: LGTM!

The plugin initialization with optional chaining is a safe pattern.


5-16: LGTM! Static analysis warning is a false positive.

The use of charCodeAt is correct for binary data. The static analysis suggestion to use codePointAt would introduce data corruption, as codePointAt can return values greater than 255, which would overflow Uint8Array slots.


18-24: LGTM! Static analysis warning is a false positive.

The use of String.fromCharCode is correct for binary data encoding. The static analysis suggestion to use fromCodePoint would be inappropriate for byte-level binary data.


26-40: LGTM!

The normalization logic comprehensively handles various data types and throws an appropriate TypeError for unsupported inputs.


86-92: LGTM!

The handler methods are simple and correctly update state or delegate to appropriate methods.


94-117: LGTM!

The port management methods appropriately handle TCP connection metadata. Returning an empty array from getDevices is correct since TCP doesn't enumerate available devices.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 3, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
17.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@github-actions
Copy link
Contributor

github-actions bot commented Dec 3, 2025

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/js/protocols/CapacitorTcp.js (1)

114-114: Use TypeError for invalid port validation.

Static analysis correctly identifies that parameter validation errors should throw TypeError rather than generic Error for better semantic clarity.

Apply this diff:

-            if (Number.isNaN(fallbackPort)) {
-                throw new Error(`Invalid port in path: ${path}`);
-            }
+            if (Number.isNaN(fallbackPort)) {
+                throw new TypeError(`Invalid port in path: ${path}`);
+            }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6ffbf1 and f7b24ca.

📒 Files selected for processing (1)
  • src/js/protocols/CapacitorTcp.js (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 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/CapacitorTcp.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/CapacitorTcp.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/CapacitorTcp.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/CapacitorTcp.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/CapacitorTcp.js
📚 Learning: 2025-12-01T17:09:13.412Z
Learnt from: haslinghuis
Repo: betaflight/betaflight-configurator PR: 0
File: :0-0
Timestamp: 2025-12-01T17:09:13.412Z
Learning: CapacitorBluetooth.connect should only dispatch the 'open' event after GATT services are resolved and notifications are started; announcing open earlier causes the first MSP write to fail with “Unable to write characteristic” and contributes to early queue load.

Applied to files:

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

Applied to files:

  • src/js/protocols/CapacitorTcp.js
🪛 GitHub Check: SonarCloud Code Analysis
src/js/protocols/CapacitorTcp.js

[warning] 114-114: new Error() is too unspecific for a type check. Use new TypeError() instead.

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


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

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


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

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


[warning] 127-127: Complete the task associated to this "TODO" comment.

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

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

5-24: LGTM: Base64 conversion correctly handles binary data.

The use of charCodeAt and fromCharCode is correct for binary data (byte values 0–255). The static analysis warnings suggesting codePointAt/fromCodePoint are false positives—those methods are designed for Unicode code points and would corrupt binary data. The inline comment at lines 10-12 explicitly documents this design choice.


26-68: LGTM: Constructor and event listeners correctly implemented.

The constructor initializes state and attaches native plugin listeners once per application lifetime, consistent with the singleton pattern used for all protocol implementations in betaflight-configurator. The past review concern about duplicate handlers has been confirmed as resolved.


134-149: LGTM: Disconnect ensures cleanup regardless of errors.

The method defensively resets state (lines 146-148) after the try-catch block, ensuring consistent cleanup even if the native plugin call fails. While line 138 sets connected = false on success and line 146 sets it again unconditionally, this redundancy is acceptable for robustness.

Comment on lines +128 to +131
} catch (e) {
console.error(`${this.logHead}Failed to connect to socket: ${e}`);
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure connected state is reset on connection failure.

If connect fails (especially during a reconnection attempt), the connected flag should be explicitly set to false to maintain consistent state.

Apply this diff:

         } catch (e) {
             console.error(`${this.logHead}Failed to connect to socket: ${e}`);
+            this.connected = false;
             this.dispatchEvent(new CustomEvent("connect", { detail: false }));
         }
🤖 Prompt for AI Agents
In src/js/protocols/CapacitorTcp.js around lines 128 to 131, the catch block for
failed socket connection does not reset the instance connected state; explicitly
set this.connected = false inside the catch before dispatching the "connect"
event so that failed connection attempts (including reconnections) leave the
object in a consistent disconnected state.

Comment on lines +151 to +184
async send(data, cb) {
if (this.connected) {
const bytes = new Uint8Array(data);
try {
const payload = uint8ArrayToBase64(bytes);
const res = await this.plugin.send({ data: payload });

if (res.success) {
this.bytesSent += bytes.byteLength;
if (cb) {
cb({
error: null,
bytesSent: bytes.byteLength,
});
}
} else {
throw new Error("Send failed");
}
} catch (e) {
console.error(`${this.logHead}Failed to send data e: ${e}`);

if (cb) {
cb({
error: e,
bytesSent: 0,
});
}
}
}

return {
bytesSent: 0,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix send method return value.

The method always returns {bytesSent: 0} (lines 181-183) regardless of whether the send succeeded and bytes were actually transmitted. This makes the return value useless for callers that don't use the callback.

Apply this diff to return the correct value:

     async send(data, cb) {
+        let actualBytesSent = 0;
+        
         if (this.connected) {
             const bytes = new Uint8Array(data);
             try {
                 const payload = uint8ArrayToBase64(bytes);
                 const res = await this.plugin.send({ data: payload });
 
                 if (res.success) {
                     this.bytesSent += bytes.byteLength;
+                    actualBytesSent = bytes.byteLength;
                     if (cb) {
                         cb({
                             error: null,
                             bytesSent: bytes.byteLength,
                         });
                     }
                 } else {
                     throw new Error("Send failed");
                 }
             } catch (e) {
                 console.error(`${this.logHead}Failed to send data e: ${e}`);
 
                 if (cb) {
                     cb({
                         error: e,
                         bytesSent: 0,
                     });
                 }
             }
         }
 
         return {
-            bytesSent: 0,
+            bytesSent: actualBytesSent,
         };
     }
🤖 Prompt for AI Agents
In src/js/protocols/CapacitorTcp.js around lines 151 to 184, the send method
always falls through to returning {bytesSent: 0} even after a successful send;
change control flow so the method returns the actual bytes sent when the plugin
reports success, and returns an appropriate failure object when it fails or an
exception occurs. Concretely: after a successful send (res.success) return {
bytesSent: bytes.byteLength } (in addition to invoking cb), in the catch block
return { bytesSent: 0, error: e } (and still call cb if present), and if not
connected keep returning { bytesSent: 0 } as now.

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

Projects

Status: App

Development

Successfully merging this pull request may close these issues.

2 participants