pokeys_lib/
lcd.rs

1//! LCD display support
2
3use crate::device::PoKeysDevice;
4use crate::error::{PoKeysError, Result};
5use crate::types::LcdMode;
6use serde::{Deserialize, Serialize};
7
8/// LCD display data structure
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct LcdData {
11    pub configuration: u8,
12    pub rows: u8,
13    pub columns: u8,
14    pub row_refresh_flags: u8,
15    pub line1: [u8; 20],
16    pub line2: [u8; 20],
17    pub line3: [u8; 20],
18    pub line4: [u8; 20],
19    pub custom_characters: [[u8; 8]; 8],
20}
21
22impl LcdData {
23    pub fn new() -> Self {
24        Self {
25            configuration: 0,
26            rows: 0,
27            columns: 0,
28            row_refresh_flags: 0,
29            line1: [0; 20],
30            line2: [0; 20],
31            line3: [0; 20],
32            line4: [0; 20],
33            custom_characters: [[0; 8]; 8],
34        }
35    }
36
37    pub fn is_enabled(&self) -> bool {
38        self.configuration != 0
39    }
40
41    pub fn get_line(&self, line: usize) -> Option<&[u8; 20]> {
42        match line {
43            1 => Some(&self.line1),
44            2 => Some(&self.line2),
45            3 => Some(&self.line3),
46            4 => Some(&self.line4),
47            _ => None,
48        }
49    }
50
51    pub fn get_line_mut(&mut self, line: usize) -> Option<&mut [u8; 20]> {
52        match line {
53            1 => Some(&mut self.line1),
54            2 => Some(&mut self.line2),
55            3 => Some(&mut self.line3),
56            4 => Some(&mut self.line4),
57            _ => None,
58        }
59    }
60
61    pub fn set_line_text(&mut self, line: usize, text: &str) -> Result<()> {
62        if !(1..=4).contains(&line) {
63            return Err(PoKeysError::Parameter("Invalid line number".to_string()));
64        }
65
66        if text.len() > 20 {
67            return Err(PoKeysError::Parameter(
68                "Text too long for LCD line".to_string(),
69            ));
70        }
71
72        let line_buffer = self.get_line_mut(line).unwrap();
73        line_buffer.fill(0);
74
75        let text_bytes = text.as_bytes();
76        line_buffer[..text_bytes.len()].copy_from_slice(text_bytes);
77
78        // Set refresh flag for this line
79        self.row_refresh_flags |= 1 << (line - 1);
80
81        Ok(())
82    }
83
84    pub fn get_line_text(&self, line: usize) -> Result<String> {
85        if !(1..=4).contains(&line) {
86            return Err(PoKeysError::Parameter("Invalid line number".to_string()));
87        }
88
89        let line_buffer = self.get_line(line).unwrap();
90
91        // Find the end of the string (first null byte)
92        let end = line_buffer.iter().position(|&b| b == 0).unwrap_or(20);
93
94        String::from_utf8(line_buffer[..end].to_vec())
95            .map_err(|_| PoKeysError::Protocol("Invalid UTF-8 in LCD text".to_string()))
96    }
97
98    pub fn clear_line(&mut self, line: usize) -> Result<()> {
99        self.set_line_text(line, "")
100    }
101
102    pub fn clear_all(&mut self) {
103        self.line1.fill(0);
104        self.line2.fill(0);
105        self.line3.fill(0);
106        self.line4.fill(0);
107        self.row_refresh_flags = 0x0F; // Refresh all lines
108    }
109
110    pub fn set_custom_character(&mut self, char_index: usize, pattern: &[u8; 8]) -> Result<()> {
111        if char_index >= 8 {
112            return Err(PoKeysError::Parameter(
113                "Invalid custom character index".to_string(),
114            ));
115        }
116
117        self.custom_characters[char_index] = *pattern;
118        Ok(())
119    }
120
121    pub fn get_custom_character(&self, char_index: usize) -> Result<[u8; 8]> {
122        if char_index >= 8 {
123            return Err(PoKeysError::Parameter(
124                "Invalid custom character index".to_string(),
125            ));
126        }
127
128        Ok(self.custom_characters[char_index])
129    }
130}
131
132impl Default for LcdData {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl PoKeysDevice {
139    /// Configure LCD display
140    pub fn configure_lcd(&mut self, rows: u8, columns: u8, mode: LcdMode) -> Result<()> {
141        if rows > 4 || columns > 20 {
142            return Err(PoKeysError::Parameter("LCD size not supported".to_string()));
143        }
144
145        self.lcd.configuration = match mode {
146            LcdMode::Direct => 1,
147            LcdMode::Buffered => 2,
148        };
149        self.lcd.rows = rows;
150        self.lcd.columns = columns;
151
152        // Send LCD configuration to device
153        self.send_request(0x70, self.lcd.configuration, rows, columns, 0)?;
154        Ok(())
155    }
156
157    /// Enable or disable LCD
158    pub fn enable_lcd(&mut self, enable: bool) -> Result<()> {
159        if enable {
160            if self.lcd.configuration == 0 {
161                // Use default configuration if not set
162                self.lcd.configuration = 1; // Direct mode
163                self.lcd.rows = 2;
164                self.lcd.columns = 16;
165            }
166        } else {
167            self.lcd.configuration = 0;
168        }
169
170        self.send_request(
171            0x70,
172            self.lcd.configuration,
173            self.lcd.rows,
174            self.lcd.columns,
175            0,
176        )?;
177        Ok(())
178    }
179
180    /// Write text to LCD line
181    pub fn lcd_write_line(&mut self, line: usize, text: &str) -> Result<()> {
182        self.lcd.set_line_text(line, text)?;
183
184        // Send line data to device
185        self.send_lcd_line_data(line)?;
186        Ok(())
187    }
188
189    /// Read text from LCD line
190    pub fn lcd_read_line(&self, line: usize) -> Result<String> {
191        self.lcd.get_line_text(line)
192    }
193
194    /// Clear LCD line
195    pub fn lcd_clear_line(&mut self, line: usize) -> Result<()> {
196        self.lcd.clear_line(line)?;
197        self.send_lcd_line_data(line)?;
198        Ok(())
199    }
200
201    /// Clear entire LCD display
202    pub fn lcd_clear_all(&mut self) -> Result<()> {
203        self.lcd.clear_all();
204
205        // Send all line data to device
206        for line in 1..=self.lcd.rows {
207            self.send_lcd_line_data(line as usize)?;
208        }
209
210        Ok(())
211    }
212
213    /// Write text at specific position
214    pub fn lcd_write_at(&mut self, line: usize, column: usize, text: &str) -> Result<()> {
215        if line < 1 || line > self.lcd.rows as usize {
216            return Err(PoKeysError::Parameter("Invalid line number".to_string()));
217        }
218
219        if column >= self.lcd.columns as usize {
220            return Err(PoKeysError::Parameter("Invalid column number".to_string()));
221        }
222
223        // Get current line content
224        let mut current_text = self.lcd.get_line_text(line).unwrap_or_default();
225
226        // Pad with spaces if necessary
227        while current_text.len() < column {
228            current_text.push(' ');
229        }
230
231        // Replace text at position
232        let mut chars: Vec<char> = current_text.chars().collect();
233        let new_chars: Vec<char> = text.chars().collect();
234
235        for (i, &ch) in new_chars.iter().enumerate() {
236            if column + i < self.lcd.columns as usize {
237                if column + i < chars.len() {
238                    chars[column + i] = ch;
239                } else {
240                    chars.push(ch);
241                }
242            }
243        }
244
245        let new_text: String = chars.into_iter().collect();
246        self.lcd_write_line(line, &new_text)
247    }
248
249    /// Set custom character pattern
250    pub fn lcd_set_custom_character(&mut self, char_index: usize, pattern: &[u8; 8]) -> Result<()> {
251        self.lcd.set_custom_character(char_index, pattern)?;
252
253        // Send custom character data to device
254        self.send_request(0x75, char_index as u8, pattern[0], pattern[1], pattern[2])?;
255
256        self.send_request(0x76, char_index as u8, pattern[3], pattern[4], pattern[5])?;
257
258        self.send_request(0x77, char_index as u8, pattern[6], pattern[7], 0)?;
259
260        Ok(())
261    }
262
263    /// Update LCD display (refresh all changed lines)
264    pub fn lcd_update(&mut self) -> Result<()> {
265        for line in 1..=self.lcd.rows {
266            if (self.lcd.row_refresh_flags & (1 << (line - 1))) != 0 {
267                self.send_lcd_line_data(line as usize)?;
268            }
269        }
270
271        self.lcd.row_refresh_flags = 0;
272        Ok(())
273    }
274
275    /// Send LCD line data to device
276    fn send_lcd_line_data(&mut self, line: usize) -> Result<()> {
277        if !(1..=4).contains(&line) {
278            return Err(PoKeysError::Parameter("Invalid line number".to_string()));
279        }
280
281        // Copy line data to avoid borrow checker issues
282        let line_data = *self.lcd.get_line(line).unwrap();
283
284        // Send line data in chunks (protocol limitation)
285        self.send_request(0x71, line as u8, line_data[0], line_data[1], line_data[2])?;
286
287        self.send_request(0x72, line as u8, line_data[3], line_data[4], line_data[5])?;
288
289        self.send_request(0x73, line as u8, line_data[6], line_data[7], line_data[8])?;
290
291        self.send_request(0x74, line as u8, line_data[9], line_data[10], line_data[11])?;
292
293        // Send remaining characters if needed
294        if self.lcd.columns > 12 {
295            // Additional requests for longer displays
296            // Implementation would continue for up to 20 characters
297        }
298
299        Ok(())
300    }
301}
302
303// Convenience functions for common LCD operations
304
305/// Display a simple message on LCD
306pub fn lcd_display_message(device: &mut PoKeysDevice, message: &str) -> Result<()> {
307    device.lcd_clear_all()?;
308
309    // Split message into lines
310    let lines: Vec<&str> = message.lines().collect();
311
312    for (i, line) in lines.iter().enumerate().take(device.lcd.rows as usize) {
313        device.lcd_write_line(i + 1, line)?;
314    }
315
316    Ok(())
317}
318
319/// Display a two-line message
320pub fn lcd_display_two_lines(device: &mut PoKeysDevice, line1: &str, line2: &str) -> Result<()> {
321    device.lcd_clear_all()?;
322    device.lcd_write_line(1, line1)?;
323    device.lcd_write_line(2, line2)?;
324    Ok(())
325}
326
327/// Create a progress bar on LCD
328pub fn lcd_progress_bar(
329    device: &mut PoKeysDevice,
330    line: usize,
331    progress: f32,
332    width: usize,
333) -> Result<()> {
334    if !(0.0..=1.0).contains(&progress) {
335        return Err(PoKeysError::Parameter(
336            "Progress must be between 0.0 and 1.0".to_string(),
337        ));
338    }
339
340    let filled_chars = (progress * width as f32) as usize;
341    let mut bar = String::new();
342
343    bar.push('[');
344    for i in 0..width {
345        if i < filled_chars {
346            bar.push('█');
347        } else {
348            bar.push(' ');
349        }
350    }
351    bar.push(']');
352
353    device.lcd_write_line(line, &bar)
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_lcd_data_creation() {
362        let lcd = LcdData::new();
363        assert!(!lcd.is_enabled());
364        assert_eq!(lcd.rows, 0);
365        assert_eq!(lcd.columns, 0);
366    }
367
368    #[test]
369    fn test_lcd_line_operations() {
370        let mut lcd = LcdData::new();
371
372        assert!(lcd.set_line_text(1, "Hello").is_ok());
373        assert_eq!(lcd.get_line_text(1).unwrap(), "Hello");
374
375        assert!(lcd.clear_line(1).is_ok());
376        assert_eq!(lcd.get_line_text(1).unwrap(), "");
377
378        // Test invalid line numbers
379        assert!(lcd.set_line_text(0, "Test").is_err());
380        assert!(lcd.set_line_text(5, "Test").is_err());
381    }
382
383    #[test]
384    fn test_lcd_text_length_limit() {
385        let mut lcd = LcdData::new();
386
387        // Text that's exactly 20 characters should work
388        let text_20 = "12345678901234567890";
389        assert!(lcd.set_line_text(1, text_20).is_ok());
390
391        // Text that's longer than 20 characters should fail
392        let text_21 = "123456789012345678901";
393        assert!(lcd.set_line_text(1, text_21).is_err());
394    }
395
396    #[test]
397    fn test_custom_characters() {
398        let mut lcd = LcdData::new();
399        let pattern = [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F];
400
401        assert!(lcd.set_custom_character(0, &pattern).is_ok());
402        assert_eq!(lcd.get_custom_character(0).unwrap(), pattern);
403
404        // Test invalid character index
405        assert!(lcd.set_custom_character(8, &pattern).is_err());
406    }
407
408    #[test]
409    fn test_progress_bar_generation() {
410        // Test progress bar string generation logic
411        let width = 10;
412        let progress = 0.5;
413        let filled_chars = (progress * width as f32) as usize;
414
415        assert_eq!(filled_chars, 5);
416
417        let mut bar = String::new();
418        bar.push('[');
419        for i in 0..width {
420            if i < filled_chars {
421                bar.push('█');
422            } else {
423                bar.push(' ');
424            }
425        }
426        bar.push(']');
427
428        assert_eq!(bar, "[█████     ]");
429    }
430}