use libm::sqrtf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Clockwise,
CounterClockwise,
}
impl Direction {
#[inline]
pub fn from_steps(steps: i64) -> Self {
if steps >= 0 {
Direction::Clockwise
} else {
Direction::CounterClockwise
}
}
#[inline]
pub fn sign(self) -> i64 {
match self {
Direction::Clockwise => 1,
Direction::CounterClockwise => -1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionPhase {
Accelerating,
Cruising,
Decelerating,
Complete,
}
#[derive(Debug, Clone)]
pub struct MotionProfile {
pub total_steps: u32,
pub direction: Direction,
pub accel_steps: u32,
pub cruise_steps: u32,
pub decel_steps: u32,
pub initial_interval_ns: u32,
pub cruise_interval_ns: u32,
pub accel_rate: f32,
pub decel_rate: f32,
}
impl MotionProfile {
pub fn asymmetric_trapezoidal(
total_steps: i64,
max_velocity: f32,
acceleration: f32,
deceleration: f32,
) -> Self {
let direction = Direction::from_steps(total_steps);
let steps = total_steps.unsigned_abs() as u32;
if steps == 0 || max_velocity <= 0.0 || acceleration <= 0.0 || deceleration <= 0.0 {
return Self::zero();
}
let t_accel = max_velocity / acceleration;
let t_decel = max_velocity / deceleration;
let accel_distance = 0.5 * acceleration * t_accel * t_accel;
let decel_distance = 0.5 * deceleration * t_decel * t_decel;
let (accel_steps, cruise_steps, decel_steps) =
if accel_distance + decel_distance >= steps as f32 {
let ratio = acceleration / (acceleration + deceleration);
let accel_steps = (steps as f32 * ratio) as u32;
let decel_steps = steps.saturating_sub(accel_steps);
(accel_steps, 0u32, decel_steps)
} else {
let accel_steps = accel_distance as u32;
let decel_steps = decel_distance as u32;
let cruise_steps = steps.saturating_sub(accel_steps + decel_steps);
(accel_steps, cruise_steps, decel_steps)
};
let initial_velocity = sqrtf(2.0 * acceleration);
let initial_interval_ns = (1_000_000_000.0 / initial_velocity) as u32;
let cruise_interval_ns = (1_000_000_000.0 / max_velocity) as u32;
Self {
total_steps: steps,
direction,
accel_steps,
cruise_steps,
decel_steps,
initial_interval_ns,
cruise_interval_ns,
accel_rate: acceleration,
decel_rate: deceleration,
}
}
pub fn symmetric_trapezoidal(
total_steps: i64,
max_velocity: f32,
acceleration: f32,
) -> Self {
Self::asymmetric_trapezoidal(total_steps, max_velocity, acceleration, acceleration)
}
pub fn zero() -> Self {
Self {
total_steps: 0,
direction: Direction::Clockwise,
accel_steps: 0,
cruise_steps: 0,
decel_steps: 0,
initial_interval_ns: u32::MAX,
cruise_interval_ns: u32::MAX,
accel_rate: 0.0,
decel_rate: 0.0,
}
}
#[inline]
pub fn is_zero(&self) -> bool {
self.total_steps == 0
}
pub fn phase_at(&self, step: u32) -> MotionPhase {
if step >= self.total_steps {
MotionPhase::Complete
} else if step < self.accel_steps {
MotionPhase::Accelerating
} else if step < self.accel_steps + self.cruise_steps {
MotionPhase::Cruising
} else {
MotionPhase::Decelerating
}
}
pub fn interval_at(&self, step: u32) -> u32 {
let phase = self.phase_at(step);
match phase {
MotionPhase::Complete => u32::MAX,
MotionPhase::Cruising => self.cruise_interval_ns,
MotionPhase::Accelerating => {
let progress = step as f32 / self.accel_steps.max(1) as f32;
let interval = self.initial_interval_ns as f32
- (self.initial_interval_ns as f32 - self.cruise_interval_ns as f32) * progress;
interval as u32
}
MotionPhase::Decelerating => {
let decel_step = step - self.accel_steps - self.cruise_steps;
let progress = decel_step as f32 / self.decel_steps.max(1) as f32;
let interval = self.cruise_interval_ns as f32
+ (self.initial_interval_ns as f32 - self.cruise_interval_ns as f32) * progress;
interval as u32
}
}
}
pub fn estimated_duration_secs(&self) -> f32 {
if self.total_steps == 0 {
return 0.0;
}
let cruise_velocity = 1_000_000_000.0 / self.cruise_interval_ns as f32;
let accel_time = if self.accel_rate > 0.0 {
cruise_velocity / self.accel_rate
} else {
0.0
};
let cruise_time = self.cruise_steps as f32 / cruise_velocity;
let decel_time = if self.decel_rate > 0.0 {
cruise_velocity / self.decel_rate
} else {
0.0
};
accel_time + cruise_time + decel_time
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symmetric_profile() {
let profile = MotionProfile::symmetric_trapezoidal(
1000, 1000.0, 2000.0, );
assert_eq!(profile.total_steps, 1000);
assert_eq!(profile.direction, Direction::Clockwise);
assert!(profile.accel_steps > 0);
assert!(profile.cruise_steps > 0);
assert_eq!(profile.accel_steps, profile.decel_steps);
}
#[test]
fn test_asymmetric_profile() {
let profile = MotionProfile::asymmetric_trapezoidal(
1000, 1000.0, 2000.0, 1000.0, );
assert!(profile.decel_steps > profile.accel_steps);
}
#[test]
fn test_triangle_profile() {
let profile = MotionProfile::symmetric_trapezoidal(
100, 10000.0, 1000.0, );
assert_eq!(profile.cruise_steps, 0);
}
#[test]
fn test_direction() {
let cw = MotionProfile::symmetric_trapezoidal(100, 1000.0, 2000.0);
let ccw = MotionProfile::symmetric_trapezoidal(-100, 1000.0, 2000.0);
assert_eq!(cw.direction, Direction::Clockwise);
assert_eq!(ccw.direction, Direction::CounterClockwise);
assert_eq!(cw.total_steps, ccw.total_steps);
}
}