Skip to main content

embedded_gui/
haptics.rs

1//! Lightweight haptic pattern sequencer for microcontrollers and embedded GUIs.
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum HapticPattern {
5    None,
6    Click,
7    DoubleClick,
8    LongPress,
9    Success,
10    Alert,
11    Custom(&'static [(u32, u8)]),
12}
13
14static CLICK_STEPS: &[(u32, u8)] = &[(30, 255)];
15static DOUBLE_CLICK_STEPS: &[(u32, u8)] = &[(40, 255), (40, 0), (40, 255)];
16static LONG_PRESS_STEPS: &[(u32, u8)] = &[(150, 200)];
17static SUCCESS_STEPS: &[(u32, u8)] = &[(80, 150), (40, 0), (120, 255)];
18static ALERT_STEPS: &[(u32, u8)] = &[(200, 255), (80, 0), (200, 255)];
19
20impl HapticPattern {
21    pub fn steps(&self) -> &'static [(u32, u8)] {
22        match self {
23            HapticPattern::None => &[],
24            HapticPattern::Click => CLICK_STEPS,
25            HapticPattern::DoubleClick => DOUBLE_CLICK_STEPS,
26            HapticPattern::LongPress => LONG_PRESS_STEPS,
27            HapticPattern::Success => SUCCESS_STEPS,
28            HapticPattern::Alert => ALERT_STEPS,
29            HapticPattern::Custom(steps) => steps,
30        }
31    }
32}
33
34#[derive(Clone, Copy, Debug)]
35pub struct HapticSequencer {
36    steps: &'static [(u32, u8)],
37    current_step: usize,
38    step_elapsed_ms: u32,
39    current_intensity: u8,
40}
41
42#[allow(clippy::derivable_impls)]
43impl Default for HapticSequencer {
44    fn default() -> Self {
45        Self {
46            steps: &[],
47            current_step: 0,
48            step_elapsed_ms: 0,
49            current_intensity: 0,
50        }
51    }
52}
53
54impl HapticSequencer {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    pub fn play(&mut self, pattern: HapticPattern) {
60        let steps = pattern.steps();
61        if steps.is_empty() {
62            self.steps = &[];
63            self.current_step = 0;
64            self.step_elapsed_ms = 0;
65            self.current_intensity = 0;
66        } else {
67            self.steps = steps;
68            self.current_step = 0;
69            self.step_elapsed_ms = 0;
70            self.current_intensity = steps[0].1;
71        }
72    }
73
74    pub fn stop(&mut self) {
75        self.play(HapticPattern::None);
76    }
77
78    pub fn tick(&mut self, dt_ms: u32) {
79        if self.steps.is_empty() {
80            return;
81        }
82
83        self.step_elapsed_ms = self.step_elapsed_ms.saturating_add(dt_ms);
84
85        while self.current_step < self.steps.len()
86            && self.step_elapsed_ms >= self.steps[self.current_step].0
87        {
88            self.step_elapsed_ms -= self.steps[self.current_step].0;
89            self.current_step += 1;
90        }
91
92        if self.current_step >= self.steps.len() {
93            // Finished pattern
94            self.steps = &[];
95            self.current_step = 0;
96            self.step_elapsed_ms = 0;
97            self.current_intensity = 0;
98        } else {
99            self.current_intensity = self.steps[self.current_step].1;
100        }
101    }
102
103    pub fn current_intensity(&self) -> u8 {
104        self.current_intensity
105    }
106
107    pub fn is_playing(&self) -> bool {
108        !self.steps.is_empty()
109    }
110}