/** * @file button_manager.cpp * @brief 三按钮管理器实现 - 双向去抖 + 长按/短按 + 超时自愈 * * 核心改进 (v2): * 1. 按下沿和释放沿都经过 BTN_DEBOUNCE_MS 稳定确认 * 2. 长按期间持续检测 (不再等到松手才触发) * 3. 按下超过 BTN_STUCK_TIMEOUT_MS 自动复位, 防止状态机卡死 * 4. 用 stableState 替代瞬时 raw 驱动状态, 消除噪声误触发 */ #include "button_manager.h" // ============================================================ // 构造 // ============================================================ ButtonManager::ButtonManager() : _cbProvision(nullptr) , _cbDismiss(nullptr) , _cbPage(nullptr) { auto initBtn = [](ButtonState& s, uint8_t pin) { s.pin = pin; s.stableState = false; s.rawState = false; s.changeTime = 0; s.pressTime = 0; s.longTriggered = false; s.shortPending = false; }; initBtn(_btnProvision, BTN_PROVISION_PIN); initBtn(_btnDismiss, BTN_ALARM_DISMISS_PIN); initBtn(_btnPage, BTN_PAGE_SWITCH_PIN); } // ============================================================ // 初始化 // ============================================================ void ButtonManager::begin() { pinMode(BTN_PROVISION_PIN, INPUT_PULLUP); pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP); pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP); // 强制初始化为未按下 (PCB 上电 GPIO 不稳, 忽略初始状态) _btnProvision.rawState = false; _btnProvision.stableState = false; _btnDismiss.rawState = false; _btnDismiss.stableState = false; _btnPage.rawState = false; _btnPage.stableState = false; Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)")); Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n", BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN); Serial.printf(" 去抖:%dms | 长按:%dms | 卡死超时:%dms\n", BTN_DEBOUNCE_MS, BTN_LONG_PRESS_MS, BTN_STUCK_TIMEOUT_MS); } // ============================================================ // 主循环轮询 // ============================================================ void ButtonManager::loop() { // 开机 10 秒静默 (PCB 上电 GPIO 不稳 + 强制初始未按下) if (millis() < 10000) return; // 按钮1: 配网 (仅长按) _processButton(_btnProvision, false, true); // 按钮2: 解除报警 (仅短按) _processButton(_btnDismiss, true, false); // 按钮3: 翻页 (仅短按) _processButton(_btnPage, true, false); } // ============================================================ // 单按钮状态机 (基于稳定状态, 非瞬时边沿) // ============================================================ void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) { uint32_t now = millis(); bool raw = _readPin(btn.pin); // true = 按下 (LOW) // ── 第1步: raw 状态变化时启动去抖计时 ── if (raw != btn.rawState) { btn.rawState = raw; btn.changeTime = now; // 不立即改变 stableState — 等稳定确认 } // ── 第2步: 去抖确认 — raw 保持稳定超过 BTN_DEBOUNCE_MS 才生效 ── bool debounced = (now - btn.changeTime >= BTN_DEBOUNCE_MS); bool stateChanged = (btn.rawState != btn.stableState); if (debounced && stateChanged) { // 状态正式切换 btn.stableState = btn.rawState; if (btn.stableState) { // ── 确认按下 ── btn.pressTime = now; btn.longTriggered = false; btn.shortPending = hasShort; // 只有支持短按的按钮才标记待判定 } else { // ── 确认释放 ── uint32_t holdTime = now - btn.pressTime; // 短按判定: 按下时长在去抖和长按阈值之间 if (btn.shortPending && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS && !btn.longTriggered) { // 根据引脚分发短按回调 if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) { _cbDismiss(); } if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) { _cbPage(); } } btn.shortPending = false; btn.longTriggered = false; } } // ── 第3步: 长按检测 (在按下稳定期间持续检查) ── if (hasLong && btn.stableState && !btn.longTriggered) { uint32_t holdTime = now - btn.pressTime; if (holdTime >= BTN_LONG_PRESS_MS) { btn.longTriggered = true; btn.shortPending = false; // 长按已触发, 不再判定短按 if (_cbProvision) { _cbProvision(); } } } // ── 第4步: 卡死自愈 — 按下超时强制复位 ── if (btn.stableState && (now - btn.pressTime >= BTN_STUCK_TIMEOUT_MS)) { Serial.printf("[按钮] ⚠️ GPIO%d 按下超时 %lums, 强制复位\n", btn.pin, now - btn.pressTime); _resetButton(btn); } } // ============================================================ // 强制复位 // ============================================================ void ButtonManager::_resetButton(ButtonState& btn) { btn.stableState = false; btn.rawState = _readPin(btn.pin); // 重新同步真实硬件状态 btn.changeTime = 0; btn.pressTime = 0; btn.longTriggered = false; btn.shortPending = false; } // ============================================================ // 读引脚 (INPUT_PULLUP → LOW = 按下) // ============================================================ bool ButtonManager::_readPin(uint8_t pin) { return digitalRead(pin) == LOW; }