Skip to content

Conversation

@haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented Oct 19, 2025

Summary by CodeRabbit

  • Refactor

    • Tab switching logic clarified: the firmware flasher tab now auto-opens and triggers connection when appropriate; disallowed tab switches are aborted with clearer handling.
    • Serial/protocol management unified for more consistent device selection, connect/disconnect, and data flow.
    • Removed firmware flasher auto-detection on port changes.
  • Bug Fixes

    • Bluetooth handling now uses protocol-aware CRC bypassing for improved connection reliability.
  • Chore

    • Minor import/order and internal cleanup adjustments.

@haslinghuis haslinghuis added this to the 2025.12 milestone Oct 19, 2025
@haslinghuis haslinghuis self-assigned this Oct 19, 2025
@haslinghuis haslinghuis moved this to App in 2025.12.0 Oct 19, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Walkthrough

Refactors serial orchestration to a data-driven _protocols array; removes WebSerial.cleanup(); removes firmware_flasher port-change auto-detect handlers; updates main tab-switch logic to special-case disallowed firmware_flasher behavior; reorders an import in serial_backend; narrows MSP CRC-bypass to a protocol-driven Bluetooth check.

Changes

Cohort / File(s) Summary
Tab-switching logic
src/js/main.js
Rewrote allowed-tab check to use includes(); special-case handling when firmware_flasher is disallowed: programmatically trigger connection/open via trigger("click"); other disallowed tabs log tabSwitchUpgradeRequired and abort; preserves tab-switch flags, cleanup, content loading, analytics under revised branches.
Serial orchestration & API
src/js/serial.js
Replaced per-protocol fields with a data-driven _protocols: [{ name, instance }]; removed legacy helpers; event forwarding iterates _protocols and injects protocolType; simplified selectProtocol, connect, disconnect, send, getDevices, requestPermissionDevice; added getters connectionId and protocol; updated signatures and optional-chaining usage.
Protocol implementation
src/js/protocols/WebSerial.js
Removed cleanup() method from WebSerial class.
Flasher port-change removal
src/js/tabs/firmware_flasher.js
Removed PORT_CHANGE_DEBOUNCE_MS, portChangeTimer, detectedSerialDevice(), onPortChange() and their EventBus bindings/cleanup; eliminated port-change auto-detect logic.
Serial backend import ordering
src/js/serial_backend.js
Moved MspHelper import to occur after Features to adjust import ordering.
MSP CRC handling
src/js/msp.js
Changed CRC-bypass condition to check serial.protocol === "bluetooth" and call serial._protocol?.shouldBypassCrc?.(expectedChecksum); when true, treat as bypassed (assign dataView, clear crcError).

Sequence Diagram(s)

sequenceDiagram
    participant UI as User UI
    participant Main as main.js
    participant Serial as Serial
    rect rgb(245,247,250)
    note over Main,UI: Tab switch (updated flow)
    end
    UI->>Main: request tab switch(tab)
    Main->>Main: allowedTabs.includes(tab)?
    alt not allowed
        alt tab == firmware_flasher
            Main->>Main: if connected/connecting -> trigger connection button (trigger("click"))
            Main->>Main: open firmware_flasher via firmware_flasher_button__link (trigger("click"))
        else other tab
            Main->>Main: log tabSwitchUpgradeRequired, abort
        end
    else allowed
        Main->>Main: set tab_switch_in_progress, cleanup, load content, analytics
        Main->>Serial: ensure protocol/connect if needed
    end
Loading
sequenceDiagram
    participant App as Application
    participant Serial as Serial (orchestrator)
    participant PList as _protocols array
    rect rgb(245,255,245)
    note over Serial: Protocol selection & connect (refactored)
    end
    App->>Serial: selectProtocol(portPath)
    Serial->>PList: find(protocol matching portPath/name)
    PList-->>Serial: protocol instance or undefined
    App->>Serial: connect(path, options)
    Serial->>Serial: protocol = selectProtocol(path)
    Serial->>PList: protocol?.connect(path, options)
    rect rgb(255,250,240)
    note over Serial: unified error handling & single callback
    end
    PList-->>Serial: result / error
    Serial->>App: callback(result)
Loading
sequenceDiagram
    participant MSP as MSP
    participant Serial as Serial
    participant Proto as Protocol
    MSP->>MSP: parse message, verify CRC
    alt CRC mismatch
        alt Serial.protocol == "bluetooth" and Proto.shouldBypassCrc(expected)
            MSP->>MSP: treat as bypassed (assign dataView, clear crcError)
        else
            MSP->>MSP: set crcError, reject message
        end
    else CRC OK
        MSP->>MSP: dispatch normally
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

RN: REFACTOR

Suggested reviewers

  • nerdCopter
  • blckmn
  • VitroidFPV

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description is entirely empty—no description text was provided by the author. The repository template requires structured information including context, motivation, and change descriptions. Without this description, reviewers cannot quickly understand the rationale for the architectural changes, what problems this PR solves, or how the refactoring maintains existing functionality. The absence of any description makes it impossible to verify compliance with the template requirements.
Title Check ❓ Inconclusive The title "Cleanup serial" is vague and uses a generic term that doesn't convey the actual scope of the changes. While the PR does involve cleanup activities (removing methods, reorganizing code), the primary focus is a substantial architectural refactoring of the serial communication system—shifting from per-protocol fields to a data-driven _protocols array, updating method signatures, and reorganizing initialization logic. The title fails to communicate this core refactoring work clearly, and a teammate scanning the repository history would not easily understand what major changes this PR introduces.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent 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 13dc315 and 8ea554a.

📒 Files selected for processing (1)
  • src/js/serial.js (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (7)
src/js/serial.js (7)

11-28: Clean refactor to data-driven protocol management.

The new _protocols array is well-structured and simplifies protocol lookup throughout the class.


33-67: LGTM—event forwarding properly centralized.

The iteration over _protocols cleanly replaces per-protocol event setup and correctly injects protocolType from the array metadata.


73-88: Function path support correctly implemented.

The addition of isFn check at line 75 properly routes function callbacks to the virtual protocol, addressing the past review concern about JSDoc vs. implementation mismatch.


95-109: Connect method cleaned up per past reviews.

The method now correctly:

  • Removes unsafe return from finally
  • Invokes callback once outside try-catch
  • Includes protocol information in error logging as requested

116-125: LGTM—disconnect properly refactored.

Callback and return moved outside try-catch, and optional chaining with nullish coalescing correctly handles missing protocol.


147-167: LGTM—comprehensive error handling in getDevices.

The method properly guards against undefined protocol (lines 152-155) and consistently returns an empty array on error paths.


196-212: LGTM—safe getter implementations.

All three getters use appropriate optional chaining and null-safe patterns to handle missing protocol state.


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.

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

Caution

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

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

147-166: getDevices(): handle null to query current or aggregate; add graceful fallback.

When protocolType is null, this now returns [], contrary to the JSDoc. Either fallback to current protocol or return the union across all protocols.

-    async getDevices(protocolType = null) {
+    async getDevices(protocolType = null) {
         try {
-            // Get the appropriate protocol
-            const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
-
-            if (!targetProtocol) {
-                console.warn(`${this.logHead} No valid protocol for getting devices`);
-                return [];
-            }
-
-            if (typeof targetProtocol.getDevices !== "function") {
-                console.error(`${this.logHead} Selected protocol does not implement getDevices`);
-                return [];
-            }
-
-            return targetProtocol.getDevices?.() || [];
+            if (!protocolType) {
+                // Aggregate across protocols, best-effort
+                const lists = await Promise.all(
+                    this._protocols.map(({ instance }) =>
+                        typeof instance.getDevices === "function" ? instance.getDevices() : Promise.resolve([]),
+                    ),
+                );
+                return lists.flat();
+            }
+            const targetProtocol = this._getProtocol?.(protocolType.toLowerCase());
+            if (!targetProtocol || typeof targetProtocol.getDevices !== "function") return [];
+            return (await targetProtocol.getDevices()) ?? [];
         } catch (error) {
             console.error(`${this.logHead} Error getting devices:`, error);
             return [];
         }
     }

36-67: Multiple listeners will break due to event detail shape inconsistency; implement standardized, backward-compatible transformation.

The transformation in serial.js wraps "receive" events as { data: event.detail, protocolType: name }, breaking downstream listeners that expect the raw Uint8Array structure:

  • handleReceiveBytes in WebSerial, WebBluetooth, and WebSocket access info.detail.byteLength, expecting a Uint8Array
  • AutoDetect.js:33 calls MSP.read(event.detail) expecting raw bytes, not { data: Uint8Array, protocolType: name }
  • webstm32.js:25 calls read_serial(event.detail.buffer) expecting a .buffer property that won't exist

Only serial_backend.js:302 (read_serial(event.detail.data)) is compatible with the new shape—this inconsistency indicates the transformation was added mid-process without fully accounting for all consumers.

Implement the suggested standardization: preserve the original detail structure and spread it alongside protocolType for backward compatibility, whether dealing with Uint8Array or structured objects.

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

18-24: Initialize protocols defensively and reduce lookup churn.

Looks good. To avoid accidental mutation and repeated linear searches, consider a tiny helper or a Map. This keeps lookups O(1) and the list immutable.

-        this._protocols = [
-            { name: "webserial", instance: new WebSerial() },
-            { name: "webbluetooth", instance: new WebBluetooth() },
-            { name: "websocket", instance: new Websocket() },
-            { name: "virtual", instance: new VirtualSerial() },
-        ];
+        this._protocols = Object.freeze([
+            { name: "webserial", instance: new WebSerial() },
+            { name: "webbluetooth", instance: new WebBluetooth() },
+            { name: "websocket", instance: new Websocket() },
+            { name: "virtual", instance: new VirtualSerial() },
+        ]);
+        this._getProtocol = (name) =>
+            this._protocols.find((p) => p.name === name)?.instance ?? null;

196-205: LGTM on getters; minor polish.

Connected and connectionId getters look good. Consider documenting the connectionId format (protocol-specific) to set expectations for consumers.

📜 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 292093a and 4a04960.

📒 Files selected for processing (4)
  • src/js/main.js (1 hunks)
  • src/js/protocols/WebSerial.js (0 hunks)
  • src/js/serial.js (8 hunks)
  • src/js/tabs/firmware_flasher.js (0 hunks)
💤 Files with no reviewable changes (2)
  • src/js/protocols/WebSerial.js
  • src/js/tabs/firmware_flasher.js
🧰 Additional context used
🧬 Code graph analysis (1)
src/js/main.js (3)
src/js/gui.js (1)
  • GUI (473-473)
src/js/gui_log.js (1)
  • gui_log (7-21)
src/js/localization.js (1)
  • i18n (7-7)
🪛 Biome (2.1.2)
src/js/serial.js

[error] 107-107: Unsafe usage of 'return'.

'return' in 'finally' overwrites the control flow statements inside 'try' and 'catch'.

(lint/correctness/noUnsafeFinally)


[error] 124-124: Unsafe usage of 'return'.

'return' in 'finally' overwrites the control flow statements inside 'try' and 'catch'.

(lint/correctness/noUnsafeFinally)


[error] 182-182: Unsafe usage of 'return'.

'return' in 'finally' overwrites the control flow statements inside 'try' and 'catch'.

(lint/correctness/noUnsafeFinally)

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 (2)
src/js/serial.js (2)

90-101: Add protocol null guard to prevent runtime errors.

If selectProtocol() returns undefined (which will happen with the bug in lines 77-82), line 95 will throw a TypeError: Cannot read property 'connect' of undefined.

Additionally, the callback is passed to the protocol at line 95 and invoked again at line 99, which can cause double-invocation for protocols that use it (e.g., VirtualSerial).

Apply this diff to add the guard and clarify callback handling:

 async connect(path, options, callback) {
     // Select the appropriate protocol based directly on the port path
     let result = false;
     try {
         this._protocol = this.selectProtocol(path);
+        if (!this._protocol) {
+            throw new Error("No protocol selected for path: " + path);
+        }
         result = await this._protocol.connect(path, options, callback);
     } catch (error) {
         console.error(`${this.logHead} Error during connection:`, error);
     }
     callback && callback(result);
     return result;
 }

164-173: Add explicit null guard for consistency and correct return value.

While the finally block issue is resolved, line 168 uses optional chaining which returns undefined when targetProtocol is null, not false. This means result becomes undefined instead of remaining false, making the return value inconsistent.

For consistency with getDevices() (lines 141-144), add an explicit null guard.

Apply this diff to add the guard:

 async requestPermissionDevice(showAllDevices = false, protocolType) {
-    let result = false;
     try {
         const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
-        result = await targetProtocol?.requestPermissionDevice(showAllDevices);
+        if (!targetProtocol) {
+            console.warn(`${this.logHead} No valid protocol for permission request`);
+            return false;
+        }
+        return await targetProtocol.requestPermissionDevice(showAllDevices);
     } catch (error) {
         console.error(`${this.logHead} Error requesting device permission:`, error);
+        return false;
     }
-    return result;
 }
📜 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 4a04960 and 5f041f8.

📒 Files selected for processing (1)
  • src/js/serial.js (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (4)
src/js/serial.js (4)

18-24: LGTM! Clean data-driven protocol initialization.

The unified _protocols array provides a cleaner approach to protocol management and enables consistent lookup patterns throughout the class.


36-67: LGTM! Event forwarding correctly iterates protocols.

The iteration pattern with destructuring and protocol name injection provides clean event propagation with proper source identification.


108-117: LGTM! Past issues with finally block resolved.

The finally block with return has been removed, and the method now uses optional chaining for safe protocol access with a single callback invocation.


185-194: LGTM! Safe getter implementations with appropriate fallbacks.

Both connected and connectionId getters use optional chaining with sensible fallback values.

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 (2)
src/js/serial.js (2)

170-179: Add null guard for consistent return value.

When targetProtocol is null or undefined, line 174's optional chaining returns undefined, not false. The method should consistently return false for missing protocols, similar to how getDevices() handles this with an explicit null guard.

Apply this diff to add the guard:

 async requestPermissionDevice(showAllDevices = false, protocolType) {
-    let result = false;
     try {
         const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
-        result = await targetProtocol?.requestPermissionDevice(showAllDevices);
+        if (!targetProtocol) {
+            console.warn(`${this.logHead} No valid protocol for permission request`);
+            return false;
+        }
+        return await targetProtocol.requestPermissionDevice(showAllDevices);
     } catch (error) {
         console.error(`${this.logHead} Error requesting device permission:`, error);
+        return false;
     }
-    return result;
 }

73-89: Add null-safety and support wss:// protocol.

The method has two issues:

  1. Null-safety: Line 83 calls .startsWith() on portPath, but the JSDoc (line 71) indicates it can be null. This will throw a TypeError.
  2. Missing wss:// support: Line 81's regex only matches tcp:// and ws://, not wss:// (WebSocket Secure).

A past review flagged both concerns and was marked as addressed, but they remain unresolved.

Apply this diff to fix both issues:

 selectProtocol(portPath) {
     // Determine which protocol to use based on port path
-    let protocol;
-
+    const s = typeof portPath === "string" ? portPath : "";
     // Select protocol based on port path. Default to webSerial for
     // typical serial device identifiers (e.g., COM1, /dev/ttyUSB0).
-    if (portPath === "virtual") {
-        protocol = this._protocols.find((p) => p.name === "virtual")?.instance;
-    } else if (portPath === "manual" || /^(tcp|ws):\/\/([A-Za-z0-9.-]+)(?::(\d+))?$/.test(portPath)) {
-        protocol = this._protocols.find((p) => p.name === "websocket")?.instance;
-    } else if (portPath.startsWith("bluetooth")) {
-        protocol = this._protocols.find((p) => p.name === "webbluetooth")?.instance;
-    } else {
-        protocol = this._protocols.find((p) => p.name === "webserial")?.instance;
-    }
-    return protocol;
+    if (s === "virtual") {
+        return this._protocols.find((p) => p.name === "virtual")?.instance;
+    } else if (s === "manual" || /^(tcp|wss?):\/\/([A-Za-z0-9.-]+)(?::(\d+))?$/.test(s)) {
+        return this._protocols.find((p) => p.name === "websocket")?.instance;
+    } else if (s.startsWith("bluetooth")) {
+        return this._protocols.find((p) => p.name === "webbluetooth")?.instance;
+    } else {
+        return this._protocols.find((p) => p.name === "webserial")?.instance;
+    }
 }
📜 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 af3cf17 and 99b2ceb.

📒 Files selected for processing (2)
  • src/js/serial.js (8 hunks)
  • src/js/tabs/firmware_flasher.js (0 hunks)
💤 Files with no reviewable changes (1)
  • src/js/tabs/firmware_flasher.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (5)
src/js/serial.js (5)

18-24: LGTM! Clean protocol initialization.

The unified _protocols array provides a clean data-driven approach for protocol management, making lookups and iteration straightforward.


36-66: LGTM! Robust event forwarding.

The event forwarding correctly iterates over protocols, uses optional chaining for safety, and properly injects the protocol name into event details for tracking.


114-123: LGTM! Clean disconnect implementation.

The method correctly uses optional chaining to prevent errors, has a single callback invocation, and properly returns the result outside the try-catch block.


142-162: LGTM! Robust device retrieval.

The method has proper null guards, validates that the protocol implements getDevices, and consistently returns an empty array on errors.


191-200: LGTM! Safe getter implementations.

Both getters correctly use optional chaining with appropriate fallback values (false for connected, null for connectionId).

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/serial.js (1)

170-179: Refactor for consistency with getDevices() pattern.

Line 174 uses optional chaining targetProtocol?.requestPermissionDevice(), which returns undefined if targetProtocol is missing. However, line 171 initializes result = false, suggesting a boolean return type. This inconsistency could confuse callers.

Consider adding explicit guards like getDevices() does (lines 147-155) for clarity and consistent error handling.

Apply this diff for consistency:

 async requestPermissionDevice(showAllDevices = false, protocolType) {
     let result = false;
     try {
         const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
+        if (!targetProtocol) {
+            console.warn(`${this.logHead} No valid protocol for permission request`);
+            return false;
+        }
         result = await targetProtocol.requestPermissionDevice(showAllDevices);
     } catch (error) {
         console.error(`${this.logHead} Error requesting device permission:`, error);
     }
     return result;
 }
📜 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 6ef9729 and f749d0b.

📒 Files selected for processing (2)
  • src/js/msp.js (1 hunks)
  • src/js/serial.js (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🧬 Code graph analysis (1)
src/js/msp.js (1)
src/js/serial.js (2)
  • serial (211-211)
  • serial (211-211)
🔇 Additional comments (7)
src/js/serial.js (7)

18-24: LGTM: Clean refactor to unified protocol collection.

The shift from individual protocol fields to a data-driven _protocols array with metadata improves maintainability and makes protocol lookup more consistent.


36-66: LGTM: Event forwarding properly updated.

The event forwarding logic correctly iterates through the new _protocols collection and properly injects the protocol name into event details using the destructured name property.


73-87: LGTM: Protocol selection with proper null-safety.

The method correctly handles null/non-string portPath by coercing to empty string, includes wss:// in the websocket regex pattern, and uses optional chaining to safely return the protocol instance.


112-121: LGTM: Disconnect with proper optional chaining.

The method correctly uses optional chaining for this._protocol?.disconnect(), returns outside the try-catch block, and invokes the callback exactly once. Good error handling.


126-135: LGTM: Send method with proper error handling.

The method correctly uses optional chaining and returns a well-formed result object { bytesSent: 0 } on error, which maintains consistency with the expected return shape.


142-162: LGTM: getDevices with comprehensive guards.

The method properly checks for missing protocol and missing method implementation, and uses optional chaining for the final call. Good defensive programming.


191-207: LGTM: Clean getter implementations.

All three getters (connected, connectionId, protocolType) properly use optional chaining and provide sensible default values when the protocol is unavailable.

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)
src/js/serial.js (1)

173-182: Return value inconsistency when protocol is not found.

If targetProtocol is undefined at line 176, the optional chaining at line 177 assigns undefined to result, which is then returned. This is inconsistent with:

  • The initialization result = false at line 174
  • The JSDoc return type Promise<Object> at line 171
  • The pattern in getDevices() which returns [] when protocol is missing

Consider adding a guard similar to getDevices() to ensure a consistent falsy value is returned.

Apply this diff to ensure consistent return behavior:

     async requestPermissionDevice(showAllDevices = false, protocolType) {
-        let result = false;
         try {
             const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
+            if (!targetProtocol) {
+                console.warn(`${this.logHead} No valid protocol for permission request`);
+                return false;
+            }
-            result = await targetProtocol?.requestPermissionDevice(showAllDevices);
+            return await targetProtocol.requestPermissionDevice(showAllDevices);
         } catch (error) {
             console.error(`${this.logHead} Error requesting device permission:`, error);
+            return false;
         }
-        return result;
     }
🧹 Nitpick comments (1)
src/js/serial.js (1)

145-165: Optional: Remove redundant optional chaining.

Line 160 uses optional chaining targetProtocol.getDevices?.() after already verifying that targetProtocol exists and implements getDevices (lines 150-158). The optional chaining is redundant here.

If desired, simplify line 160:

-            return targetProtocol.getDevices?.() || [];
+            return targetProtocol.getDevices() || [];
📜 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 16afa83 and 5d952ee.

📒 Files selected for processing (1)
  • src/js/serial.js (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (6)
src/js/serial.js (6)

18-24: LGTM: Clean data-driven protocol management.

The unified _protocols array with metadata provides a maintainable, consistent approach for protocol management.


36-66: LGTM: Event forwarding correctly updated.

The refactored event forwarding properly iterates over the new _protocols structure and injects protocol type information into event details.


73-87: LGTM: Protocol selection with proper null-safety.

The method correctly handles null/undefined portPath values and includes wss:// in the protocol detection regex. The optional chaining allows undefined returns, which are handled by try-catch in the calling code per the maintainer's design choice.


94-108: LGTM: Connection logic with enhanced error reporting.

The method correctly uses try-catch to handle potential protocol selection failures, with improved error logging that includes both the path and protocol information as requested. Callback is invoked exactly once after the try-catch block.


115-124: LGTM: Disconnect logic with proper null-safety.

The method correctly uses optional chaining for safe protocol access and invokes the callback exactly once.


194-210: LGTM: Getters with proper null-safety.

All getter implementations correctly use optional chaining or ternary checks to safely handle cases where this._protocol may be undefined, with appropriate default values.

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/serial.js (1)

173-182: Fix: method returns undefined instead of false when protocol not found.

When targetProtocol is undefined (e.g., invalid protocolType), the optional chaining on line 177 causes result to become undefined, overwriting the false initialization. This is inconsistent with the method's boolean return pattern used elsewhere.

Apply this diff to ensure consistent false return:

 async requestPermissionDevice(showAllDevices = false, protocolType) {
-    let result = false;
     try {
         const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
-        result = await targetProtocol?.requestPermissionDevice(showAllDevices);
+        if (!targetProtocol) {
+            console.warn(`${this.logHead} No valid protocol for permission request`);
+            return false;
+        }
+        return await targetProtocol.requestPermissionDevice(showAllDevices);
     } catch (error) {
         console.error(`${this.logHead} Error requesting device permission:`, error);
+        return false;
     }
-    return result;
 }
🧹 Nitpick comments (2)
src/js/serial.js (2)

115-124: Consider optional chaining for graceful null handling.

The method correctly handles errors and avoids the finally return issue from past reviews. However, this._protocol.disconnect() on line 118 will throw if the protocol is null (e.g., before any connection). While the try-catch handles this, optional chaining would be more graceful:

 async disconnect(callback) {
     let result = false;
     try {
-        result = await this._protocol.disconnect();
+        result = await this._protocol?.disconnect() ?? false;
     } catch (error) {
         console.error(`${this.logHead} Error during disconnect:`, error);
     }
     callback?.(result);
     return result;
 }

129-138: Consider optional chaining for consistency.

The method correctly returns { bytesSent: 0 } on error. For consistency with the graceful null handling in other methods, consider using optional chaining:

 async send(data, callback) {
     let result;
     try {
-        result = await this._protocol.send(data, callback);
+        result = await this._protocol?.send(data, callback) ?? { bytesSent: 0 };
     } catch (error) {
         result = { bytesSent: 0 };
         console.error(`${this.logHead} Error sending data:`, error);
     }
     return result;
 }
📜 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 563e89f and c62d4c5.

📒 Files selected for processing (2)
  • src/js/msp.js (1 hunks)
  • src/js/serial.js (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/msp.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (6)
src/js/serial.js (6)

18-24: LGTM! Clean data-driven protocol management.

The refactor to a _protocols array simplifies protocol management and enables uniform iteration for event forwarding and lookups.


36-66: LGTM! Event forwarding properly implemented.

The iteration over _protocols correctly sets up event forwarding with protocol type injection, making events more informative.


73-87: LGTM! Protocol selection is robust.

The method correctly handles null/undefined portPath, includes wss:// support, and uses optional chaining appropriately. The past critical bug with _getProtocol has been resolved.


94-108: LGTM! Connect error handling is consistent.

The method correctly handles connection errors with try-catch, includes protocol information in error logging as discussed in past reviews, and ensures single callback invocation outside the try block.


145-165: LGTM! Robust device retrieval with proper guards.

The method includes comprehensive null checks and method existence validation. The optional chaining on line 160 is redundant after the check on line 155, but this doesn't affect correctness.


194-210: LGTM! Getters properly handle null protocol.

All three getters use appropriate optional chaining and provide sensible fallback values when this._protocol is null.

@nerdCopter

This comment was marked as outdated.

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

Caution

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

⚠️ Outside diff range comments (1)
src/js/serial.js (1)

146-166: Behavior mismatch: getDevices() doesn’t honor “specific type or current protocol”.
With protocolType=null it returns [], not current protocol devices; also not awaited, so try/catch won’t catch async errors.

Apply this diff:

-            const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
+            let targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
+            if (!targetProtocol) {
+                // Fallback: use current protocol, else default to webserial.
+                targetProtocol = this._protocol ?? this._protocols.find((p) => p.name === "webserial")?.instance;
+            }
@@
-            if (!targetProtocol) {
+            if (!targetProtocol) {
                 console.warn(`${this.logHead} No valid protocol for getting devices`);
                 return [];
             }
@@
-            return targetProtocol.getDevices?.() || [];
+            return (await targetProtocol.getDevices()) ?? [];

Based on learnings.

♻️ Duplicate comments (2)
src/js/serial.js (2)

96-107: Add protocol guard to avoid avoidable throw; keep single-callback pattern.
Try/catch logs the error, but guarding keeps logs cleaner and control explicit.

Apply this diff:

-        try {
-            this._protocol = this.selectProtocol(path);
-            result = await this._protocol.connect(path, options);
+        try {
+            this._protocol = this.selectProtocol(path);
+            if (this._protocol) {
+                result = await this._protocol.connect(path, options);
+            } else {
+                console.error(`${this.logHead} No protocol selected for path '${path}'.`);
+            }
         } catch (error) {

129-139: Fix double-callback risk and add explicit protocol guard in send.
The protocol may also invoke the callback; then Serial calls it again. Align with connect/disconnect and own the public callback.

Apply this diff:

-    async send(data, callback) {
-        let result;
-        try {
-            result = (await this._protocol?.send(data, callback)) ?? { bytesSent: 0 };
-        } catch (error) {
-            result = { bytesSent: 0 };
-            console.error(`${this.logHead} Error sending data:`, error);
-        }
-        callback?.(result);
-        return result;
-    }
+    async send(data, callback) {
+        let result = { bytesSent: 0 };
+        try {
+            if (!this._protocol || typeof this._protocol.send !== "function") {
+                throw new Error("No protocol available for send");
+            }
+            // Serial owns public callbacks; do not pass callback into protocol.
+            result = await this._protocol.send(data);
+        } catch (error) {
+            console.error(`${this.logHead} Error sending data:`, error);
+        }
+        callback?.(result);
+        return result;
+    }

Check if any protocol send still expects a callback:

#!/usr/bin/env bash
rg -nP -C1 '\bsend\s*\(([^\)]*callback[^\)]*)\)' src/js/protocols
🧹 Nitpick comments (2)
src/js/serial.js (2)

36-67: Harden event forwarding: guard non-object details and normalize 'receive' shape.
Spreading a non-object detail will throw. Also, keep a consistent payload shape for 'receive'.

Apply this diff:

-                    instance.addEventListener(eventType, (event) => {
-                        let newDetail;
-                        if (event.type === "receive") {
-                            // For 'receive' events, we need to handle the data differently
-                            newDetail = {
-                                data: event.detail,
-                                protocolType: name,
-                            };
-                        } else {
-                            // For other events, we can use the detail directly
-                            newDetail = {
-                                ...event.detail,
-                                protocolType: name,
-                            };
-                        }
+                    instance.addEventListener(eventType, (event) => {
+                        const baseDetail =
+                            event.type === "receive"
+                                ? { data: event.detail }
+                                : (event && typeof event.detail === "object" && event.detail !== null)
+                                    ? { ...event.detail }
+                                    : { value: event?.detail };
+                        const newDetail = { ...baseDetail, protocolType: name };
                         // Dispatch the event with the new detail
                         this.dispatchEvent(
                             new CustomEvent(event.type, {
                                 detail: newDetail,
                                 bubbles: event.bubbles,
                                 cancelable: event.cancelable,
                             }),
                         );
                     });

174-183: Guard invalid protocol types and accept public API synonyms.
JSDoc uses “serial/bluetooth”, registry uses “webserial/webbluetooth”. Add mapping and guard.

Apply this diff:

-    async requestPermissionDevice(showAllDevices = false, protocolType) {
-        let result = false;
-        try {
-            const targetProtocol = this._protocols.find((p) => p.name === protocolType?.toLowerCase())?.instance;
-            result = await targetProtocol?.requestPermissionDevice(showAllDevices);
-        } catch (error) {
-            console.error(`${this.logHead} Error requesting device permission:`, error);
-        }
-        return result;
-    }
+    async requestPermissionDevice(showAllDevices = false, protocolType) {
+        const normalized = protocolType?.toLowerCase?.();
+        const mapped =
+            normalized === "serial" ? "webserial" :
+            normalized === "bluetooth" ? "webbluetooth" :
+            (normalized === "ws" || normalized === "wss" || normalized === "websocket") ? "websocket" :
+            normalized === "virtual" ? "virtual" :
+            normalized;
+        try {
+            const targetProtocol = this._protocols.find((p) => p.name === mapped)?.instance;
+            if (!targetProtocol || typeof targetProtocol.requestPermissionDevice !== "function") {
+                console.warn(`${this.logHead} No valid protocol for permission request: ${protocolType}`);
+                return false;
+            }
+            return await targetProtocol.requestPermissionDevice(showAllDevices);
+        } catch (error) {
+            console.error(`${this.logHead} Error requesting device permission:`, error);
+            return false;
+        }
+    }

Based on learnings.

📜 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 c62d4c5 and 13dc315.

📒 Files selected for processing (1)
  • src/js/serial.js (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T22:13:09.136Z
Learnt from: blckmn
PR: betaflight/betaflight-configurator#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/serial.js
🔇 Additional comments (3)
src/js/serial.js (3)

18-24: Protocol registry LGTM.
Initialization is clear and self-contained. No issues here.


116-124: Disconnect flow LGTM.
Optional chaining with nullish coalesce is fine; callback invoked once afterward.


195-211: Getters LGTM.
Accessors are null-safe and consistent with protocol selection.

@sonarqubecloud
Copy link

@github-actions
Copy link
Contributor

@nerdCopter
Copy link
Member

i dont understand how coderabbitAI went from "(1)" duplicate comment to "(2)", and are now different than the original.

@haslinghuis
Copy link
Member Author

Already told the bot it is already handled in try catch block.

@nerdCopter
Copy link
Member

unsure how to "fully" test, but i noticed during flashing i was able to tab-out, then upon return to flashing tab, flashing continued unobstructed.

@haslinghuis haslinghuis merged commit da27f22 into betaflight:master Oct 20, 2025
8 checks passed
@github-project-automation github-project-automation bot moved this from App to Done in 2025.12.0 Oct 20, 2025
@haslinghuis haslinghuis deleted the fix-serial branch October 20, 2025 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants