From 9c290aa33ea6692a0ab886521e38c03484f6c042 Mon Sep 17 00:00:00 2001 From: Brandon Satrom Date: Thu, 16 Apr 2026 20:08:00 -0700 Subject: [PATCH 1/2] fix(firmware): reject spurious button clicks from RF/noise coupling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the rate-limit debounce with a multi-sample stability filter that requires 3 consecutive matching reads (~200-300ms) before accepting a state change. The prior logic accepted any single LOW sample caught at the 100ms poll instant, so microsecond-scale transients — such as cellular modem RF coupling onto the external button wire — were registering as phantom clicks and triggering demo/transit lock. Also gate the Cygnet onboard button (PC13 USER_BTN) behind DEBUG_MODE. PC13 is noise-prone and adds another surface for false LOW reads in field units that already have an external panel-mount button. Fix the double-click window so it waits the full triple-click timeout, matching the single-click path. This prevents a slow intended triple from short-circuiting into a demo-lock toggle. Co-Authored-By: Claude Opus 4.7 --- songbird-firmware/src/rtos/SongbirdTasks.cpp | 44 +++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/songbird-firmware/src/rtos/SongbirdTasks.cpp b/songbird-firmware/src/rtos/SongbirdTasks.cpp index a1eee2d..c05690b 100644 --- a/songbird-firmware/src/rtos/SongbirdTasks.cpp +++ b/songbird-firmware/src/rtos/SongbirdTasks.cpp @@ -39,14 +39,16 @@ static SongbirdConfig s_currentConfig; // ============================================================================= static bool s_lastButtonState = HIGH; // Button is active-low with pull-up -static uint32_t s_lastButtonChange = 0; // Debounce timing -static const uint32_t BUTTON_DEBOUNCE_MS = 50; - -// Multi-click detection: 1=mute, 2=transit lock, 3=demo lock +static uint8_t s_stableCount = 0; // Consecutive samples matching candidate state +// Require N consecutive matching samples before accepting a state change. At the +// MainTask poll rate of MAIN_LOOP_INTERVAL_MS (100ms), 3 samples = ~200-300ms of +// sustained state, which rejects RF-coupled transients (microseconds) from the +// cellular modem while staying well below a real finger press. +static const uint8_t BUTTON_STABLE_SAMPLES = 3; + +// Multi-click detection: 1=transit lock, 2=demo lock, 3=mute static uint8_t s_clickCount = 0; // Number of clicks in current window static uint32_t s_firstClickTime = 0; // Time of first click -static const uint32_t MULTI_CLICK_WINDOW_MS = 600; // Window between clicks -static const uint32_t SINGLE_CLICK_DELAY_MS = 700; // Delay before single-click action static const uint32_t TRIPLE_CLICK_TIMEOUT_MS = 1000; // Total window for triple-click // ============================================================================= @@ -484,18 +486,25 @@ void MainTask(void* pvParameters) { // For now, we don't automatically sleep - let the device run continuously // Handle user button: 1-click=transit lock, 2-click=demo lock, 3-click=mute - // Read both buttons (either can trigger) - both are active-low with pull-up - bool currentButtonState = digitalRead(BUTTON_PIN) && digitalRead(BUTTON_PIN_ALT); + // External panel-mount button only in release builds. BUTTON_PIN_ALT is the + // Cygnet onboard USER_BTN on PC13, which is noise-prone and has long-wire + // susceptibility to cellular RF — included only in debug builds. + #ifdef DEBUG_MODE + bool rawButtonState = digitalRead(BUTTON_PIN) && digitalRead(BUTTON_PIN_ALT); + #else + bool rawButtonState = digitalRead(BUTTON_PIN); + #endif uint32_t now = millis(); - // Handle button state change with debounce - if (currentButtonState != s_lastButtonState) { - if (now - s_lastButtonChange > BUTTON_DEBOUNCE_MS) { - s_lastButtonChange = now; - s_lastButtonState = currentButtonState; + // Multi-sample stability filter: only accept a state change after + // BUTTON_STABLE_SAMPLES consecutive reads agree with the new state. + if (rawButtonState != s_lastButtonState) { + if (++s_stableCount >= BUTTON_STABLE_SAMPLES) { + s_lastButtonState = rawButtonState; + s_stableCount = 0; // Button pressed (active low) - if (currentButtonState == LOW) { + if (rawButtonState == LOW) { s_clickCount++; if (s_clickCount == 1) { s_firstClickTime = now; @@ -507,6 +516,8 @@ void MainTask(void* pvParameters) { #endif } } + } else { + s_stableCount = 0; } // Process click actions after timing window @@ -522,8 +533,9 @@ void MainTask(void* pvParameters) { audioToggleMute(); s_clickCount = 0; } - // Check for double-click (demo lock) - triple-click already caught above - else if (s_clickCount == 2 && elapsed >= MULTI_CLICK_WINDOW_MS) { + // Check for double-click (demo lock) - wait the full triple-click + // timeout so an in-flight 3rd click can still promote to a triple. + else if (s_clickCount == 2 && elapsed >= TRIPLE_CLICK_TIMEOUT_MS) { // Double-click detected - toggle demo lock #ifdef DEBUG_MODE DEBUG_SERIAL.println("[MainTask] Double-click - toggling demo lock"); From a9ee580282a4006cf6a397c4dd53ee6f3f3bbe97 Mon Sep 17 00:00:00 2001 From: Brandon Satrom Date: Thu, 16 Apr 2026 20:09:51 -0700 Subject: [PATCH 2/2] chore(firmware): bump version to 1.7.2 Co-Authored-By: Claude Opus 4.7 --- songbird-firmware/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/songbird-firmware/platformio.ini b/songbird-firmware/platformio.ini index 359aeae..f8861b5 100644 --- a/songbird-firmware/platformio.ini +++ b/songbird-firmware/platformio.ini @@ -33,7 +33,7 @@ build_flags = ; Product configuration -D PRODUCT_UID=\"com.blues.songbird\" - -D FIRMWARE_VERSION=\"1.7.1\" + -D FIRMWARE_VERSION=\"1.7.2\" ; Enable HAL modules -D HAL_TIM_MODULE_ENABLED -D HAL_PWR_MODULE_ENABLED