stepper_motion/motor/
builder.rs

1//! Builder pattern for StepperMotor.
2
3use embedded_hal::delay::DelayNs;
4use embedded_hal::digital::OutputPin;
5
6use crate::config::units::{DegreesPerSec, DegreesPerSecSquared, Microsteps};
7use crate::config::{MechanicalConstraints, MotorConfig, SystemConfig};
8use crate::error::{ConfigError, Error, Result};
9
10use super::driver::StepperMotor;
11use super::state::Idle;
12
13/// Builder for creating StepperMotor instances.
14pub struct StepperMotorBuilder<STEP, DIR, DELAY>
15where
16    STEP: OutputPin,
17    DIR: OutputPin,
18    DELAY: DelayNs,
19{
20    step_pin: Option<STEP>,
21    dir_pin: Option<DIR>,
22    delay: Option<DELAY>,
23    name: Option<heapless::String<32>>,
24    steps_per_revolution: Option<u16>,
25    microsteps: Option<Microsteps>,
26    gear_ratio: f32,
27    max_velocity: Option<DegreesPerSec>,
28    max_acceleration: Option<DegreesPerSecSquared>,
29    invert_direction: bool,
30    constraints: Option<MechanicalConstraints>,
31    backlash_steps: i64,
32}
33
34impl<STEP, DIR, DELAY> Default for StepperMotorBuilder<STEP, DIR, DELAY>
35where
36    STEP: OutputPin,
37    DIR: OutputPin,
38    DELAY: DelayNs,
39{
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl<STEP, DIR, DELAY> StepperMotorBuilder<STEP, DIR, DELAY>
46where
47    STEP: OutputPin,
48    DIR: OutputPin,
49    DELAY: DelayNs,
50{
51    /// Create a new builder.
52    pub fn new() -> Self {
53        Self {
54            step_pin: None,
55            dir_pin: None,
56            delay: None,
57            name: None,
58            steps_per_revolution: None,
59            microsteps: None,
60            gear_ratio: 1.0,
61            max_velocity: None,
62            max_acceleration: None,
63            invert_direction: false,
64            constraints: None,
65            backlash_steps: 0,
66        }
67    }
68
69    /// Set the STEP pin.
70    pub fn step_pin(mut self, pin: STEP) -> Self {
71        self.step_pin = Some(pin);
72        self
73    }
74
75    /// Set the DIR pin.
76    pub fn dir_pin(mut self, pin: DIR) -> Self {
77        self.dir_pin = Some(pin);
78        self
79    }
80
81    /// Set the delay provider.
82    pub fn delay(mut self, delay: DELAY) -> Self {
83        self.delay = Some(delay);
84        self
85    }
86
87    /// Set the motor name.
88    pub fn name(mut self, name: &str) -> Self {
89        self.name = heapless::String::try_from(name).ok();
90        self
91    }
92
93    /// Set steps per revolution (base motor steps before microstepping).
94    pub fn steps_per_revolution(mut self, steps: u16) -> Self {
95        self.steps_per_revolution = Some(steps);
96        self
97    }
98
99    /// Set microstep configuration.
100    pub fn microsteps(mut self, microsteps: Microsteps) -> Self {
101        self.microsteps = Some(microsteps);
102        self
103    }
104
105    /// Set gear ratio.
106    pub fn gear_ratio(mut self, ratio: f32) -> Self {
107        self.gear_ratio = ratio;
108        self
109    }
110
111    /// Set maximum velocity in degrees per second.
112    pub fn max_velocity(mut self, velocity: DegreesPerSec) -> Self {
113        self.max_velocity = Some(velocity);
114        self
115    }
116
117    /// Set maximum acceleration in degrees per second squared.
118    pub fn max_acceleration(mut self, acceleration: DegreesPerSecSquared) -> Self {
119        self.max_acceleration = Some(acceleration);
120        self
121    }
122
123    /// Set direction inversion.
124    pub fn invert_direction(mut self, invert: bool) -> Self {
125        self.invert_direction = invert;
126        self
127    }
128
129    /// Set backlash compensation in steps.
130    ///
131    /// Backlash is applied on direction changes to compensate for mechanical play.
132    pub fn backlash_steps(mut self, steps: i64) -> Self {
133        self.backlash_steps = steps;
134        self
135    }
136
137    /// Configure from a MotorConfig.
138    pub fn from_motor_config(mut self, config: &MotorConfig) -> Self {
139        self.name = Some(config.name.clone());
140        self.steps_per_revolution = Some(config.steps_per_revolution);
141        self.microsteps = Some(config.microsteps);
142        self.gear_ratio = config.gear_ratio;
143        self.max_velocity = Some(config.max_velocity);
144        self.max_acceleration = Some(config.max_acceleration);
145        self.invert_direction = config.invert_direction;
146        self.constraints = Some(MechanicalConstraints::from_config(config));
147        // Extract backlash compensation if configured (convert degrees to steps)
148        if let Some(backlash_deg) = config.backlash_compensation {
149            let steps_per_degree = config.steps_per_degree();
150            self.backlash_steps = (backlash_deg.0 * steps_per_degree) as i64;
151        }
152        self
153    }
154
155    /// Configure from SystemConfig by motor name.
156    pub fn from_config(self, config: &SystemConfig, motor_name: &str) -> Result<Self> {
157        let motor_config = config
158            .motor(motor_name)
159            .ok_or_else(|| Error::Config(ConfigError::MotorNotFound(
160                heapless::String::try_from(motor_name).unwrap_or_default(),
161            )))?;
162
163        Ok(self.from_motor_config(motor_config))
164    }
165
166    /// Build the StepperMotor.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if required fields are missing.
171    pub fn build(self) -> Result<StepperMotor<STEP, DIR, DELAY, Idle>> {
172        let step_pin = self.step_pin.ok_or_else(|| {
173            Error::Config(ConfigError::ParseError(
174                heapless::String::try_from("step_pin is required").unwrap(),
175            ))
176        })?;
177
178        let dir_pin = self.dir_pin.ok_or_else(|| {
179            Error::Config(ConfigError::ParseError(
180                heapless::String::try_from("dir_pin is required").unwrap(),
181            ))
182        })?;
183
184        let delay = self.delay.ok_or_else(|| {
185            Error::Config(ConfigError::ParseError(
186                heapless::String::try_from("delay is required").unwrap(),
187            ))
188        })?;
189
190        let name = self.name.unwrap_or_else(|| {
191            heapless::String::try_from("motor").unwrap()
192        });
193
194        let constraints = if let Some(c) = self.constraints {
195            c
196        } else {
197            // Build constraints from individual fields
198            let steps = self.steps_per_revolution.ok_or_else(|| {
199                Error::Config(ConfigError::ParseError(
200                    heapless::String::try_from("steps_per_revolution is required").unwrap(),
201                ))
202            })?;
203
204            let microsteps = self.microsteps.unwrap_or(Microsteps::FULL);
205            let max_velocity = self.max_velocity.ok_or_else(|| {
206                Error::Config(ConfigError::ParseError(
207                    heapless::String::try_from("max_velocity is required").unwrap(),
208                ))
209            })?;
210
211            let max_acceleration = self.max_acceleration.ok_or_else(|| {
212                Error::Config(ConfigError::ParseError(
213                    heapless::String::try_from("max_acceleration is required").unwrap(),
214                ))
215            })?;
216
217            // Create a temporary MotorConfig to compute constraints
218            let config = MotorConfig {
219                name: name.clone(),
220                steps_per_revolution: steps,
221                microsteps,
222                gear_ratio: self.gear_ratio,
223                max_velocity,
224                max_acceleration,
225                invert_direction: self.invert_direction,
226                limits: None,
227                backlash_compensation: None,
228            };
229
230            MechanicalConstraints::from_config(&config)
231        };
232
233        Ok(StepperMotor::new(
234            step_pin,
235            dir_pin,
236            delay,
237            constraints,
238            name,
239            self.invert_direction,
240            self.backlash_steps,
241        ))
242    }
243}