Files
Thomas Göttgens 576a1bb008 Fix trackball dropping short presses and losing the click when tilted (#11599)
* Fix trackball dropping short presses and losing the click when tilted

* Do not let a direction counter overwrite an emitted press event

* Accept the first press interrupt when the clock still reads zero

* Classify a press released before the first poll by its latched time
2026-08-26 20:00:42 +00:00

165 lines
5.5 KiB
C++

// Unit tests for TrackballInterruptBase::updatePress(): short/long classification, the interrupt
// latch that keeps a tap released between polls, and repeat timing across the millis() rollover.
#include "TestUtil.h"
#include "UptimeClock.h"
#include "input/TrackballInterruptBase.h"
#include <unity.h>
namespace
{
// updatePress() is protected: expose it without touching any GPIO.
class PressProbe : public TrackballInterruptBase
{
public:
PressProbe() : TrackballInterruptBase("tbpress") {}
using TrackballInterruptBase::PressResult;
using TrackballInterruptBase::updatePress;
};
constexpr uint32_t LONG_PRESS_MS = 500;
constexpr uint32_t LONG_REPEAT_MS = 300;
constexpr uint32_t START_MS = 100000;
} // namespace
void setUp(void)
{
Time::setTestMillis(START_MS);
}
void tearDown(void)
{
Time::useRealClock();
}
void test_tap_released_before_poll_emits_short()
{
PressProbe tb;
TEST_ASSERT_TRUE(PressProbe::PressResult::Short == tb.updatePress(true, START_MS, false));
}
void test_held_then_released_under_threshold_emits_short()
{
PressProbe tb;
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(true, START_MS, true));
Time::advanceTestMillis(LONG_PRESS_MS - 1);
TEST_ASSERT_TRUE(PressProbe::PressResult::Short == tb.updatePress(false, 0, false));
}
void test_still_held_under_threshold_emits_nothing()
{
PressProbe tb;
tb.updatePress(true, START_MS, true);
Time::advanceTestMillis(LONG_PRESS_MS / 2);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, true));
}
void test_hold_emits_long_repeats_at_the_repeat_interval()
{
PressProbe tb;
tb.updatePress(true, START_MS, true);
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
Time::advanceTestMillis(LONG_REPEAT_MS - 1);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, true));
Time::advanceTestMillis(1);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
}
void test_release_after_long_press_emits_nothing()
{
PressProbe tb;
tb.updatePress(true, START_MS, true);
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
Time::advanceTestMillis(10);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, false));
}
void test_press_after_release_is_tracked_again()
{
PressProbe tb;
tb.updatePress(true, START_MS, true);
Time::advanceTestMillis(10);
TEST_ASSERT_TRUE(PressProbe::PressResult::Short == tb.updatePress(false, 0, false));
Time::advanceTestMillis(10);
const uint32_t secondIrq = Time::getMillis();
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(true, secondIrq, true));
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
}
void test_idle_poll_emits_nothing()
{
PressProbe tb;
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, false));
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, true));
}
void test_hold_across_the_millis_wrap()
{
PressProbe tb;
const uint32_t nearWrap = 0xFFFFFF00u;
Time::setTestMillis(nearWrap);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(true, nearWrap, true));
// Crosses the 32-bit rollover mid-press; a raw millis() compare would fire or stall here.
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
}
// A delayed first poll must not report a long hold as a tap just because the pin is already high.
void test_long_hold_released_before_first_poll_is_not_short()
{
PressProbe tb;
const uint32_t irq = Time::getMillis();
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(true, irq, false));
}
// A repeat emitted exactly when the clock reads 0 must not be mistaken for "no repeat sent yet".
void test_repeat_emitted_at_clock_zero_still_waits()
{
PressProbe tb;
const uint32_t beforeWrap = 0u - LONG_PRESS_MS; // start + LONG_PRESS_MS wraps to exactly 0
Time::setTestMillis(beforeWrap);
tb.updatePress(true, beforeWrap, true);
Time::advanceTestMillis(LONG_PRESS_MS);
TEST_ASSERT_EQUAL_UINT32(0, Time::getMillis());
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
Time::advanceTestMillis(LONG_REPEAT_MS - 1);
TEST_ASSERT_TRUE(PressProbe::PressResult::None == tb.updatePress(false, 0, true));
Time::advanceTestMillis(1);
TEST_ASSERT_TRUE(PressProbe::PressResult::LongRepeat == tb.updatePress(false, 0, true));
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_tap_released_before_poll_emits_short);
RUN_TEST(test_held_then_released_under_threshold_emits_short);
RUN_TEST(test_still_held_under_threshold_emits_nothing);
RUN_TEST(test_hold_emits_long_repeats_at_the_repeat_interval);
RUN_TEST(test_release_after_long_press_emits_nothing);
RUN_TEST(test_press_after_release_is_tracked_again);
RUN_TEST(test_idle_poll_emits_nothing);
RUN_TEST(test_hold_across_the_millis_wrap);
RUN_TEST(test_long_hold_released_before_first_poll_is_not_short);
RUN_TEST(test_repeat_emitted_at_clock_zero_still_waits);
exit(UNITY_END());
}
void loop() {}