Skip to main content

obniz_rust/
display.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/// QR code error correction levels
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum QrCorrectionType {
11    #[serde(rename = "L")]
12    Low,
13    #[serde(rename = "M")]
14    Medium,
15    #[serde(rename = "Q")]
16    Quality,
17    #[serde(rename = "H")]
18    High,
19}
20
21/// Color depth for raw display data
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub enum DisplayRawColorDepth {
24    #[serde(rename = "1")]
25    OneBit,
26    #[serde(rename = "4")]
27    FourBit,
28    #[serde(rename = "16")]
29    SixteenBit,
30}
31
32/// Display configuration for raw data
33#[derive(Debug, Clone)]
34pub struct RawDisplayConfig {
35    pub width: u16,
36    pub height: u16,
37    pub color_depth: DisplayRawColorDepth,
38    pub data: Vec<u16>,
39}
40
41/// Pin assignment configuration for display modules
42#[derive(Debug, Clone)]
43pub struct PinAssignment {
44    pub pin: u8,
45    pub module_name: String,
46    pub pin_name: String,
47}
48
49/// Display manager for obniz device
50#[derive(Debug, Clone)]
51pub struct DisplayManager {
52    obniz: Obniz,
53}
54
55impl DisplayManager {
56    pub fn new(obniz: Obniz) -> Self {
57        Self { obniz }
58    }
59
60    /// Display text on the obniz screen
61    pub async fn text(&self, text: &str) -> ObnizResult<()> {
62        if text.is_empty() {
63            return Err(ObnizError::Generic("Text cannot be empty".to_string()));
64        }
65
66        let request = json!([{"display": {"text": text}}]);
67        let message = Message::from(request.to_string());
68
69        self.obniz
70            .send_message(message)
71            .map_err(|e| ObnizError::Connection(e.to_string()))
72    }
73
74    /// Clear the display
75    pub async fn clear(&self) -> ObnizResult<()> {
76        let request = json!([{"display": {"clear": true}}]);
77        let message = Message::from(request.to_string());
78
79        self.obniz
80            .send_message(message)
81            .map_err(|e| ObnizError::Connection(e.to_string()))
82    }
83
84    /// Display a QR code with specified error correction level
85    pub async fn qr(&self, text: &str, correction_type: QrCorrectionType) -> ObnizResult<()> {
86        if text.is_empty() {
87            return Err(ObnizError::Generic("QR text cannot be empty".to_string()));
88        }
89
90        let request = json!([{
91            "display": {
92                "qr": {
93                    "text": text,
94                    "correction": correction_type
95                }
96            }
97        }]);
98        let message = Message::from(request.to_string());
99
100        self.obniz
101            .send_message(message)
102            .map_err(|e| ObnizError::Connection(e.to_string()))
103    }
104
105    /// Display raw pixel data
106    pub async fn raw(&self, config: RawDisplayConfig) -> ObnizResult<()> {
107        if config.data.is_empty() {
108            return Err(ObnizError::Generic("Raw data cannot be empty".to_string()));
109        }
110
111        if config.width == 0 || config.height == 0 {
112            return Err(ObnizError::Generic(
113                "Width and height must be greater than 0".to_string(),
114            ));
115        }
116
117        let expected_length = match config.color_depth {
118            DisplayRawColorDepth::OneBit => (config.width * config.height).div_ceil(8),
119            DisplayRawColorDepth::FourBit => (config.width * config.height).div_ceil(2),
120            DisplayRawColorDepth::SixteenBit => config.width * config.height,
121        };
122
123        if config.data.len() != expected_length as usize {
124            return Err(ObnizError::Generic(format!(
125                "Data length mismatch. Expected {expected_length} but got {}",
126                config.data.len()
127            )));
128        }
129
130        let request = json!([{
131            "display": {
132                "raw": {
133                    "width": config.width,
134                    "height": config.height,
135                    "color_depth": config.color_depth,
136                    "data": config.data
137                }
138            }
139        }]);
140        let message = Message::from(request.to_string());
141
142        self.obniz
143            .send_message(message)
144            .map_err(|e| ObnizError::Connection(e.to_string()))
145    }
146
147    /// Set display brightness (0-100)
148    pub async fn brightness(&self, level: u8) -> ObnizResult<()> {
149        if level > 100 {
150            return Err(ObnizError::Generic(
151                "Brightness level must be between 0-100".to_string(),
152            ));
153        }
154
155        let request = json!([{
156            "display": {
157                "brightness": level
158            }
159        }]);
160        let message = Message::from(request.to_string());
161
162        self.obniz
163            .send_message(message)
164            .map_err(|e| ObnizError::Connection(e.to_string()))
165    }
166
167    /// Set display contrast (0-100)
168    pub async fn contrast(&self, level: u8) -> ObnizResult<()> {
169        if level > 100 {
170            return Err(ObnizError::Generic(
171                "Contrast level must be between 0-100".to_string(),
172            ));
173        }
174
175        let request = json!([{
176            "display": {
177                "contrast": level
178            }
179        }]);
180        let message = Message::from(request.to_string());
181
182        self.obniz
183            .send_message(message)
184            .map_err(|e| ObnizError::Connection(e.to_string()))
185    }
186
187    /// Assign pins for display modules
188    pub async fn pin_assign(&self, assignments: Vec<PinAssignment>) -> ObnizResult<()> {
189        if assignments.is_empty() {
190            return Err(ObnizError::Generic(
191                "Pin assignments cannot be empty".to_string(),
192            ));
193        }
194
195        for assignment in &assignments {
196            if assignment.pin > 11 {
197                return Err(ObnizError::InvalidPin(assignment.pin));
198            }
199            if assignment.module_name.is_empty() || assignment.pin_name.is_empty() {
200                return Err(ObnizError::Generic(
201                    "Module name and pin name cannot be empty".to_string(),
202                ));
203            }
204        }
205
206        let mut pin_config = json!({});
207        for assignment in assignments {
208            pin_config[format!("io{}", assignment.pin)] = json!({
209                assignment.module_name: assignment.pin_name
210            });
211        }
212
213        let request = json!([pin_config]);
214        let message = Message::from(request.to_string());
215
216        self.obniz
217            .send_message(message)
218            .map_err(|e| ObnizError::Connection(e.to_string()))
219    }
220
221    /// Draw a pixel at specified coordinates
222    pub async fn pixel(&self, x: u16, y: u16, color: bool) -> ObnizResult<()> {
223        let request = json!([{
224            "display": {
225                "pixel": {
226                    "x": x,
227                    "y": y,
228                    "color": color
229                }
230            }
231        }]);
232        let message = Message::from(request.to_string());
233
234        self.obniz
235            .send_message(message)
236            .map_err(|e| ObnizError::Connection(e.to_string()))
237    }
238
239    /// Draw a line from (x1, y1) to (x2, y2)
240    pub async fn line(&self, x1: u16, y1: u16, x2: u16, y2: u16, color: bool) -> ObnizResult<()> {
241        let request = json!([{
242            "display": {
243                "line": {
244                    "x1": x1,
245                    "y1": y1,
246                    "x2": x2,
247                    "y2": y2,
248                    "color": color
249                }
250            }
251        }]);
252        let message = Message::from(request.to_string());
253
254        self.obniz
255            .send_message(message)
256            .map_err(|e| ObnizError::Connection(e.to_string()))
257    }
258
259    /// Draw a rectangle
260    pub async fn rect(
261        &self,
262        x: u16,
263        y: u16,
264        width: u16,
265        height: u16,
266        filled: bool,
267        color: bool,
268    ) -> ObnizResult<()> {
269        if width == 0 || height == 0 {
270            return Err(ObnizError::Generic(
271                "Width and height must be greater than 0".to_string(),
272            ));
273        }
274
275        let request = json!([{
276            "display": {
277                "rect": {
278                    "x": x,
279                    "y": y,
280                    "width": width,
281                    "height": height,
282                    "filled": filled,
283                    "color": color
284                }
285            }
286        }]);
287        let message = Message::from(request.to_string());
288
289        self.obniz
290            .send_message(message)
291            .map_err(|e| ObnizError::Connection(e.to_string()))
292    }
293
294    /// Draw a circle
295    pub async fn circle(
296        &self,
297        x: u16,
298        y: u16,
299        radius: u16,
300        filled: bool,
301        color: bool,
302    ) -> ObnizResult<()> {
303        if radius == 0 {
304            return Err(ObnizError::Generic(
305                "Radius must be greater than 0".to_string(),
306            ));
307        }
308
309        let request = json!([{
310            "display": {
311                "circle": {
312                    "x": x,
313                    "y": y,
314                    "radius": radius,
315                    "filled": filled,
316                    "color": color
317                }
318            }
319        }]);
320        let message = Message::from(request.to_string());
321
322        self.obniz
323            .send_message(message)
324            .map_err(|e| ObnizError::Connection(e.to_string()))
325    }
326
327    /// Set text size
328    pub async fn text_size(&self, size: u8) -> ObnizResult<()> {
329        if size == 0 {
330            return Err(ObnizError::Generic(
331                "Text size must be greater than 0".to_string(),
332            ));
333        }
334
335        let request = json!([{
336            "display": {
337                "text_size": size
338            }
339        }]);
340        let message = Message::from(request.to_string());
341
342        self.obniz
343            .send_message(message)
344            .map_err(|e| ObnizError::Connection(e.to_string()))
345    }
346
347    /// Set text position
348    pub async fn text_pos(&self, x: u16, y: u16) -> ObnizResult<()> {
349        let request = json!([{
350            "display": {
351                "text_pos": {
352                    "x": x,
353                    "y": y
354                }
355            }
356        }]);
357        let message = Message::from(request.to_string());
358
359        self.obniz
360            .send_message(message)
361            .map_err(|e| ObnizError::Connection(e.to_string()))
362    }
363}
364
365/// Legacy trait for backward compatibility
366pub trait ObnizDisplay {
367    fn display_text(&self, text: &str) -> ObnizResult<()>;
368    fn display_clear(&self) -> ObnizResult<()>;
369    fn display_qr(&self, text: &str, correction_type: QrCorrectionType) -> ObnizResult<()>;
370    fn display_raw(&self, config: RawDisplayConfig) -> ObnizResult<()>;
371    fn display_pin_assign(&self, assignments: Vec<PinAssignment>) -> ObnizResult<()>;
372}
373
374impl ObnizDisplay for Obniz {
375    fn display_text(&self, text: &str) -> ObnizResult<()> {
376        let rt = tokio::runtime::Handle::try_current()
377            .map_err(|_| ObnizError::Generic("No tokio runtime available".to_string()))?;
378
379        rt.block_on(async { self.display().text(text).await })
380    }
381
382    fn display_clear(&self) -> ObnizResult<()> {
383        let rt = tokio::runtime::Handle::try_current()
384            .map_err(|_| ObnizError::Generic("No tokio runtime available".to_string()))?;
385
386        rt.block_on(async { self.display().clear().await })
387    }
388
389    fn display_qr(&self, text: &str, correction_type: QrCorrectionType) -> ObnizResult<()> {
390        let rt = tokio::runtime::Handle::try_current()
391            .map_err(|_| ObnizError::Generic("No tokio runtime available".to_string()))?;
392
393        rt.block_on(async { self.display().qr(text, correction_type).await })
394    }
395
396    fn display_raw(&self, config: RawDisplayConfig) -> ObnizResult<()> {
397        let rt = tokio::runtime::Handle::try_current()
398            .map_err(|_| ObnizError::Generic("No tokio runtime available".to_string()))?;
399
400        rt.block_on(async { self.display().raw(config).await })
401    }
402
403    fn display_pin_assign(&self, assignments: Vec<PinAssignment>) -> ObnizResult<()> {
404        let rt = tokio::runtime::Handle::try_current()
405            .map_err(|_| ObnizError::Generic("No tokio runtime available".to_string()))?;
406
407        rt.block_on(async { self.display().pin_assign(assignments).await })
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_qr_correction_type_serialization() {
417        use serde_json;
418
419        let correction = QrCorrectionType::Low;
420        let serialized = serde_json::to_string(&correction).unwrap();
421        assert_eq!(serialized, "\"L\"");
422
423        let correction = QrCorrectionType::High;
424        let serialized = serde_json::to_string(&correction).unwrap();
425        assert_eq!(serialized, "\"H\"");
426    }
427
428    #[test]
429    fn test_color_depth_serialization() {
430        use serde_json;
431
432        let depth = DisplayRawColorDepth::OneBit;
433        let serialized = serde_json::to_string(&depth).unwrap();
434        assert_eq!(serialized, "\"1\"");
435
436        let depth = DisplayRawColorDepth::SixteenBit;
437        let serialized = serde_json::to_string(&depth).unwrap();
438        assert_eq!(serialized, "\"16\"");
439    }
440
441    #[test]
442    fn test_raw_display_config_validation() {
443        // Test valid config
444        let config = RawDisplayConfig {
445            width: 128,
446            height: 64,
447            color_depth: DisplayRawColorDepth::OneBit,
448            data: vec![0; 1024], // 128 * 64 / 8 = 1024 bytes for 1-bit
449        };
450        assert_eq!(config.width, 128);
451        assert_eq!(config.height, 64);
452    }
453
454    #[test]
455    fn test_pin_assignment_creation() {
456        let assignment = PinAssignment {
457            pin: 5,
458            module_name: "spi".to_string(),
459            pin_name: "mosi".to_string(),
460        };
461
462        assert_eq!(assignment.pin, 5);
463        assert_eq!(assignment.module_name, "spi");
464        assert_eq!(assignment.pin_name, "mosi");
465    }
466}