Skip to main content

ph_haptics/
dsl.rs

1use crate::motor::MotorKind;
2use ph_curves::{MonotonicCurveLut256, Rounding};
3
4/// How a program repeats.
5#[derive(Copy, Clone, Debug, Eq, PartialEq)]
6pub enum LoopMode {
7    /// Play once and stop.
8    Once,
9    /// Restart from instruction 0 forever.
10    Forever,
11    /// Play exactly `n` cycles and stop; `Count(0)` finishes on the first poll.
12    Count(u32),
13}
14
15/// A curve-shaped transition from `from` to `to`.
16#[derive(Copy, Clone, Debug)]
17pub struct Ramp<C = MonotonicCurveLut256> {
18    /// Segment duration in milliseconds.
19    pub duration_ms: u32,
20    /// Starting normalized level (`0..=u16::MAX`).
21    pub from: u16,
22    /// Ending normalized level (`0..=u16::MAX`).
23    pub to: u16,
24    /// Monotonic curve used to shape progress over time.
25    pub curve: C,
26    /// Output quantization step (minimum `1`).
27    pub step: u16,
28    /// Quantization rounding mode.
29    pub rounding: Rounding,
30    /// Minimum milliseconds between output changes.
31    pub min_dt_ms: u32,
32    /// Optional per-segment LRA frequency override.
33    pub lra_frequency_hz: Option<u16>,
34    /// Optional ending LRA frequency for frequency sweeps.
35    ///
36    /// This is used only when [`Self::lra_frequency_hz`] also contains the
37    /// starting frequency; otherwise it is ignored.
38    pub lra_frequency_hz_to: Option<u16>,
39}
40
41impl<C> Ramp<C> {
42    /// Build a new ramp segment.
43    ///
44    /// A zero `duration_ms` is normalized to one millisecond. Direct struct
45    /// construction does not perform this normalization.
46    pub const fn new(duration_ms: u32, from: u16, to: u16, curve: C) -> Self {
47        Self {
48            duration_ms: normalize_duration(duration_ms),
49            from,
50            to,
51            curve,
52            step: 1,
53            rounding: Rounding::Nearest,
54            min_dt_ms: 0,
55            lra_frequency_hz: None,
56            lra_frequency_hz_to: None,
57        }
58    }
59
60    /// Override quantization settings.
61    ///
62    /// A zero `step` is normalized to `1`.
63    pub const fn with_quantization(mut self, step: u16, rounding: Rounding) -> Self {
64        self.step = normalize_step(step);
65        self.rounding = rounding;
66        self
67    }
68
69    /// Set a minimum time between consecutive output changes.
70    pub const fn with_min_dt(mut self, min_dt_ms: u32) -> Self {
71        self.min_dt_ms = min_dt_ms;
72        self
73    }
74
75    /// Override LRA frequency for this segment.
76    pub const fn with_lra_frequency(mut self, frequency_hz: u16) -> Self {
77        self.lra_frequency_hz = Some(frequency_hz);
78        self
79    }
80}
81
82/// A single DSL instruction.
83#[derive(Copy, Clone, Debug)]
84pub enum Instruction<C = MonotonicCurveLut256> {
85    /// Curve-shaped level change.
86    Ramp(Ramp<C>),
87    /// Constant non-zero or zero level for a duration.
88    Hold {
89        /// Segment duration in milliseconds.
90        duration_ms: u32,
91        /// Normalized level (`0..=u16::MAX`).
92        level: u16,
93        /// Optional per-segment LRA frequency override.
94        lra_frequency_hz: Option<u16>,
95    },
96    /// Explicit silent gap (level = 0).
97    Pause {
98        /// Segment duration in milliseconds.
99        duration_ms: u32,
100    },
101}
102
103impl<C> Instruction<C> {
104    /// Construct a ramp, normalizing a zero duration to one millisecond.
105    pub const fn ramp(duration_ms: u32, from: u16, to: u16, curve: C) -> Self {
106        Self::Ramp(Ramp::new(duration_ms, from, to, curve))
107    }
108
109    /// Construct a hold, normalizing a zero duration to one millisecond.
110    pub const fn hold(duration_ms: u32, level: u16) -> Self {
111        Self::Hold {
112            duration_ms: normalize_duration(duration_ms),
113            level,
114            lra_frequency_hz: None,
115        }
116    }
117
118    /// Construct an LRA hold with an explicit frequency.
119    ///
120    /// A zero duration is normalized to one millisecond.
121    pub const fn hold_with_lra_frequency(duration_ms: u32, level: u16, frequency_hz: u16) -> Self {
122        Self::Hold {
123            duration_ms: normalize_duration(duration_ms),
124            level,
125            lra_frequency_hz: Some(frequency_hz),
126        }
127    }
128
129    /// Construct a pause, normalizing a zero duration to one millisecond.
130    pub const fn pause(duration_ms: u32) -> Self {
131        Self::Pause {
132            duration_ms: normalize_duration(duration_ms),
133        }
134    }
135
136    /// Segment duration in milliseconds.
137    pub const fn duration_ms(&self) -> u32 {
138        match self {
139            Self::Ramp(ramp) => ramp.duration_ms,
140            Self::Hold { duration_ms, .. } | Self::Pause { duration_ms } => *duration_ms,
141        }
142    }
143}
144
145/// A haptics program: motor target + instruction stream + repeat mode.
146#[derive(Copy, Clone, Debug)]
147pub struct Program<'a, C = MonotonicCurveLut256> {
148    motor: MotorKind,
149    instructions: &'a [Instruction<C>],
150    loop_mode: LoopMode,
151}
152
153impl<'a, C> Program<'a, C> {
154    /// Create a new program with [`LoopMode::Once`].
155    pub const fn new(motor: MotorKind, instructions: &'a [Instruction<C>]) -> Self {
156        Self {
157            motor,
158            instructions,
159            loop_mode: LoopMode::Once,
160        }
161    }
162
163    /// Return a copy with the given loop mode.
164    pub const fn with_loop_mode(mut self, loop_mode: LoopMode) -> Self {
165        self.loop_mode = loop_mode;
166        self
167    }
168
169    /// Return a copy set to [`LoopMode::Forever`].
170    pub const fn repeat_forever(self) -> Self {
171        self.with_loop_mode(LoopMode::Forever)
172    }
173
174    /// Target motor type for this program.
175    pub const fn motor(&self) -> MotorKind {
176        self.motor
177    }
178
179    /// Program loop mode.
180    pub const fn loop_mode(&self) -> LoopMode {
181        self.loop_mode
182    }
183
184    /// Instruction sequence.
185    pub const fn instructions(&self) -> &'a [Instruction<C>] {
186        self.instructions
187    }
188
189    /// Whether there are no instructions.
190    pub const fn is_empty(&self) -> bool {
191        self.instructions.is_empty()
192    }
193
194    /// Total duration of one cycle in milliseconds, saturating at [`u32::MAX`].
195    pub fn total_duration_ms(&self) -> u32 {
196        let mut total = 0u32;
197        let mut index = 0usize;
198
199        while index < self.instructions.len() {
200            total = total.saturating_add(self.instructions[index].duration_ms());
201            index += 1;
202        }
203
204        total
205    }
206}
207
208pub(crate) const fn normalize_step(step: u16) -> u16 {
209    if step == 0 { 1 } else { step }
210}
211
212const fn normalize_duration(duration_ms: u32) -> u32 {
213    if duration_ms == 0 { 1 } else { duration_ms }
214}