Skip to main content

obniz_rust/
pwm.rs

1use serde::{Deserialize, Serialize};
2use serde_json::json;
3use tokio_tungstenite::tungstenite::protocol::Message;
4
5use crate::error::{ObnizError, ObnizResult};
6use crate::obniz::Obniz;
7
8/// PWM modulation types
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum ModulationType {
12    Am, // Amplitude Modulation
13}
14
15/// PWM configuration
16#[derive(Debug, Clone)]
17pub struct PwmConfig {
18    pub io_pin: u8,
19    pub frequency: u32,
20    pub pulse_width_ms: f64,
21}
22
23/// Modulation configuration
24#[derive(Debug, Clone)]
25pub struct ModulationConfig {
26    pub modulation_type: ModulationType,
27    pub symbol_length_ms: f64,
28    pub data: Vec<u8>,
29}
30
31/// Individual PWM channel controller
32#[derive(Debug)]
33pub struct PwmChannel {
34    channel: u8,
35    obniz: Obniz,
36}
37
38impl PwmChannel {
39    pub fn new(channel: u8, obniz: Obniz) -> Self {
40        Self { channel, obniz }
41    }
42
43    pub fn channel_key(&self) -> String {
44        format!("pwm{}", self.channel)
45    }
46
47    /// Initialize PWM channel with IO pin
48    pub async fn init(&self, io_pin: u8) -> ObnizResult<()> {
49        if self.channel > 5 {
50            return Err(ObnizError::Generic("PWM channel must be 0-5".to_string()));
51        }
52
53        if io_pin > 11 {
54            return Err(ObnizError::InvalidPin(io_pin));
55        }
56
57        let channel_key = self.channel_key();
58        let request = json!([{&channel_key: {"io": io_pin}}]);
59        let message = Message::from(request.to_string());
60
61        self.obniz
62            .send_message(message)
63            .map_err(|e| ObnizError::Connection(e.to_string()))
64    }
65
66    /// Set PWM frequency (1 Hz to 80,000,000 Hz)
67    pub async fn set_frequency(&self, frequency: u32) -> ObnizResult<()> {
68        if frequency == 0 || frequency > 80_000_000 {
69            return Err(ObnizError::Generic(
70                "Frequency must be between 1 and 80,000,000 Hz".to_string(),
71            ));
72        }
73
74        let channel_key = self.channel_key();
75        let request = json!([{&channel_key: {"freq": frequency}}]);
76        let message = Message::from(request.to_string());
77
78        self.obniz
79            .send_message(message)
80            .map_err(|e| ObnizError::Connection(e.to_string()))
81    }
82
83    /// Set pulse width in milliseconds
84    pub async fn set_pulse_width(&self, pulse_width_ms: f64) -> ObnizResult<()> {
85        if pulse_width_ms < 0.0 {
86            return Err(ObnizError::Generic("Pulse width must be >= 0".to_string()));
87        }
88
89        let channel_key = self.channel_key();
90        let request = json!([{&channel_key: {"pulse": pulse_width_ms}}]);
91        let message = Message::from(request.to_string());
92
93        self.obniz
94            .send_message(message)
95            .map_err(|e| ObnizError::Connection(e.to_string()))
96    }
97
98    /// Set duty cycle as percentage (0.0 to 100.0)
99    pub async fn set_duty_cycle(&self, frequency: u32, duty_percent: f64) -> ObnizResult<()> {
100        if !(0.0..=100.0).contains(&duty_percent) {
101            return Err(ObnizError::Generic(
102                "Duty cycle must be between 0 and 100%".to_string(),
103            ));
104        }
105
106        // Calculate pulse width from duty cycle and frequency
107        let period_ms = 1000.0 / frequency as f64;
108        let pulse_width_ms = period_ms * duty_percent / 100.0;
109
110        self.set_frequency(frequency).await?;
111        self.set_pulse_width(pulse_width_ms).await
112    }
113
114    /// Configure PWM with all parameters
115    pub async fn configure(&self, config: PwmConfig) -> ObnizResult<()> {
116        self.init(config.io_pin).await?;
117        self.set_frequency(config.frequency).await?;
118        self.set_pulse_width(config.pulse_width_ms).await
119    }
120
121    /// Set up modulation
122    pub async fn modulate(&self, config: ModulationConfig) -> ObnizResult<()> {
123        if config.symbol_length_ms < 0.05 || config.symbol_length_ms > 1000.0 {
124            return Err(ObnizError::Generic(
125                "Symbol length must be between 0.05 and 1000 ms".to_string(),
126            ));
127        }
128
129        if config.data.is_empty() {
130            return Err(ObnizError::Generic(
131                "Modulation data cannot be empty".to_string(),
132            ));
133        }
134
135        // Validate data values (should be 0 or 1 for binary data)
136        for &value in &config.data {
137            if value > 1 {
138                return Err(ObnizError::Generic(
139                    "Modulation data must contain only 0 and 1".to_string(),
140                ));
141            }
142        }
143
144        let channel_key = self.channel_key();
145        let request = json!([{&channel_key: {
146            "modulate": {
147                "type": config.modulation_type,
148                "symbol_length": config.symbol_length_ms,
149                "data": config.data
150            }
151        }}]);
152        let message = Message::from(request.to_string());
153
154        self.obniz
155            .send_message(message)
156            .map_err(|e| ObnizError::Connection(e.to_string()))
157    }
158
159    /// Generate square wave with 50% duty cycle
160    pub async fn square_wave(&self, io_pin: u8, frequency: u32) -> ObnizResult<()> {
161        let config = PwmConfig {
162            io_pin,
163            frequency,
164            pulse_width_ms: 500.0 / frequency as f64, // 50% duty cycle
165        };
166        self.configure(config).await
167    }
168
169    /// Generate servo control signal (20ms period, 1-2ms pulse width)
170    pub async fn servo(&self, io_pin: u8, angle: f64) -> ObnizResult<()> {
171        if !(0.0..=180.0).contains(&angle) {
172            return Err(ObnizError::Generic(
173                "Servo angle must be between 0 and 180 degrees".to_string(),
174            ));
175        }
176
177        // Standard servo: 1ms = 0°, 1.5ms = 90°, 2ms = 180°
178        let pulse_width_ms = 1.0 + (angle / 180.0);
179
180        let config = PwmConfig {
181            io_pin,
182            frequency: 50, // 20ms period
183            pulse_width_ms,
184        };
185        self.configure(config).await
186    }
187
188    /// Deinitialize PWM channel
189    pub async fn deinit(&self) -> ObnizResult<()> {
190        let channel_key = self.channel_key();
191        let request = json!([{&channel_key: null}]);
192        let message = Message::from(request.to_string());
193
194        self.obniz
195            .send_message(message)
196            .map_err(|e| ObnizError::Connection(e.to_string()))
197    }
198}
199
200/// PWM manager for handling multiple channels
201#[derive(Debug, Clone)]
202pub struct PwmManager {
203    obniz: Obniz,
204}
205
206impl PwmManager {
207    pub fn new(obniz: Obniz) -> Self {
208        Self { obniz }
209    }
210
211    /// Get specific PWM channel (0-5)
212    pub fn channel(&self, channel: u8) -> ObnizResult<PwmChannel> {
213        if channel > 5 {
214            return Err(ObnizError::Generic("PWM channel must be 0-5".to_string()));
215        }
216        Ok(PwmChannel::new(channel, self.obniz.clone()))
217    }
218
219    /// Configure PWM channel
220    pub async fn configure_channel(&self, channel: u8, config: PwmConfig) -> ObnizResult<()> {
221        self.channel(channel)?.configure(config).await
222    }
223
224    /// Set frequency for specific channel
225    pub async fn set_channel_frequency(&self, channel: u8, frequency: u32) -> ObnizResult<()> {
226        self.channel(channel)?.set_frequency(frequency).await
227    }
228
229    /// Set pulse width for specific channel
230    pub async fn set_channel_pulse_width(
231        &self,
232        channel: u8,
233        pulse_width_ms: f64,
234    ) -> ObnizResult<()> {
235        self.channel(channel)?.set_pulse_width(pulse_width_ms).await
236    }
237
238    /// Set duty cycle for specific channel
239    pub async fn set_channel_duty_cycle(
240        &self,
241        channel: u8,
242        frequency: u32,
243        duty_percent: f64,
244    ) -> ObnizResult<()> {
245        self.channel(channel)?
246            .set_duty_cycle(frequency, duty_percent)
247            .await
248    }
249
250    /// Generate square wave on specific channel
251    pub async fn square_wave(&self, channel: u8, io_pin: u8, frequency: u32) -> ObnizResult<()> {
252        self.channel(channel)?.square_wave(io_pin, frequency).await
253    }
254
255    /// Control servo on specific channel
256    pub async fn servo(&self, channel: u8, io_pin: u8, angle: f64) -> ObnizResult<()> {
257        self.channel(channel)?.servo(io_pin, angle).await
258    }
259
260    /// Deinitialize specific channel
261    pub async fn deinit_channel(&self, channel: u8) -> ObnizResult<()> {
262        self.channel(channel)?.deinit().await
263    }
264
265    /// Deinitialize all PWM channels
266    pub async fn deinit_all(&self) -> ObnizResult<()> {
267        for channel in 0..=5 {
268            if let Err(e) = self.deinit_channel(channel).await {
269                eprintln!("Failed to deinitialize PWM channel {channel}: {e}");
270            }
271        }
272        Ok(())
273    }
274
275    /// Utility function to calculate pulse width from duty cycle
276    pub fn duty_cycle_to_pulse_width(frequency: u32, duty_percent: f64) -> f64 {
277        let period_ms = 1000.0 / frequency as f64;
278        period_ms * duty_percent / 100.0
279    }
280
281    /// Utility function to calculate duty cycle from pulse width
282    pub fn pulse_width_to_duty_cycle(frequency: u32, pulse_width_ms: f64) -> f64 {
283        let period_ms = 1000.0 / frequency as f64;
284        (pulse_width_ms / period_ms * 100.0).clamp(0.0, 100.0)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_pwm_config_creation() {
294        let config = PwmConfig {
295            io_pin: 5,
296            frequency: 1000,
297            pulse_width_ms: 0.5,
298        };
299
300        assert_eq!(config.io_pin, 5);
301        assert_eq!(config.frequency, 1000);
302        assert_eq!(config.pulse_width_ms, 0.5);
303    }
304
305    #[test]
306    fn test_modulation_config_creation() {
307        let config = ModulationConfig {
308            modulation_type: ModulationType::Am,
309            symbol_length_ms: 100.0,
310            data: vec![0, 1, 1, 0],
311        };
312
313        assert_eq!(config.modulation_type, ModulationType::Am);
314        assert_eq!(config.symbol_length_ms, 100.0);
315        assert_eq!(config.data, vec![0, 1, 1, 0]);
316    }
317
318    #[test]
319    fn test_duty_cycle_calculations() {
320        // Test duty cycle to pulse width conversion
321        let pulse_width = PwmManager::duty_cycle_to_pulse_width(1000, 50.0);
322        assert_eq!(pulse_width, 0.5); // 50% of 1ms period
323
324        // Test pulse width to duty cycle conversion
325        let duty_cycle = PwmManager::pulse_width_to_duty_cycle(1000, 0.25);
326        assert_eq!(duty_cycle, 25.0); // 0.25ms of 1ms period
327    }
328
329    #[test]
330    fn test_modulation_type_serialization() {
331        use serde_json;
332
333        let mod_type = ModulationType::Am;
334        let serialized = serde_json::to_string(&mod_type).unwrap();
335        assert_eq!(serialized, "\"am\"");
336    }
337
338    #[test]
339    fn test_channel_key_generation() {
340        assert_eq!(format!("pwm{}", 0), "pwm0");
341        assert_eq!(format!("pwm{}", 5), "pwm5");
342    }
343}