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}