/* hw_button.c — hardware button debounce + gesture detection (Issue #682) * * Debounce FSM: * IDLE → (raw press detected) → DEBOUNCING * DEBOUNCING → (still pressed after BTN_DEBOUNCE_MS) → HELD * HELD → (released) → classify press type, back to IDLE * * Press types: * SHORT held < BTN_LONG_MIN_MS from confirmed start * LONG held >= BTN_LONG_MIN_MS * * Sequence detection (operates on classified presses): * Buffer up to 3 presses. Recognised patterns: * [SHORT, SHORT, LONG] -> BTN_EVENT_REARM_COMBO (fires on LONG release) * [SHORT] + BTN_COMMIT_MS timeout -> BTN_EVENT_PARK * Sequence reset after BTN_SEQ_TIMEOUT_MS from first press. */ #include "hw_button.h" #include "config.h" #ifndef TEST_HOST #include "stm32f7xx_hal.h" #endif /* ---- Timing defaults (override in config.h) ---- */ #ifndef BTN_DEBOUNCE_MS #define BTN_DEBOUNCE_MS 20u #endif #ifndef BTN_LONG_MIN_MS #define BTN_LONG_MIN_MS 1500u #endif #ifndef BTN_COMMIT_MS #define BTN_COMMIT_MS 500u #endif #ifndef BTN_SEQ_TIMEOUT_MS #define BTN_SEQ_TIMEOUT_MS 3000u #endif /* ---- Press type ---- */ typedef enum { _PT_SHORT = 1u, _PT_LONG = 2u, } _press_type_t; /* ---- Debounce state ---- */ typedef enum { _BTN_IDLE, _BTN_DEBOUNCING, _BTN_HELD, } _btn_state_t; static _btn_state_t s_state = _BTN_IDLE; static uint32_t s_trans_ms = 0u; /* timestamp of last FSM transition */ static bool s_pressed = false; /* ---- Sequence buffer ---- */ #define _SEQ_MAX 3u static _press_type_t s_seq[_SEQ_MAX]; static uint8_t s_seq_len = 0u; static uint32_t s_seq_first_ms = 0u; static uint32_t s_seq_last_ms = 0u; /* ---- GPIO read ---- */ #ifdef TEST_HOST static bool s_test_raw = false; void hw_button_inject(bool pressed) { s_test_raw = pressed; } static bool _read_raw(void) { return s_test_raw; } #else static bool _read_raw(void) { return HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_RESET; /* active-low */ } #endif void hw_button_init(void) { #ifndef TEST_HOST __HAL_RCC_GPIOC_CLK_ENABLE(); /* BTN_PORT assumed GPIOC; adjust if needed */ GPIO_InitTypeDef g = {0}; g.Pin = BTN_PIN; g.Mode = GPIO_MODE_INPUT; g.Pull = GPIO_PULLUP; HAL_GPIO_Init(BTN_PORT, &g); #endif s_state = _BTN_IDLE; s_seq_len = 0u; s_pressed = false; } bool hw_button_is_pressed(void) { return s_pressed; } /* Record a classified press into the sequence buffer and check for patterns. */ static hw_btn_event_t _record_press(_press_type_t pt, uint32_t now_ms) { if (s_seq_len == 0u) { s_seq_first_ms = now_ms; } s_seq_last_ms = now_ms; if (s_seq_len < _SEQ_MAX) { s_seq[s_seq_len++] = pt; } /* Check REARM_COMBO: SHORT + SHORT + LONG */ if (s_seq_len == 3u && s_seq[0] == _PT_SHORT && s_seq[1] == _PT_SHORT && s_seq[2] == _PT_LONG) { s_seq_len = 0u; return BTN_EVENT_REARM_COMBO; } return BTN_EVENT_NONE; } hw_btn_event_t hw_button_tick(uint32_t now_ms) { bool raw = _read_raw(); /* ---- Debounce FSM ---- */ switch (s_state) { case _BTN_IDLE: if (raw) { s_state = _BTN_DEBOUNCING; s_trans_ms = now_ms; } break; case _BTN_DEBOUNCING: if (!raw) { /* Released before debounce elapsed — bounce, ignore */ s_state = _BTN_IDLE; } else if ((now_ms - s_trans_ms) >= BTN_DEBOUNCE_MS) { s_state = _BTN_HELD; s_trans_ms = now_ms; /* record confirmed press-start time */ s_pressed = true; } break; case _BTN_HELD: if (!raw) { s_pressed = false; uint32_t held_ms = now_ms - s_trans_ms; _press_type_t pt = (held_ms >= BTN_LONG_MIN_MS) ? _PT_LONG : _PT_SHORT; s_state = _BTN_IDLE; hw_btn_event_t ev = _record_press(pt, now_ms); if (ev != BTN_EVENT_NONE) { return ev; } } break; } /* ---- Sequence timeout / commit check (only when not currently held) ---- */ if (s_state == _BTN_IDLE && s_seq_len > 0u) { uint32_t since_first = now_ms - s_seq_first_ms; uint32_t since_last = now_ms - s_seq_last_ms; /* Whole sequence window expired — abandon */ if (since_first >= BTN_SEQ_TIMEOUT_MS) { s_seq_len = 0u; return BTN_EVENT_NONE; } /* Single short press + BTN_COMMIT_MS of quiet -> PARK */ if (s_seq_len == 1u && s_seq[0] == _PT_SHORT && since_last >= BTN_COMMIT_MS) { s_seq_len = 0u; return BTN_EVENT_PARK; } } return BTN_EVENT_NONE; }