Haptic Feedback (short and long press)

This commit is contained in:
Thomas Göttgens
2026-05-18 22:52:37 +02:00
parent 0c1457b5dc
commit 8a8e4b841a
4 changed files with 148 additions and 1 deletions

View File

@@ -0,0 +1,86 @@
#include "HapticFeedback.h"
#ifdef HAPTIC_FEEDBACK_PIN
#include <Arduino.h>
#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

View File

@@ -0,0 +1,47 @@
#pragma once
#include "configuration.h"
#ifdef HAPTIC_FEEDBACK_PIN
#include "concurrency/OSThread.h"
#include <stdint.h>
// 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

View File

@@ -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

View File

@@ -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)