From ac6893210ce1d949400a9a3c9429cb34ed86451d Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 3 Aug 2026 10:51:29 -0400 Subject: [PATCH] feat: button handler with press detection and combo support Co-authored-by: Cursor --- src/button_handler.cpp | 110 +++++++++++++++++++++++++++++++++++++++++ src/button_handler.h | 38 ++++++++++++++ src/main.cpp | 32 ++++++++++-- 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/button_handler.cpp create mode 100644 src/button_handler.h diff --git a/src/button_handler.cpp b/src/button_handler.cpp new file mode 100644 index 0000000..edf9759 --- /dev/null +++ b/src/button_handler.cpp @@ -0,0 +1,110 @@ +#include "button_handler.h" +#include "power_manager.h" +#include "settings.h" + +void ButtonHandler::begin(PowerManager& power, SettingsManager& settings) { + _power = &power; + _settings = &settings; + + pinMode(PIN_BTN_TOP, INPUT_PULLUP); + pinMode(PIN_BTN_UP, INPUT_PULLUP); + pinMode(PIN_BTN_DOWN, INPUT_PULLUP); + + Serial.println("[buttons] Initialized (G1=random, G9=next, G10=pause)"); +} + +ButtonEvent ButtonHandler::poll() { + unsigned long now = millis(); + + bool topNow = (digitalRead(PIN_BTN_TOP) == LOW); + bool upNow = (digitalRead(PIN_BTN_UP) == LOW); + bool downNow = (digitalRead(PIN_BTN_DOWN) == LOW); + + // Track press start times + if (topNow && !_btnTopPressed) { + _btnTopPressTime = now; + _btnTopPressed = true; + } else if (!topNow) { + _btnTopPressed = false; + } + + if (upNow && !_btnUpPressed) { + _btnUpPressTime = now; + _btnUpPressed = true; + } else if (!upNow) { + _btnUpPressed = false; + } + + if (downNow && !_btnDownPressed) { + _btnDownPressTime = now; + _btnDownPressed = true; + } else if (!downNow) { + _btnDownPressed = false; + } + + // Check combos first (higher priority) + // BTN_TOP + BTN_DOWN held 3s = deep sleep + if (_btnTopPressed && _btnDownPressed) { + unsigned long holdTime = now - max(_btnTopPressTime, _btnDownPressTime); + if (holdTime >= COMBO_HOLD_MS) { + Serial.println("[buttons] Combo: deep sleep"); + _power->flashLED(255, 0, 0, 500); + _power->enterDeepSleep(); + return ButtonEvent::DeepSleep; + } + } + + // BTN_UP held 5s = factory reset + if (_btnUpPressed && !_btnTopPressed && !_btnDownPressed) { + if (now - _btnUpPressTime >= FACTORY_RESET_HOLD_MS) { + Serial.println("[buttons] Combo: factory reset"); + _power->flashLED(255, 255, 0, 1000); + _settings->factoryReset(); + return ButtonEvent::FactoryReset; + } + } + + // Single press detection (on release, with debounce) + if (!debounced(now)) { + return ButtonEvent::None; + } + + // BTN_TOP released (was short press) + if (!topNow && _btnTopPressTime > 0 && + (now - _btnTopPressTime < COMBO_HOLD_MS) && + (now - _btnTopPressTime > BUTTON_DEBOUNCE_MS)) { + _btnTopPressTime = 0; + _power->flashLED(255, 255, 255); + Serial.println("[buttons] BTN_TOP: random photo"); + _lastDebounce = now; + return ButtonEvent::RandomPhoto; + } + + // BTN_UP released (was short press) + if (!upNow && _btnUpPressTime > 0 && + (now - _btnUpPressTime < FACTORY_RESET_HOLD_MS) && + (now - _btnUpPressTime > BUTTON_DEBOUNCE_MS)) { + _btnUpPressTime = 0; + _power->flashLED(255, 255, 255); + Serial.println("[buttons] BTN_UP: next photo"); + _lastDebounce = now; + return ButtonEvent::NextPhoto; + } + + // BTN_DOWN released (was short press) + if (!downNow && _btnDownPressTime > 0 && + (now - _btnDownPressTime < COMBO_HOLD_MS) && + (now - _btnDownPressTime > BUTTON_DEBOUNCE_MS)) { + _btnDownPressTime = 0; + _power->flashLED(255, 255, 255); + Serial.println("[buttons] BTN_DOWN: play/pause"); + _lastDebounce = now; + return ButtonEvent::PlayPause; + } + + return ButtonEvent::None; +} + +bool ButtonHandler::debounced(unsigned long now) { + return (now - _lastDebounce) > BUTTON_DEBOUNCE_MS; +} diff --git a/src/button_handler.h b/src/button_handler.h new file mode 100644 index 0000000..efbecf6 --- /dev/null +++ b/src/button_handler.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "config.h" + +class PowerManager; +class SettingsManager; + +enum class ButtonEvent : uint8_t { + None, + NextPhoto, + RandomPhoto, + PlayPause, + DeepSleep, + FactoryReset +}; + +class ButtonHandler { +public: + void begin(PowerManager& power, SettingsManager& settings); + ButtonEvent poll(); + +private: + PowerManager* _power = nullptr; + SettingsManager* _settings = nullptr; + + unsigned long _btnTopPressTime = 0; + unsigned long _btnUpPressTime = 0; + unsigned long _btnDownPressTime = 0; + + bool _btnTopPressed = false; + bool _btnUpPressed = false; + bool _btnDownPressed = false; + + unsigned long _lastDebounce = 0; + + bool debounced(unsigned long now); +}; diff --git a/src/main.cpp b/src/main.cpp index 8a07405..d43eac7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,10 +4,12 @@ #include "settings.h" #include "wifi_manager.h" #include "power_manager.h" +#include "button_handler.h" SettingsManager settingsManager; WiFiManager wifiManager; PowerManager powerManager; +ButtonHandler buttonHandler; void setup() { auto cfg = M5.config(); @@ -19,15 +21,37 @@ void setup() { powerManager.begin(); settingsManager.begin(); wifiManager.begin(settingsManager); + buttonHandler.begin(powerManager, settingsManager); - Serial.printf("[main] Battery: %d%%, Charging: %s\n", - powerManager.getBatteryPercent(), - powerManager.isCharging() ? "yes" : "no"); Serial.println("[main] Boot complete"); } void loop() { M5.update(); powerManager.updateBatteryLED(); - delay(1000); + + ButtonEvent event = buttonHandler.poll(); + switch (event) { + case ButtonEvent::NextPhoto: + Serial.println("[main] → Next photo requested"); + break; + case ButtonEvent::RandomPhoto: + Serial.println("[main] → Random photo requested"); + break; + case ButtonEvent::PlayPause: + Serial.println("[main] → Play/Pause toggled"); + break; + case ButtonEvent::DeepSleep: + case ButtonEvent::FactoryReset: + break; // Handled internally + case ButtonEvent::None: + break; + default: { + ButtonEvent unreachable = event; + (void)unreachable; + break; + } + } + + delay(10); // 100Hz poll rate for responsive buttons }