esp_hal_servo/
servo.rs

1//! Servo motor driver implementation.
2//!
3//! This module provides the [`Servo`] struct for controlling servo motors
4//! and the [`Dir`] enum for specifying rotation direction.
5
6use crate::{servo_config::ServoConfig, utils};
7use core::{
8    marker::PhantomData,
9    ops::{Neg, Range},
10};
11use esp_hal::{
12    gpio::DriveMode,
13    ledc::{
14        Ledc,
15        channel::{self, Channel, ChannelHW, ChannelIFace},
16        timer::{Timer, TimerHW, TimerIFace, TimerSpeed},
17    },
18};
19use log::{info, trace};
20
21/// Direction of servo rotation.
22#[derive(PartialEq, Eq, Clone, Copy, Debug)]
23pub enum Dir {
24    /// Clockwise, increases angle.
25    CW,
26    /// Counter-clockwise, decreases angle.
27    CCW,
28}
29
30/// Servo motor driver instance.
31///
32/// This struct represents a servo motor connected to an ESP32 LEDC channel.
33/// It provides methods for controlling the servo position using either angle-based
34/// or step-based control.
35pub struct Servo<'a, S: TimerSpeed> {
36    name: &'static str,
37    channel: Channel<'a, S>,
38    /// Valid duty cycle range in absolute values (e.g., 102..491 for SG90 with 12-bit).
39    /// This corresponds to the pulse width range of the servo.
40    pub duty_range: Range<f32>,
41    config: ServoConfig,
42    /// Cached max duty value for further calculations.
43    max_duty: f32,
44    /// Current direction. Clockwise or counter-clockwise.
45    direction: Dir,
46    /// Current duty in absolute value (0..max_duty).
47    current_duty: f32,
48    _p: PhantomData<&'a mut ()>,
49}
50
51impl<'d, S: TimerSpeed> Servo<'d, S> {
52    /// Creates new servo driver instance for LEDC channel.
53    ///
54    /// # Arguments
55    ///
56    /// * `name` - Name identifier for the servo (for logging)
57    /// * `config` - Servo configuration
58    /// * `ledc` - LEDC peripheral instance
59    /// * `timer` - Configured timer instance (use `ServoConfig::configure_timer` to create it)
60    /// * `channel_num` - Channel number (e.g., `channel::Number::Channel0`)
61    /// * `pin` - GPIO pin to use for PWM output
62    pub fn new<'a>(
63        name: &'static str,
64        config: ServoConfig,
65        ledc: &mut Ledc<'a>,
66        timer: &'a Timer<'a, S>,
67        channel_num: channel::Number,
68        pin: impl esp_hal::gpio::OutputPin + 'a,
69    ) -> Result<Servo<'a, S>, channel::Error>
70    where
71        Timer<'a, S>: TimerHW<S> + TimerIFace<S>,
72    {
73        // Calculate max duty before configuring channel
74        let max_duty = match timer.duty() {
75            Some(duty) => (1u32 << duty as u32) - 1,
76            None => 4095, // Default to 12-bit if not configured
77        } as f32;
78
79        // Calculate duty range in absolute values
80        let duty_range = config.calc_duty_range(max_duty);
81
82        let mut channel = ledc.channel(channel_num, pin);
83        channel.configure(channel::config::Config {
84            timer,
85            duty_pct: 0,
86            drive_mode: DriveMode::PushPull,
87        })?;
88
89        let center_duty = duty_range.start + (duty_range.end - duty_range.start) / 2.0;
90        channel.set_duty_hw(center_duty as u32);
91
92        info!(
93            "{name} servo: duty_range={duty_range:?}, center_duty={center_duty}",
94            name = name,
95            duty_range = duty_range,
96            center_duty = center_duty,
97        );
98
99        Ok(Servo::<'a> {
100            name,
101            channel,
102            duty_range,
103            config,
104            direction: Dir::CW,
105            current_duty: center_duty,
106            max_duty,
107            _p: PhantomData,
108        })
109    }
110
111    /// Sets servo to specified angle in degrees.
112    /// Note: turn to angle takes some time depending on servo speed.
113    /// Use [`calc_delay_ms()`](Self::calc_delay_ms) to get delay time based on angle and speed.
114    /// Returns true if angle was changed, false if already at that position.
115    pub fn set_angle(&mut self, angle: f32) -> bool {
116        let new_duty = self.config.angle_to_duty(angle, &self.duty_range);
117
118        let delta = new_duty - self.current_duty;
119        if delta > utils::EPSILON {
120            self.direction = Dir::CW;
121        } else if delta < utils::EPSILON.neg() {
122            self.direction = Dir::CCW;
123        } else {
124            return false;
125        }
126
127        self.channel.set_duty_hw(new_duty as u32);
128        self.current_duty = new_duty;
129        true
130    }
131
132    /// Returns current angle value in degrees.
133    pub fn get_angle(&self) -> f32 {
134        self.config
135            .duty_to_angle(self.current_duty, self.config.max_angle, &self.duty_range)
136    }
137
138    /// Set servo to move new direction.
139    /// Returns old direction if direction was actually changes.
140    pub fn set_dir(&mut self, dir: Dir) -> Option<Dir> {
141        if self.direction != dir {
142            let old = self.direction;
143            self.direction = dir;
144            Some(old)
145        } else {
146            None
147        }
148    }
149
150    /// Returns current direction value.
151    pub fn get_dir(&self) -> Dir {
152        self.direction
153    }
154
155    /// Makes step in percentage of total range.
156    /// Returns false if servo reaches min or max position.
157    /// See also [`step()`](Self::step) for absolute duty-based stepping.
158    /// Note: Step takes some time depending on servo speed.
159    pub fn step_pct(&mut self, step_pct: u8) -> Result<bool, channel::Error> {
160        let step = (step_pct as f32 / 100.0) * self.duty_range();
161        self.step(step)
162    }
163
164    /// Makes step in absolute duty units should be lesser than [`duty_range()`](Self::duty_range).
165    /// Return false if servo reaches min or max position.
166    /// See also [`step_pct()`](Self::step_pct) for percentage-based stepping.
167    /// Note: Step takes some time depending on servo speed.
168    pub fn step(&mut self, step_size: f32) -> Result<bool, channel::Error> {
169        let new_duty = self.calc_duty(step_size);
170
171        // Compare with epsilon to avoid floating point precision issues
172        if utils::approx_eq(new_duty, self.current_duty) {
173            return Ok(false);
174        }
175
176        // hardware method has better resolution
177        self.channel.set_duty_hw(new_duty as u32);
178        self.current_duty = new_duty;
179        trace!(
180            "{} servo step({}) to duty={}/{}",
181            &self.name, step_size, new_duty, self.max_duty
182        );
183        Ok(true)
184    }
185
186    /// Returns the size of the duty range (difference between max and min duty values).
187    /// This is not the number of steps, but the range size in duty units.
188    pub fn duty_range(&self) -> f32 {
189        self.duty_range.end - self.duty_range.start
190    }
191
192    /// Returns the servo rotation speed in degrees per second.
193    pub fn speed(&self) -> f32 {
194        self.config.speed_deg_per_sec
195    }
196
197    /// Sets the servo rotation speed in degrees per second.
198    /// True is new speed is valid and set, false otherwise.
199    pub fn set_speed(&mut self, speed_deg_per_sec: f32) -> bool {
200        if speed_deg_per_sec <= 0.0 {
201            false
202        } else {
203            self.config.speed_deg_per_sec = speed_deg_per_sec;
204            true
205        }
206    }
207
208    /// Util method for calculating delay time in milliseconds based on rotation angle and servo speed.
209    pub fn calc_delay_ms(&self, angle_deg: f32) -> Option<u32> {
210        if angle_deg > self.config.max_angle {
211            return None;
212        }
213
214        let delay_sec = angle_deg / self.config.speed_deg_per_sec;
215        Some((delay_sec * 1000.0) as u32)
216    }
217
218    /// Calculates new duty based on current direction and step size.
219    /// Returns clamped duty value within valid range.
220    fn calc_duty(&self, step: f32) -> f32 {
221        let new_duty = match self.direction {
222            Dir::CW => {
223                // Move clockwise (increase duty)
224                self.current_duty + step
225            }
226            Dir::CCW => {
227                // Move counter-clockwise (decrease duty)
228                self.current_duty - step
229            }
230        };
231
232        let min_duty = self.duty_range.start;
233        let max_duty = self.duty_range.end - utils::EPSILON;
234        new_duty.clamp(min_duty, max_duty)
235    }
236}