feat: button handler with press detection and combo support

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 10:51:29 -04:00
parent 437aab90c0
commit ac6893210c
3 changed files with 176 additions and 4 deletions

110
src/button_handler.cpp Normal file
View File

@@ -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;
}

38
src/button_handler.h Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include <Arduino.h>
#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);
};

View File

@@ -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
}