Skip to main content

ph_haptics/
compiled.rs

1use crate::dsl::Program;
2use crate::error::Error;
3use crate::motor::MotorConfig;
4use crate::motor::MotorProfile;
5use crate::runtime::Runner;
6use ph_curves::MonotonicCurveLut256;
7
8/// A named, compile-time haptics definition emitted by code generation.
9#[derive(Copy, Clone, Debug)]
10pub struct CompiledHapticDef<'a> {
11    /// Symbolic name from the source haptics definition file.
12    pub name: &'a str,
13    /// Typed DSL program ready for [`crate::Runner`].
14    pub program: Program<'a, MonotonicCurveLut256>,
15    /// Optional motor profile associated with this haptic.
16    ///
17    /// The runtime uses it for kick injection, level floors and ceilings, and
18    /// gamma mapping. `None` disables all profile-driven runtime transforms.
19    pub profile: Option<&'a MotorProfile>,
20}
21
22impl<'a> CompiledHapticDef<'a> {
23    /// Build a compiled haptic definition.
24    ///
25    /// Program validity and motor compatibility are checked when constructing a
26    /// runner, not here.
27    pub const fn new(
28        name: &'a str,
29        program: Program<'a, MonotonicCurveLut256>,
30        profile: Option<&'a MotorProfile>,
31    ) -> Self {
32        Self {
33            name,
34            program,
35            profile,
36        }
37    }
38
39    /// Build a [`Runner`] for this compiled haptic.
40    pub fn runner(&'a self, motor: MotorConfig) -> Result<Runner<'a, MonotonicCurveLut256>, Error> {
41        Runner::from_compiled(self, motor)
42    }
43
44    /// Build and immediately start a [`Runner`] for this compiled haptic.
45    pub fn runner_started(
46        &'a self,
47        motor: MotorConfig,
48        now_ms: u32,
49    ) -> Result<Runner<'a, MonotonicCurveLut256>, Error> {
50        Runner::from_compiled_started(self, motor, now_ms)
51    }
52}
53
54/// Helper around a generated `COMPILED_HAPTICS` slice.
55#[derive(Copy, Clone, Debug)]
56pub struct CompiledCatalog<'a> {
57    defs: &'a [CompiledHapticDef<'a>],
58}
59
60/// Lookup/runner errors for [`CompiledCatalog`].
61#[derive(Copy, Clone, Debug, Eq, PartialEq)]
62pub enum CompiledCatalogError {
63    /// Named haptic was not found.
64    NotFound,
65    /// Runner construction failed (e.g. motor mismatch).
66    Runner(Error),
67}
68
69impl From<Error> for CompiledCatalogError {
70    fn from(value: Error) -> Self {
71        Self::Runner(value)
72    }
73}
74
75impl core::fmt::Display for CompiledCatalogError {
76    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77        match self {
78            Self::NotFound => f.write_str("no compiled haptic with that name"),
79            Self::Runner(error) => write!(f, "runner construction failed: {error}"),
80        }
81    }
82}
83
84impl core::error::Error for CompiledCatalogError {
85    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
86        match self {
87            Self::NotFound => None,
88            Self::Runner(error) => Some(error),
89        }
90    }
91}
92
93impl<'a> CompiledCatalog<'a> {
94    /// Create a catalog from generated definitions.
95    pub const fn new(defs: &'a [CompiledHapticDef<'a>]) -> Self {
96        Self { defs }
97    }
98
99    /// Return all compiled definitions.
100    pub const fn definitions(&self) -> &'a [CompiledHapticDef<'a>] {
101        self.defs
102    }
103
104    /// Whether the catalog is empty.
105    pub const fn is_empty(&self) -> bool {
106        self.defs.is_empty()
107    }
108
109    /// Find the first compiled haptic with an ASCII case-insensitive name match.
110    pub fn find(&self, name: &str) -> Option<&'a CompiledHapticDef<'a>> {
111        let mut index = 0usize;
112        while index < self.defs.len() {
113            let def = &self.defs[index];
114            if def.name.eq_ignore_ascii_case(name) {
115                return Some(def);
116            }
117            index += 1;
118        }
119        None
120    }
121
122    /// Find a compiled haptic by name and return an error if missing.
123    pub fn require(&self, name: &str) -> Result<&'a CompiledHapticDef<'a>, CompiledCatalogError> {
124        self.find(name).ok_or(CompiledCatalogError::NotFound)
125    }
126
127    /// Build a [`Runner`] for a named compiled haptic.
128    pub fn runner(
129        &self,
130        name: &str,
131        motor: MotorConfig,
132    ) -> Result<Runner<'a, MonotonicCurveLut256>, CompiledCatalogError> {
133        let def = self.require(name)?;
134        def.runner(motor).map_err(CompiledCatalogError::from)
135    }
136
137    /// Build and start a [`Runner`] for a named compiled haptic.
138    pub fn runner_started(
139        &self,
140        name: &str,
141        motor: MotorConfig,
142        now_ms: u32,
143    ) -> Result<Runner<'a, MonotonicCurveLut256>, CompiledCatalogError> {
144        let def = self.require(name)?;
145        def.runner_started(motor, now_ms)
146            .map_err(CompiledCatalogError::from)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::dsl::Instruction;
154    use crate::motor::{ErmConfig, LraConfig, MotorKind};
155
156    #[test]
157    fn compiled_definition_runner_helpers_work() {
158        let instructions: [Instruction<MonotonicCurveLut256>; 1] = [Instruction::hold(10, 20_000)];
159        let program = Program::new(MotorKind::Erm, &instructions);
160        let def = CompiledHapticDef::new("tap", program, None);
161
162        let mut runner = def
163            .runner_started(MotorConfig::Erm(ErmConfig::new(255)), 10)
164            .unwrap();
165        let frame = runner.poll(10);
166        assert!(frame.is_active());
167    }
168
169    #[test]
170    fn compiled_catalog_lookup_and_runner_work() {
171        let a_instructions: [Instruction<MonotonicCurveLut256>; 1] =
172            [Instruction::hold(10, 30_000)];
173        let b_instructions: [Instruction<MonotonicCurveLut256>; 1] =
174            [Instruction::hold(10, 40_000)];
175        let a = CompiledHapticDef::new("tap", Program::new(MotorKind::Erm, &a_instructions), None);
176        let b = CompiledHapticDef::new("buzz", Program::new(MotorKind::Lra, &b_instructions), None);
177        let defs = [a, b];
178        let catalog = CompiledCatalog::new(&defs);
179
180        let found = catalog.require("TAP").unwrap();
181        assert_eq!(found.name, "tap");
182
183        let mut runner = catalog
184            .runner_started("tap", MotorConfig::Erm(ErmConfig::new(255)), 0)
185            .unwrap();
186        assert!(runner.poll(0).is_active());
187
188        let err = catalog
189            .runner("buzz", MotorConfig::Erm(ErmConfig::new(255)))
190            .unwrap_err();
191        assert_eq!(
192            err,
193            CompiledCatalogError::Runner(Error::MotorKindMismatch {
194                expected: MotorKind::Lra,
195                got: MotorKind::Erm
196            })
197        );
198
199        let err = catalog
200            .runner("missing", MotorConfig::Lra(LraConfig::new(100, 200)))
201            .unwrap_err();
202        assert_eq!(err, CompiledCatalogError::NotFound);
203    }
204}