Sitelet https://github.com/betaflight/betaflight/pull/14976
Skip to content

FIX: BEEPER_USB suppression when battery present and configurator active - #14976

Merged
blckmn merged 10 commits into
betaflight:masterfrom
haslinghuis:fix-beeper-usb
Jul 4, 2026
Merged

blckmn merged 10 commits into
betaflight:masterfrom
haslinghuis:fix-beeper-usb

Conversation

@haslinghuis

@haslinghuis haslinghuis commented Mar 8, 2026 •

Copy link
Copy Markdown
Member

FIX: BEEPER_USB suppression when battery present and configurator active

TL;DR

BEEPER_USB ("ON_USB") is meant to silence the beeper while the board is connected via USB. Over the years the detection heuristic was rewritten repeatedly, and the logic ended up duplicated across three sound paths reading different flag fields. This PR unifies the suppression behind a single beeperUsbSuppressed() helper, fixes the confirmed user-reported DShot-beacon bug, and closes a set of follow-up gaps (F1–F5) found in a fresh re-review.


History of BEEPER_USB

The ON_USB beeper flag has a long history of bugs going back to 2016, each fix introducing a new detection heuristic that broke a different scenario:

1. Original feature (2016) — 8129f47c6

Added beeper -ON_USB to let users silence the piezo when powered via USB. Detection used raw voltage: feature(FEATURE_VBAT) && (batteryCellCount < 2). Only affected the beeper() entry point; DShot beacons did not exist yet.

2. Battery init race (2017) — PR #4121 (0e19f7701)

Bug: gyro calibration beep was suppressed on battery power because battery detection hadn't finished yet (cell count still 0 during init). Fix changed the check to getBatteryState() == BATTERY_NOT_PRESENT and added a BATTERY_INIT state. (Issues #3901, #4107)

3. Off-by-one in flag macro (2018) — PR #6062 (52b8fa531)

Bug: copy-paste error used BEEPER_GET_FLAG(BEEPER_USB - 1) instead of BEEPER_GET_FLAG(BEEPER_USB), so the flag never matched. One-line fix.

4. DShot beacon suppression (2025) — PR #14869 (1d95d67b6)

Bug: DShot RX_LOST beacon fired while connected to configurator. The old check used usbCableIsInserted() which was too coarse — it also blocked legitimate field-retrieval beacons when USB was connected but idle. Fix introduced mspSerialIsConfiguratorActive() (5-second MSP activity timeout) for the RX_LOST beacon path.

5. Piezo still sounded with battery present (2025) — PR #14976 (1145883a2)

Bug: the piezo beeper() guard still used getBatteryState() == BATTERY_NOT_PRESENT. When a battery was connected AND the configurator was active, the check failed and beeps passed through. Fix replaced the battery check with mspSerialIsConfiguratorActive() in beeper(), and added a USB guard to the DShot beacon RX_SET path.

6. DShot beacon checked wrong flag field — 564b978ac / 45bfc1a00

Bug: the DShot beacon RX_SET guard from step 5 checked dshotBeaconOffFlags for BEEPER_USB, but the user sets it in beeper_off_flags. Also: sequence playback had no USB check, and the same inline logic was duplicated in three places.

Root cause: USB suppression logic was scattered and inconsistent

The USB suppression check (beeper_off_flags & BEEPER_USB && mspSerialIsConfiguratorActive()) was duplicated inline across three paths, with no single source of truth. This led to two bugs:

  1. DShot beacon path checked wrong flag field — used dshotBeaconOffFlags instead of beeper_off_flags. The user sets BEEPER_USB in beeper_off_flags, so the DShot check never matched.
  2. Sequence playback had no USB check — once a beep sequence was queued (e.g., during a 5-second MSP timeout gap), beeperUpdate() played it without re-checking USB suppression.

Initial fix: beeperUsbSuppressed() helper

Extracted a single helper function as the sole owner of the suppression logic, used consistently in all three sound paths:

  • beeper() — primary defense: prevents queuing new sequences + silences existing state
  • beeperUpdate() sequence playback — secondary defense: catches sequences queued during MSP timeout gaps
  • DShot beacon RX_SET path — suppresses ESC beacons (the confirmed user-reported bug)

User feedback (2026-04-25): BOXBEEPERON via TX switch still sounds

mituritsyn reported activating beeper mode from the modes tab with a TX switch. Confirmed running the fix branch; sound source was the motors (DShot ESC beacons), not the piezo. The DShot beacon path was checking dshotBeaconOffFlags for BEEPER_USB while the user sets it in beeper_off_flags. The beeperUsbSuppressed() refactor eliminates this class of bug — all paths now read from the same flag field.


Fresh gap analysis (independent re-review)

The branch was re-reviewed from scratch rather than trusting the existing write-up. The refactor was confirmed correct (the wrong-flag-field bug is genuinely gone), but the re-review surfaced five further gaps — all now fixed in this PR.

ID Severity Gap Fix
F1 High beeperUsbSuppressed() keyed solely off mspSerialIsConfiguratorActive() (a 5 s MSP-activity proxy). Any break in configurator polling > 5 s un-muted both the piezo and the DShot beacon while still physically on USB, and the "ON_USB" flag no longer reflected actual USB state. OR-in usbCableIsInserted() (literal USB-detect pin, valid at boot and immune to polling gaps), keeping mspSerialIsConfiguratorActive() as a fallback for targets without a detect pin and for wireless MSP links.
F2 Medium The power-on beep in fc/init.c drives BEEP_ON directly, bypassing beeper(), so it sounded on every USB boot. Honour BEEPER_USB inline via usbCableIsInserted() (MSP isn't up yet, but USB detect is valid).
F3 Low/Med The two DShot beacon paths use different suppression criteria (RX_LOST: configurator-active alone; RX_SET: requires BEEPER_USB), which looked like the wrong-flag-field bug. Documented the intentional asymmetry in-code (lost-model finder vs. user-triggered beacon).
F4 Medium The beeperUpdate() sequence-playback secondary defense was untested. Added a positive control + queue-then-suppress regression test.
F5 Low beeper(BEEPER_USB) in cms_menu_blackbox.c is a no-op (NULL sequence); one call also immediately preceded systemResetToMsc(). Removed both dead calls.

Note on F1: the gyro-calibrated boot beep needed no separate fix — it routes through beeper() and is closed automatically by F1, since usbCableIsInserted() is valid before MSP comes up.


Suppression flow (after this PR)

Both diagrams share the single source of truth introduced by the refactor:

beeperUsbSuppressed() =
    (beeper_off_flags has BEEPER_USB)
    AND (usbCableIsInserted() OR mspSerialIsConfiguratorActive())   // F1

Piezo path — beeper() → beeperUpdate()

RX_LOST, RX_SET and every other mode enter through beeper() and share one suppression gate (primary defense), then a second identical check on playback (secondary defense, F4).

flowchart TD
    A1["RX lost (failsafe.c)<br/>beeper(BEEPER_RX_LOST)"] --> B
    A2["BOXBEEPERON AUX<br/>beeperUpdate(): beeper(BEEPER_RX_SET)"] --> B
    A3["other modes<br/>arming / battery / gyro / ..."] --> B
    B["beeper(mode)"] --> C{"mode == SILENCE<br/>OR beeperUsbSuppressed()<br/>OR BOXBEEPERMUTE ?"}
    C -- yes --> S["beeperSilence()<br/>no piezo"]
    C -- no --> D["queue sequence<br/>priority-gated"]
    D --> E["beeperUpdate(): BeepOn step"]
    E --> F{"beeper_off_flags has mode ?<br/>OR beeperUsbSuppressed() ?<br/>(secondary defense, F4)"}
    F -- yes --> G["skip BEEP_ON<br/>LED still flashes"]
    F -- no --> H["BEEP_ON — piezo sounds 🔊"]
Loading

DShot ESC beacon path — beeperUpdate() (USE_DSHOT)

The RX_LOST and RX_SET branches use different suppression criteria by design (F3): RX_LOST (lost-model finder) is silenced whenever a configurator is attached regardless of BEEPER_USB; RX_SET (user-triggered) is only silenced when the user set BEEPER_USB.

flowchart TD
    U["beeperUpdate() — USE_DSHOT"] --> M{"areMotorsRunning() ?"}
    M -- yes --> X["no beacon"]
    M -- no --> R1{"activeMode == BEEPER_RX_LOST ?"}
    R1 -- yes --> L1{"!mspSerialIsConfiguratorActive()<br/>AND dshotBeaconOffFlags lacks RX_LOST ?<br/><b>(no BEEPER_USB check)</b>"}
    L1 -- yes --> REQ["beacon requested"]
    L1 -- no --> X
    R1 -- no --> R2{"BOXBEEPERON active<br/>AND failsafeIsReceivingRxData()<br/>AND dshotBeaconOffFlags lacks RX_SET<br/>AND !beeperUsbSuppressed()<br/><b>(requires BEEPER_USB)</b> ?"}
    R2 -- yes --> REQ
    R2 -- no --> X
    REQ --> G{"past disarm guard delay<br/>AND !isTryingToArm()<br/>AND interval since last beacon ?"}
    G -- yes --> W["dshotCommandWrite(beacon tone)<br/>motors sound 🔊"]
    G -- no --> X
Loading

Other bypass paths (lower priority, not addressed here)

Path Location Mechanism Notes
Crash recovery flight/pid.c Direct BEEP_ON, bypasses beeper() Only while armed/flying — not a bench/configurator scenario, intentionally left as-is

Key files

  • src/main/io/beeper.c — beeperUsbSuppressed() helper, all three call sites, F1/F3
  • src/main/io/beeper.h — beeperMode_e enum, BEEPER_GET_FLAG macro
  • src/main/fc/init.c — system-init boot beep (F2)
  • src/main/cms/cms_menu_blackbox.c — no-op removal (F5)
  • src/test/unit/beeper_unittest.cc — 14 tests (F1 + F4 additions)
  • src/main/msp/msp_serial.c — mspSerialIsConfiguratorActive() (5 s timeout)
  • src/main/drivers/usb_io.c — usbCableIsInserted() (gated on USE_USB_DETECT)
  • src/main/pg/beeper.h — beeperConfig_t (beeper_off_flags, dshotBeaconOffFlags)

Testing

  • make TARGET=SITL builds clean.
  • src/test/unit/beeper_unittest.cc: 14 tests pass (10 original + F1 ×2 + F4 ×2).
  • Firmware ARM/CMS build of cms_menu_blackbox.c (F5) relies on CI — USE_USB_MSC is not present in SITL and the change only removes two no-op statements.

Summary by CodeRabbit

  • Bug Fixes

    • Adjusted beeper silence logic when MSP configurator is active.
    • Updated RX_SET beacon behavior to handle USB and configurator interactions.
  • Tests

    • Added unit tests for beeper module functionality.

@haslinghuis haslinghuis added this to the 2026.6 milestone Mar 8, 2026
@haslinghuis haslinghuis self-assigned this Mar 8, 2026
@haslinghuis haslinghuis moved this to Bugfix in 2026.6.0 Mar 8, 2026
@github-actions

github-actions Bot commented Mar 8, 2026

Copy link
Copy Markdown

Do you want to test this code? You can flash it directly from the Betaflight App:

  • Simply put #14976 (this pull request number) in the Select commit field in the Firmware Flasher tab (you need to Enable expert mode, Show release candidates and Development).

WARNING: It may be unstable. Use only for testing!

@coderabbitai

coderabbitai Bot commented Mar 8, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Fixes two gaps in the BEEPER_USB (ON_USB) suppression option by replacing battery-state checks with MSP-configurator-active checks in beeper logic, ensuring piezo and DShot beacon remain silent when USB configurator is active. Includes comprehensive unit test suite for beeper module with test stubs.

Changes

Cohort / File(s) Summary
Beeper logic fixes
src/main/io/beeper.c
In beeper(), replaces getBatteryState() == BATTERY_NOT_PRESENT condition with mspSerialIsConfiguratorActive() to properly suppress piezo when USB configurator is active. In beeperUpdate(), adds guard to RX_SET beacon path to exclude USB flag when MSP configurator is active, preventing beacon trigger via transmitter during configurator connection.
Test infrastructure
src/test/Makefile
Adds beeper unit test configuration: includes beeper.c source and defines USE_BEEPER and USE_DSHOT flags for test compilation.
Beeper unit tests
src/test/unit/beeper_unittest.cc
New comprehensive GoogleTest suite for beeper module with 380 lines covering battery state, configurator activity, box beeper mute, DShot beacon RX_SET/LOSS logic, USB flag interactions, and edge cases. Includes test fixture setup and extern "C" stubs simulating MSP configurator, RC mode, failsafe, motor state, battery, DShot writes, timing, and hardware peripherals.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #14869: Direct predecessor PR that introduced MSP-configurator-active gating in beeper logic; this PR completes the fix by addressing remaining gaps in piezo and DShot beacon suppression.
  • #14904: Modifies beeper.c's DShot beacon gating and timing logic with related beacon suppression and control flow adjustments.
  • #14672: Alters beeper.c DShot/RX_SET beacon gating conditions when USB/MSP configurator is active, addressing similar beeper suppression paths.

Suggested labels

Testing Required

Suggested reviewers

  • nerdCopter
  • ledvinap
  • KarateBrot
  • blckmn
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: fixing BEEPER_USB suppression behavior when battery is present and configurator is active.
Linked Issues check ✅ Passed The PR fully addresses both gaps identified in issue #14975: Gap 1 (beeper() piezo path) replaces battery check with configurator-active check [14975], and Gap 2 (beeperUpdate() RX_SET path) adds BEEPER_USB + configurator-active guard [14975]. Tests validate the truth table [14975].
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #14975: beeper.c modifications at the two identified locations, test infrastructure additions, and no unrelated modifications to failsafe.c, msp_serial.c, or beeper.h.
Description check ✅ Passed PR description is comprehensive and well-structured, covering historical context, root cause analysis, specific gaps, fixes, suppression flow diagrams, testing, and alignment with issue #14975.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@nerdCopter nerdCopter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • approving untested pending/deferring proper testing.

@mituritsyn

Copy link
Copy Markdown
Contributor

I am still able to activate the beeper mode.
id: "53355ccf-2afe-46a5-8c4b-b239d78406a1"

@haslinghuis

Copy link
Copy Markdown
Member Author

@mituritsyn - please be more specific - which beeper modes can be activated and under which circumstances.

@bradselph bradselph left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed and independently verified. This fix is correct. LGTM.

I independently audited issue #14975 and arrived at the exact same two changes before discovering this PR — which gives me high confidence in the
correctness.

Gap 1 (beeper() line 260): The original condition getBatteryState() == BATTERY_NOT_PRESENT meant BEEPER_USB only worked on USB-powered bench setups —
completely useless for the real-world case of tuning with a flight battery connected. Replacing it with mspSerialIsConfiguratorActive() correctly keys off
the actual configurator connection state, which is what the user intent behind "ON_USB" always was.

Gap 2 (beeperUpdate() line 446-448): The AUX-triggered DShot beacon path had zero awareness of BEEPER_USB. The new guard !((dshotBeaconOffFlags &
BEEPER_GET_FLAG(BEEPER_USB)) && mspSerialIsConfiguratorActive()) is structurally consistent with how the RX_LOST path (line 438) already suppresses
beacons when the configurator is connected. Good symmetry.

@blckmn blckmn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good fix. The logic change from battery-state proxy to direct configurator-active check is the right approach, and the unit tests cover the regression well.

@mituritsyn

Copy link
Copy Markdown
Contributor

@mituritsyn - please be more specific - which beeper modes can be activated and under which circumstances.

I am able to activate beeper mode from modes tab with tx switch.
What I expect is that, as much as possible, sounds will be suppressed when the quad is connected to a configurator with USB option disabled.
I agree, this is tricky case. But let’s imagine what this feature is all about: you want to set up your drone in the evening, your neighbors and relatives are already resting, and the drone starts beeping with all sorts of alerts like a cheap Chinese toy. So, I think the beeper mode should be silenced too when the USB option is unchecked.

@haslinghuis

Copy link
Copy Markdown
Member Author

@mituritsyn added new commit - please verify

@mituritsyn

mituritsyn commented Mar 19, 2026 •

Copy link
Copy Markdown
Contributor

@mituritsyn added new commit - please verify

Nope, still able to activate the beeper mode.
Sorry, somehow flashed old version, let me recheck

@mituritsyn

Copy link
Copy Markdown
Contributor

the latest version, beeper mode still can be activated

version

Betaflight / STM32F405 (F405) 2026.6.0-alpha Mar 19 2026 / 12:29:05 (def9380) MSP API: 1.48

beeper

Disabled: ON_USB

@haslinghuis

Copy link
Copy Markdown
Member Author

@mituritsyn - can you build the PR locally - to avoid cloud build caching issues ?

As covered in the analysis, this path (beeperUpdate →
beeper(BEEPER_RX_SET)) is guarded by the beeper() check on every tick. It
should be caught if mspSerialIsConfiguratorActive() returns true.

The gap 3 fix we just applied wouldn't change this specific scenario since the
guard in beeper() runs before a sequence is ever queued.

The most productive next step is asking mituritsyn the two questions from the
analysis:

  1. Are they running the fix branch? On master, the old getBatteryState() ==
    BATTERY_NOT_PRESENT check fails whenever a battery is connected — which is
    exactly this scenario (TX switch needs battery+RX).
  2. Piezo or motors? If it's ESC DShot beacons, that's a separate flag
    (dshotBeaconOffFlags).

Without that info we're guessing.

@haslinghuis

haslinghuis commented Apr 7, 2026 •

Copy link
Copy Markdown
Member Author

@mituritsyn ON_USB is no longer working / replaced with a check for MSP activity. Perhaps should include a check for WAS_EVER_ARMED like in #6548 (comment)

@mituritsyn

Copy link
Copy Markdown
Contributor

to avoid cloud build caching issues

I've run into this issue a couple of times, so now I always try to compare the hash of the latest commit with the firmware version

2. Piezo or motors?

I'm talking about the DSHOT beeper, since the problem originally stemmed from a bug affecting the DSHOT functionality.

@mituritsyn ON_USB is no longer working / replaced with a check for MSP activity. Perhaps should include a check for WAS_EVER_ARMED like in #6548 (comment)

give me few minutes to test it

@mituritsyn

Copy link
Copy Markdown
Contributor

Last commit 1a53bff. I can still enable the dshot beep with the USB connected.

version
# Betaflight / STM32F405 (F405) 2026.6.0-alpha Apr  8 2026 / 10:27:11 (1a53bff37) MSP API: 1.48
# config rev: 31cc942
# board: manufacturer_id: JHEF, board_name: JHEF405PRO
# beeper
beeper -ON_USB

2. Piezo or motors?

I don't think that should make a difference. In fact, there should be a “beeper” mode, and restrictions should apply to its activation just as they do to “arm.” The specific method used to implement this functionality (DSHOT or piezo) should simply follow this logic.

image

@nerdCopter

Copy link
Copy Markdown
Member

@SupaflyFPV , maybe i recall wrongly, but hadn't you solved this? or was it not merged? or maybe i'm wrong.

@SupaflyFPV

Copy link
Copy Markdown
Contributor

@SupaflyFPV , maybe i recall wrongly, but hadn't you solved this? or was it not merged? or maybe i'm wrong.

as I understand it this is an update to my PR, in changing the gate to supress the buzzer...my version stopped buzzer activity on usb connection broadly, and this PR stops it on connection to Configurato - is that correct @coderabbitai?

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@SupaflyFPV — that's a good summary of the relationship between the two PRs. Here's a more precise breakdown:

Your PR #14672 gated the DShot beacon suppression on usbCableIsInserted() — meaning the beacon is silenced whenever any USB cable is physically plugged in to the FC, regardless of whether Configurator (or anything else) is actually communicating over it.

This PR #14976 takes a different and more targeted approach:

  1. Piezo path (beeper()): The old check was getBatteryState() == BATTERY_NOT_PRESENT — meaning BEEPER_USB suppression only worked when no battery was connected. PR #14976 replaces that with mspSerialIsConfiguratorActive(), so suppression now fires whenever the Configurator is actively talking MSP, independent of battery state.

  2. DShot RX_SET path (beeperUpdate()): Adds a new guard combining BEEPER_USB flag + mspSerialIsConfiguratorActive() to block the AUX-switch-triggered beacon while the Configurator is active.

The key semantic difference is:

  • Your PR #14672 added USB/VCP gating so the beacon is suppressed when the flight controller is connected to USB.
  • PR #14976 narrows that trigger to configurator activity specifically — so, for example, USB serial logging or other non-configurator USB tools would not suppress the beeper, but an active Configurator session would.

Whether usbCableIsInserted() or mspSerialIsConfiguratorActive() is the better semantic for the suppression condition is actually what's at the heart of the ongoing discussion in this PR (and what mituritsyn's testing is probing). The current testing suggests there may still be a gap where the DShot RX_SET beacon fires even with the Configurator connected.


🧠 Learnings used
Learnt from: ledvinap
Repo: betaflight/betaflight PR: 14578
File: src/main/telemetry/jetiexbus.c:547-556
Timestamp: 2025-08-12T18:26:17.016Z
Learning: For Betaflight Jeti ExBus backport PR `#14578`: ledvinap prefers to keep the PR focused on backporting fixes from iNavFlight rather than expanding into broader architectural improvements. Additional improvements to TX/RX state machine should be handled in separate PRs.

Learnt from: nerdCopter
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2025-10-29T16:03:23.891Z
Learning: In Betaflight firmware reviews, do not suggest adding DEBUG_PRINTF, logging, or diagnostic output. Betaflight follows embedded systems best practices with silent error handling using return codes and graceful fallbacks. Debug output was explicitly removed per Betaflight standards (see commit 12b2813ac in PR `#14733`).

Learnt from: nerdCopter
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:55:17.785Z
Learning: For betaflight/betaflight reviews, nerdCopter prefers minimizing nitpicks. Avoid purely cosmetic suggestions (e.g., Unicode vs ASCII, redundant null checks) and focus comments on functional correctness, cross-target build safety, and measurable maintainability wins.

Learnt from: pzencar
Repo: betaflight/betaflight PR: 14984
File: src/main/cli/settings.c:1277-1278
Timestamp: 2026-03-11T20:27:37.179Z
Learning: In Betaflight PR `#14984` (vbat sag compensation), when `vbat_sag_target >= vbat_sag_max_voltage`, `vbatRangeToCompensate` in `mixer_init.c` remains 0 because the assignment is guarded by `vbatFull > targetVoltage`. This is a safe, intentional no-op — no explicit cross-parameter validation is required, though `validateAndFixConfig()` in `src/main/config/config.c` is the appropriate place to add any such guard in the future.

Learnt from: blckmn
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2026-03-20T17:48:49.927Z
Learning: PR `#15016` (betaflight/betaflight) adds an ESP32-S3 platform scaffold with intentional stub driver implementations (ADC, I2C, SPI, DMA, EXTI, IO, UART, USB VCP, PWM, DShot, LED strip, config flash, etc.). All stubs and TODO comments are deliberate placeholders. Real implementations will be added in a follow-up PR. Do not flag unimplemented stubs or TODO items in this PR or its direct follow-ups as issues.

@SupaflyFPV

SupaflyFPV commented Apr 9, 2026 •

Copy link
Copy Markdown
Contributor

tbh I prefer my setup because it means you can work on your quad with usb connected and the buzzer wont go off when you power, generally quite useful if you need quick changes without having to power a radio....without having to faff with the configurator if you don't want to. But that's just me. This is not a hill I'm willing to die on if others prefer it changed.

@haslinghuis

Copy link
Copy Markdown
Member Author

@coderabbitai - please update #14976 (comment) and take it to account analyses in #14860.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

I need to fetch the specific issue #14860 from the betaflight GitHub repo directly.


🏁 Script executed:

#!/bin/bash
# Fetch issue `#14860` details
gh issue view 14860 --repo betaflight/betaflight --comments

Length of output: 150954


@haslinghuis Here is the updated summary, incorporating the analyses from issue #14860:


Updated analysis for this PR (#14976) in light of issue #14860:

Relationship Between PR #14672, #14976, and #14869

These three PRs form an interconnected fix chain for beeper/beacon suppression:

  • PR #14672 (SupaflyFPV): Added USB-based suppression gated on usbCableIsInserted(), intending to silence beepers while working on the quad via USB. However, this introduced a regression: usbCableIsInserted() checks the USB protocol state machine (dev_state != USBD_STATE_DEFAULT), not physical cable presence. On boards without a hardware USB-detect pin (the majority of 500+ supported targets), the state machine can persist after the cable is physically disconnected, causing the function to always return true and permanently suppressing RX_LOST beeping.

  • PR #14976 (this PR): Fixes the BEEPER_USB suppression logic by replacing the battery-state check in the piezo path with mspSerialIsConfiguratorActive(), and adds the same guard to the AUX-switch-triggered DShot RX_SET beacon path. This is the right semantic — suppress when the configurator is actively communicating, not when USB is physically present.

  • PR #14869: Applies the same mspSerialIsConfiguratorActive() fix to the failsafe RX_LOST path in failsafe.c, which is a separate beeper trigger path not covered by PR #14976. The two PRs are complementary: #14976 handles the piezo and RX_SET paths; #14869 handles the RX_LOST failsafe path.

Remaining Edge Case (from #14860 testing)

@cybrengineer confirmed in #14860 that after flashing #14869, one edge case persists:

USB plugged in (no TX) → battery plugged → wait → USB disconnected → no beeping within 30 seconds

This may be caused by interaction between the 5-second MSP_ACTIVITY_DEFAULT_TIMEOUT_MS window and the scenario where the MSP port accumulates recent activity during the configurator session, and then after USB disconnect the last-activity timestamp is still fresh — but the 30-second window suggests something deeper than just the 5-second holdoff. This edge case should be tracked even after both PRs are merged.

Impact on the mituritsyn Report

The report that the DShot RX_SET beacon (AUX-switch-triggered beeper mode) can still be activated while the configurator is connected is exactly what PR #14976's beeperUpdate() guard is intended to fix. The fix adds BEEPER_USB && mspSerialIsConfiguratorActive() to that code path. The mspSerialIsConfiguratorActive() approach is confirmed to be reliable by the #14869 testing.

Summary Table

Path Fixed by Mechanism
Piezo beeper (battery present) PR #14976 mspSerialIsConfiguratorActive() replaces getBatteryState() == BATTERY_NOT_PRESENT
DShot RX_SET (AUX switch) PR #14976 New BEEPER_USB && mspSerialIsConfiguratorActive() guard in beeperUpdate()
DShot RX_LOST (failsafe) PR #14869 mspSerialIsConfiguratorActive() replaces usbCableIsInserted() in failsafe.c

🧠 Learnings used
Learnt from: ledvinap
Repo: betaflight/betaflight PR: 14578
File: src/main/telemetry/jetiexbus.c:547-556
Timestamp: 2025-08-12T18:26:17.016Z
Learning: For Betaflight Jeti ExBus backport PR `#14578`: ledvinap prefers to keep the PR focused on backporting fixes from iNavFlight rather than expanding into broader architectural improvements. Additional improvements to TX/RX state machine should be handled in separate PRs.

Learnt from: nerdCopter
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:55:17.785Z
Learning: For betaflight/betaflight reviews, nerdCopter prefers minimizing nitpicks. Avoid purely cosmetic suggestions (e.g., Unicode vs ASCII, redundant null checks) and focus comments on functional correctness, cross-target build safety, and measurable maintainability wins.

Learnt from: nerdCopter
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2025-10-29T16:03:23.891Z
Learning: In Betaflight firmware reviews, do not suggest adding DEBUG_PRINTF, logging, or diagnostic output. Betaflight follows embedded systems best practices with silent error handling using return codes and graceful fallbacks. Debug output was explicitly removed per Betaflight standards (see commit 12b2813ac in PR `#14733`).

Learnt from: pzencar
Repo: betaflight/betaflight PR: 14984
File: src/main/cli/settings.c:1277-1278
Timestamp: 2026-03-11T20:27:37.179Z
Learning: In Betaflight PR `#14984` (vbat sag compensation), when `vbat_sag_target >= vbat_sag_max_voltage`, `vbatRangeToCompensate` in `mixer_init.c` remains 0 because the assignment is guarded by `vbatFull > targetVoltage`. This is a safe, intentional no-op — no explicit cross-parameter validation is required, though `validateAndFixConfig()` in `src/main/config/config.c` is the appropriate place to add any such guard in the future.

Learnt from: blckmn
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2026-03-20T17:48:49.927Z
Learning: PR `#15016` (betaflight/betaflight) adds an ESP32-S3 platform scaffold with intentional stub driver implementations (ADC, I2C, SPI, DMA, EXTI, IO, UART, USB VCP, PWM, DShot, LED strip, config flash, etc.). All stubs and TODO comments are deliberate placeholders. Real implementations will be added in a follow-up PR. Do not flag unimplemented stubs or TODO items in this PR or its direct follow-ups as issues.

Learnt from: Quick-Flash
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2026-03-24T04:14:07.129Z
Learning: In Betaflight PR `#15024` (`remove_int_yaw` branch), the yaw PID integral term upscaling (`pidRuntime.pidCoefficient[FD_YAW].Ki *= 2.5f` in `src/main/flight/pid_init.c`) being made unconditional is intentional. Previously this scaling was skipped only when `use_integrated_yaw` was enabled; with integrated yaw removed entirely, the scaling now always applies on the yaw axis. Quick-Flash (author) confirmed this is the correct and expected behavior.

Learnt from: Quick-Flash
Repo: betaflight/betaflight PR: 14772
File: src/main/drivers/usb_cdc_debug.h:1-15
Timestamp: 2025-11-24T23:28:08.347Z
Learning: In Betaflight, compile-time opt-in debug features (e.g., USE_USB_CDC_DEBUG) that are explicitly intended for development and testing purposes are acceptable, even though diagnostic output in production code was removed. The restriction on DEBUG_PRINTF and logging applies to production builds, not to dev-only features that developers manually enable during testing.

Learnt from: pzencar
Repo: betaflight/betaflight PR: 14984
File: src/main/flight/mixer_init.c:371-381
Timestamp: 2026-03-14T11:41:05.935Z
Learning: In Betaflight PR `#14984` (vbat sag compensation), `mixerRuntime.vbatTarget` is intentionally NOT clamped to `mixerRuntime.vbatFull` in `mixerInitProfile()` in `src/main/flight/mixer_init.c`. Setting `vbat_sag_target` above `vbat_sag_max_voltage` is a supported use case for the throttle compensation path (`applyVBatSagThrottleScale`): it causes a persistent throttle boost even at full charge (e.g., 4S feel on 3S, or HV feel on non-HV packs). Do not flag the absence of a `vbatTarget > vbatFull` clamp as a bug.

Learnt from: pzencar
Repo: betaflight/betaflight PR: 14984
File: src/main/config/config.c:277-280
Timestamp: 2026-03-14T11:48:47.470Z
Learning: In Betaflight PR `#14984` (vbat sag compensation), `vbat_sag_target` is explicitly allowed to exceed `vbat_sag_max_voltage` in `validateAndFixConfig()` in `src/main/config/config.c`. This is intentional: setting target above max_voltage enables a persistent throttle boost (e.g., 4S feel on 3S, or HV feel on non-HV packs). No cross-parameter clamping of `vbat_sag_target` against `vbat_sag_max_voltage` should be added.

Learnt from: demvlad
Repo: betaflight/betaflight PR: 14350
File: src/main/fc/rc_modes.c:59-59
Timestamp: 2026-04-01T05:29:05.919Z
Learning: In betaflight/betaflight, `rc_modes.h` line 46 documents a mandatory convention: whenever a new flight mode (boxId) is added before `BOXID_FLIGHTMODE_LAST` in the `boxId_e` enum, the `PG_REGISTER_ARRAY` version for `modeActivationConditions` (PG_MODE_ACTIVATION_PROFILE) in `src/main/fc/rc_modes.c` MUST be incremented. This resets the RC modes configuration on upgrade and is intentional. Do not flag such PG version bumps as unnecessary when they accompany a new flight mode addition.

Learnt from: Quick-Flash
Repo: betaflight/betaflight PR: 15056
File: src/main/common/filter.c:169-193
Timestamp: 2026-04-05T18:12:04.711Z
Learning: In betaflight/betaflight PR `#15056`, the Chamberlin SVF Butterworth lowpass (`butterworthFilterUpdate` using `f = 2*sin(pi*freq*dt)`) is only stable for filterFreq < ~0.172/dt (~34% of Nyquist). Quick-Flash agreed to switch to the TPT (Topology-Preserving Transform) form using `g = tan(pi*freq*dt)` for the butterworth filter, which is stable all the way to Nyquist. The notch SVF implementations (`notchUpdate`/`rpmNotchUpdate`) already use `f = sn/cs` (i.e., tan), so they are already TPT-stable. Only `butterworthFilterUpdate/Apply` in `src/main/common/filter.c` needs the TPT conversion.

Learnt from: haslinghuis
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2025-10-06T14:46:51.765Z
Learning: For the betaflight/betaflight repository, automatically enhance minimal or missing PR descriptions to include comprehensive information (What Changed, Why This Change, Testing) while preserving any existing content, images, and auto-generated summaries. Users often rely on AI-generated summaries instead of writing descriptions.

Learnt from: Quick-Flash
Repo: betaflight/betaflight PR: 15056
File: src/main/common/filter.c:204-215
Timestamp: 2026-04-02T05:49:19.619Z
Learning: In the betaflight/betaflight codebase, Nyquist-frequency clamping (e.g., `filterFreq = MIN(filterFreq, 0.499f / dt)`) for SVF notch filters (`notchUpdate`, `rpmNotchUpdate` in `src/main/common/filter.c`) should be applied at the call sites (e.g., `dynNotchUpdate`, `rpmFilterUpdate`, PID/gyro init paths), not inside the FAST_CODE update functions themselves, to avoid adding branches to the hot path. Quick-Flash explicitly prefers this pattern for performance reasons.

Learnt from: Quick-Flash
Repo: betaflight/betaflight PR: 15056
File: src/main/common/filter.c:204-215
Timestamp: 2026-04-02T05:54:17.869Z
Learning: In the betaflight/betaflight codebase, Nyquist-frequency clamping for SVF notch filters (`notchUpdate`, `rpmNotchUpdate` in `src/main/common/filter.c`) is already handled at the call sites and does NOT need to be added inside the FAST_CODE update functions. The RPM filter call site uses `constrainf((harmonic+1) * getMotorFrequencyHz(motorIndex), rpmFilter.minHz, rpmFilter.maxHz)` before calling `rpmNotchUpdate`, and the dynamic notch internal logic prevents frequencies from approaching Nyquist. Do not flag the absence of in-function Nyquist clamping in these SVF notch update functions as a bug.

Learnt from: haslinghuis
Repo: betaflight/betaflight PR: 0
File: :0-0
Timestamp: 2025-11-18T23:49:48.929Z
Learning: When reviewing changes to the Betaflight codebase that add entries to statically-sized arrays (like I2C pin mappings in bus_i2c_atbsp_init.c), always verify that the number of new entries doesn't exceed the array size limit defined by constants like I2C_PIN_SEL_MAX in the corresponding header files, and flag potential array overflow issues before they cause compilation errors.

Replace getBatteryState() == BATTERY_NOT_PRESENT with
mspSerialIsConfiguratorActive() in the beeper() piezo path so BEEPER_USB
is honoured regardless of battery state.

Add BEEPER_USB + mspSerialIsConfiguratorActive() guard to the
AUX-switch-triggered DShot beacon (RX_SET) path in beeperUpdate().

Add beeper_unittest with 10 tests covering the BEEPER_USB truth table.

Fixes betaflight#14975
  active before allowing BEEP_ON during sequence playback.
  beeper_off_flags (where the user sets BEEPER_USB) instead of
  dshotBeaconOffFlags. This matches the piezo path in beeper() and the sequence
  playback guard.
@haslinghuis

Copy link
Copy Markdown
Member Author

Fresh gap analysis (independent re-review)

Re-reviewed the current branch state (45bfc1a00) from scratch rather than trusting the existing write-up. The refactor is confirmed in place — beeperUsbSuppressed() at io/beeper.c:97-101, used in beeper() (:265), beeperUpdate() BeepOn (:498-499) and the DShot RX_SET path (:452). The wrong-flag-field bug (reading dshotBeaconOffFlags for BEEPER_USB) is genuinely gone. Below are findings the previous analysis did not capture.

F1 — BEEPER_USB no longer keys off USB at all (High)

beeperUsbSuppressed() ANDs the flag with mspSerialIsConfiguratorActive() only — a 5 s MSP-activity window (msp/msp_serial.c:613, MSP_ACTIVITY_DEFAULT_TIMEOUT_MS = 5000). usbCableIsInserted() (drivers/usb_io.h:27, already used in fc/tasks.c) is never consulted. Two consequences:

  • (a) Polling-gap un-mute. Any break in configurator MSP traffic > 5 s (link stall, USB hiccup, or simply a configurator view that pauses status polling) flips suppression off, and both the piezo (beeper.c:498-499) and the motor beacon (beeper.c:449-452) resume while the board is still physically on USB. The fix is only as reliable as uninterrupted polling — it is not a USB guarantee.
  • (b) False positive on wireless MSP. A Bluetooth / ELRS / wireless MSP link counts as "configurator active", so BEEPER_USB silences beeps with no USB attached — contradicting both the flag name and its description (beeper.h:56 "boards have beeper powered USB connected").

Suggestion: gate on usbCableIsInserted() (alone, or OR'd with config-active) to restore the documented semantics and eliminate the polling-gap window.

F2 — Boot-time emitters are unguarded, but could be closed cheaply (Medium)

Confirmed unguarded against BEEPER_USB: fc/init.c:777 (system-init beep loop — only checks BEEPER_SYSTEM_INIT) and sensors/gyro.c:266 (BEEPER_GYRO_CALIBRATED, fires before MSP is active). The PR notes these but says "a different detection method would be needed" — worth highlighting that usbCableIsInserted() is already available and is valid at boot (unlike mspSerialIsConfiguratorActive()). If F1's suggestion is adopted, both of these close for free.

(Minor: the PR body's line refs have drifted — init is 771-784, not 720/726; mspSerialIsConfiguratorActive() is at msp_serial.c:613, not 611.)

F3 — Undocumented asymmetry between the two DShot beacon paths (Low/Medium)

The two adjacent blocks in beeperUpdate() use different suppression premises:

  • RX_LOST (beeper.c:442-444): bare !mspSerialIsConfiguratorActive() — suppresses regardless of whether BEEPER_USB is set.
  • RX_SET (beeper.c:449-452): !beeperUsbSuppressed() — suppresses only if BEEPER_USB is set.

Probably intentional (lost-model finder vs. user-triggered beacon), but two neighbouring guards reading from different premises is exactly the "checked the wrong condition" class of bug this PR set out to kill. A one-line comment stating the asymmetry is deliberate would prevent the next regression.

F4 — Test gap: the claimed "secondary defense" is untested (Medium)

beeper_unittest.cc covers beeper() piezo, DShot RX_SET, DShot RX_LOST and mute — good. But there is no test for the beeperUpdate() sequence-playback re-check, which is the stated reason that guard exists: queue a sequence while config is inactive, then assert it goes silent mid-playback once mspSerialIsConfiguratorActive() flips true. That "queued during the 5 s gap" scenario is the PR's own justification and is currently unverified. (No boot-beep regression test either, but that's lower priority.)

F5 — beeper(BEEPER_USB) is a silent no-op (Low, pre-existing)

cms/cms_menu_blackbox.c:211,217 call beeper(BEEPER_USB), but BEEPER_USB has a NULL sequence in beeperTable (beeper.c:236), so beeper() returns at :272 doing nothing. Pre-existing and out of scope, but flagging in case those call sites were meant to signal something.


Bottom line: the refactor correctly fixes the reported wrong-flag-field bug and unifies the three paths. The remaining structural gap is that "USB" suppression is now a configurator-activity proxy with a 5 s cliff (F1) — addressing that with usbCableIsInserted() would also close the boot-time paths (F2). F4 is the most actionable quick win.

beeperUsbSuppressed() previously keyed solely off mspSerialIsConfiguratorActive(),
a 5s MSP-activity proxy. That had two gaps:

- any break in configurator MSP polling >5s un-muted both the piezo and the
  DShot beacon while still physically on USB
- the suppression never consulted actual USB state, so the "ON_USB" flag no
  longer reflected its name

OR-in usbCableIsInserted() (the literal USB detect pin, valid even at boot and
immune to polling gaps), keeping mspSerialIsConfiguratorActive() as a fallback
for targets without a USB detect pin and for wireless MSP links. As a side
effect the gyro-calibrated boot beep, which routes through beeper(), is now
correctly suppressed on USB.

Adds a usbCableIsInserted() test stub and two tests covering USB-cable-present
suppression and the no-flag pass-through.
The power-on beep in init.c drives BEEP_ON directly, bypassing
beeper()/beeperUsbSuppressed(), so it sounded on every USB boot even with
BEEPER_USB configured. MSP is not active this early, but usbCableIsInserted()
is already valid, so honour BEEPER_USB here directly.

The gyro-calibrated boot beep needs no change: it routes through beeper() and
is covered by F1.
The two DShot beacon paths in beeperUpdate() use different USB-suppression
criteria by design: RX_LOST (lost-model finder) is silenced whenever a
configurator is attached regardless of BEEPER_USB, while RX_SET (user-triggered
via AUX) is only silenced when the user set the BEEPER_USB flag. Comment the
rationale so the difference is not mistaken for the wrong-flag-field bug this
series fixed.
The BeepOn step in beeperUpdate() re-checks beeperUsbSuppressed() so a sequence
queued while unsuppressed is still silenced if USB suppression engages before it
plays (e.g. the configurator reconnects during a >5s MSP polling gap). This path
was previously untested. Adds a positive control plus the queue-then-suppress
regression test.
BEEPER_USB has a NULL sequence in beeperTable, so beeper(BEEPER_USB) returns
without producing any sound. One call also immediately preceded
systemResetToMsc() (the system resets before any tone could play). Remove both
dead calls. beeper(BEEPER_BLACKBOX_ERASE) is unaffected, so the beeper.h include
remains in use.
@haslinghuis

Copy link
Copy Markdown
Member Author

Thanks for the independent audit — much appreciated, and great that you converged on the same two gaps from #14975. One heads-up: both of those approvals were against an earlier revision (1145883a2, the original two-change fix). The branch has since been refactored and extended (F1–F5, see the updated description + the two flow diagrams), so a couple of points are now stale:

Gap 1 (battery-state → configurator-active). Still correct in spirit, but the exact condition you praised has moved into a single helper and gained a term (F1):

beeperUsbSuppressed() =
    (beeper_off_flags & BEEPER_USB)
    && (usbCableIsInserted() || mspSerialIsConfiguratorActive());

The configurator-active case you validated still suppresses (it's an OR), so this is a superset, not a regression. The one new behavior to re-bless: physically on USB with the configurator closed/idle + BEEPER_USB set now suppresses too (previously it sounded). That's deliberate — it restores the literal "ON_USB" meaning and closes a >5 s MSP-polling-gap window plus the boot-time beep, but it does diverge from the "key off configurator, not USB" framing, so flagging it explicitly.

Gap 2 (the DShot beacon guard). This is the important one: the snippet you marked as good symmetry —

!((dshotBeaconOffFlags & BEEPER_GET_FLAG(BEEPER_USB)) && mspSerialIsConfiguratorActive())

— was actually the bug. It reads BEEPER_USB out of dshotBeaconOffFlags, but the user sets BEEPER_USB in beeper_off_flags, so it never matched. That's the root of the "motors still beep on a TX switch" report. It's now gone; the RX_SET path uses !beeperUsbSuppressed(), which reads the correct field. The remaining asymmetry between the RX_LOST and RX_SET beacon paths (RX_LOST suppresses on configurator-active alone, regardless of BEEPER_USB) is intentional and is now documented in-code — see the second diagram in the description.

Net: no functional regression vs. your review; current HEAD is strictly more correct on Gap 2 and a superset on Gap 1. A fresh pass over the F1 usbCableIsInserted() term would be the only thing worth re-confirming. 🙏

@blckmn
blckmn merged commit 57f5814 into betaflight:master Jul 4, 2026
53 checks passed
@github-project-automation github-project-automation Bot moved this from Bugfix to Done in 2026.6.0 Jul 4, 2026
@haslinghuis
haslinghuis deleted the fix-beeper-usb branch July 4, 2026 18:24
gwlim pushed a commit to gwlim/betaflight that referenced this pull request Jul 25, 2026
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.

6 participants