lcd1602-driver 0.3.0

A LCD1602 driver
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
//! [`Lcd`] is the main driver for LCD1602

use embedded_hal::delay::DelayNs;

use crate::{
    command::{Font, LineMode, MoveDirection, RAMType, ShiftType, State},
    state::LcdState,
};

mod init;

pub use init::Config;

mod impls;

/// [`Lcd`] is the main struct to drive a LCD1602
pub struct Lcd<'a, 'b, Sender, Delayer, const READABLE: bool>
where
    Delayer: DelayNs,
{
    sender: &'a mut Sender,
    delayer: &'b mut Delayer,
    state: LcdState,
    poll_interval_us: u32,
}

/// [`CGRAMGraph`] represent a graph inside a CGRAM, that can be draw on screen
#[derive(Default)]
pub struct CGRAMGraph {
    /// This is the upper part of the graph.  
    /// It shows for both 5x8 and 5x11 Font
    pub upper: [u8; 8],
    /// This is the lower part of the graph.  
    /// It shows only 5x11 Font.
    ///
    /// Although this part is only 3 u8, but in LCD hardware, it will occupied a full graph point.
    pub lower: Option<[u8; 3]>,
}

/// All basic command to control LCD1602
#[allow(missing_docs)]
pub trait Basic {
    fn write_byte_to_cur(&mut self, byte: u8);

    /// Note:  
    /// We allow write Font5x11 graph even under Font5x8 mode.
    fn write_graph_to_cgram(&mut self, index: u8, graph_data: &CGRAMGraph);

    fn clean_display(&mut self);

    fn return_home(&mut self);

    fn set_line_mode(&mut self, line: LineMode);

    fn get_line_mode(&self) -> LineMode;

    fn set_font(&mut self, font: Font);

    fn get_font(&self) -> Font;

    fn set_display_state(&mut self, display: State);

    fn get_display_state(&self) -> State;

    fn set_cursor_state(&mut self, cursor: State);

    fn get_cursor_state(&self) -> State;

    fn get_ram_type(&self) -> RAMType;

    fn set_cursor_blink_state(&mut self, blink: State);

    fn get_cursor_blink_state(&self) -> State;

    fn set_direction(&mut self, dir: MoveDirection);

    fn get_direction(&self) -> MoveDirection;

    fn set_shift_type(&mut self, shift: ShiftType);

    fn get_shift_type(&self) -> ShiftType;

    fn set_cursor_pos(&mut self, pos: (u8, u8));

    fn set_cgram_addr(&mut self, addr: u8);

    fn get_cursor_pos(&self) -> (u8, u8);

    fn shift_cursor_or_display(&mut self, shift_type: ShiftType, dir: MoveDirection);

    fn get_display_offset(&self) -> u8;

    fn set_poll_interval(&mut self, interval_us: u32);

    fn get_poll_interval_us(&self) -> u32;

    fn get_line_capacity(&self) -> u8;

    /// Note:
    /// Due to driver implementation, this function may have actual effect, or not
    fn set_backlight(&mut self, backlight: State);

    fn get_backlight(&self) -> State;

    fn calculate_pos_by_offset(&self, start: (u8, u8), offset: (i8, i8)) -> (u8, u8);

    /// Wait for specified milliseconds
    fn delay_ms(&mut self, ms: u32);

    /// Wait for specified microseconds
    fn delay_us(&mut self, us: u32);
}

/// Basic read functions for the LCD
#[allow(missing_docs)]
pub trait BasicRead: Basic {
    fn read_u8_from_cur(&mut self) -> u8;
}

/// Useful command to control LCD1602
pub trait Ext: Basic {
    /// toggle entire display on and off (it doesn't toggle backlight)
    fn toggle_display(&mut self) {
        match self.get_display_state() {
            State::Off => self.set_display_state(State::On),
            State::On => self.set_display_state(State::Off),
        }
    }

    /// write [char] to current position
    /// In default implementation, character only support
    /// from ASCII 0x20 (white space) to ASCII 0x7D (`}`)
    fn write_char_to_cur(&mut self, char: char) {
        assert!(
            self.get_ram_type() == RAMType::DDRam,
            "Current in CGRAM, use .set_cursor_pos() to change to DDRAM"
        );

        // map char out side of ASCII 0x20 and 0x7D to full rectangle
        let out_byte = match char.is_ascii() {
            true if (0x20 <= char as u8) && (char as u8 <= 0x7D) => char as u8,
            _ => 0xFF,
        };

        self.write_byte_to_cur(out_byte);
    }

    /// write string to current position
    fn write_str_to_cur(&mut self, str: &str) {
        str.chars().for_each(|char| self.write_char_to_cur(char));
    }

    /// write a byte to specific position
    fn write_byte_to_pos(&mut self, byte: u8, pos: (u8, u8)) {
        self.set_cursor_pos(pos);

        self.write_byte_to_cur(byte);
    }

    /// write a char to specific position
    fn write_char_to_pos(&mut self, char: char, pos: (u8, u8)) {
        self.set_cursor_pos(pos);
        self.write_char_to_cur(char);
    }

    /// write string to specific position
    fn write_str_to_pos(&mut self, str: &str, pos: (u8, u8)) {
        self.set_cursor_pos(pos);
        self.write_str_to_cur(str);
    }

    /// write custom graph to current position
    ///
    /// If you write 5x11 Font graph, but only want to access upper part of the graph in 5x8 Font mode,  
    /// you will need to shift `index` one bit left to get correct graph.
    fn write_graph_to_cur(&mut self, index: u8) {
        match self.get_font() {
            Font::Font5x8 => assert!(index < 8, "index too big, should less than 8 for 5x8 Font"),
            Font::Font5x11 => assert!(index < 4, "index too big, should less than 4 for 5x11 Font"),
        }

        self.write_byte_to_cur(match self.get_font() {
            Font::Font5x8 => index,
            Font::Font5x11 => index << 1,
        });
    }

    /// write custom graph to specific position
    ///
    /// If you write 5x11 Font graph, but only want to access upper part of the graph in 5x8 Font mode,  
    /// you will need to shift `index` one bit left to get correct graph.
    fn write_graph_to_pos(&mut self, index: u8, pos: (u8, u8)) {
        match self.get_font() {
            Font::Font5x8 => assert!(index < 8, "Only 8 graphs allowed in CGRAM for 5x8 Font"),
            Font::Font5x11 => assert!(index < 4, "Only 4 graphs allowed in CGRAM for 5x11 Font"),
        }

        self.write_byte_to_pos(
            match self.get_font() {
                Font::Font5x8 => index,
                Font::Font5x11 => index << 1,
            },
            pos,
        );
    }

    /// change cursor position with relative offset
    fn offset_cursor_pos(&mut self, offset: (i8, i8)) {
        self.set_cursor_pos(self.calculate_pos_by_offset(self.get_cursor_pos(), offset));
    }
}

/// Useful commands to read data from the LCD
pub trait ExtRead: Ext + BasicRead {
    /// read a byte from specific position
    fn read_byte_from_pos(&mut self, pos: (u8, u8)) -> u8 {
        let original_pos = self.get_cursor_pos();
        self.set_cursor_pos(pos);
        let data = self.read_u8_from_cur();
        self.set_cursor_pos(original_pos);
        data
    }

    /// read custom graph data from CGRAM
    ///
    /// We always read graph data as Font 5x11 mode,  
    /// user will take response to check whether the second part is needed.
    fn read_graph_from_cgram(&mut self, index: u8) -> CGRAMGraph {
        match self.get_font() {
            Font::Font5x8 => assert!(index < 8, "index too big, should less than 8 for 5x8 Font"),
            Font::Font5x11 => assert!(index < 4, "index too big, should less than 4 for 5x11 Font"),
        }

        // convert index to cgram address
        self.set_cgram_addr(
            index
                .checked_shl(match self.get_font() {
                    Font::Font5x8 => 3,
                    Font::Font5x11 => 4,
                })
                .unwrap(),
        );

        let mut graph = CGRAMGraph::default();

        graph
            .upper
            .iter_mut()
            .for_each(|line| *line = self.read_u8_from_cur());

        graph.lower = Some([0u8; 3]);

        graph
            .lower
            .as_mut()
            .unwrap()
            .iter_mut()
            .for_each(|line| *line = self.read_u8_from_cur());

        graph
    }
}

/// The style of the offset display window
pub enum MoveStyle {
    /// Always move to left
    ForceMoveLeft,
    /// Always move to right
    ForceMoveRight,
    /// Top left of display window won't cross display boundary
    NoCrossBoundary,
    /// Automatic find the shortest path
    Shortest,
}

/// The flip style of split flap display
pub enum FlipStyle {
    /// Flip first character to target character, then flip next one
    Sequential,
    /// Flip all characters at once, automatically stop when the characters reach the target one
    Simultaneous,
}

/// Show animation on LCD1602
pub trait Anim: Ext {
    /// Make the entire screen blink
    ///
    /// # Arguments
    ///
    /// * `count` - the number of times to blink the screen. If the value is `0`, the screen will blink endless.
    /// * `interval_us` - The interval (in microseconds) at which the screen state changes
    fn full_display_blink(&mut self, count: u32, interval_us: u32) {
        match count == 0 {
            true => loop {
                self.delay_us(interval_us);
                self.toggle_display();
            },
            false => {
                (0..count * 2).for_each(|_| {
                    self.delay_us(interval_us);
                    self.toggle_display();
                });
            }
        }
    }

    /// Typewriter-style display
    ///
    /// # Arguments
    ///
    /// * `str` - string to display
    /// * `delay_us` - The interval (in microseconds) of each character show up
    fn typewriter_write(&mut self, str: &str, delay_us: u32) {
        str.chars().for_each(|char| {
            self.delay_us(delay_us);
            self.write_char_to_cur(char);
        })
    }

    /// Split-Flap-style display
    ///
    /// # Arguments
    ///
    /// * `str` - string to display
    /// * `fs` - flip style, see [FlipStyle]
    /// * `max_flip_cnt` - The maximum number of times to flip the display before reaching the target character
    /// * `per_flip_delay_us` - The delay (in microseconds) between each flip. It is recommended to set this value to at least `100_000`.
    /// * `per_char_flip_delay_us` - Used in [FlipStyle::Sequential] mode, this is the time (in microseconds) to wait between flipping each character
    fn split_flap_write(
        &mut self,
        str: &str,
        fs: FlipStyle,
        max_flip_cnt: Option<u8>,
        per_flip_delay_us: u32,
        per_char_flip_delay_us: Option<u32>,
    ) {
        // Checking if all characters are suitable for split flap effect (should in ASCII 0x20 to 0x7D)
        let test_result = str
            .chars()
            .all(|char| char.is_ascii() && (0x20 <= char as u8) && (char as u8 <= 0x7D));

        assert!(test_result, "Currently only support ASCII 0x20 to 0x7D");

        let mut cursor_state_changed = false;

        // turn off cursor, since it will always shift to next position
        if self.get_cursor_state() != State::Off {
            self.set_cursor_state(State::Off);
            cursor_state_changed = true;
        }

        match fs {
            FlipStyle::Sequential => {
                assert!(
                    per_char_flip_delay_us.is_some(),
                    "Should set some per char delay in Sequential Mode"
                );
                str.chars().for_each(|char| {
                    let cur_byte = char as u8;

                    let flap_start_byte = match max_flip_cnt {
                        None => 0x20,
                        Some(max_flip_cnt) => {
                            if cur_byte - max_flip_cnt < 0x20 {
                                0x20
                            } else {
                                cur_byte - max_flip_cnt
                            }
                        }
                    };

                    let cur_pos = self.get_cursor_pos();

                    self.delay_us(per_char_flip_delay_us.unwrap());
                    (flap_start_byte..=cur_byte).for_each(|byte| {
                        self.delay_us(per_flip_delay_us);
                        self.write_byte_to_pos(byte, cur_pos);
                    });
                })
            }
            FlipStyle::Simultaneous => {
                let min_char_byte = str.chars().min().unwrap() as u8;
                let max_char_byte = str.chars().max().unwrap() as u8;
                let str_len = str.chars().count();

                let flap_start_byte = match max_flip_cnt {
                    None => 0x20,
                    Some(max_flip_cnt) => {
                        if max_char_byte - min_char_byte > max_flip_cnt {
                            min_char_byte
                        } else if max_char_byte - max_flip_cnt < 0x20 {
                            0x20
                        } else {
                            max_char_byte - max_flip_cnt
                        }
                    }
                };

                let start_pos = self.get_cursor_pos();

                (flap_start_byte..=max_char_byte).for_each(|cur_byte| {
                    self.delay_us(per_flip_delay_us);

                    str.char_indices()
                        .filter(|&(_, target_char)| cur_byte <= target_char as u8) // filter character that still need to flip
                        .for_each(|(index, _)| {
                            let cur_pos = match self.get_direction() {
                                MoveDirection::RightToLeft => {
                                    self.calculate_pos_by_offset(start_pos, (-(index as i8), 0))
                                }
                                MoveDirection::LeftToRight => {
                                    self.calculate_pos_by_offset(start_pos, (index as i8, 0))
                                }
                            };
                            self.write_byte_to_pos(cur_byte, cur_pos);
                        });
                });

                // after the flip finished, we cannot ensure cursor position (since .filter() method)
                // move cursor to string end
                let end_pos = match self.get_direction() {
                    MoveDirection::RightToLeft => {
                        self.calculate_pos_by_offset(start_pos, (-((str_len) as i8), 0))
                    }
                    MoveDirection::LeftToRight => {
                        self.calculate_pos_by_offset(start_pos, ((str_len as i8), 0))
                    }
                };
                self.set_cursor_pos(end_pos);
            }
        }

        // remember to restore cursor state
        if cursor_state_changed {
            self.set_cursor_state(State::On);
        }
    }

    /// Move the display window to the specified position (measured from the upper-left corner of the display)
    ///
    /// # Arguments
    ///
    /// * `target_pos` - The target position of the display window
    /// * `ms` - The style of movement, see [MoveStyle]
    /// * `display_state_when_shift` - Whether to turn off the screen during the move
    /// * `delay_us_per_step` - The delay (in microseconds) between each step of the move
    fn shift_display_to_pos(
        &mut self,
        target_pos: u8,
        ms: MoveStyle,
        display_state_when_shift: State,
        delay_us_per_step: u32,
    ) {
        let before_pos = self.get_display_offset();

        // if target position is current position, just return
        if before_pos == target_pos {
            return;
        }

        let line_capacity = self.get_line_capacity();

        let before_state = self.get_display_state();

        self.set_display_state(display_state_when_shift);

        // calculate offset distance
        let (distance, direction) = match ms {
            MoveStyle::ForceMoveLeft => {
                if target_pos < before_pos {
                    (before_pos - target_pos, MoveDirection::RightToLeft)
                } else {
                    (
                        line_capacity - (target_pos - before_pos),
                        MoveDirection::RightToLeft,
                    )
                }
            }

            MoveStyle::ForceMoveRight => {
                if target_pos > before_pos {
                    (target_pos - before_pos, MoveDirection::LeftToRight)
                } else {
                    (
                        line_capacity - (before_pos - target_pos),
                        MoveDirection::LeftToRight,
                    )
                }
            }

            MoveStyle::NoCrossBoundary => {
                if target_pos > before_pos {
                    (target_pos - before_pos, MoveDirection::LeftToRight)
                } else {
                    (before_pos - target_pos, MoveDirection::RightToLeft)
                }
            }

            MoveStyle::Shortest => {
                if target_pos > before_pos {
                    if target_pos - before_pos <= line_capacity / 2 {
                        (target_pos - before_pos, MoveDirection::LeftToRight)
                    } else {
                        (
                            line_capacity - (target_pos - before_pos),
                            MoveDirection::RightToLeft,
                        )
                    }
                } else {
                    #[allow(clippy::collapsible_else_if)]
                    if before_pos - target_pos <= line_capacity / 2 {
                        (before_pos - target_pos, MoveDirection::RightToLeft)
                    } else {
                        (
                            line_capacity - (before_pos - target_pos),
                            MoveDirection::LeftToRight,
                        )
                    }
                }
            }
        };

        (0..(distance)).for_each(|_| {
            self.delay_us(delay_us_per_step);
            self.shift_cursor_or_display(ShiftType::CursorAndDisplay, direction);
        });

        // restore original display state
        self.set_display_state(before_state);
    }
}