From 8a8e4b841a7ab0e838d28ead7a3101820bdc65fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 18 May 2026 22:52:37 +0200 Subject: [PATCH] Haptic Feedback (short and long press) --- src/input/HapticFeedback.cpp | 86 ++++++++++++++++++++++ src/input/HapticFeedback.h | 47 ++++++++++++ src/input/InputBroker.cpp | 13 ++++ variants/nrf52840/t-impulse-plus/variant.h | 3 +- 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/input/HapticFeedback.cpp create mode 100644 src/input/HapticFeedback.h diff --git a/src/input/HapticFeedback.cpp b/src/input/HapticFeedback.cpp new file mode 100644 index 000000000..ef277779c --- /dev/null +++ b/src/input/HapticFeedback.cpp @@ -0,0 +1,86 @@ +#include "HapticFeedback.h" + +#ifdef HAPTIC_FEEDBACK_PIN + +#include + +#ifdef HAPTIC_FEEDBACK_ACTIVE_LOW +#define HAPTIC_FEEDBACK_ON_STATE LOW +#define HAPTIC_FEEDBACK_OFF_STATE HIGH +#else +#define HAPTIC_FEEDBACK_ON_STATE HIGH +#define HAPTIC_FEEDBACK_OFF_STATE LOW +#endif + +HapticFeedback *hapticFeedback = nullptr; + +void initHapticFeedback() +{ + if (!hapticFeedback) + hapticFeedback = new HapticFeedback(); +} + +HapticFeedback::HapticFeedback() : concurrency::OSThread("Haptic") +{ + pinMode(HAPTIC_FEEDBACK_PIN, OUTPUT); + digitalWrite(HAPTIC_FEEDBACK_PIN, HAPTIC_FEEDBACK_OFF_STATE); +} + +void HapticFeedback::motorWrite(bool on) +{ + digitalWrite(HAPTIC_FEEDBACK_PIN, on ? HAPTIC_FEEDBACK_ON_STATE : HAPTIC_FEEDBACK_OFF_STATE); +} + +void HapticFeedback::pulse(uint16_t durationMs) +{ + motorWrite(true); + pulseOffAt = millis() + durationMs; + if (pulseOffAt == 0) // disambiguate from "no pulse active" sentinel on millis() wrap + pulseOffAt = 1; + setIntervalFromNow(durationMs); +} + +void HapticFeedback::armDelayedPulse(uint16_t delayMs, uint16_t durationMs) +{ + delayedPulseAt = millis() + delayMs; + if (delayedPulseAt == 0) + delayedPulseAt = 1; + delayedPulseDuration = durationMs; + setIntervalFromNow(delayMs); +} + +void HapticFeedback::cancelDelayedPulse() +{ + delayedPulseAt = 0; +} + +int32_t HapticFeedback::runOnce() +{ + uint32_t now = millis(); + + // End an in-flight pulse if its time has come. + if (pulseOffAt != 0 && (int32_t)(now - pulseOffAt) >= 0) { + motorWrite(false); + pulseOffAt = 0; + } + + // Fire an armed delayed pulse if its time has come. + if (delayedPulseAt != 0 && (int32_t)(now - delayedPulseAt) >= 0) { + uint16_t dur = delayedPulseDuration; + delayedPulseAt = 0; + pulse(dur); + } + + // Sleep until the next scheduled event, or idle long if nothing pending. + uint32_t next = 0; + if (pulseOffAt != 0) + next = pulseOffAt; + if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0)) + next = delayedPulseAt; + if (next == 0) + return 60 * 1000; // nothing pending — idle for a minute + int32_t delay = (int32_t)(next - now); + return delay > 0 ? delay : 0; +} + +#endif // HAPTIC_FEEDBACK_PIN diff --git a/src/input/HapticFeedback.h b/src/input/HapticFeedback.h new file mode 100644 index 000000000..cb0f94873 --- /dev/null +++ b/src/input/HapticFeedback.h @@ -0,0 +1,47 @@ +#pragma once + +#include "configuration.h" + +#ifdef HAPTIC_FEEDBACK_PIN + +#include "concurrency/OSThread.h" +#include + +// Drives short, non-blocking pulses on a GPIO-controlled vibration motor. +// A variant opts in by defining HAPTIC_FEEDBACK_PIN; HAPTIC_FEEDBACK_ACTIVE_LOW +// inverts the drive polarity (default: active-high — pin HIGH = motor on). +// +// Used by the touch button to produce button-like haptic feedback. Coexists +// with ExternalNotificationModule if both target the same pin — pulses are +// fire-and-forget, no synchronization, last writer wins. +class HapticFeedback : public concurrency::OSThread +{ + public: + HapticFeedback(); + + // Turn motor on now, schedule off after durationMs. + void pulse(uint16_t durationMs = 30); + + // Schedule a one-shot pulse to fire delayMs from now. + void armDelayedPulse(uint16_t delayMs, uint16_t durationMs = 30); + + // Cancel a previously-armed delayed pulse (no effect if none pending). + void cancelDelayedPulse(); + + protected: + int32_t runOnce() override; + + private: + uint32_t pulseOffAt = 0; // millis() when current pulse should end (0 = no pulse active) + uint32_t delayedPulseAt = 0; // millis() when armed pulse should fire (0 = nothing armed) + uint16_t delayedPulseDuration = 0; + + void motorWrite(bool on); +}; + +extern HapticFeedback *hapticFeedback; + +// Lazy-instantiate the global on first call. Safe to call repeatedly. +void initHapticFeedback(); + +#endif // HAPTIC_FEEDBACK_PIN diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 42ab7f70d..1b4b64cad 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -2,6 +2,7 @@ #include "PowerFSM.h" // needed for event trigger #include "configuration.h" #include "graphics/Screen.h" +#include "input/HapticFeedback.h" #include "modules/ExternalNotificationModule.h" #if ARCH_PORTDUINO @@ -237,6 +238,18 @@ void InputBroker::Init() } touchBacklightActive = false; }; +#endif +#if defined(HAPTIC_FEEDBACK_PIN) + // Haptic feedback: short pulse on touch contact, and a second short + // pulse when the long-press fires (BACK). The delayed pulse delay + // matches touchConfig.longPressTime's default (500 ms). + touchConfig.suppressLeadUpSound = true; + initHapticFeedback(); + touchConfig.onPress = []() { + hapticFeedback->pulse(30); + hapticFeedback->armDelayedPulse(500, 30); + }; + touchConfig.onRelease = []() { hapticFeedback->cancelDelayedPulse(); }; #endif TouchButtonThread->initButton(touchConfig); #endif diff --git a/variants/nrf52840/t-impulse-plus/variant.h b/variants/nrf52840/t-impulse-plus/variant.h index 7aee32c46..4c789d7ab 100644 --- a/variants/nrf52840/t-impulse-plus/variant.h +++ b/variants/nrf52840/t-impulse-plus/variant.h @@ -147,9 +147,10 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define EXTERNAL_FLASH_USE_QSPI // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -// Vibration Motor (active-low, used as notification output) +// Vibration Motor (GPIO active-high) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define LED_NOTIFICATION D19 // P0.22 +#define HAPTIC_FEEDBACK_PIN LED_NOTIFICATION // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // IMU (ICM20948 on Wire1)