Skip to content

Fix waiting for DFU forever#4884

Open
haslinghuis wants to merge 7 commits intobetaflight:masterfrom
haslinghuis:fix-waiting-for-dfu
Open

Fix waiting for DFU forever#4884
haslinghuis wants to merge 7 commits intobetaflight:masterfrom
haslinghuis:fix-waiting-for-dfu

Conversation

@haslinghuis
Copy link
Member

@haslinghuis haslinghuis commented Feb 27, 2026

When flashing on a fresh browser DFU has no permission and waits forever.
Added a button to let user request required DFU permissions.

image image

Summary by CodeRabbit

  • New Features

    • In-app DFU permission flow with a centered request button and controls to show/hide the prompt and trigger permission requests.
    • Background "wait-for-DFU" detection to recognize already-authorized devices and reduce unnecessary prompts.
  • Bug Fixes / Reliability

    • Improved DFU detection and permission handling with clearer fallbacks, retries and enhanced error logging.
  • Localization

    • Added English UI string: "Click to connect to DFU device".

@haslinghuis haslinghuis added this to the 2026.6 milestone Feb 27, 2026
@haslinghuis haslinghuis self-assigned this Feb 27, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bf66b9 and ed898c3.

📒 Files selected for processing (2)
  • src/js/protocols/webstm32.js
  • src/js/protocols/webusbdfu.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/protocols/webusbdfu.js

Walkthrough

Adds a DFU permission UI and flow: polling for already-authorized DFU devices via a new waitForDfu, integration into webstm32 DFU reboot handling with UI notification and timed fallback permission request, plus a new localization string for the permission prompt.

Changes

Cohort / File(s) Summary
Localization
locales/en/messages.json
Added firmwareFlasherClickToConnectDfu translation key.
Firmware Flasher UI
src/components/tabs/FirmwareFlasherTab.vue
Added dfuAuthRequired state, DFU permission button/UI and styles, requestDfuPermission() in component API, and exposed showDfuPermission/hideDfuPermission on TABS.firmware_flasher.
DFU Protocol — webstm32
src/js/protocols/webstm32.js
Reworked DFU reboot handling to call waitForDfu(...), handle found vs DFU_AUTH_REQUIRED, attempt to invoke TABS.firmware_flasher.showDfuPermission() when auth is required, and use error handling paths when UI is unavailable or permission fails.
DFU Protocol — webusbdfu
src/js/protocols/webusbdfu.js
Added DFU_AUTH_REQUIRED export, DFUAuthRequiredError class, and async waitForDfu(timeout = 10000, interval = 500) to poll navigator.usb.getDevices() and resolve on matching DFU device or reject with DFU_AUTH_REQUIRED / DFUAuthRequiredError.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as FirmwareFlasherTab
    participant Protocol as webstm32
    participant DFU as webusbdfu
    participant USB as navigator.usb

    Note over Protocol: DFU reboot detected
    Protocol->>DFU: waitForDfu(5000)
    DFU->>USB: getDevices()
    alt Device Found
        USB-->>DFU: matching device
        DFU->>DFU: handleNewDevice()
        DFU-->>Protocol: device
        Protocol->>Protocol: resume flashing
    else Device Not Found / Auth Required
        USB-->>DFU: no matching devices
        DFU-->>Protocol: reject (DFU_AUTH_REQUIRED)
        Protocol->>UI: showDfuPermission()
        UI->>User: display "Click to connect to DFU device"
        User->>UI: clicks button
        UI->>DFU: requestDfuPermission() / navigator.usb.requestDevice()
        alt Permission Granted
            DFU-->>Protocol: device acquired
            Protocol->>Protocol: resume flashing
        else Permission Denied / Timeout
            UI-->>Protocol: permission failed
            Protocol->>Protocol: handleError()
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • blckmn
  • nerdCopter
  • VitroidFPV
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: addressing the issue of waiting indefinitely for DFU permission on fresh browser sessions.
Description check ✅ Passed The description explains the problem and solution but lacks detail on implementation scope and omits the recommended format elements like issue references.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/tabs/FirmwareFlasherTab.vue`:
- Around line 955-970: The DFU prompt state is being cleared unconditionally
which hides the retry prompt on cancel/deny; update requestDfuPermission so it
sets state.dfuAuthRequired = true before calling DFU.requestPermission(), remove
the unconditional state.dfuAuthRequired = false at the start and from the
finally block, and instead only set state.dfuAuthRequired = false when a device
is actually returned (i.e. inside the if (device) branch after
DFU.requestPermission()). Keep state.dfuAuthRequired true on denied/cancelled or
on error so the UI can still show the retry/authorization prompt; reference the
requestDfuPermission function, state.dfuAuthRequired flag, and
DFU.requestPermission() call when making the change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb618b4 and f81ce18.

📒 Files selected for processing (4)
  • locales/en/messages.json
  • src/components/tabs/FirmwareFlasherTab.vue
  • src/js/protocols/webstm32.js
  • src/js/protocols/webusbdfu.js

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.

🧹 Nitpick comments (2)
src/components/tabs/FirmwareFlasherTab.vue (2)

3329-3343: Consider using theme variables instead of hardcoded colors.

The hardcoded colors (#8bc34a, #111) are inconsistent with the rest of the component which uses CSS variables like var(--primary-500), var(--success-600), etc. Using theme variables would ensure the button adapts correctly to any theme changes.

Also, the :deep() selector may not be necessary here since .dfu-auth-request is in this component's own template rather than a child component.

Suggested refactor
 /* DFU permission button accent styling */
-:deep(.dfu-auth-request .dfu-permission) {
-    background: `#8bc34a` !important; /* green */
-    border-color: `#8bc34a` !important;
-    color: `#111` !important;
+.dfu-auth-request .dfu-permission {
+    background: var(--success-600) !important;
+    border-color: var(--success-700) !important;
+    color: var(--surface-50) !important;
     box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2) inset;
 }

-:deep(.dfu-auth-request .dfu-permission:hover) {
+.dfu-auth-request .dfu-permission:hover {
     filter: brightness(0.95);
 }

-:deep(.dfu-auth-request p) {
+.dfu-auth-request p {
     margin-bottom: 8px !important;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/tabs/FirmwareFlasherTab.vue` around lines 3329 - 3343, Replace
the hardcoded colors and unnecessary :deep() usage in the DFU permission styles:
update the selectors (.dfu-auth-request .dfu-permission, .dfu-auth-request
.dfu-permission:hover, .dfu-auth-request p) to use the component/theme CSS
variables (e.g., var(--success-600) or var(--primary-500) for background/border
and var(--text-high) or var(--black) for color) instead of `#8bc34a` and `#111`, and
remove the :deep(...) wrapper since .dfu-auth-request is in the same template so
plain selectors will suffice; keep the existing box-shadow and hover brightness
effect but reference variables where appropriate.

10-15: Consider moving inline styles to the scoped CSS section.

The inline style="text-align: center; margin-top: 16px" could be consolidated into the .dfu-auth-request class in the scoped CSS for consistency with the rest of the component.

Suggested refactor

In template:

-<div v-if="state.dfuAuthRequired" class="dfu-auth-request" style="text-align: center; margin-top: 16px">
+<div v-if="state.dfuAuthRequired" class="dfu-auth-request">

In CSS (around line 3329):

+.dfu-auth-request {
+    text-align: center;
+    margin-top: 16px;
+}
+
 :deep(.dfu-auth-request .dfu-permission) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/tabs/FirmwareFlasherTab.vue` around lines 10 - 15, Remove the
inline style on the div that uses state.dfuAuthRequired and move those rules
into the component's scoped CSS: delete style="text-align: center; margin-top:
16px" from the <div class="dfu-auth-request"> and add equivalent declarations
(text-align: center; margin-top: 16px) to the .dfu-auth-request selector in the
<style scoped> block of FirmwareFlasherTab.vue so styling is consistent with the
rest of the component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/tabs/FirmwareFlasherTab.vue`:
- Around line 3329-3343: Replace the hardcoded colors and unnecessary :deep()
usage in the DFU permission styles: update the selectors (.dfu-auth-request
.dfu-permission, .dfu-auth-request .dfu-permission:hover, .dfu-auth-request p)
to use the component/theme CSS variables (e.g., var(--success-600) or
var(--primary-500) for background/border and var(--text-high) or var(--black)
for color) instead of `#8bc34a` and `#111`, and remove the :deep(...) wrapper since
.dfu-auth-request is in the same template so plain selectors will suffice; keep
the existing box-shadow and hover brightness effect but reference variables
where appropriate.
- Around line 10-15: Remove the inline style on the div that uses
state.dfuAuthRequired and move those rules into the component's scoped CSS:
delete style="text-align: center; margin-top: 16px" from the <div
class="dfu-auth-request"> and add equivalent declarations (text-align: center;
margin-top: 16px) to the .dfu-auth-request selector in the <style scoped> block
of FirmwareFlasherTab.vue so styling is consistent with the rest of the
component.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f81ce18 and feafad6.

📒 Files selected for processing (1)
  • src/components/tabs/FirmwareFlasherTab.vue

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.

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

130-161: LGTM! Clean implementation for polling already-authorized DFU devices.

The polling logic correctly uses navigator.usb.getDevices() to find devices the user has previously granted permission to, with appropriate error handling and defensive coding for usbDevices.filters.

One minor optional improvement: consider defining the error code as a constant to avoid string comparison fragility in callers:

💡 Optional: Use a constant for the error code
+// At the top of the file or in a shared constants module
+export const DFU_AUTH_REQUIRED = "DFU_AUTH_REQUIRED";
+
 // In the method
-throw new Error("DFU_AUTH_REQUIRED");
+throw new Error(DFU_AUTH_REQUIRED);

This allows callers to import and compare against the constant rather than a magic string.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/protocols/webusbdfu.js` around lines 130 - 161, The code throws a
magic-string error "DFU_AUTH_REQUIRED" in waitForDfu; define and export a named
constant (e.g. DFU_AUTH_REQUIRED) and throw new Error(DFU_AUTH_REQUIRED)
instead, update any callers to import and compare against that constant (replace
string literal checks with the constant) to avoid fragile string comparisons;
ensure the constant is declared near other protocol/errors exports so it can be
imported where needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/js/protocols/webusbdfu.js`:
- Around line 130-161: The code throws a magic-string error "DFU_AUTH_REQUIRED"
in waitForDfu; define and export a named constant (e.g. DFU_AUTH_REQUIRED) and
throw new Error(DFU_AUTH_REQUIRED) instead, update any callers to import and
compare against that constant (replace string literal checks with the constant)
to avoid fragile string comparisons; ensure the constant is declared near other
protocol/errors exports so it can be imported where needed.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between feafad6 and 84231b6.

📒 Files selected for processing (2)
  • src/js/protocols/webstm32.js
  • src/js/protocols/webusbdfu.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/protocols/webstm32.js

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.

🧹 Nitpick comments (2)
src/js/protocols/webstm32.js (1)

135-149: Make fallback permission timeout cancellable.

The delayed fallback setTimeout is not tracked, so it can still fire after state reset/navigation. Store the timeout id and clear it during cleanup/error paths to avoid stale prompts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/protocols/webstm32.js` around lines 135 - 149, The setTimeout that
calls DFU.requestPermission should be cancellable: store its id (e.g.
this.dfuPermissionTimeout = setTimeout(...)) when scheduling and clear it
(clearTimeout(this.dfuPermissionTimeout); this.dfuPermissionTimeout = null) in
error/cleanup paths such as inside handleError() and any state-reset/navigation
cleanup logic so the fallback prompt can't run after teardown; update
DFU.requestPermission().then/.catch branches to clear the timeout as well to
ensure it cannot fire later.
src/js/protocols/webusbdfu.js (1)

162-164: Prefer structured error signaling over matching Error.message.

At Line 163, throwing new Error(DFU_AUTH_REQUIRED) works, but it tightly couples callers to message text. A dedicated error.code (or custom error type) is more robust for control flow.

Proposed hardening
-        throw new Error(DFU_AUTH_REQUIRED);
+        const error = new Error("DFU authorization required");
+        error.code = DFU_AUTH_REQUIRED;
+        throw error;

Then in callers, check e?.code === DFU_AUTH_REQUIRED.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/protocols/webusbdfu.js` around lines 162 - 164, The code currently
throws new Error(DFU_AUTH_REQUIRED) which forces callers to match message text;
change the throw site in webusbdfu.js (the throw new Error(DFU_AUTH_REQUIRED)
line) to produce structured error signaling instead—either instantiate a
dedicated error class (e.g., class DFUAuthRequiredError extends Error) and throw
that, or create an Error and set a stable property (e.g., err.code =
DFU_AUTH_REQUIRED) before throwing; update callers to check e?.code ===
DFU_AUTH_REQUIRED (or use instanceof DFUAuthRequiredError) for control flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/js/protocols/webstm32.js`:
- Around line 135-149: The setTimeout that calls DFU.requestPermission should be
cancellable: store its id (e.g. this.dfuPermissionTimeout = setTimeout(...))
when scheduling and clear it (clearTimeout(this.dfuPermissionTimeout);
this.dfuPermissionTimeout = null) in error/cleanup paths such as inside
handleError() and any state-reset/navigation cleanup logic so the fallback
prompt can't run after teardown; update DFU.requestPermission().then/.catch
branches to clear the timeout as well to ensure it cannot fire later.

In `@src/js/protocols/webusbdfu.js`:
- Around line 162-164: The code currently throws new Error(DFU_AUTH_REQUIRED)
which forces callers to match message text; change the throw site in
webusbdfu.js (the throw new Error(DFU_AUTH_REQUIRED) line) to produce structured
error signaling instead—either instantiate a dedicated error class (e.g., class
DFUAuthRequiredError extends Error) and throw that, or create an Error and set a
stable property (e.g., err.code = DFU_AUTH_REQUIRED) before throwing; update
callers to check e?.code === DFU_AUTH_REQUIRED (or use instanceof
DFUAuthRequiredError) for control flow.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84231b6 and 5bf66b9.

📒 Files selected for processing (3)
  • src/components/tabs/FirmwareFlasherTab.vue
  • src/js/protocols/webstm32.js
  • src/js/protocols/webusbdfu.js

@sonarqubecloud
Copy link

@github-actions
Copy link
Contributor

🎉 Do you want to test this code? 🎉

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

@haslinghuis haslinghuis linked an issue Feb 28, 2026 that may be closed by this pull request
@Pancronos
Copy link

Not working. Regular app.betaflight works.
image

@haslinghuis
Copy link
Member Author

haslinghuis commented Feb 28, 2026

@Pancronos tested with URL provided here - after a few seconds ...

image

Also your version is outdated

image

@Pancronos
Copy link

I tested the pr4884 app, https://pr4884.betaflight-app-preview.pages.dev/ got the green message, connected to dfu, nothing....went in a loop.
Now just tried again and seems to work.....weird. Chrome lately goes crazy, need to change browser :)

@haslinghuis
Copy link
Member Author

Being conservative here for development - still using chromium here - Version 131.0.6778.204 (Official Build) built on Ubuntu 22.04.5 LTS (64-bit)

Also using firefox for browsing - and chromium purely for app development.

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

Projects

Status: App

Development

Successfully merging this pull request may close these issues.

Configurator fail to reboot

3 participants