stepper_motion/motor/
driver.rs

1//! Stepper motor driver.
2//!
3//! Generic over embedded-hal 1.0 pin types with type-state safety.
4
5use core::marker::PhantomData;
6
7use embedded_hal::delay::DelayNs;
8use embedded_hal::digital::OutputPin;
9
10use crate::config::units::{Degrees, Steps};
11use crate::config::MechanicalConstraints;
12use crate::error::{Error, MotorError, Result};
13use crate::motion::{Direction, MotionExecutor, MotionPhase, MotionProfile};
14
15use super::position::Position;
16use super::state::{Idle, MotorState, Moving, StateName};
17
18/// Stepper motor driver with type-state safety.
19///
20/// Generic over:
21/// - `STEP`: STEP pin type (must implement `OutputPin`)
22/// - `DIR`: DIR pin type (must implement `OutputPin`)
23/// - `DELAY`: Delay provider (must implement `DelayNs`)
24/// - `STATE`: Type-state marker (defaults to `Idle`)
25pub struct StepperMotor<STEP, DIR, DELAY, STATE = Idle>
26where
27    STEP: OutputPin,
28    DIR: OutputPin,
29    DELAY: DelayNs,
30    STATE: MotorState,
31{
32    /// STEP pin (pulse to move one step).
33    step_pin: STEP,
34
35    /// DIR pin (high = CW, low = CCW, or inverted).
36    dir_pin: DIR,
37
38    /// Delay provider for step timing.
39    delay: DELAY,
40
41    /// Current absolute position.
42    position: Position,
43
44    /// Current direction (cached to avoid unnecessary pin writes).
45    current_direction: Option<Direction>,
46
47    /// Mechanical constraints from configuration.
48    constraints: MechanicalConstraints,
49
50    /// Motor name for logging/debugging.
51    name: heapless::String<32>,
52
53    /// Whether direction pin logic is inverted.
54    invert_direction: bool,
55
56    /// Backlash compensation in steps (applied on direction change).
57    backlash_steps: i64,
58
59    /// Motion executor for current move (if any).
60    executor: Option<MotionExecutor>,
61
62    /// Type-state marker.
63    _state: PhantomData<STATE>,
64}
65
66impl<STEP, DIR, DELAY, STATE> StepperMotor<STEP, DIR, DELAY, STATE>
67where
68    STEP: OutputPin,
69    DIR: OutputPin,
70    DELAY: DelayNs,
71    STATE: MotorState + StateName,
72{
73    /// Get the motor name.
74    #[inline]
75    pub fn name(&self) -> &str {
76        self.name.as_str()
77    }
78
79    /// Get current position in steps.
80    #[inline]
81    pub fn position_steps(&self) -> Steps {
82        self.position.steps()
83    }
84
85    /// Get current position in degrees.
86    #[inline]
87    pub fn position_degrees(&self) -> Degrees {
88        self.position.degrees()
89    }
90
91    /// Get the mechanical constraints.
92    #[inline]
93    pub fn constraints(&self) -> &MechanicalConstraints {
94        &self.constraints
95    }
96
97    /// Get the current state name.
98    #[inline]
99    pub fn state_name(&self) -> &'static str {
100        STATE::name()
101    }
102}
103
104impl<STEP, DIR, DELAY> StepperMotor<STEP, DIR, DELAY, Idle>
105where
106    STEP: OutputPin,
107    DIR: OutputPin,
108    DELAY: DelayNs,
109{
110    /// Create a new motor in the Idle state.
111    pub(crate) fn new(
112        step_pin: STEP,
113        dir_pin: DIR,
114        delay: DELAY,
115        constraints: MechanicalConstraints,
116        name: heapless::String<32>,
117        invert_direction: bool,
118        backlash_steps: i64,
119    ) -> Self {
120        Self {
121            step_pin,
122            dir_pin,
123            delay,
124            position: Position::new(constraints.steps_per_degree),
125            current_direction: None,
126            constraints,
127            name,
128            invert_direction,
129            backlash_steps,
130            executor: None,
131            _state: PhantomData,
132        }
133    }
134
135    /// Start a move to an absolute position in degrees.
136    ///
137    /// Returns a motor in the `Moving` state.
138    pub fn move_to(
139        mut self,
140        target: Degrees,
141    ) -> core::result::Result<StepperMotor<STEP, DIR, DELAY, Moving>, (Self, Error)> {
142        // Calculate steps to target
143        let target_steps = Steps::from_degrees(target, self.constraints.steps_per_degree);
144        let delta_steps = target_steps.0 - self.position.steps().0;
145
146        if delta_steps == 0 {
147            // Already at target, return self unchanged
148            return Err((self, Error::Motion(crate::error::MotionError::MoveTooShort {
149                steps: 0,
150                minimum: 1,
151            })));
152        }
153
154        // Check limits - extract limit value before potentially moving self
155        let limit_check = self.constraints.limits.as_ref().and_then(|limits| {
156            if limits.apply(target_steps.0).is_none() {
157                Some(if delta_steps > 0 {
158                    limits.max_steps
159                } else {
160                    limits.min_steps
161                })
162            } else {
163                None
164            }
165        });
166
167        if let Some(limit) = limit_check {
168            return Err((
169                self,
170                Error::Motor(MotorError::LimitExceeded {
171                    position: target_steps.0,
172                    limit,
173                }),
174            ));
175        }
176
177        // Create motion profile
178        let profile = MotionProfile::symmetric_trapezoidal(
179            delta_steps,
180            self.constraints.max_velocity_steps_per_sec,
181            self.constraints.max_acceleration_steps_per_sec2,
182        );
183
184        // Set direction
185        let direction = profile.direction;
186        if self.set_direction(direction).is_err() {
187            return Err((self, Error::Motor(MotorError::PinError)));
188        }
189
190        // Create executor
191        let executor = MotionExecutor::new(profile);
192
193        // Transition to Moving state
194        Ok(StepperMotor {
195            step_pin: self.step_pin,
196            dir_pin: self.dir_pin,
197            delay: self.delay,
198            position: self.position,
199            current_direction: self.current_direction,
200            constraints: self.constraints,
201            name: self.name,
202            invert_direction: self.invert_direction,
203            backlash_steps: self.backlash_steps,
204            executor: Some(executor),
205            _state: PhantomData,
206        })
207    }
208
209    /// Move by a relative amount in degrees.
210    pub fn move_by(
211        self,
212        delta: Degrees,
213    ) -> core::result::Result<StepperMotor<STEP, DIR, DELAY, Moving>, (Self, Error)> {
214        let target = Degrees(self.position.degrees().0 + delta.0);
215        self.move_to(target)
216    }
217
218    /// Set the current position as the origin (zero).
219    pub fn set_origin(&mut self) {
220        self.position.set_origin();
221    }
222
223    /// Set the current position to a specific value.
224    pub fn set_position(&mut self, degrees: Degrees) {
225        self.position.set_degrees(degrees);
226    }
227
228    /// Execute a named trajectory from a registry.
229    ///
230    /// This method looks up the trajectory by name, validates it against
231    /// the motor's constraints, and executes it to completion.
232    ///
233    /// # Arguments
234    ///
235    /// * `trajectory_name` - Name of the trajectory in the registry
236    /// * `registry` - The trajectory registry to look up the trajectory
237    ///
238    /// # Returns
239    ///
240    /// Returns `Ok(self)` with the motor back in Idle state after the move completes.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if:
245    /// - The trajectory is not found in the registry
246    /// - The trajectory's target motor doesn't match this motor's name
247    /// - The move fails due to limits or hardware errors
248    pub fn execute(
249        self,
250        trajectory_name: &str,
251        registry: &crate::trajectory::TrajectoryRegistry,
252    ) -> core::result::Result<Self, (Self, Error)> {
253        // Look up trajectory
254        let trajectory = match registry.get(trajectory_name) {
255            Some(t) => t,
256            None => {
257                // Build error with available names
258                let mut msg: heapless::String<64> = heapless::String::new();
259                let _ = msg.push_str("trajectory '");
260                let _ = msg.push_str(trajectory_name);
261                let _ = msg.push_str("' not found");
262                return Err((
263                    self,
264                    Error::Trajectory(crate::error::TrajectoryError::InvalidName(msg)),
265                ));
266            }
267        };
268
269        // Verify this trajectory is for this motor
270        if trajectory.motor.as_str() != self.name.as_str() {
271            let mut msg: heapless::String<64> = heapless::String::new();
272            let _ = msg.push_str("trajectory '");
273            let _ = msg.push_str(trajectory_name);
274            let _ = msg.push_str("' is for motor '");
275            let _ = msg.push_str(trajectory.motor.as_str());
276            let _ = msg.push_str("'");
277            return Err((
278                self,
279                Error::Trajectory(crate::error::TrajectoryError::InvalidName(msg)),
280            ));
281        }
282
283        // Execute the move to the target position
284        let target = trajectory.target_degrees;
285        self.move_to_blocking(target)
286    }
287
288    /// Move to an absolute position and run to completion (blocking).
289    ///
290    /// This is a convenience method that combines `move_to` and `run_to_completion`.
291    pub fn move_to_blocking(
292        self,
293        target: Degrees,
294    ) -> core::result::Result<Self, (Self, Error)> {
295        match self.move_to(target) {
296            Ok(moving) => {
297                match moving.run_to_completion() {
298                    Ok(idle) => Ok(idle),
299                    Err(e) => {
300                        // In practice, step errors are rare and typically unrecoverable
301                        // We can't return the motor in a good state here
302                        panic!("Motor step error during move: {:?}", e);
303                    }
304                }
305            }
306            Err(e) => Err(e),
307        }
308    }
309
310    fn set_direction(&mut self, direction: Direction) -> core::result::Result<(), ()> {
311        if self.current_direction == Some(direction) {
312            return Ok(());
313        }
314
315        let pin_high = match direction {
316            Direction::Clockwise => !self.invert_direction,
317            Direction::CounterClockwise => self.invert_direction,
318        };
319
320        if pin_high {
321            self.dir_pin.set_high().map_err(|_| ())?;
322        } else {
323            self.dir_pin.set_low().map_err(|_| ())?;
324        }
325
326        self.current_direction = Some(direction);
327        Ok(())
328    }
329}
330
331impl<STEP, DIR, DELAY> StepperMotor<STEP, DIR, DELAY, Moving>
332where
333    STEP: OutputPin,
334    DIR: OutputPin,
335    DELAY: DelayNs,
336{
337    /// Execute one step pulse.
338    ///
339    /// Returns `true` if the move is complete.
340    pub fn step(&mut self) -> Result<bool> {
341        let executor = self.executor.as_mut().ok_or(MotorError::NotInitialized)?;
342
343        if executor.is_complete() {
344            return Ok(true);
345        }
346
347        // Generate step pulse
348        self.step_pin.set_high().map_err(|_| MotorError::PinError)?;
349
350        // Pulse width (typically 1-10 microseconds is sufficient)
351        self.delay.delay_us(2);
352
353        self.step_pin.set_low().map_err(|_| MotorError::PinError)?;
354
355        // Update position
356        let direction = executor.profile().direction;
357        self.position.move_steps(direction.sign());
358
359        // Get delay for next step
360        let interval_ns = executor.current_interval_ns();
361
362        // Advance executor
363        let has_more = executor.advance();
364
365        if has_more {
366            // Delay until next step (subtract pulse width)
367            let delay_ns = interval_ns.saturating_sub(2000);
368            if delay_ns > 0 {
369                self.delay.delay_ns(delay_ns);
370            }
371        }
372
373        Ok(!has_more)
374    }
375
376    /// Check if the move is complete.
377    #[inline]
378    pub fn is_complete(&self) -> bool {
379        self.executor
380            .as_ref()
381            .map(|e| e.is_complete())
382            .unwrap_or(true)
383    }
384
385    /// Get move progress (0.0 to 1.0).
386    #[inline]
387    pub fn progress(&self) -> f32 {
388        self.executor.as_ref().map(|e| e.progress()).unwrap_or(1.0)
389    }
390
391    /// Get current motion phase.
392    #[inline]
393    pub fn phase(&self) -> MotionPhase {
394        self.executor
395            .as_ref()
396            .map(|e| e.phase())
397            .unwrap_or(MotionPhase::Complete)
398    }
399
400    /// Complete the move and return to Idle state.
401    ///
402    /// This should be called after `is_complete()` returns true or
403    /// to abandon a move in progress.
404    pub fn finish(self) -> StepperMotor<STEP, DIR, DELAY, Idle> {
405        StepperMotor {
406            step_pin: self.step_pin,
407            dir_pin: self.dir_pin,
408            delay: self.delay,
409            position: self.position,
410            current_direction: self.current_direction,
411            constraints: self.constraints,
412            name: self.name,
413            invert_direction: self.invert_direction,
414            backlash_steps: self.backlash_steps,
415            executor: None,
416            _state: PhantomData,
417        }
418    }
419
420    /// Run the move to completion (blocking).
421    pub fn run_to_completion(mut self) -> Result<StepperMotor<STEP, DIR, DELAY, Idle>> {
422        while !self.is_complete() {
423            self.step()?;
424        }
425        Ok(self.finish())
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    // Tests require embedded-hal-mock, which is in dev-dependencies
432}