Skip to main content

obniz_rust/
io.rs

1use serde::{Deserialize, Serialize};
2use serde_json::json;
3use tokio_tungstenite::tungstenite::protocol::Message;
4
5use crate::error::{validate_pin, ObnizError, ObnizResult};
6use crate::obniz::Obniz;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "kebab-case")]
10pub enum OutputType {
11    PushPull5v,
12    PushPull3v,
13    OpenDrain,
14}
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub enum PullType {
19    PullUp5v,
20    PullUp3v,
21    PullDown,
22    Float,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum Direction {
28    Input,
29    Output,
30}
31
32#[derive(Debug, Clone)]
33pub struct IoConfig {
34    pub direction: Direction,
35    pub value: Option<bool>,
36    pub output_type: Option<OutputType>,
37    pub pull_type: Option<PullType>,
38    pub stream: Option<bool>,
39}
40
41#[derive(Debug)]
42pub struct IoPin {
43    pin: u8,
44    obniz: Obniz,
45}
46
47impl IoPin {
48    pub fn new(pin: u8, obniz: Obniz) -> Self {
49        Self { pin, obniz }
50    }
51
52    pub fn pin_key(&self) -> String {
53        format!("io{}", self.pin)
54    }
55
56    /// Get the current state of the pin
57    pub async fn get(&self) -> ObnizResult<bool> {
58        validate_pin(self.pin)?;
59
60        let pin_key = self.pin_key();
61        let request = json!([{&pin_key: "get"}]);
62        let message = Message::from(request.to_string());
63
64        let response = self
65            .obniz
66            .send_await_response(message, pin_key.clone())
67            .await
68            .map_err(|e| ObnizError::Connection(e.to_string()))?;
69
70        // Parse the response to extract the boolean value
71        // Response format is typically [{"io1": false}]
72        if let Some(array) = response.as_array() {
73            if let Some(first_item) = array.first() {
74                if let Some(value) = first_item.get(&pin_key) {
75                    return value.as_bool().ok_or_else(|| {
76                        ObnizError::IoOperation("Invalid response format".to_string())
77                    });
78                }
79            }
80        }
81
82        // Fallback: try direct object access
83        if let Some(value) = response.get(&pin_key) {
84            value
85                .as_bool()
86                .ok_or_else(|| ObnizError::IoOperation("Invalid response format".to_string()))
87        } else {
88            Err(ObnizError::IoOperation(format!(
89                "No response for pin {}",
90                self.pin
91            )))
92        }
93    }
94
95    /// Set the pin to a specific value
96    pub async fn set(&self, value: bool) -> ObnizResult<()> {
97        validate_pin(self.pin)?;
98
99        let pin_key = self.pin_key();
100        let request = json!([{&pin_key: value}]);
101        let message = Message::from(request.to_string());
102
103        self.obniz
104            .send_message(message)
105            .map_err(|e| ObnizError::Connection(e.to_string()))?;
106        Ok(())
107    }
108
109    /// Configure the pin with detailed settings
110    pub async fn configure(&self, config: IoConfig) -> ObnizResult<()> {
111        validate_pin(self.pin)?;
112        let pin_key = self.pin_key();
113        let mut pin_config = json!({
114            "direction": config.direction
115        });
116
117        if let Some(value) = config.value {
118            pin_config["value"] = json!(value);
119        }
120
121        if let Some(output_type) = config.output_type {
122            pin_config["output_type"] = json!(output_type);
123        }
124
125        if let Some(pull_type) = config.pull_type {
126            pin_config["pull_type"] = json!(pull_type);
127        }
128
129        if let Some(stream) = config.stream {
130            pin_config["stream"] = json!(stream);
131        }
132
133        let request = json!([{&pin_key: pin_config}]);
134        let message = Message::from(request.to_string());
135
136        self.obniz
137            .send_message(message)
138            .map_err(|e| ObnizError::Connection(e.to_string()))?;
139        Ok(())
140    }
141
142    /// Set the pin as input with optional stream mode
143    pub async fn set_as_input(&self, enable_stream: bool) -> ObnizResult<()> {
144        validate_pin(self.pin)?;
145        let config = IoConfig {
146            direction: Direction::Input,
147            value: None,
148            output_type: None,
149            pull_type: None,
150            stream: Some(enable_stream),
151        };
152        self.configure(config).await
153    }
154
155    /// Set the pin as output with a specific value
156    pub async fn set_as_output(&self, value: bool) -> ObnizResult<()> {
157        validate_pin(self.pin)?;
158        let config = IoConfig {
159            direction: Direction::Output,
160            value: Some(value),
161            output_type: None,
162            pull_type: None,
163            stream: None,
164        };
165        self.configure(config).await
166    }
167
168    /// Set the output type of the pin
169    pub async fn set_output_type(&self, output_type: OutputType) -> ObnizResult<()> {
170        validate_pin(self.pin)?;
171        let pin_key = self.pin_key();
172        let request = json!([{&pin_key: {"output_type": output_type}}]);
173        let message = Message::from(request.to_string());
174
175        self.obniz
176            .send_message(message)
177            .map_err(|e| ObnizError::Connection(e.to_string()))?;
178        Ok(())
179    }
180
181    /// Set the pull type of the pin
182    pub async fn set_pull_type(&self, pull_type: PullType) -> ObnizResult<()> {
183        validate_pin(self.pin)?;
184        let pin_key = self.pin_key();
185        let request = json!([{&pin_key: {"pull_type": pull_type}}]);
186        let message = Message::from(request.to_string());
187
188        self.obniz
189            .send_message(message)
190            .map_err(|e| ObnizError::Connection(e.to_string()))?;
191        Ok(())
192    }
193
194    /// Register a callback for this pin's state changes (stream mode)
195    /// This will automatically enable stream mode for the pin
196    pub async fn on_change<F>(&self, callback: F) -> ObnizResult<()>
197    where
198        F: Fn(bool) + Send + Sync + 'static,
199    {
200        validate_pin(self.pin)?;
201        // First, enable stream mode for this pin
202        self.set_as_input(true).await?;
203
204        let pin_key = self.pin_key();
205        let pin_key_clone = pin_key.clone();
206
207        self.obniz
208            .register_callback(pin_key, move |response| {
209                // Parse the response to extract the pin value
210                if let Some(value) = response.get(&pin_key_clone) {
211                    if let Some(bool_value) = value.as_bool() {
212                        callback(bool_value);
213                    }
214                }
215            })
216            .map_err(|e| ObnizError::CallbackError(e.to_string()))?;
217
218        Ok(())
219    }
220
221    /// Enable stream mode for this pin without setting up a callback
222    pub async fn enable_stream(&self) -> ObnizResult<()> {
223        self.set_as_input(true).await
224    }
225
226    /// Disable stream mode for this pin
227    pub async fn disable_stream(&self) -> ObnizResult<()> {
228        self.set_as_input(false).await
229    }
230
231    /// Remove the callback for this pin
232    pub fn remove_callback(&self) -> ObnizResult<()> {
233        validate_pin(self.pin)?;
234        let pin_key = self.pin_key();
235        self.obniz
236            .unregister_callback(pin_key)
237            .map_err(|e| ObnizError::CallbackError(e.to_string()))
238    }
239
240    /// Deinitialize the pin
241    pub async fn deinit(&self) -> ObnizResult<()> {
242        validate_pin(self.pin)?;
243        let pin_key = self.pin_key();
244        let request = json!([{&pin_key: null}]);
245        let message = Message::from(request.to_string());
246
247        self.obniz
248            .send_message(message)
249            .map_err(|e| ObnizError::Connection(e.to_string()))?;
250        Ok(())
251    }
252}
253
254/// IO Manager for handling multiple pins
255#[derive(Debug)]
256pub struct IoManager {
257    obniz: Obniz,
258}
259
260impl IoManager {
261    pub fn new(obniz: Obniz) -> Self {
262        Self { obniz }
263    }
264
265    /// Get a specific pin (0-11)
266    pub fn pin(&self, pin: u8) -> ObnizResult<IoPin> {
267        validate_pin(pin)?;
268        Ok(IoPin::new(pin, self.obniz.clone()))
269    }
270
271    /// Get the current state of a pin
272    pub async fn get_pin(&self, pin: u8) -> ObnizResult<bool> {
273        self.pin(pin)?.get().await
274    }
275
276    /// Set a pin to a specific value
277    pub async fn set_pin(&self, pin: u8, value: bool) -> ObnizResult<()> {
278        self.pin(pin)?.set(value).await
279    }
280
281    /// Configure a pin with detailed settings
282    pub async fn configure_pin(&self, pin: u8, config: IoConfig) -> ObnizResult<()> {
283        self.pin(pin)?.configure(config).await
284    }
285
286    /// Set a pin as input with optional stream mode
287    pub async fn set_pin_as_input(&self, pin: u8, enable_stream: bool) -> ObnizResult<()> {
288        self.pin(pin)?.set_as_input(enable_stream).await
289    }
290
291    /// Set a pin as output with a specific value
292    pub async fn set_pin_as_output(&self, pin: u8, value: bool) -> ObnizResult<()> {
293        self.pin(pin)?.set_as_output(value).await
294    }
295
296    /// Register a callback for a pin's state changes (stream mode)
297    /// This will automatically enable stream mode for the pin
298    pub async fn set_pin_callback<F>(&self, pin: u8, callback: F) -> ObnizResult<()>
299    where
300        F: Fn(bool) + Send + Sync + 'static,
301    {
302        self.pin(pin)?.on_change(callback).await
303    }
304
305    /// Remove the callback for a pin
306    pub fn remove_pin_callback(&self, pin: u8) -> ObnizResult<()> {
307        self.pin(pin)?.remove_callback()
308    }
309
310    /// Enable stream mode for a pin without setting up a callback
311    pub async fn enable_pin_stream(&self, pin: u8) -> ObnizResult<()> {
312        self.pin(pin)?.enable_stream().await
313    }
314
315    /// Disable stream mode for a pin
316    pub async fn disable_pin_stream(&self, pin: u8) -> ObnizResult<()> {
317        self.pin(pin)?.disable_stream().await
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::error::validate_pin;
325
326    #[test]
327    fn test_pin_validation() {
328        // Valid pins
329        assert!(validate_pin(0).is_ok());
330        assert!(validate_pin(5).is_ok());
331        assert!(validate_pin(11).is_ok());
332
333        // Invalid pins
334        assert!(validate_pin(12).is_err());
335        assert!(validate_pin(255).is_err());
336    }
337
338    // #[test]
339    // fn test_pin_key_generation() {
340    //     let obniz = create_mock_obniz();
341    //     let pin = IoPin::new(5, obniz);
342    //     assert_eq!(pin.pin_key(), "io5");
343    //
344    //     let pin = IoPin::new(0, obniz);
345    //     assert_eq!(pin.pin_key(), "io0");
346    // }
347
348    #[test]
349    fn test_io_config_serialization() {
350        let config = IoConfig {
351            direction: Direction::Output,
352            value: Some(true),
353            output_type: Some(OutputType::PushPull5v),
354            pull_type: Some(PullType::PullUp5v),
355            stream: Some(false),
356        };
357
358        // Test that the config can be created without errors
359        assert_eq!(config.direction, Direction::Output);
360        assert_eq!(config.value, Some(true));
361    }
362
363    #[test]
364    fn test_output_type_serialization() {
365        use serde_json;
366
367        let output_type = OutputType::PushPull5v;
368        let serialized = serde_json::to_string(&output_type).unwrap();
369        assert_eq!(serialized, "\"push-pull5v\"");
370
371        let output_type = OutputType::OpenDrain;
372        let serialized = serde_json::to_string(&output_type).unwrap();
373        assert_eq!(serialized, "\"open-drain\"");
374    }
375
376    #[test]
377    fn test_pull_type_serialization() {
378        use serde_json;
379
380        let pull_type = PullType::PullUp5v;
381        let serialized = serde_json::to_string(&pull_type).unwrap();
382        assert_eq!(serialized, "\"pull-up5v\"");
383
384        let pull_type = PullType::Float;
385        let serialized = serde_json::to_string(&pull_type).unwrap();
386        assert_eq!(serialized, "\"float\"");
387    }
388
389    #[test]
390    fn test_direction_serialization() {
391        use serde_json;
392
393        let direction = Direction::Input;
394        let serialized = serde_json::to_string(&direction).unwrap();
395        assert_eq!(serialized, "\"input\"");
396
397        let direction = Direction::Output;
398        let serialized = serde_json::to_string(&direction).unwrap();
399        assert_eq!(serialized, "\"output\"");
400    }
401
402    // Helper function to create a mock Obniz instance for testing
403    // Note: This would need to be implemented with proper mocking
404    // fn create_mock_obniz() -> Obniz {
405    //     // This is a placeholder - in a real implementation, you'd use a mock
406    //     // or test framework to create a mock Obniz instance
407    //     unimplemented!("Mock Obniz creation for testing")
408    // }
409}