ph-haptics 0.1.0

Host-compiled haptics DSL and no-std, no-alloc scheduling runtime modeling ERM and LRA motors using ph-curves
Documentation
use crate::motor::MotorKind;
use ph_curves::{MonotonicCurveLut256, Rounding};

/// How a program repeats.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LoopMode {
    /// Play once and stop.
    Once,
    /// Restart from instruction 0 forever.
    Forever,
    /// Play exactly `n` cycles and stop; `Count(0)` finishes on the first poll.
    Count(u32),
}

/// A curve-shaped transition from `from` to `to`.
#[derive(Copy, Clone, Debug)]
pub struct Ramp<C = MonotonicCurveLut256> {
    /// Segment duration in milliseconds.
    pub duration_ms: u32,
    /// Starting normalized level (`0..=u16::MAX`).
    pub from: u16,
    /// Ending normalized level (`0..=u16::MAX`).
    pub to: u16,
    /// Monotonic curve used to shape progress over time.
    pub curve: C,
    /// Output quantization step (minimum `1`).
    pub step: u16,
    /// Quantization rounding mode.
    pub rounding: Rounding,
    /// Minimum milliseconds between output changes.
    pub min_dt_ms: u32,
    /// Optional per-segment LRA frequency override.
    pub lra_frequency_hz: Option<u16>,
    /// Optional ending LRA frequency for frequency sweeps.
    ///
    /// This is used only when [`Self::lra_frequency_hz`] also contains the
    /// starting frequency; otherwise it is ignored.
    pub lra_frequency_hz_to: Option<u16>,
}

impl<C> Ramp<C> {
    /// Build a new ramp segment.
    ///
    /// A zero `duration_ms` is normalized to one millisecond. Direct struct
    /// construction does not perform this normalization.
    pub const fn new(duration_ms: u32, from: u16, to: u16, curve: C) -> Self {
        Self {
            duration_ms: normalize_duration(duration_ms),
            from,
            to,
            curve,
            step: 1,
            rounding: Rounding::Nearest,
            min_dt_ms: 0,
            lra_frequency_hz: None,
            lra_frequency_hz_to: None,
        }
    }

    /// Override quantization settings.
    ///
    /// A zero `step` is normalized to `1`.
    pub const fn with_quantization(mut self, step: u16, rounding: Rounding) -> Self {
        self.step = normalize_step(step);
        self.rounding = rounding;
        self
    }

    /// Set a minimum time between consecutive output changes.
    pub const fn with_min_dt(mut self, min_dt_ms: u32) -> Self {
        self.min_dt_ms = min_dt_ms;
        self
    }

    /// Override LRA frequency for this segment.
    pub const fn with_lra_frequency(mut self, frequency_hz: u16) -> Self {
        self.lra_frequency_hz = Some(frequency_hz);
        self
    }
}

/// A single DSL instruction.
#[derive(Copy, Clone, Debug)]
pub enum Instruction<C = MonotonicCurveLut256> {
    /// Curve-shaped level change.
    Ramp(Ramp<C>),
    /// Constant non-zero or zero level for a duration.
    Hold {
        /// Segment duration in milliseconds.
        duration_ms: u32,
        /// Normalized level (`0..=u16::MAX`).
        level: u16,
        /// Optional per-segment LRA frequency override.
        lra_frequency_hz: Option<u16>,
    },
    /// Explicit silent gap (level = 0).
    Pause {
        /// Segment duration in milliseconds.
        duration_ms: u32,
    },
}

impl<C> Instruction<C> {
    /// Construct a ramp, normalizing a zero duration to one millisecond.
    pub const fn ramp(duration_ms: u32, from: u16, to: u16, curve: C) -> Self {
        Self::Ramp(Ramp::new(duration_ms, from, to, curve))
    }

    /// Construct a hold, normalizing a zero duration to one millisecond.
    pub const fn hold(duration_ms: u32, level: u16) -> Self {
        Self::Hold {
            duration_ms: normalize_duration(duration_ms),
            level,
            lra_frequency_hz: None,
        }
    }

    /// Construct an LRA hold with an explicit frequency.
    ///
    /// A zero duration is normalized to one millisecond.
    pub const fn hold_with_lra_frequency(duration_ms: u32, level: u16, frequency_hz: u16) -> Self {
        Self::Hold {
            duration_ms: normalize_duration(duration_ms),
            level,
            lra_frequency_hz: Some(frequency_hz),
        }
    }

    /// Construct a pause, normalizing a zero duration to one millisecond.
    pub const fn pause(duration_ms: u32) -> Self {
        Self::Pause {
            duration_ms: normalize_duration(duration_ms),
        }
    }

    /// Segment duration in milliseconds.
    pub const fn duration_ms(&self) -> u32 {
        match self {
            Self::Ramp(ramp) => ramp.duration_ms,
            Self::Hold { duration_ms, .. } | Self::Pause { duration_ms } => *duration_ms,
        }
    }
}

/// A haptics program: motor target + instruction stream + repeat mode.
#[derive(Copy, Clone, Debug)]
pub struct Program<'a, C = MonotonicCurveLut256> {
    motor: MotorKind,
    instructions: &'a [Instruction<C>],
    loop_mode: LoopMode,
}

impl<'a, C> Program<'a, C> {
    /// Create a new program with [`LoopMode::Once`].
    pub const fn new(motor: MotorKind, instructions: &'a [Instruction<C>]) -> Self {
        Self {
            motor,
            instructions,
            loop_mode: LoopMode::Once,
        }
    }

    /// Return a copy with the given loop mode.
    pub const fn with_loop_mode(mut self, loop_mode: LoopMode) -> Self {
        self.loop_mode = loop_mode;
        self
    }

    /// Return a copy set to [`LoopMode::Forever`].
    pub const fn repeat_forever(self) -> Self {
        self.with_loop_mode(LoopMode::Forever)
    }

    /// Target motor type for this program.
    pub const fn motor(&self) -> MotorKind {
        self.motor
    }

    /// Program loop mode.
    pub const fn loop_mode(&self) -> LoopMode {
        self.loop_mode
    }

    /// Instruction sequence.
    pub const fn instructions(&self) -> &'a [Instruction<C>] {
        self.instructions
    }

    /// Whether there are no instructions.
    pub const fn is_empty(&self) -> bool {
        self.instructions.is_empty()
    }

    /// Total duration of one cycle in milliseconds, saturating at [`u32::MAX`].
    pub fn total_duration_ms(&self) -> u32 {
        let mut total = 0u32;
        let mut index = 0usize;

        while index < self.instructions.len() {
            total = total.saturating_add(self.instructions[index].duration_ms());
            index += 1;
        }

        total
    }
}

pub(crate) const fn normalize_step(step: u16) -> u16 {
    if step == 0 { 1 } else { step }
}

const fn normalize_duration(duration_ms: u32) -> u32 {
    if duration_ms == 0 { 1 } else { duration_ms }
}