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>
284 lines
8.7 KiB
C
284 lines
8.7 KiB
C
/*
|
|
* test_hw_button.c — Unit tests for hw_button debounce/gesture driver (Issue #682).
|
|
*
|
|
* Build (host, no hardware):
|
|
* gcc -I include -I test/stubs -DTEST_HOST \
|
|
* -o /tmp/test_hw_button src/hw_button.c test/test_hw_button.c
|
|
*
|
|
* All tests use hw_button_inject() to drive the simulated GPIO pin.
|
|
* All timing is driven through the now_ms argument passed to hw_button_tick().
|
|
*/
|
|
|
|
/* ---- Stubs: prevent STM32 HAL from being included in non-TEST_HOST path ---- */
|
|
#define STM32F7XX_HAL_H
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
|
|
/* ---- Include implementation directly ---- */
|
|
#include "../src/hw_button.c"
|
|
|
|
/* ---- Test framework ---- */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static int g_pass = 0;
|
|
static int g_fail = 0;
|
|
|
|
#define ASSERT(cond, msg) \
|
|
do { \
|
|
if (cond) { g_pass++; } \
|
|
else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \
|
|
} while (0)
|
|
|
|
/* ---- Helpers ---- */
|
|
|
|
/* Advance time by `ms` ticks with the pin held at `pressed`.
|
|
* Returns the last non-NONE event seen, or BTN_EVENT_NONE. */
|
|
static hw_btn_event_t _tick_n(uint32_t *now, uint32_t ms, bool pressed)
|
|
{
|
|
hw_btn_event_t last = BTN_EVENT_NONE;
|
|
hw_button_inject(pressed);
|
|
for (uint32_t i = 0; i < ms; i++) {
|
|
(*now)++;
|
|
hw_btn_event_t ev = hw_button_tick(*now);
|
|
if (ev != BTN_EVENT_NONE) last = ev;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/* Reset driver state between tests. */
|
|
static void _reset(uint32_t *now)
|
|
{
|
|
*now = 0;
|
|
hw_button_inject(false);
|
|
hw_button_init();
|
|
/* Drain any pending sequence state */
|
|
for (int i = 0; i < 100; i++) hw_button_tick((*now)++);
|
|
}
|
|
|
|
/* ---- Tests ---- */
|
|
|
|
static void test_idle_no_event(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
hw_button_inject(false);
|
|
for (int i = 0; i < 1000; i++) {
|
|
hw_btn_event_t ev = hw_button_tick(now++);
|
|
ASSERT(ev == BTN_EVENT_NONE, "idle: no event while pin released");
|
|
}
|
|
}
|
|
|
|
static void test_debounce_bounce_ignored(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press for only 10 ms (< BTN_DEBOUNCE_MS=20) then release */
|
|
hw_btn_event_t ev = _tick_n(&now, 10, true);
|
|
ASSERT(ev == BTN_EVENT_NONE, "bounce < debounce: no event during press");
|
|
ev = _tick_n(&now, BTN_COMMIT_MS + 50, false);
|
|
ASSERT(ev == BTN_EVENT_NONE, "bounce < debounce: no event after release");
|
|
}
|
|
|
|
static void test_debounce_confirmed(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press for BTN_DEBOUNCE_MS + 1 ms — debounce should confirm */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 1, true);
|
|
ASSERT(hw_button_is_pressed(), "debounce passed: is_pressed() == true");
|
|
}
|
|
|
|
static void test_short_press_park(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press for BTN_DEBOUNCE_MS + 10 ms (short: well < BTN_LONG_MIN_MS) */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
/* Release */
|
|
_tick_n(&now, 1, false);
|
|
ASSERT(!hw_button_is_pressed(), "short press: released");
|
|
|
|
/* Wait BTN_COMMIT_MS quiet -> should fire PARK */
|
|
hw_btn_event_t ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) {
|
|
now++;
|
|
hw_btn_event_t e = hw_button_tick(now);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_PARK, "short press + quiet: BTN_EVENT_PARK");
|
|
}
|
|
|
|
static void test_long_press_no_park(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press for BTN_LONG_MIN_MS + 100 ms (long press) */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 100, true);
|
|
/* Release and wait */
|
|
hw_btn_event_t ev = _tick_n(&now, BTN_COMMIT_MS + 10, false);
|
|
/* Lone LONG press has no defined gesture */
|
|
ASSERT(ev == BTN_EVENT_NONE, "lone long press: no event");
|
|
}
|
|
|
|
static void test_rearm_combo_fires(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press 1: SHORT (30 ms hold + release) */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
/* Brief gap between presses (< BTN_COMMIT_MS so no PARK fires) */
|
|
_tick_n(&now, 100, false);
|
|
|
|
/* Press 2: SHORT */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
_tick_n(&now, 100, false);
|
|
|
|
/* Press 3: LONG — combo fires on release */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 50, true);
|
|
hw_btn_event_t ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (int i = 0; i < 5; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_REARM_COMBO, "SHORT+SHORT+LONG: BTN_EVENT_REARM_COMBO");
|
|
}
|
|
|
|
static void test_rearm_combo_resets_buffer(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Fire combo once */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 100, false);
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 100, false);
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + BTN_LONG_MIN_MS + 50, true);
|
|
hw_button_inject(false);
|
|
hw_btn_event_t ev = BTN_EVENT_NONE;
|
|
for (int i = 0; i < 5; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_REARM_COMBO, "first combo fires");
|
|
|
|
/* After combo, a lone short press should produce PARK */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_PARK, "after combo: short press fires PARK");
|
|
}
|
|
|
|
static void test_seq_timeout_clears_buffer(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* TWO short presses — seq_len=2 (no PARK since it's not a lone SHORT).
|
|
* Then wait BTN_SEQ_TIMEOUT_MS — buffer should be abandoned silently. */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
_tick_n(&now, 100, false); /* gap between presses (< BTN_COMMIT_MS) */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
|
|
/* Wait BTN_SEQ_TIMEOUT_MS — seq_len=2 buffer expires silently */
|
|
hw_btn_event_t ev = _tick_n(&now, BTN_SEQ_TIMEOUT_MS + 10, false);
|
|
ASSERT(ev == BTN_EVENT_NONE, "seq timeout (seq_len=2): buffer cleared, no event");
|
|
|
|
/* Fresh lone short press should produce PARK as a new sequence */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_PARK, "after timeout: fresh short press fires PARK");
|
|
}
|
|
|
|
static void test_is_pressed_tracks_held(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
ASSERT(!hw_button_is_pressed(), "initially not pressed");
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 1, true);
|
|
ASSERT(hw_button_is_pressed(), "after debounce: is_pressed true");
|
|
_tick_n(&now, 1, false);
|
|
ASSERT(!hw_button_is_pressed(), "after release: is_pressed false");
|
|
}
|
|
|
|
static void test_short_press_before_commit_no_park(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Press and release */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true);
|
|
_tick_n(&now, 1, false);
|
|
|
|
/* Wait less than BTN_COMMIT_MS — no PARK yet */
|
|
hw_btn_event_t ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (uint32_t i = 0; i < BTN_COMMIT_MS - 50; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_NONE, "before commit window: no PARK yet");
|
|
}
|
|
|
|
static void test_two_shorts_no_park(void)
|
|
{
|
|
uint32_t now = 0;
|
|
hw_button_init();
|
|
|
|
/* Two short presses close together — seq_len=2 does not trigger PARK */
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false);
|
|
_tick_n(&now, 100, false);
|
|
_tick_n(&now, BTN_DEBOUNCE_MS + 10, true); _tick_n(&now, 1, false);
|
|
|
|
hw_btn_event_t ev = BTN_EVENT_NONE;
|
|
hw_button_inject(false);
|
|
for (uint32_t i = 0; i < BTN_COMMIT_MS + 10; i++) {
|
|
hw_btn_event_t e = hw_button_tick(now++);
|
|
if (e != BTN_EVENT_NONE) ev = e;
|
|
}
|
|
ASSERT(ev == BTN_EVENT_NONE, "two shorts + quiet: no PARK (seq_len=2, not lone SHORT)");
|
|
}
|
|
|
|
/* ---- main ---- */
|
|
int main(void)
|
|
{
|
|
printf("=== test_hw_button ===\n");
|
|
|
|
test_idle_no_event();
|
|
test_debounce_bounce_ignored();
|
|
test_debounce_confirmed();
|
|
test_short_press_park();
|
|
test_long_press_no_park();
|
|
test_rearm_combo_fires();
|
|
test_rearm_combo_resets_buffer();
|
|
test_seq_timeout_clears_buffer();
|
|
test_is_pressed_tracks_held();
|
|
test_short_press_before_commit_no_park();
|
|
test_two_shorts_no_park();
|
|
|
|
printf("\nResults: %d passed, %d failed\n", g_pass, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|