pokeys_lib/
pwm.rs

1//! PWM (Pulse Width Modulation) support
2
3use crate::device::PoKeysDevice;
4use crate::error::{PoKeysError, Result};
5use serde::{Deserialize, Serialize};
6
7/// PWM channel mapping according to specification
8/// PWM1 = pin 22, PWM2 = pin 21, PWM3 = pin 20, PWM4 = pin 19, PWM5 = pin 18, PWM6 = pin 17
9const PWM_PIN_MAP: [u8; 6] = [22, 21, 20, 19, 18, 17];
10
11/// Servo type definitions
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum ServoType {
14    /// 180-degree servo with calibrated 0° and 180° positions
15    OneEighty { pos_0: u32, pos_180: u32 },
16    /// 360-degree position servo (multi-turn with position feedback)
17    ThreeSixtyPosition { pos_0: u32, pos_360: u32 },
18    /// 360-degree speed servo (continuous rotation)
19    ThreeSixtySpeed {
20        stop: u32,
21        clockwise: u32,
22        anti_clockwise: u32,
23    },
24}
25
26/// Servo configuration for a specific pin
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ServoConfig {
29    pub pin: u8,
30    pub servo_type: ServoType,
31}
32
33impl ServoConfig {
34    /// Create new 180-degree servo configuration
35    pub fn one_eighty(pin: u8, pos_0: u32, pos_180: u32) -> Self {
36        Self {
37            pin,
38            servo_type: ServoType::OneEighty { pos_0, pos_180 },
39        }
40    }
41
42    /// Create new 360-degree position servo configuration
43    pub fn three_sixty_position(pin: u8, pos_0: u32, pos_360: u32) -> Self {
44        Self {
45            pin,
46            servo_type: ServoType::ThreeSixtyPosition { pos_0, pos_360 },
47        }
48    }
49
50    /// Create new 360-degree speed servo configuration
51    pub fn three_sixty_speed(pin: u8, stop: u32, clockwise: u32, anti_clockwise: u32) -> Self {
52        Self {
53            pin,
54            servo_type: ServoType::ThreeSixtySpeed {
55                stop,
56                clockwise,
57                anti_clockwise,
58            },
59        }
60    }
61
62    /// Set servo to specific angle (0-180 degrees for OneEighty, 0-360 for ThreeSixtyPosition)
63    pub fn set_angle(&self, device: &mut PoKeysDevice, angle: f32) -> Result<()> {
64        let duty = match &self.servo_type {
65            ServoType::OneEighty { pos_0, pos_180 } => {
66                if !(0.0..=180.0).contains(&angle) {
67                    return Err(PoKeysError::Parameter(
68                        "Angle must be between 0.0 and 180.0 degrees".to_string(),
69                    ));
70                }
71                let range = *pos_180 as f32 - *pos_0 as f32;
72                (*pos_0 as f32 + (angle / 180.0) * range) as u32
73            }
74            ServoType::ThreeSixtyPosition { pos_0, pos_360 } => {
75                if !(0.0..=360.0).contains(&angle) {
76                    return Err(PoKeysError::Parameter(
77                        "Angle must be between 0.0 and 360.0 degrees".to_string(),
78                    ));
79                }
80                let range = *pos_360 as f32 - *pos_0 as f32;
81                (*pos_0 as f32 + (angle / 360.0) * range) as u32
82            }
83            ServoType::ThreeSixtySpeed { .. } => {
84                return Err(PoKeysError::Parameter(
85                    "Cannot set angle on speed servo. Use set_speed() instead".to_string(),
86                ));
87            }
88        };
89
90        device.set_pwm_duty_cycle_for_pin(self.pin, duty)
91    }
92
93    /// Set servo speed (-100.0 to 100.0, where 0 is stop, positive is clockwise)
94    pub fn set_speed(&self, device: &mut PoKeysDevice, speed: f32) -> Result<()> {
95        let duty = match &self.servo_type {
96            ServoType::ThreeSixtySpeed {
97                stop,
98                clockwise,
99                anti_clockwise,
100            } => {
101                if !(-100.0..=100.0).contains(&speed) {
102                    return Err(PoKeysError::Parameter(
103                        "Speed must be between -100.0 and 100.0".to_string(),
104                    ));
105                }
106
107                if speed == 0.0 {
108                    *stop
109                } else if speed > 0.0 {
110                    // Clockwise: interpolate between stop and clockwise
111                    let range = *clockwise as f32 - *stop as f32;
112                    (*stop as f32 + (speed / 100.0) * range) as u32
113                } else {
114                    // Anti-clockwise: interpolate between stop and anti_clockwise
115                    let range = *anti_clockwise as f32 - *stop as f32;
116                    (*stop as f32 + (speed.abs() / 100.0) * range) as u32
117                }
118            }
119            _ => {
120                return Err(PoKeysError::Parameter(
121                    "Cannot set speed on position servo. Use set_angle() instead".to_string(),
122                ));
123            }
124        };
125
126        device.set_pwm_duty_cycle_for_pin(self.pin, duty)
127    }
128
129    /// Stop the servo (for speed servos)
130    pub fn stop(&self, device: &mut PoKeysDevice) -> Result<()> {
131        match &self.servo_type {
132            ServoType::ThreeSixtySpeed { stop, .. } => {
133                device.set_pwm_duty_cycle_for_pin(self.pin, *stop)
134            }
135            _ => Err(PoKeysError::Parameter(
136                "Stop command only applies to speed servos".to_string(),
137            )),
138        }
139    }
140}
141
142/// PWM data structure matching the protocol specification
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct PwmData {
145    /// PWM period (shared among all channels)
146    pub pwm_period: u32,
147    /// PWM duty cycle values for channels 1-6
148    pub pwm_values: [u32; 6],
149    /// Enabled channels bitmask (bit 0 = PWM1, bit 1 = PWM2, etc.)
150    pub enabled_channels: u8,
151}
152
153impl Default for PwmData {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl PwmData {
160    pub fn new() -> Self {
161        Self {
162            pwm_period: 0,
163            pwm_values: [0; 6],
164            enabled_channels: 0,
165        }
166    }
167
168    /// Get PWM channel index from pin number (17-22)
169    pub fn pin_to_channel(pin: u8) -> Result<usize> {
170        match pin {
171            22 => Ok(0), // PWM1
172            21 => Ok(1), // PWM2
173            20 => Ok(2), // PWM3
174            19 => Ok(3), // PWM4
175            18 => Ok(4), // PWM5
176            17 => Ok(5), // PWM6
177            _ => Err(PoKeysError::Parameter(format!(
178                "Pin {} does not support PWM. PWM is only supported on pins 17-22",
179                pin
180            ))),
181        }
182    }
183
184    /// Get pin number from PWM channel index
185    pub fn channel_to_pin(channel: usize) -> Result<u8> {
186        if channel >= 6 {
187            return Err(PoKeysError::Parameter(format!(
188                "Invalid PWM channel {}. Valid channels are 0-5",
189                channel
190            )));
191        }
192        Ok(PWM_PIN_MAP[channel])
193    }
194
195    /// Enable or disable a PWM channel
196    pub fn set_channel_enabled(&mut self, channel: usize, enabled: bool) -> Result<()> {
197        if channel >= 6 {
198            return Err(PoKeysError::Parameter(format!(
199                "Invalid PWM channel {}. Valid channels are 0-5",
200                channel
201            )));
202        }
203
204        if enabled {
205            self.enabled_channels |= 1 << channel;
206        } else {
207            self.enabled_channels &= !(1 << channel);
208        }
209        Ok(())
210    }
211
212    /// Check if a PWM channel is enabled
213    pub fn is_channel_enabled(&self, channel: usize) -> bool {
214        if channel >= 6 {
215            return false;
216        }
217        (self.enabled_channels & (1 << channel)) != 0
218    }
219
220    /// Set PWM duty cycle for a channel
221    pub fn set_duty_cycle(&mut self, channel: usize, duty: u32) -> Result<()> {
222        if channel >= 6 {
223            return Err(PoKeysError::Parameter(format!(
224                "Invalid PWM channel {}. Valid channels are 0-5",
225                channel
226            )));
227        }
228        self.pwm_values[channel] = duty;
229        Ok(())
230    }
231
232    /// Get PWM duty cycle for a channel
233    pub fn get_duty_cycle(&self, channel: usize) -> Result<u32> {
234        if channel >= 6 {
235            return Err(PoKeysError::Parameter(format!(
236                "Invalid PWM channel {}. Valid channels are 0-5",
237                channel
238            )));
239        }
240        Ok(self.pwm_values[channel])
241    }
242}
243
244impl PoKeysDevice {
245    /// Set PWM configuration using command 0xCB
246    pub fn set_pwm_configuration(&mut self) -> Result<()> {
247        let mut data = [0u8; 32];
248
249        // PWM enabled channels bitmask
250        data[0] = self.pwm.enabled_channels;
251
252        // PWM values (LSB first)
253        for i in 0..6 {
254            let value = self.pwm.pwm_values[i];
255            let base_idx = 1 + (i * 4);
256            data[base_idx] = (value & 0xFF) as u8;
257            data[base_idx + 1] = ((value >> 8) & 0xFF) as u8;
258            data[base_idx + 2] = ((value >> 16) & 0xFF) as u8;
259            data[base_idx + 3] = ((value >> 24) & 0xFF) as u8;
260        }
261
262        // PWM period (LSB first)
263        let period = self.pwm.pwm_period;
264        data[25] = (period & 0xFF) as u8;
265        data[26] = ((period >> 8) & 0xFF) as u8;
266        data[27] = ((period >> 16) & 0xFF) as u8;
267        data[28] = ((period >> 24) & 0xFF) as u8;
268
269        self.send_request_with_data(0xCB, 1, 0, 0, 0, &data)?;
270        Ok(())
271    }
272
273    /// Update only PWM duty values using command 0xCB
274    pub fn update_pwm_duty_values(&mut self) -> Result<()> {
275        let mut data = [0u8; 32];
276
277        // PWM enabled channels bitmask
278        data[0] = self.pwm.enabled_channels;
279
280        // PWM values (LSB first)
281        for i in 0..6 {
282            let value = self.pwm.pwm_values[i];
283            let base_idx = 1 + (i * 4);
284            data[base_idx] = (value & 0xFF) as u8;
285            data[base_idx + 1] = ((value >> 8) & 0xFF) as u8;
286            data[base_idx + 2] = ((value >> 16) & 0xFF) as u8;
287            data[base_idx + 3] = ((value >> 24) & 0xFF) as u8;
288        }
289
290        self.send_request_with_data(0xCB, 1, 1, 0, 0, &data)?;
291        Ok(())
292    }
293
294    /// Get PWM configuration using command 0xCB
295    pub fn get_pwm_configuration(&mut self) -> Result<()> {
296        let response = self.send_request(0xCB, 0, 0, 0, 0)?;
297
298        // Parse response according to specification
299        if response.len() >= 38 {
300            // PWM enabled channels
301            self.pwm.enabled_channels = response[9];
302
303            // PWM values (LSB first)
304            for i in 0..6 {
305                let base_idx = 10 + (i * 4);
306                if base_idx + 3 < response.len() {
307                    self.pwm.pwm_values[i] = response[base_idx] as u32
308                        | ((response[base_idx + 1] as u32) << 8)
309                        | ((response[base_idx + 2] as u32) << 16)
310                        | ((response[base_idx + 3] as u32) << 24);
311                }
312            }
313
314            // PWM period (LSB first)
315            if response.len() >= 38 {
316                self.pwm.pwm_period = response[34] as u32
317                    | ((response[35] as u32) << 8)
318                    | ((response[36] as u32) << 16)
319                    | ((response[37] as u32) << 24);
320            }
321        }
322
323        Ok(())
324    }
325
326    /// Set PWM period (shared among all channels)
327    pub fn set_pwm_period(&mut self, period: u32) -> Result<()> {
328        if period == 0 {
329            return Err(PoKeysError::Parameter(
330                "PWM period cannot be zero".to_string(),
331            ));
332        }
333
334        self.pwm.pwm_period = period;
335        self.set_pwm_configuration()
336    }
337
338    /// Get PWM period
339    pub fn get_pwm_period(&self) -> u32 {
340        self.pwm.pwm_period
341    }
342
343    /// Set PWM duty cycle for a pin (17-22)
344    pub fn set_pwm_duty_cycle_for_pin(&mut self, pin: u8, duty: u32) -> Result<()> {
345        let channel = PwmData::pin_to_channel(pin)?;
346        self.pwm.set_duty_cycle(channel, duty)?;
347        self.update_pwm_duty_values()
348    }
349
350    /// Get PWM duty cycle for a pin (17-22)
351    pub fn get_pwm_duty_cycle_for_pin(&self, pin: u8) -> Result<u32> {
352        let channel = PwmData::pin_to_channel(pin)?;
353        self.pwm.get_duty_cycle(channel)
354    }
355
356    /// Enable or disable PWM for a pin (17-22)
357    pub fn enable_pwm_for_pin(&mut self, pin: u8, enabled: bool) -> Result<()> {
358        let channel = PwmData::pin_to_channel(pin)?;
359        self.pwm.set_channel_enabled(channel, enabled)?;
360        self.set_pwm_configuration()
361    }
362
363    /// Check if PWM is enabled for a pin (17-22)
364    pub fn is_pwm_enabled_for_pin(&self, pin: u8) -> Result<bool> {
365        let channel = PwmData::pin_to_channel(pin)?;
366        Ok(self.pwm.is_channel_enabled(channel))
367    }
368
369    /// Set PWM duty cycle as percentage (0.0 to 100.0) for a pin
370    pub fn set_pwm_duty_cycle_percent_for_pin(&mut self, pin: u8, percent: f32) -> Result<()> {
371        if !(0.0..=100.0).contains(&percent) {
372            return Err(PoKeysError::Parameter(
373                "PWM duty cycle percentage must be between 0.0 and 100.0".to_string(),
374            ));
375        }
376
377        let duty = ((percent / 100.0) * self.pwm.pwm_period as f32) as u32;
378        self.set_pwm_duty_cycle_for_pin(pin, duty)
379    }
380
381    /// Get PWM duty cycle as percentage for a pin
382    pub fn get_pwm_duty_cycle_percent_for_pin(&self, pin: u8) -> Result<f32> {
383        let duty = self.get_pwm_duty_cycle_for_pin(pin)?;
384        if self.pwm.pwm_period == 0 {
385            return Ok(0.0);
386        }
387        Ok((duty as f32 / self.pwm.pwm_period as f32) * 100.0)
388    }
389}
390
391/// Simple PWM function for easy servo control
392pub fn simple_pwm(
393    device: &mut PoKeysDevice,
394    pin: u8,
395    frequency_hz: u32,
396    duty_percent: f32,
397) -> Result<()> {
398    // Validate pin
399    PwmData::pin_to_channel(pin)?;
400
401    // PoKeys PWM operates at 25MHz clock frequency
402    // Calculate period in clock cycles: period_seconds × 25,000,000
403    let period = if frequency_hz > 0 {
404        25_000_000 / frequency_hz // 25MHz clock cycles for the given frequency
405    } else {
406        return Err(PoKeysError::Parameter(
407            "Frequency must be greater than 0".to_string(),
408        ));
409    };
410
411    // Set period
412    device.set_pwm_period(period)?;
413
414    // Enable PWM for the pin
415    device.enable_pwm_for_pin(pin, true)?;
416
417    // Set duty cycle
418    device.set_pwm_duty_cycle_percent_for_pin(pin, duty_percent)
419}