Skip to content

Conversation

haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented Jun 10, 2025

  • use fixed timeout to reduce buffer overflow issues
  • fix yarn test
  • update yarn test to use latest firmware
  • adds msp monitoring queue monitoring tool for testing

image

image

image

Summary by CodeRabbit

  • New Features

    • Interactive MSP Debug Dashboard, real-time queue monitor, stress-test framework, browser test runner, lazy-loaded debug tools and global debug/test APIs.
  • Bug Fixes

    • Robust first-run/config handling; test environments treated as compatible; callbacks suppressed on CRC errors.
  • Refactor

    • Simplified send/retry behavior with fixed TIMEOUT, timing measurements, long-run warnings, streamlined deduplication and connection checks; jumbo-frame tracking removed.
  • Documentation

    • Added comprehensive MSP Debug Tools README.
  • Tests

    • Updated tests to new API version and adjusted test callbacks.

@haslinghuis haslinghuis added this to the 11.0 milestone Jun 10, 2025
@haslinghuis haslinghuis self-assigned this Jun 10, 2025
Copy link
Contributor

coderabbitai bot commented Jun 10, 2025

Warning

Rate limit exceeded

@haslinghuis has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f39a39b and 306a51c.

📒 Files selected for processing (2)
  • src/js/msp/MSPHelper.js (1 hunks)
  • src/js/msp/debug/msp_queue_monitor.js (1 hunks)

Walkthrough

Replaces adaptive MSP timeout/flags with a fixed TIMEOUT and simplifies send_message; removes jumbo-frame tracking; adds dynamic import bootstrap for MSP debug tools and many new debug modules (README, barrel, dashboard, queue monitor, stress tester, test runner); treats test environments as compatible and bumps one test API version.

Changes

Cohort / File(s) Change Summary
MSP core changes
src/js/msp.js
Removed messageIsJumboFrame and JUMBO_FRAME_SIZE_LIMIT; removed adaptive MIN_TIMEOUT/MAX_TIMEOUT/timeout; added fixed TIMEOUT = 1000; per-request start timestamp added; simplified connection check and duplicate detection; timer created only for new requests; timer callbacks log long-run warnings and clear timers; removed adaptive timeout scaling; adjusted send/callback invocation semantics.
MSP helper adjustment
src/js/MSPHelper.js
process_data now invokes callbacks only when there is no CRC error (removed callbackOnError handling); timer cleanup and callback removal preserved.
Main bootstrap
src/js/main.js
Added dynamic import of ./msp/debug/msp_debug_tools.js with dev logs and failure warning; made firstRun retrieval null-safe (getConfig("firstRun") ?? {}) and guarded static-tab initialization.
Browser compatibility
src/js/utils/checkBrowserCompatibility.js
Added isTestEnvironment detection (Node/Jest) and treat test environments as compatible in the compatibility check.
Tests
test/js/msp/MSPHelper.test.js
Bumped API version constant from API_VERSION_1_46 to API_VERSION_1_47; removed callbackOnError: true from one test entry.
Debug tooling barrel & wiring
src/js/msp/debug/index.js
src/js/msp/debug/msp_debug_tools.js
New barrel and wiring file: re-exports debug modules, prints console banner, and auto-starts basic monitoring on dev hosts via dynamic import and status listener.
Debug documentation
src/js/msp/debug/MSP_DEBUG_README.md
New README documenting MSP Debug Tools features, APIs, usage, tests/scenarios, dashboard interactions, and troubleshooting.
Debug dashboard UI & API
src/js/msp/debug/msp_debug_dashboard.js
New MSPDebugDashboard class and exported singleton mspDebugDashboard; builds DOM/CSS UI, canvas live metrics, pause/resume behavior, keyboard shortcut, throttled rendering, and exposes window.MSPDebug API.
Queue monitor instrumentation
src/js/msp/debug/msp_queue_monitor.js
New MSPQueueMonitor class and lazy mspQueueMonitor proxy; instruments MSP methods to collect metrics, alerts, thresholds, listener notifications, analysis, reporting, and supports destroy/restore.
Stress test harness
src/js/msp/debug/msp_stress_test.js
New MSPStressTest class and lazy mspStressTest wrapper implementing nine stress tests, suite runner, integration with monitor, reporting/grading, and public test APIs.
Browser test runner
src/js/msp/debug/msp_test_runner.js
New MSPTestRunner exposing quick monitoring, runTest/runFullSuite, analyze/report, stress scenarios, quickHealthCheck, and global window.MSPTestRunner.

Sequence Diagram(s)

MSP send_message with fixed TIMEOUT

sequenceDiagram
    participant Caller
    participant MSP
    participant Serial
    participant Timer

    Caller->>MSP: send_message(code, data, callback_sent, callback_msp)
    MSP->>MSP: if (!serial.connected) -> call callback_msp and return
    MSP->>MSP: check duplicate via callbacks.some(...)
    alt send permitted
        MSP->>MSP: create request { code, start: performance.now(), ... }
        MSP->>Timer: setTimeout(timerFn, TIMEOUT)
        MSP->>Serial: serial.send(bufferOut, sendCallback)
        Serial-->>MSP: sendCallback(bytesSent)
        MSP->>MSP: compute executionTime, set obj.stop
        MSP->>Caller: call callback_sent only if bytesSent == bufferOut.byteLength
        Timer-->>MSP: timerFn fires -> clear timer, log if executionTime > 5000ms, handle timeout (no adaptive scaling)
    else duplicate or disconnected
        MSP->>Caller: skip send / call callback_msp
    end
Loading

Dynamic loading of MSP Debug Tools (high-level)

sequenceDiagram
    participant App as Main App
    participant Import as Dynamic Import
    participant Tools as MSP Debug Tools
    participant Monitor as mspQueueMonitor
    participant Dashboard as mspDebugDashboard
    participant Stress as mspStressTest

    App->>Import: import('./msp/debug/msp_debug_tools.js')
    Import-->>Tools: load modules, print console banner
    alt dev host (localhost/127.0.0.1)
        Tools->>Monitor: dynamic import and startMonitoring(2000)
        Tools->>App: console guidance messages
    end
    Tools->>Dashboard: initialize UI on demand
    Dashboard->>Monitor: subscribe for live updates
    Dashboard->>Stress: trigger stress tests
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

Tested

Suggested reviewers

  • VitroidFPV
  • nerdCopter
  • McGiverGim

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The PR description lists the main intents (fixed timeout, test fixes/updates, and added MSP monitoring tools) with checkboxes and screenshots, so it conveys high-level objectives, but it does not follow the repository template structure nor does it document several review-critical details. Missing items that affect reviewability include explicit mention of breaking or public API changes (the send_message signature change and removed properties/constants), a concise file-level summary of the large new debug modules (paths and exported/global APIs), and CI/test results or reproduction instructions for the "fix yarn test" claim. Because those omissions materially affect reviewers and downstream consumers, I cannot conclusively mark the description as complete. Expand the PR description to include a concise list of changed files/modules (notably src/js/msp.js and the new src/js/msp/debug/* files), explicitly document any breaking API changes (send_message signature, removed constants/properties), and add CI/vitest output or a link plus reproduction steps for the test fixes; also note the new global APIs (window.MSPDebug, window.MSPTestRunner) and whether consumers must update integrations, and confirm the intended commit/squash strategy before merge.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Improve msp send" is concise and directly reflects the core changes to the MSP send logic in src/js/msp.js (fixed TIMEOUT, send_message refactor, jumbo-frame cleanup), so it is related and specific rather than generic. However the PR also introduces substantial new debugging and testing tooling (msp debug modules, queue monitor, stress tests, dashboard) and test updates which the title does not surface. Under the repository's guidelines a focused, partially-related title is acceptable, so this check passes. Reviewers would benefit from a slightly broader title or an expanded one-line subtitle to make the full scope obvious at a glance.

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

🔭 Outside diff range comments (1)
src/js/msp.js (1)

402-413: ⚠️ Potential issue

_validateSendMessage ignores doCallbackOnError flag

Error-side invocation of callback_msp is unconditional. That breaks the original contract implied by the doCallbackOnError parameter and can trigger “double callbacks” or resolve a Promise that expected an actual response.

-_validateSendMessage(code, callback_msp) {
+_validateSendMessage(code, callback_msp, doCallbackOnError = false) {-    if (callback_msp) {
+    if (doCallbackOnError && callback_msp) {
         callback_msp();
     }

You’ll have to forward the flag from send_message.

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

423-431: Unused field – callbackOnError is never referenced

_createRequestObject stores callbackOnError, but no subsequent code reads it after the refactor. Either remove the property or reinstate the error-handling path.

Eliminating dead fields simplifies debugging and reduces cognitive load.


457-467: Aggressive timeout decay may under-shoot MIN_TIMEOUT

this.timeout-- happens on every send, regardless of frequency, and can push the value below MIN_TIMEOUT when timeout has already been clamped lower elsewhere.

-if (this.timeout > this.MIN_TIMEOUT) {
-    this.timeout--;
-}
+this.timeout = Math.max(this.MIN_TIMEOUT, this.timeout - 1);

A simple guard prevents negative or sub-minimum timeouts.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between dafacbd and 8c40cfe.

📒 Files selected for processing (1)
  • src/js/msp.js (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/js/msp.js (2)
src/js/serial.js (2)
  • serial (364-364)
  • serial (364-364)
src/js/gui.js (1)
  • GUI (484-484)
🔇 Additional comments (1)
src/js/msp.js (1)

415-418: Duplicate detection is too coarse

_isDuplicateRequest only checks equality of code; it ignores the payload (data).
Two requests with the same MSP code but different parameters will be treated as duplicates and the second payload may never be transmitted.

Consider comparing a (code, dataHash) tuple or always sending when dataLength > 0 (even if duplicate).

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9a05f3a and 3dd8ae1.

📒 Files selected for processing (1)
  • src/js/msp.js (1 hunks)

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)
src/js/msp.js (3)

390-398: Unused allocation for duplicates

requestObj is instantiated even when the call is considered a duplicate and then discarded. This happens on every duplicate invocation and can become a noticeable GC hot-spot under heavy traffic.

Move the allocation inside the if (!isDuplicateRequest) block so it is only created when needed.


445-456: Timer remains referenced after giving up – clear it to aid GC

When MAX_RETRIES is exceeded the function bails out but does not clearTimeout(requestObj.timer);.
While the timer has already fired, keeping the handle prevents some JS engines from reclaiming it promptly.

 if (requestObj.attempts >= this.MAX_RETRIES) {
     console.error(`MSP: Request ${requestObj.code} exceeded max retries (${this.MAX_RETRIES}), giving up`);
-    // Remove from callbacks to prevent memory leak
+    clearTimeout(requestObj.timer);          // 💡 explicit cleanup
+    // Remove from callbacks to prevent memory leak
     this._removeRequestFromCallbacks(requestObj);

411-413: Adaptive back-off step might starve high-latency links

this.timeout is decreased by a fixed 5 ms on every successful send. On slow links (e.g. Bluetooth) this can quickly shrink below the real RTT, leading to unnecessary retries. Consider decaying by a percentage (e.g. timeout *= 0.9) or pinning a lower bound based on recent RTT measurements instead of a constant 5 ms.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 031b3ec and 072b49c.

📒 Files selected for processing (1)
  • src/js/msp.js (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/js/msp.js (3)
src/js/serial.js (2)
  • serial (364-364)
  • serial (364-364)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-40)
src/js/gui.js (1)
  • GUI (484-484)

@haslinghuis haslinghuis force-pushed the improve-msp branch 3 times, most recently from eb77f13 to 4fcad77 Compare June 10, 2025 23:40
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)
src/js/msp.js (1)

399-420: ⚠️ Potential issue

Duplicate requests silently drop callbacks

When isDuplicateRequest is true, the new callback_msp is not stored anywhere, so callers issuing concurrent identical requests will never be notified.
This regresses previous behaviour and breaks APIs that rely on per-call completion.

Quick remedy: attach additional callbacks to the existing request object instead of discarding them.

-const isDuplicateRequest = this.callbacks.some((instance) => instance.requestKey === requestKey);
+const existingReq = this.callbacks.find((i) => i.requestKey === requestKey);
+const isDuplicateRequest = Boolean(existingReq);-if (!isDuplicateRequest) {
+if (!isDuplicateRequest) {
     this._setupTimeout(requestObj, bufferOut);
     this.callbacks.push(requestObj);
+} else if (callback_msp) {
+    // fan-out: queue extra callback(s) on the first request
+    (existingReq.extraCallbacks ??= []).push(callback_msp);
 }

And, when the response arrives, invoke extraCallbacks as well.

🧹 Nitpick comments (4)
src/js/msp.js (4)

376-389: Hash function may collide & yield negative keys

_createRequestKey uses a simple 32-bit rolling hash that can:

  1. Produce the same hash for different payloads (higher collision risk on large traffic).
  2. Return negative numbers, generating keys like 123:-842150451 which are harder to eyeball/debug.

Consider switching to a safer deterministic scheme, e.g. CRC-32 or base64 of the first 4 bytes plus length, to lower collision odds and keep keys human-readable.


421-428: Inefficient duplicate scan — O(N) each call

this.callbacks.some() does a linear search every time send_message is invoked.
With MAX_QUEUE_SIZE already tracked, switching to a Map<requestKey, requestObj> would give O(1) look-ups and simplify removal logic.


433-438: Timer handle lost if _setupTimeout is called twice

_setupTimeout overwrites requestObj.timer without clearing a possibly existing one.
Although current flow prevents re-entrance, guarding against future changes is cheap:

+if (requestObj.timer) {
+    clearTimeout(requestObj.timer);
+}
 requestObj.timer = setTimeout(() => {
     this._handleTimeout(requestObj, bufferOut);
 }, this.TIMEOUT);

439-472: Retry path never resets start — metrics become meaningless

requestObj.start is set once in send_message but never updated on retries, so any later duration calculations accumulate total wall-time, not per-attempt latency.

Either reset start inside _handleTimeout before re-arming the timer or drop it altogether if no longer used.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 072b49c and 20f3c6a.

📒 Files selected for processing (1)
  • src/js/msp.js (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/js/msp.js (2)
src/js/serial.js (2)
  • serial (364-364)
  • serial (364-364)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-40)
🔇 Additional comments (1)
src/js/msp.js (1)

69-73:

✅ Verification successful

Confirm removal of legacy this.timeout references

You have introduced a fixed TIMEOUT constant but removed the old adaptive this.timeout.
Please grep the rest of the file / project for stale this.timeout usages to avoid undefined-property bugs at runtime.


🏁 Script executed:

#!/bin/bash
# Find any remaining references to the old timeout field
rg --line-number $'this\\.timeout'

Length of output: 665


No stale this.timeout references found

A full‐repo search for this.timeout only returns occurrences of this.timeout_array in src/js/gui.js, not the legacy this.timeout property. There are no remaining direct references to the removed adaptive timeout field—this concern can be closed.

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

♻️ Duplicate comments (1)
src/js/msp.js (1)

482-491: _removeRequestFromCallbacks leaves Map/key tracking unresolved

If you adopt the Map optimisation above, ensure this method also deletes the requestKey entry to keep the structures in sync.

🧹 Nitpick comments (4)
src/js/msp.js (4)

69-73: Expose retry parameters instead of hard-coding them

MAX_RETRIES, MAX_QUEUE_SIZE, and TIMEOUT are baked into the module, making it impossible to tune behaviour from the outside (e.g. per-board, per-environment, unit-tests). Consider reading them from CONFIGURATOR, a constructor argument, or at least exporting setters.


399-420: Duplicate check performs O(n) scans on every call

this.callbacks.some(...) linearly scans the queue. Under heavy traffic (e.g. telemetry floods) the cost grows quadratically.

Two easy wins:

  1. Keep a Map<requestKey, requestObj> to get O(1) look-ups and removals.
  2. Build bufferOut after the duplicate check to avoid unnecessary encoding work when the request is dropped.

433-438: Timer double-cleared – harmless but needless

_handleTimeout calls clearTimeout(requestObj.timer) (l.464) and _removeRequestFromCallbacks does the same. The second call is redundant and can be omitted to save a micro-op.


439-480: Constant timeout may hammer dead links

Retries fire every fixed TIMEOUT ms. On an unplugged device this means 10 exact re-transmits per second, wasting bandwidth and log space.
An exponential back-off (e.g. TIMEOUT * 2 ** attempts) curbs network noise and still converges quickly when the link is restored.

🛑 Comments failed to post (1)
src/js/msp.js (1)

376-389: 🛠️ Refactor suggestion

_createRequestKey collision risk & signed-shift bug

The DJB-style hash on a Uint8Array will inevitably collide for different payloads and, because & 0xffffffff yields a signed 32-bit value, the string key can even become negative.
A wrong “duplicate” classification silently drops tracking / retries.

-        let hash = 0;
-        for (const byte of data) {
-            hash = ((hash << 5) - hash + byte) & 0xffffffff;
-        }
-        return `${code}:${hash}`;
+        // Use a stable unsigned hash (FNV-1a) to minimise collisions
+        let hash = 2166136261 >>> 0;
+        for (const byte of data) {
+            hash ^= byte;
+            hash = Math.imul(hash, 16777619) >>> 0;
+        }
+        return `${code}:${hash.toString(16)}`;
📝 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.

    // Helper function to create a unique key for request identification
    _createRequestKey(code, data) {
        if (!data || data.length === 0) {
            return `${code}:empty`;
        }

        // Use a stable unsigned hash (FNV-1a) to minimise collisions
        let hash = 2166136261 >>> 0;
        for (const byte of data) {
            hash ^= byte;
            hash = Math.imul(hash, 16777619) >>> 0;
        }

        return `${code}:${hash.toString(16)}`;
    },
🤖 Prompt for AI Agents
In src/js/msp.js around lines 376 to 389, the _createRequestKey function uses a
DJB-style hash that can produce collisions and negative values due to signed
32-bit shifts. To fix this, replace the hash calculation with a more robust
hashing method that avoids collisions and ensures the hash is always unsigned
and positive. For example, use a standard hashing library or implement a
non-signed hash function that produces a unique, non-negative key string for the
given data.

src/js/msp.js Outdated
}
// Create unique key combining code and data
const requestKey = this._createRequestKey(code, data);
const isDuplicateRequest = this.callbacks.some((instance) => instance.requestKey === requestKey);
Copy link

Choose a reason for hiding this comment

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

why not return here if a dupe is detected ? saves your the conditions below. looks like the code is not following "one return per function" convention anyways.

Copy link

@AJ92 AJ92 Jun 11, 2025

Choose a reason for hiding this comment

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

and, why not "rate-limit" the events from sliders at the source ? i bet there are different event types that need different handling anyways.... like those motor sliders, where probably first and last event is very important, but all events in between can be lost without any noticeable effect for the user, but then there are events that cannot be lost, like transmission of cli stuff ?

src/js/msp.js Outdated
Comment on lines 417 to 423
this._setupTimeout(requestObj, bufferOut);
this.callbacks.push(requestObj);
}

this.callbacks.push(obj);

// always send messages with data payload (even when there is a message already in the queue)
if (data || !requestExists) {
if (this.timeout > this.MIN_TIMEOUT) {
this.timeout--;
}

// Send message if it has data or is a new request
if (data || !isDuplicateRequest) {
serial.send(bufferOut, (sendInfo) => {
Copy link

@AJ92 AJ92 Jun 11, 2025

Choose a reason for hiding this comment

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

(note the line range if this comment)

i would probably start the timeout and call send only if the queue is empty, or if the previous "send" succeeded?

otherwise the buffers of the serial protocols are immediately filled and can overflow

src/js/msp.js Outdated
);

// Check if max retries exceeded OR queue is too large
if (requestObj.attempts >= this.MAX_RETRIES || this.callbacks.length > this.MAX_QUEUE_SIZE) {
Copy link

Choose a reason for hiding this comment

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

i would perform the queue size check prior to insertion of the objects, in the send_message function

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/js/msp.js (4)

69-73: Freeze or namespace retry-config constants to prevent accidental mutation

Defining MAX_RETRIES, MAX_QUEUE_SIZE and TIMEOUT directly on the mutable MSP object means any consumer can overwrite them at runtime:

MSP.TIMEOUT = 1;   // 💥

If you really want them tweakable, expose an explicit setter.
Otherwise, lock them down:

-    MAX_RETRIES: 10,
-    MAX_QUEUE_SIZE: 50,
-    TIMEOUT: 1000,
+    MAX_RETRIES: 10,
+    MAX_QUEUE_SIZE: 50,
+    TIMEOUT: 1000,
 }
+
+Object.freeze(MSP);            // or freeze only MSP.retryConfig sub-object

376-389: Hashing scheme is fragile & collision-prone

_createRequestKey builds a 32-bit additive hash; for large or similar payloads collisions are easy, producing false “duplicates”.
Additionally hash may end up negative, so keys like 42:-123 and 42:327375 look different but are produced from different sessions of the same data length.

Consider:

  • Use a stable digest (e.g. DJB2, MurmurHash) or crypto.subtle.digest('SHA-1', data) when available.
  • Fall back to btoa(String.fromCharCode(...data)) for short payloads.
  • At minimum, >>> 0 to keep the value unsigned.

421-428: Guard callback_sent with a functional check

promise() passes false, so callback_sent() may be a boolean.
A trivial typeof guard avoids accidental false is not a function mistakes if the signature changes upstream.

-if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
+if (sendInfo.bytesSent === bufferOut.byteLength && typeof callback_sent === "function") {
     callback_sent();
 }

479-489: O(N²) removal can hurt with large queues

indexOf + splice is fine for the 50-item cap, but if the limit is raised later it becomes quadratic.
Storing callbacks in a Map<requestKey, requestObj> would give O(1) insert/remove/lookup, simplify duplicate handling, and eliminate linear scans elsewhere.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8b03408 and debfc36.

📒 Files selected for processing (1)
  • src/js/msp.js (2 hunks)
🔇 Additional comments (2)
src/js/msp.js (2)

433-438: Timeout set-up looks good

_setupTimeout is concise and centralises timer arming – nice.


439-477: Retry handler: no back-off & relies on sendInfo.bytesSent

  1. A fixed 1 s retry interval can hammer sluggish links. Consider exponential back-off or TIMEOUT * (1 + attempts) to avoid congestion.
  2. If the serial driver does not populate bytesSent, sendInfo.bytesSent === bufferOut.byteLength evaluates false ⇒ the request aborts immediately. Validate field presence or fall back to truthiness.
- if (sendInfo.bytesSent === bufferOut.byteLength) {
+ if (!sendInfo || sendInfo.bytesSent === undefined || sendInfo.bytesSent === bufferOut.byteLength) {
  1. You re-assign requestObj.timer but never clear it inside _handleTimeout.
    Although the fired timer auto-invalidates, explicitly clearTimeout(requestObj.timer) before re-arming removes any doubt.

@nerdCopter
Copy link
Member

checking the coderrabbit "nitpicks"? i'm finding them useful outside of this project.

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

396-416: Log uses stale property this.timeout

The warning logs this.timeout, which no longer exists. Use this.TIMEOUT.

-                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${this.timeout} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
+                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${this.TIMEOUT} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,

60-60: Consider configurable TIMEOUT

A single 1000 ms works for consistency, but exposing it via settings/env (with a sane default) aids testing across devices/links without code changes.

Do you want a small patch to read an override from local storage or a query flag for the debug tools?

📜 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 ef9218c and 4a17c89.

📒 Files selected for processing (2)
  • src/js/msp.js (4 hunks)
  • src/js/msp/debug/MSP_DEBUG_README.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/msp/debug/MSP_DEBUG_README.md
🧰 Additional context used
🧬 Code graph analysis (1)
src/js/msp.js (3)
src/js/serial.js (2)
  • serial (319-319)
  • serial (319-319)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-38)
src/js/gui.js (1)
  • GUI (465-465)
🔇 Additional comments (3)
src/js/msp.js (3)

423-424: Good: only signal callback_sent on full send

Guarding on bytesSent === bufferOut.byteLength avoids false positives.


288-290: Strict equality is correct here

Nice micro-fix to avoid accidental truthiness traps.


67-67: Comment clarity LGTM

Naming the CLI buffer intent helps.

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

Caution

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

⚠️ Outside diff range comments (2)
test/js/msp/MSPHelper.test.js (1)

14-37: Test contradicts new “no callback on CRC error” behavior

process_data now suppresses callbacks on CRC failure, but this test still expects the callback to fire. Either update the test (if the new behavior is intended) or revert the MSPHelper change. Recommended here: align the test to expect no callback.

Apply this diff if keeping the new behavior:

-            expect(callbackCalled).toEqual(true);
+            expect(callbackCalled).toEqual(false);
src/js/msp/MSPHelper.js (1)

1757-1776: Suppressing callbacks on CRC errors may regress retries and tooling

Callbacks are now skipped when crcError === true. Several callsites (e.g., dataflash reads) previously used the callback to trigger retries and error handling; suppressing it can stall flows until timeout and breaks the existing “error surfaced via callback” contract.

Prefer always invoking the callback and letting the consumer decide:

-            if (!crcError) {
-                // fire callback
-                if (callback) {
-                    callback({ command: code, data: data, length: data.byteLength, crcError: crcError });
-                }
-            } else {
-                console.warn(`code: ${code} - crc failed. No callback`);
-            }
+            // Always notify; consumer handles crcError and retries.
+            if (callback) {
+                const payload = crcError
+                    ? { command: code, data: new DataView(new ArrayBuffer(0)), length: 0, crcError: true }
+                    : { command: code, data, length: data.byteLength, crcError: false };
+                callback(payload);
+            }
🧹 Nitpick comments (1)
src/js/msp/MSPHelper.js (1)

2709-2747: Minor: redundant bit_set usage

mask |= bit_set(mask, idx) is redundant; bit_set likely returns mask | (1<<idx). Use assignment for clarity.

-                    mask |= bit_set(mask, bitIndex + 12);
+                    mask = bit_set(mask, bitIndex + 12);

(Apply similarly to other bit_set sites in this block.)

📜 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 4a17c89 and 9d3cee3.

📒 Files selected for processing (3)
  • src/js/msp.js (4 hunks)
  • src/js/msp/MSPHelper.js (1 hunks)
  • test/js/msp/MSPHelper.test.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/msp.js
🧰 Additional context used
🧬 Code graph analysis (1)
test/js/msp/MSPHelper.test.js (1)
src/js/fc.js (1)
  • FC (131-990)
🔇 Additional comments (5)
test/js/msp/MSPHelper.test.js (2)

6-6: Bump to API_VERSION_1_47 looks good

Import update is consistent with the PR goal to test against latest firmware.


81-81: Setting FC.CONFIG.apiVersion to 1.47 is correct

This ensures the MSP_BOARD_INFO expectations match the newer payload layout.

src/js/msp/MSPHelper.js (3)

11-11: API_VERSION_1_47 import is correct

Needed for new 1.47 feature gates added below.


242-244: Verified: FC schema includes the 1.47 fields — no changes required

numberOfRateProfiles, RC_TUNING.throttle_HOVER, SENSOR_CONFIG.opticalflow_hardware (and SENSOR_CONFIG_ACTIVE via spread), and MCU_INFO {id,name} are defined in src/js/fc.js (numberOfRateProfiles L35; throttle_HOVER L299; opticalflow_hardware L596; MCU_INFO L601); MSPHelper reads/writes them (src/js/msp/MSPHelper.js L242–244, L472–474, L1230–1249, L1854–1857, L2237–2240); VirtualFC provides defaults (src/js/VirtualFC.js L269–272).


2455-2509: dataflashRead may no longer retry on CRC error

This path relies on the response callback to detect crcError and call onDataCallback(address, null) to force a retry. With callbacks suppressed on CRC error, this logic won’t run; reads may hang until timeout.

Please confirm MSP.send_message now invokes the MSP callback on timeout with an error indicator so dataflashRead can retry. Otherwise, keep firing the callback from process_data even on CRC errors (see prior diff).

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

Caution

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

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

395-401: Fix logging: replace this.timeout with this.TIMEOUT (this.timeout is undefined)

this.timeout is undefined and will log "undefined" — change to this.TIMEOUT. Location: src/js/msp.js:397

Apply:

-                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${this.timeout} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
+                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${this.TIMEOUT} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,

393-413: Add bounded retries and re‑arm timer on MSP timeout; drop dead requests after N attempts

File: src/js/msp.js
Lines: 393-413

-        if (!requestExists) {
-            obj.timer = setTimeout(() => {
-                console.warn(
-                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
-                        this.timeout
-                    } QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
-                );
-                serial.send(bufferOut, (_sendInfo) => {
-                    obj.stop = performance.now();
-                    const executionTime = Math.round(obj.stop - obj.start);
-                    // We should probably give up connection if the request takes too long ?
-                    if (executionTime > 5000) {
-                        console.warn(
-                            `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
-                        );
-                    }
-
-                    clearTimeout(obj.timer); // prevent leaks
-                });
-            }, this.TIMEOUT);
-        }
+        if (!requestExists) {
+            const MAX_RETRIES = 3;
+            const scheduleRetry = () => {
+                obj.timer = setTimeout(() => {
+                    obj.attempts = (obj.attempts || 0) + 1;
+                    console.warn(
+                        `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${this.TIMEOUT} ATTEMPT: ${obj.attempts}/${MAX_RETRIES} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
+                    );
+                    serial.send(bufferOut, (_sendInfo) => {
+                        obj.stop = performance.now();
+                        const executionTime = Math.round(obj.stop - obj.start);
+                        if (executionTime > 5000) {
+                            console.warn(
+                                `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
+                            );
+                        }
+
+                        if (obj.attempts < MAX_RETRIES) {
+                            scheduleRetry();
+                        } else {
+                            clearTimeout(obj.timer);
+                            const idx = this.callbacks.indexOf(obj);
+                            if (idx !== -1) this.callbacks.splice(idx, 1); // drop dead request
+                            // Optional: emit an explicit error to the original caller (e.g. callback_msp) instead of silent drop
+                        }
+                    });
+                }, this.TIMEOUT);
+            };
+            scheduleRetry();
+        }

Single resend currently leaves requests stuck; this adds bounded retries and removes the callback entry after MAX_RETRIES. Confirm desired UX on final failure: silent drop vs. invoking callback_msp with an error payload.

♻️ Duplicate comments (1)
src/js/msp.js (1)

415-419: Fix duplicate callback handling to prevent queue growth

send_message pushes duplicate callback objects when a request with the same code already exists (no timer is set for duplicates), while the response path only removes entries from dataHandler.callbacks (MSPHelper). As a result, entries in MSP.callbacks can accumulate.

  • Problem: duplicates are pushed without timers in src/js/msp.js (send_message — ~lines 382–415).
  • Evidence: removal happens in src/js/msp/MSPHelper.js (process_data loop that splices dataHandler.callbacks — ~lines 1757–1767), not from MSP.callbacks.
  • Fix (choose one): attach duplicate callbacks to the existing request object (store callbacks as an array) OR ensure duplicates get timers and update the response handler to remove all matching entries (splice all items with same code/requestBuffer) when a reply arrives.
  • Also: make timer cleanup consistent (use clearTimeout for timeouts).
🧹 Nitpick comments (1)
src/js/msp.js (1)

382-385: Dedupe on code + payload size (low-risk improvement)

Code-only dedupe can misclassify concurrent requests that share code but differ in payload. Without changing the callback resolution model, you can cheaply include payload size by leveraging the already-built bufferOut.

Apply this minimal diff (order matters):

-        const requestExists = this.callbacks.some((instance) => instance.code === code);
-
-        const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
+        const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
+        const requestExists = this.callbacks.some(
+            (instance) => instance.code === code && instance.requestBuffer?.byteLength === bufferOut.byteLength
+        );
📜 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 9d3cee3 and 363fb56.

📒 Files selected for processing (1)
  • src/js/msp.js (4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-19T20:41:42.428Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:377-382
Timestamp: 2025-09-19T20:41:42.428Z
Learning: In the MSP.send_message function in src/js/msp.js, the doCallbackOnError parameter was removed as it was never used in production code throughout the entire codebase.

Applied to files:

  • src/js/msp.js
📚 Learning: 2025-09-19T20:42:20.314Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.314Z
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/msp.js
📚 Learning: 2025-09-19T20:41:44.246Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.246Z
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/msp.js
📚 Learning: 2025-08-27T19:36:13.733Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4579
File: src/js/tabs/pid_tuning.js:80-82
Timestamp: 2025-08-27T19:36:13.733Z
Learning: MSP_STATUS_EX calls in serial_backend.js and setup.js are intentionally unguarded because they are used for status fetching on page load to initialize profiles and are safe to call on older firmware versions. Only conditional/optional MSP_STATUS_EX usage (like in pid_tuning.js) needs version gating to API_VERSION_25_12.

Applied to files:

  • src/js/msp.js
🧬 Code graph analysis (1)
src/js/msp.js (3)
src/js/serial.js (2)
  • serial (319-319)
  • serial (319-319)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-38)
src/js/gui.js (1)
  • GUI (465-465)
🔇 Additional comments (5)
src/js/msp.js (5)

60-60: Fixed, single source of truth for timeout looks good

Defining a constant timeout is a good simplification for stability.


65-68: No action needed

Comment-only/tiny change.


286-289: Idempotent listener registration

Using strict equality with indexOf prevents duplicate handlers. Looks good.


374-380: Signature cleanup: removed unused parameter

Dropping doCallbackOnError simplifies the API and reflects actual usage. Early return still invokes callback_msp for fast-fail UX. LGTM.


420-421: Send confirmation gate is correct

Guarding callback_sent on exact byte count avoids false positives. LGTM.

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

Caution

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

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

394-414: Unbounded pending on no-response: add bounded retry + cleanup

Verified: requests are pushed into this.callbacks (src/js/msp.js) and only removed when MSPHelper processes a reply (clearInterval + splice in src/js/msp/MSPHelper.js ~1764). callbacks_cleanup exists (src/js/msp.js ~440) but if the target never replies a callback entry stays in the queue indefinitely. Add a small retry budget and remove stale entries after retries.

Minimal change suggestion (unchanged from original):

 const obj = {
   code,
   requestBuffer: bufferOut,
   callback: callback_msp,
+  attempts: 0,
   start: performance.now(),
 };
 obj.timer = setTimeout(() => {
+    obj.attempts += 1;
     console.warn(
       `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
         this.TIMEOUT
       } QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
     );
-    serial.send(bufferOut, (_sendInfo) => {
+    const MAX_RETRIES = 1; // one resend after the initial send
+    if (obj.attempts > MAX_RETRIES) {
+        clearTimeout(obj.timer);
+        const idx = this.callbacks.indexOf(obj);
+        if (idx > -1) this.callbacks.splice(idx, 1);
+        return;
+    }
+    serial.send(bufferOut, (_sendInfo) => {
         obj.stop = performance.now();
         const executionTime = Math.round(obj.stop - obj.start);
         // We should probably give up connection if the request takes too long ?
         if (executionTime > 5000) {
             console.warn(
                 `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
             );
         }
         clearTimeout(obj.timer); // prevent leaks
     });
 }, this.TIMEOUT);
♻️ Duplicate comments (1)
src/js/msp.js (1)

416-416: Avoid pushing duplicates without callbacks

Pushing every duplicate grows the queue unnecessarily when no callback_msp is provided.

-        this.callbacks.push(obj);
+        // Avoid queuing noop duplicates
+        if (!requestExists || callback_msp) {
+            this.callbacks.push(obj);
+        }
🧹 Nitpick comments (1)
src/js/msp.js (1)

60-60: Fix TIMEOUT reference in log message

The warning logs ${this.timeout} but the constant is TIMEOUT, so the message shows undefined.

-                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
-                        this.timeout
-                    } QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
+                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
+                        this.TIMEOUT
+                    } QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,

Also applies to: 394-414

📜 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 363fb56 and abc5383.

📒 Files selected for processing (1)
  • src/js/msp.js (4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-19T20:41:42.428Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:377-382
Timestamp: 2025-09-19T20:41:42.428Z
Learning: In the MSP.send_message function in src/js/msp.js, the doCallbackOnError parameter was removed as it was never used in production code throughout the entire codebase.

Applied to files:

  • src/js/msp.js
📚 Learning: 2025-09-19T20:42:20.314Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.314Z
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/msp.js
📚 Learning: 2025-09-19T20:41:44.246Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.246Z
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/msp.js
📚 Learning: 2025-08-27T19:36:13.733Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4579
File: src/js/tabs/pid_tuning.js:80-82
Timestamp: 2025-08-27T19:36:13.733Z
Learning: MSP_STATUS_EX calls in serial_backend.js and setup.js are intentionally unguarded because they are used for status fetching on page load to initialize profiles and are safe to call on older firmware versions. Only conditional/optional MSP_STATUS_EX usage (like in pid_tuning.js) needs version gating to API_VERSION_25_12.

Applied to files:

  • src/js/msp.js
🧬 Code graph analysis (1)
src/js/msp.js (3)
src/js/serial.js (2)
  • serial (319-319)
  • serial (319-319)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-38)
src/js/gui.js (1)
  • GUI (465-465)
🔇 Additional comments (5)
src/js/msp.js (5)

383-386: Duplicate detection scope is acceptable (code + payload size)

This aligns with the safer, simple approach noted in your learnings; avoids the tab-switch regressions from requestKey schemes.


421-422: bytesSent guard before callback_sent is solid

Prevents false positives on partial writes.


286-288: Strict equality check LGTM

Prevents accidental truthiness matches for listener existence.


65-65: Minor doc polish LGTM

Comment fix improves clarity.


374-380: Good cleanup — removed unused doCallbackOnError param; early-return behavior is consistent

Sandbox verification failed (search scripts errored). Run: rg -nP --type=js 'send_message\s*([^)],[^)],[^)],[^)],[^)]*)' and confirm no callers pass five arguments to send_message.

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

394-412: Single retry then potential hang/leak — add a terminal outcome
Timer resends once and clears its own timer, but if that resend also fails and no response ever arrives, obj remains in callbacks indefinitely. Over time this can bloat the queue and degrade lookups.

Consider a bounded retry budget plus cleanup. Example:

+// near TIMEOUT
+RETRY_LIMIT: 1,
…
 if (!requestExists) {
   obj.timer = setTimeout(() => {
+    obj.attempts = (obj.attempts || 0) + 1;
     console.warn(
       `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
     );
     serial.send(bufferOut, (_sendInfo) => {
       obj.stop = performance.now();
       const executionTime = Math.round(obj.stop - obj.start);
       // We should probably give up connection if the request takes too long ?
       if (executionTime > 5000) {
         console.warn(
           `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
         );
       }
-      clearTimeout(obj.timer); // prevent leaks
+      clearTimeout(obj.timer);
+      if (obj.attempts <= this.RETRY_LIMIT) {
+        // re-arm a final timeout
+        obj.timer = setTimeout(() => {
+          // give up: drop the request to avoid leaks
+          clearTimeout(obj.timer);
+          const idx = this.callbacks.indexOf(obj);
+          if (idx > -1) this.callbacks.splice(idx, 1);
+          console.warn(`MSP: max retries exceeded for code ${code}`);
+        }, this.TIMEOUT);
+      }
     });
   }, this.TIMEOUT);
 }

If you prefer zero follow-up retries, skip the resend and just fail/cleanup after TIMEOUT.

🧹 Nitpick comments (3)
src/js/msp.js (3)

60-60: Fixed 1s TIMEOUT — consider making it configurable per transport/profile
Hard‑coded 1000ms works, but links vary (WebSerial vs BLE). Consider a small config surface (e.g., MSP.TIMEOUT from settings/env) with a sane default of 1000ms. Keeps tests and future tuning simpler.


383-386: Duplicate detection may collide on same-length payloads
requestExists matches on (code && bufferOut.byteLength). Two different payloads of equal length will be treated as duplicates, which alters timer/setup behavior and could skew latency/backoff for the second request.

Minimal hardening without touching callback resolution:

-const requestExists = this.callbacks.some(
-    (instance) => instance.code === code && instance.requestBuffer?.byteLength === bufferOut.byteLength,
-);
+const view = new Uint8Array(bufferOut);
+const keyCrc = this.crc8_dvb_s2_data(view, 0, view.length);
+const requestExists = this.callbacks.some(
+    (i) => i.code === code &&
+           i.requestBuffer?.byteLength === bufferOut.byteLength &&
+           this.crc8_dvb_s2_data(new Uint8Array(i.requestBuffer), 0, i.requestBuffer.byteLength) === keyCrc
+);

This stays local (no new keys/indices) and reduces false positives while preserving the simple resolution model that previously proved stable.


311-353: Nit: prefer const over let for non-reassigned buffers
Minor readability/intent improvement only.

-        let bufferOut = new ArrayBuffer(bufferSize);
-        let bufView = new Uint8Array(bufferOut);
+        const bufferOut = new ArrayBuffer(bufferSize);
+        const bufView = new Uint8Array(bufferOut);

Applies similarly in encode_message_v2.

📜 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 abc5383 and 16f1a95.

📒 Files selected for processing (1)
  • src/js/msp.js (3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-19T20:41:42.428Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:377-382
Timestamp: 2025-09-19T20:41:42.428Z
Learning: In the MSP.send_message function in src/js/msp.js, the doCallbackOnError parameter was removed as it was never used in production code throughout the entire codebase.

Applied to files:

  • src/js/msp.js
📚 Learning: 2025-09-19T20:42:20.314Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.314Z
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/msp.js
📚 Learning: 2025-09-19T20:41:44.246Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.246Z
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/msp.js
📚 Learning: 2025-08-27T19:36:13.733Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4579
File: src/js/tabs/pid_tuning.js:80-82
Timestamp: 2025-08-27T19:36:13.733Z
Learning: MSP_STATUS_EX calls in serial_backend.js and setup.js are intentionally unguarded because they are used for status fetching on page load to initialize profiles and are safe to call on older firmware versions. Only conditional/optional MSP_STATUS_EX usage (like in pid_tuning.js) needs version gating to API_VERSION_25_12.

Applied to files:

  • src/js/msp.js
🧬 Code graph analysis (1)
src/js/msp.js (3)
src/js/serial.js (2)
  • serial (319-319)
  • serial (319-319)
src/js/data_storage.js (1)
  • CONFIGURATOR (7-38)
src/js/gui.js (1)
  • GUI (465-465)
🔇 Additional comments (5)
src/js/msp.js (5)

65-65: Comment/intent clarity: LGTM
Typo fix and intent (“character output”) are clear.


286-286: Strict equality in listener registration: LGTM
Correct, avoids accidental truthy matches.


414-414: Incorrect — duplicates are already removed by process_data

process_data iterates dataHandler.callbacks in reverse and splices every entry whose code matches (clearInterval + splice + callback), so duplicate entries are removed. I couldn't find any callbacks.push in src/js/msp; the referenced this.callbacks.push(obj) is not present. See src/js/msp/MSPHelper.js around lines ~1734 and ~1764–1767.

Likely an incorrect or invalid review comment.


419-420: No change needed — send() callbacks report full buffer length on completion.
Serial.send delegates to _protocol.send; WebBluetooth, WebSerial and WebSocket implementations call the callback with bytesSent: data.byteLength (and update protocol.bytesSent), so comparing sendInfo.bytesSent === bufferOut.byteLength is correct.


374-381: Confirm no callers rely on silent early-return from send_message

send_message now early-returns when (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) but still calls callback_msp(); confirm no callers relied on the previous “silent” behavior after doCallbackOnError was removed. File: src/js/msp.js (lines 374–381).

Run locally to find callsites: rg -nP --hidden -S --glob '!node_modules' -g '.js' -g '.ts' 'send_message\s*(' || true

@haslinghuis
Copy link
Member Author

@coderabbitai full review

Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

✅ Actions performed

Full review triggered.

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

Caution

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

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

1768-1775: Suppressing callbacks on CRC error breaks retries and tests

This change prevents invoking the callback on CRC errors. It breaks:

  • dataflashRead()’s retry path (expects callback on CRC error)
  • The existing test “refuses to process data with crc-error” (expects the callback to fire with crcError=true)

Recommend always invoking the callback and letting callers decide using the crcError flag.

Apply this diff:

-            if (!crcError) {
-                // fire callback
-                if (callback) {
-                    callback({ command: code, data: data, length: data.byteLength, crcError: crcError });
-                }
-            } else {
-                console.warn(`code: ${code} - crc failed. No callback`);
-            }
+            // Always notify; let the callee decide based on crcError
+            if (callback) {
+                callback({ command: code, data: data, length: data.byteLength, crcError });
+            }
♻️ Duplicate comments (4)
src/js/msp/debug/msp_test_runner.js (1)

320-333: Drive overflow from actual MAX_QUEUE_SIZE (not a fixed 100)

Use the device’s configured queue size to avoid false positives/negatives.

-                // Send more requests than queue can handle
-                for (let i = 0; i < 100; i++) {
-                    promises.push(window.MSP.promise(101, null).catch((err) => ({ error: err.message })));
-                }
+                const overBy = 10;
+                const maxQueue = (window.MSP && Number.isFinite(window.MSP.MAX_QUEUE_SIZE))
+                  ? window.MSP.MAX_QUEUE_SIZE
+                  : 100;
+                const attempts = maxQueue + overBy;
+                for (let i = 0; i < attempts; i++) {
+                    promises.push(window.MSP.promise(101, null).catch((err) => ({ error: err.message })));
+                }
@@
-                return {
-                    requestsSent: 100,
-                    successful,
-                    rejected: 100 - successful,
-                };
+                return {
+                    requestsSent: attempts,
+                    successful,
+                    rejected: attempts - successful,
+                };
src/js/msp/debug/msp_stress_test.js (3)

41-54: Derive status from the test and its semantics (don’t hardcode success)

Generic checks misclassify scenarios (e.g., duplicates should be rejected). Also, suite marks status using getTestStatus(result) without context.

Apply:

-    getTestStatus(result) {
-        if (!result || typeof result !== "object") return "FAILED";
-        if (result.error) return "FAILED";
-        if (result.memoryLeakDetected === true) return "FAILED";
-        if (result.overflowHandled === false) return "FAILED";
-        if (result.timeoutOccurred === true && result.recoveryTime > 2000) return "FAILED";
-        if (result.recoverySuccessful === false) return "FAILED";
-        if (result.failed && result.failed > 0) return "FAILED";
-        if (result.duplicateRejections && result.duplicateRejections > 0) return "FAILED";
-        if (result.leaked && result.leaked > 0) return "FAILED";
-        if (result.failedWhileDisconnected && result.failedWhileDisconnected > 0 && result.recoverySuccessful === false) return "FAILED";
-        // Add more checks as needed for new failure signals
-        return "PASSED";
-    }
+    getTestStatus(testName, result) {
+        if (!result || typeof result !== "object") return "FAILED";
+        if (result.error) return "FAILED";
+        switch (testName) {
+            case "Duplicate Request Handling": {
+                const sent = result.duplicatesSent ?? 0;
+                const dupRej = result.duplicateRejections ?? 0;
+                const successful = result.successful ?? 0;
+                return sent > 0 && dupRej === sent && successful === 0 ? "PASSED" : "FAILED";
+            }
+            case "Timeout Recovery": {
+                const timeout = result.timeoutOccurred === true;
+                const recovered = result.recoverySuccessful !== false;
+                const cleaned = result.queueCleanedUp !== false;
+                const withinSLO = typeof result.recoveryTime !== "number" || result.recoveryTime <= 2000;
+                return timeout && recovered && cleaned && withinSLO ? "PASSED" : "FAILED";
+            }
+            case "Memory Leak Detection":
+                return result.memoryLeakDetected ? "FAILED" : "PASSED";
+            case "Queue Overflow Handling":
+                return result.overflowHandled === true ? "PASSED" : "FAILED";
+            case "Connection Disruption":
+                return result.recoverySuccessful !== false ? "PASSED" : "FAILED";
+            case "Performance Under Load":
+                return typeof result.successRate === "number" ? (result.successRate >= 0.9 ? "PASSED" : "FAILED") : "PASSED";
+            default:
+                break;
+        }
+        if (typeof result.failed === "number" && result.failed > 0) return "FAILED";
+        if (result.memoryLeakDetected === true) return "FAILED";
+        if (result.overflowHandled === false) return "FAILED";
+        if (result.recoverySuccessful === false) return "FAILED";
+        if (result.timeoutOccurred === true && typeof result.recoveryTime === "number" && result.recoveryTime > 2000) return "FAILED";
+        if (typeof result.leaked === "number" && result.leaked > 0) return "FAILED";
+        return "PASSED";
+    }
@@
-                        status: this.getTestStatus(result),
+                        status: this.getTestStatus(testDef.name, result),

Also applies to: 89-96


666-673: Single-test path also hardcodes PASSED; reuse status derivation

Mirror the suite behavior.

-            const result = await testMethod();
-            return {
-                name: testName,
-                status: "PASSED",
-                result,
-                metrics: this.monitor.getStatus(),
-            };
+            const result = await testMethod();
+            return {
+                name: testName,
+                status: this.getTestStatus(
+                    {
+                        "queue-flooding": "Queue Flooding",
+                        "rapid-fire": "Rapid Fire Requests",
+                        "duplicates": "Duplicate Request Handling",
+                        "timeout-recovery": "Timeout Recovery",
+                        "memory-leaks": "Memory Leak Detection",
+                        "concurrent-mixed": "Concurrent Mixed Requests",
+                        "queue-overflow": "Queue Overflow Handling",
+                        "connection-disruption": "Connection Disruption",
+                        "performance-load": "Performance Under Load",
+                    }[testName] || testName,
+                    result
+                ),
+                result,
+                metrics: this.monitor.getStatus(),
+            };

399-441: Simulated disconnection toggles the wrong object

send_message likely checks the module-scope serial.connected, not this.msp.serial.connected; current test may not exercise the branch.

Proposed change (adjust import path as needed):

+// import the same serial used by MSP send path
+import { serial } from "../msp.js"; // verify actual path
@@
-        const originalConnected = this.msp.serial?.connected;
+        const originalConnected = serial?.connected;
@@
-            if (this.msp.serial) {
-                this.msp.serial.connected = false;
-            }
+            if (serial) serial.connected = false;
@@
-            if (this.msp.serial) {
-                this.msp.serial.connected = originalConnected;
-            }
+            if (serial) serial.connected = originalConnected;
@@
-            if (this.msp.serial) {
-                this.msp.serial.connected = originalConnected;
-            }
+            if (serial) serial.connected = originalConnected;
#!/bin/bash
# Locate the canonical serial used by MSP and how connected is referenced
rg -n -C2 --type=js '\bserial\b.*connected' src | sed -n '1,200p'
🧹 Nitpick comments (19)
src/js/utils/checkBrowserCompatibility.js (2)

31-35: Harden Chromium detection (UA‑CH brands) and use strict equality

Some Chromium browsers expose brands like "Google Chrome" or "Microsoft Edge" instead of only "Chromium". Also, prefer ===.

Apply:

-    if (navigator.userAgentData) {
-        return navigator.userAgentData.brands.some((brand) => {
-            return brand.brand == "Chromium";
-        });
-    }
+    if (navigator.userAgentData) {
+        return navigator.userAgentData.brands.some(({ brand }) =>
+            ["Chromium", "Google Chrome", "Microsoft Edge"].includes(brand)
+        );
+    }

77-85: Silence logs under test to reduce noisy output

Now that tests short‑circuit via isTestEnvironment, gate logs to keep CI output clean.

-    console.log("User Agent: ", navigator.userAgentData);
-    console.log("Native: ", isNative);
-    console.log("Chromium: ", isChromium);
-    console.log("Web Serial: ", isWebSerial);
-    console.log("OS: ", getOS());
-
-    console.log("Android: ", isAndroid());
-    console.log("iOS: ", isIOS());
-    console.log("Capacitor web: ", isCapacitorWeb());
+    if (!isTestEnvironment) {
+        console.log("User Agent: ", navigator.userAgentData);
+        console.log("Native: ", isNative);
+        console.log("Chromium: ", isChromium);
+        console.log("Web Serial: ", isWebSerial);
+        console.log("OS: ", getOS());
+        console.log("Android: ", isAndroid());
+        console.log("iOS: ", isIOS());
+        console.log("Capacitor web: ", isCapacitorWeb());
+    }
src/js/msp/MSPHelper.js (1)

2455-2508: MSP.send_message signature changed; remove the 5th argument

send_message no longer accepts a 5th parameter. Passing true here is a no-op and can mislead future readers. Also, given the CRC-callback behavior change above, this call currently won’t retry.

Apply this diff:

     MSP.send_message(
         MSPCodes.MSP_DATAFLASH_READ,
         outData,
         false,
-        function (response) {
+        function (response) {-        },
-        true,
+        }
     );
src/js/msp.js (1)

385-391: Duplicate detection is O(n · payload) per send; cache CRC on request objects

You recompute CRC for every in-flight entry. Cache a requestCrc on each request to reduce per-send work.

Apply this diff:

-        const requestExists = this.callbacks.some(
+        const requestExists = this.callbacks.some(
             (i) =>
                 i.code === code &&
                 i.requestBuffer?.byteLength === bufferOut.byteLength &&
-                this.crc8_dvb_s2_data(new Uint8Array(i.requestBuffer), 0, i.requestBuffer.byteLength) === keyCrc,
+                i.requestCrc === keyCrc,
         );
…
         const obj = {
             code,
             requestBuffer: bufferOut,
+            requestCrc: keyCrc,
             callback: callback_msp,
             start: performance.now(),
         };
src/js/msp/debug/msp_queue_monitor.js (2)

94-113: Duplicate metric uses req.data which is never set

MSP stores requestBuffer (and not data). This logic will undercount duplicates with payloads.

Suggestion: compare against req.requestBuffer (length + CRC) to approximate payload equality.


432-446: JSON reports will stringify Maps as {}

requestsByCode and errorsByType are Maps. Convert to plain objects in the report to preserve data.

Apply this diff:

-            rawMetrics: this.metrics,
+            rawMetrics: {
+                ...this.metrics,
+                requestsByCode: Object.fromEntries(this.metrics.requestsByCode),
+                errorsByType: Object.fromEntries(this.metrics.errorsByType),
+            },
src/js/main.js (1)

21-29: Gate debug tools behind DEV to avoid shipping in prod

This unconditional dynamic import will load debug tooling in production builds. If that’s not intended, gate with import.meta.env.DEV (and optionally localhost).

Apply this diff:

-import("./msp/debug/msp_debug_tools.js")
+if (import.meta.env.DEV) {
+  import("./msp/debug/msp_debug_tools.js")
     .then(() => {
         console.log("🔧 MSP Debug Tools loaded for development environment");
         console.log("• Press Ctrl+Shift+M to toggle debug dashboard");
         console.log("• Use MSPTestRunner.help() for all commands");
     })
     .catch((err) => {
         console.warn("Failed to load MSP debug tools:", err);
     });
+}
src/js/msp/debug/msp_debug_tools.js (2)

7-11: Redundant import of msp_queue_monitor

You statically import the monitor and also dynamically import it later. Drop one to avoid confusion.


36-52: Dev detection via hostname only

Consider also checking import.meta.env.DEV to avoid accidental activation in locally served prod bundles.

src/js/msp/debug/msp_debug_dashboard.js (7)

794-806: Fix repeated DOM writes: compare strings, not mixed types

textContent is a string; comparing it to numbers/objects forces unnecessary updates every tick.

Apply:

-        if (element && element.textContent !== value) {
-            element.textContent = value;
-        }
+        if (element) {
+            const newText = String(value ?? "");
+            if (element.textContent !== newText) {
+                element.textContent = newText;
+            }
+        }

131-134: Guard against early execution before document.body exists

Creating the singleton at import time may run before DOM is ready.

-        document.body.appendChild(this.container);
+        if (document.body) {
+            document.body.appendChild(this.container);
+        } else {
+            window.addEventListener('DOMContentLoaded', () => {
+                document.body.appendChild(this.container);
+            }, { once: true });
+        }

192-205: Deduplicate conflicting CSS rules (.test-result-item, .alert-item, #updates-status)

Same selectors are defined twice with different styles; order-dependent and brittle.

Remove one set and keep a single definition per selector (prefer the more detailed ones at lines 375–399, 411–418, 420–432).

Also applies to: 342-357, 411-418, 420-432


857-864: Harden queue item rendering when fields are missing

item.age can be undefined/NaN; render a placeholder to avoid “NaNms”.

-                        <span>Age: ${Math.round(item.age)}ms</span>
+                        <span>Age: ${
+                          Number.isFinite(item.age) ? Math.round(item.age) + "ms" : "—"
+                        }</span>

1156-1191: Gate global debug surface from production builds

Avoid shipping a powerful global API in prod.

-// Add global shortcut and console commands
-window.MSPDebug = {
+// Add global shortcut and console commands (dev only)
+if ((typeof process === 'undefined') || process?.env?.NODE_ENV !== 'production') {
+  window.MSPDebug = {
     dashboard: mspDebugDashboard,
     monitor: mspQueueMonitor,
     stressTest: mspStressTest,
@@
-};
+  };
+}

884-887: Remove redundant alias method

_updateChart merely forwards to updateChart; drop it.

-    _updateChart(status) {
-        this.updateChart(status);
-    }
+    // (removed)

Also update the single call site to call updateChart directly.


613-625: Unify default monitoring interval

The button uses 500ms, global startMonitoring() passes no interval. Pick one default to avoid confusion.

Also applies to: 1164-1166

src/js/msp/debug/msp_test_runner.js (2)

43-49: Make periodic logging time-based, not modulo Date.now()

Modulo can fire multiple times or starve depending on tick timing.

-            // Log every 10 seconds if monitoring
-            if (Date.now() % 10000 < 500) {
-                console.log(
+            // Log ~every 10s
+            const now = Date.now();
+            this._lastQuickLogTs = this._lastQuickLogTs ?? 0;
+            if (now - this._lastQuickLogTs >= 10_000) {
+                this._lastQuickLogTs = now;
+                console.log(
                     `📊 MSP Status: Queue=${status.currentQueueSize}/${status.maxQueueSize}, Requests=${status.metrics.totalRequests}, AvgTime=${Math.round(status.metrics.avgResponseTime)}ms`,
                 );
             }

220-286: Health-check robustness

Consider a bounded overall timeout to avoid hanging if MSP stalls.

src/js/msp/debug/msp_stress_test.js (1)

132-141: Use device MAX_QUEUE_SIZE for flood test, not a magic 110

Avoid under/over-stressing on different builds.

-        const requestCount = 110; // More than default MAX_QUEUE_SIZE
+        const requestCount = (this.msp.MAX_QUEUE_SIZE || 100) + 10;
📜 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 e62fa71 and f39a39b.

📒 Files selected for processing (12)
  • src/js/main.js (2 hunks)
  • src/js/msp.js (4 hunks)
  • src/js/msp/MSPHelper.js (1 hunks)
  • src/js/msp/debug/MSP_DEBUG_README.md (1 hunks)
  • src/js/msp/debug/index.js (1 hunks)
  • src/js/msp/debug/msp_debug_dashboard.js (1 hunks)
  • src/js/msp/debug/msp_debug_tools.js (1 hunks)
  • src/js/msp/debug/msp_queue_monitor.js (1 hunks)
  • src/js/msp/debug/msp_stress_test.js (1 hunks)
  • src/js/msp/debug/msp_test_runner.js (1 hunks)
  • src/js/utils/checkBrowserCompatibility.js (1 hunks)
  • test/js/msp/MSPHelper.test.js (2 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-19T20:41:42.428Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:377-382
Timestamp: 2025-09-19T20:41:42.428Z
Learning: In the MSP.send_message function in src/js/msp.js, the doCallbackOnError parameter was removed as it was never used in production code throughout the entire codebase.

Applied to files:

  • src/js/msp/MSPHelper.js
  • src/js/msp.js
  • src/js/msp/debug/msp_queue_monitor.js
📚 Learning: 2025-09-19T20:42:20.314Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:42:20.314Z
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/msp.js
📚 Learning: 2025-09-19T20:41:44.246Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp.js:384-391
Timestamp: 2025-09-19T20:41:44.246Z
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/msp.js
📚 Learning: 2025-08-27T19:36:13.733Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4579
File: src/js/tabs/pid_tuning.js:80-82
Timestamp: 2025-08-27T19:36:13.733Z
Learning: MSP_STATUS_EX calls in serial_backend.js and setup.js are intentionally unguarded because they are used for status fetching on page load to initialize profiles and are safe to call on older firmware versions. Only conditional/optional MSP_STATUS_EX usage (like in pid_tuning.js) needs version gating to API_VERSION_25_12.

Applied to files:

  • src/js/msp.js
📚 Learning: 2025-08-26T23:02:46.827Z
Learnt from: haslinghuis
PR: betaflight/betaflight-configurator#4510
File: src/js/msp/debug/msp_queue_monitor.js:140-156
Timestamp: 2025-08-26T23:02:46.827Z
Learning: The user haslinghuis indicated that defensive programming suggestions for performance.now() and requestObj.start in MSP queue monitoring code were unnecessary as the code was previously approved. The user prefers not to add defensive guards in this context.

Applied to files:

  • src/js/msp/debug/msp_queue_monitor.js
🔇 Additional comments (14)
src/js/utils/checkBrowserCompatibility.js (1)

71-76: Test override for compatibility check looks good

Solid guard against ReferenceError and a pragmatic way to keep tests passing irrespective of WebSerial/USB/BLE support.

test/js/msp/MSPHelper.test.js (3)

6-6: API version bump to 1.47

Import update looks correct and matches the production code path.


81-81: Aligns test setup with API 1.47

Setting FC.CONFIG.apiVersion = API_VERSION_1_47; is consistent with the new parsing branches.


14-37: Test expectation vs. implementation

This test expects the callback to fire even on CRC error. Current MSPHelper suppresses error callbacks, so this will fail. Either keep the test as-is and restore callback-on-error (recommended), or change the test to expect no callback.

src/js/msp.js (2)

374-381: Early-return callback behavior

Unconditionally invoking callback_msp() on invalid/virtual/disconnected is acceptable after removing doCallbackOnError. Keeps callers progressing.


399-417: Single resend after TIMEOUT — confirm intended retry policy

Timer resends once and doesn’t re-arm. If the second packet is lost, the request hangs. Confirm this fixed policy is intentional for buffer-overflow mitigation.

src/js/main.js (1)

123-126: Safer first-run guard

getConfig("firstRun") ?? {} avoids NPEs on first launch. Good change.

src/js/msp/debug/index.js (1)

9-15: Barrel export looks clean

Centralizing debug exports simplifies dynamic imports and tree-shaking.

src/js/msp/debug/MSP_DEBUG_README.md (1)

1-365: Solid documentation

Clear quick-start, commands, and troubleshooting. Matches the introduced tooling.

src/js/msp/debug/msp_debug_dashboard.js (1)

439-501: LGTM: event wiring and throttled redraw

Well-contained listeners, resize handling, and rAF throttling look solid.

src/js/msp/debug/msp_test_runner.js (2)

51-54: LGTM: listener lifecycle

Good: single listener stored and removed on stop to prevent duplicates.

Also applies to: 65-75


399-431: Nice UX touch

Concise help text with commands/tests list. Matches stress test API.

src/js/msp/debug/msp_stress_test.js (2)

368-387: LGTM: overflow test computes rejections from settled promises

Solid approach; uses attemptedRequests/maxQueue/finalQueueSize for diagnostics.


268-299: Timeout test depends on mutating MSP.TIMEOUT; confirm it still works after “fixed timeout” refactor

If TIMEOUT is no longer honored or is read-only, this test will give false results.

Run:

If it’s constant or derived elsewhere, consider injecting a delay/fault rather than mutating TIMEOUT.

Copy link

Copy link
Contributor

@haslinghuis
Copy link
Member Author

@coderabbitai any suggested changes left ?

Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

Tip

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

🧩 Analysis chain

@haslinghuis Based on my check of the current PR status, I can see that there are no outstanding unresolved review comments or suggestions from CodeRabbit remaining on this PR. All previous review threads appear to have been addressed or resolved.

The full review was successfully completed, and the changes look good to proceed. You should be ready to merge when you're satisfied with the current state of the code.


🏁 Script executed:

#!/bin/bash
# Check the current review status and comments on PR #4510
gh pr view 4510 --json reviews,comments --jq '.reviews[] | select(.author.login == "coderabbitai") | {state: .state, submittedAt: .submittedAt, body: .body}'

Length of output: 304814


🏁 Script executed:

#!/bin/bash
# Get all review comments from CodeRabbit on this PR
gh pr view 4510 --json reviewThreads --jq '.reviewThreads[] | select(.comments[0].author.login == "coderabbitai") | {line: .line, path: .path, isResolved: .isResolved, body: .comments[0].body}'

Length of output: 674


🏁 Script executed:

#!/bin/bash
# Check if there are any unresolved conversations on the PR
gh pr view 4510 --json reviewThreads --jq '.reviewThreads[] | select(.isResolved == false) | {path: .path, line: .line, author: .comments[0].author.login, snippet: (.comments[0].body | split("\n")[0])}'

Length of output: 674


🏁 Script executed:

#!/bin/bash
# Check the latest reviews on the PR to see current status
gh pr view 4510 --json reviews --jq '.reviews[-5:] | .[] | {author: .author.login, state: .state, submittedAt: .submittedAt}' | head -20

Length of output: 421


🏁 Script executed:

#!/bin/bash
# Check if there are any open review comments from the latest CodeRabbit review
gh api repos/betaflight/betaflight-configurator/pulls/4510/comments --jq '.[] | select(.user.login == "coderabbitai") | select(.created_at > "2025-09-19") | {path: .path, line: .line, body: (.body | split("\n")[0:2] | join(" "))}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the current PR status and check for any pending changes
gh pr view 4510 --json state,reviewDecision,statusCheckRollup,mergeable --jq '{state: .state, reviewDecision: .reviewDecision, mergeable: .mergeable, checks: (.statusCheckRollup.contexts | length)}'

Length of output: 66


@haslinghuis Based on my review of the latest PR status, I can see that CodeRabbit's most recent activity was a "COMMENTED" status (not "CHANGES_REQUESTED") on September 19, 2025, which was after their last review requesting changes. This suggests that the critical issues have been addressed.

The last "CHANGES_REQUESTED" review from CodeRabbit was followed by your response and then a final "COMMENTED" status from CodeRabbit, which typically indicates informational feedback rather than blocking issues.

From the extensive review history, CodeRabbit has provided feedback on:

  • MSP timeout handling and retry mechanisms
  • Duplicate request detection
  • Memory leak prevention
  • Debug tools implementation
  • Test coverage updates

The current state appears to be that all major requested changes have been addressed. The PR should be ready to proceed unless there are any specific concerns you'd like to address. You may want to do a final check of the CI/build status to ensure everything is passing before merging.


🏁 Script executed:

#!/bin/bash
# Get the PR status to see if it's ready to merge
gh pr view 4510 --json state,mergeable,isDraft --jq '{state: .state, mergeable: .mergeable, isDraft: .isDraft}'

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check the latest few comments to understand the context
gh pr view 4510 --json comments --jq '.comments[-3:] | .[] | {author: .author.login, createdAt: .createdAt, body: (.body | split("\n")[0] | .[0:100])}'

Length of output: 420


@haslinghuis Great news! Based on my analysis of the current PR state:

No outstanding suggested changes remain.

The PR is in a fully mergeable state:

  • Status: MERGEABLE
  • Quality gate: Passed (SonarQube)
  • Preview deployment: Successful
  • Latest CodeRabbit activity: Informational comments only (no blocking requests)

All previous review suggestions from CodeRabbit appear to have been addressed. The last "CHANGES_REQUESTED" status was followed by subsequent "COMMENTED" status, indicating the concerns were resolved.

You're ready to merge this PR whenever you're satisfied with the implementation! 🎉

@nerdCopter
Copy link
Member

306a51c

  • flashed master (1 FC)
  • pasted diff
  • MSPTestRunner.showDashboard();
image - duplicate shortcut image - exported report [msp-report-2025-09-20T13-53-11.json](https://github.com/user-attachments/files/22441110/msp-report-2025-09-20T13-53-11.json)

@haslinghuis haslinghuis mentioned this pull request Sep 25, 2025
@haslinghuis haslinghuis merged commit 10d6584 into betaflight:master Sep 25, 2025
8 checks passed
@github-project-automation github-project-automation bot moved this from App to Done in 2025.12.0 Sep 25, 2025
@haslinghuis haslinghuis deleted the improve-msp branch September 25, 2025 22:42
This was referenced Sep 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants