saltylab-firmware/src/hw_button.c
sl-firmware 4affd6d0cb feat: Hardware button park/disarm/re-arm (Issue #682)
Add hw_button driver (PC2 active-low, 20ms debounce) with gesture detection:
- Single short press + 500ms quiet -> BTN_EVENT_PARK
- SHORT+SHORT+LONG combo (within 3s) -> BTN_EVENT_REARM_COMBO

New BALANCE_PARKED state: PID frozen, motors off, quick re-arm via button
combo without the 3-second arm interlock required from DISARMED.

FC_BTN (0x404) CAN frame sent to Orin on each event:
  event_id 1=PARKED, 2=UNPARKED, 3=UNPARK_FAILED (pitch > 20 deg)

Includes 11 unit tests (1016 assertions) exercising debounce, bounce
rejection, short/long classification, sequence detection, and timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:10:10 -04:00

180 lines
4.7 KiB
C

/* 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;
}