i2c_character_display/
lib.rs

1//! This Rust `embedded-hal`-based library is a simple way to control a character display that has either a [HD44780](https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller)
2//! or [AiP31068](https://support.newhavendisplay.com/hc/en-us/articles/4414486901783--AiP31068) controller with an I2C interface
3//! in an embedded, `no_std` environment. A number of I2C interfaces are supported:
4//!
5//! - **[Adafruit I2C/SPI LCD Backpack](https://www.adafruit.com/product/292)** - This is a simple I2C adapter for HD44780 character displays that can be used with either I2C
6//!   or SPI. It is available from Adafruit and other retailers. This library only supports the I2C interface of this adapter.
7//! - **PCF8574-based I2C adapter** - These adapters are ubiquitous on eBay and AliExpress and have no clear branding. Furthermore, some HD44780-based character
8//!   display makers, such as [Surenoo](https://www.surenoo.com), integrate a PCF8574T directly on the display board enabling I2C connections without a seperate adapter.
9//!   The most common pin wiring uses 4 data pins and 3 control pins. Most models have the display's 4-bit mode data pins connected to P4-P7 of the PCF8574.
10//!   This library supports that configuration, though it would be straightforward to add support for other pin configurations.
11//! - **AiP31068** - This is a character display controller with a built-in I2C support. The command set is similar to the HD44780, but the controller
12//!   operates in 8-bit mode and is initialized differently.  Examples of displays that use this controller include the [Surenoo SLC1602O](https://www.surenoo.com/products/8109143).
13//! - **ST7032i** - This is an I2C character display controller used with LCD displays. It is similar to the HD44780, but with some differences in the command set.
14//!   Examples of displays that use this controller include the [Surenoo SLC1602K3](https://www.surenoo.com/collections/81733622/products/8131705).
15//!
16//! Key features include:
17//! - Convenient high-level API for controlling many types of character display
18//! - Support for custom characters
19//! - Backlight control on hardwarware that supports it
20//! - `core::fmt::Write` implementation for easy use with the `write!` macro
21//! - Compatible with the `embedded-hal` traits v1.0 and later
22//! - Support for character displays that uses multiple HD44780 drivers, such as the 40x4 display
23//! - Optional support for the `defmt` and `ufmt` logging frameworks
24//! - Optional support for reading from the display on controllers and adapters that support it
25//!
26//! ## Usage
27//! Add this to your `Cargo.toml`:
28//! ```toml
29//! [dependencies]
30//! i2c-character-display = { version = "0.4", features = ["defmt"] }
31//! ```
32//! The `features = ["defmt"]` line is optional and enables the `defmt` feature, which allows the library's errors to be used with the `defmt` logging
33//! framework. Another optional feature is `features = ["ufmt"]`, which enables the `ufmt` feature, allowing the `uwriteln!` and `uwrite!` macros to be used.
34//!
35//! Then select the appropriate adapter for your display:
36//! ```rust
37//! use i2c_character_display::{AdafruitLCDBackpack, CharacterDisplayPCF8574T, LcdDisplayType};
38//! use embedded_hal::delay::DelayMs;
39//! use embedded_hal::i2c::I2c;
40//!
41//! // board setup
42//! let i2c = ...; // I2C peripheral
43//! let delay = ...; // DelayMs implementation
44//!
45//! // It is recommended that the `i2c` object be wrapped in an `embedded_hal_bus::i2c::CriticalSectionDevice` so that it can be shared between
46//! // multiple peripherals.
47//!
48//! // Adafruit backpack for a single HD44780 controller
49//! let mut lcd = AdafruitLCDBackpack::new(i2c, LcdDisplayType::Lcd16x2, delay);
50//! // PCF8574T adapter for a single HD44780 controller
51//! let mut lcd = CharacterDisplayPCF8574T::new(i2c, LcdDisplayType::Lcd16x2, delay);
52//! // Character display with dual HD44780 controllers using a single PCF8574T I2C adapter
53//! let mut lcd = CharacterDisplayDualHD44780::new(i2c, LcdDisplayType::Lcd40x4, delay);
54//! // Character display with the AiP31068 controller
55//! let mut lcd = CharacterDisplayAIP31068::new(i2c, LcdDisplayType::Lcd16x2, delay);
56//! // Character display with the ST7032i controller
57//! let mut lcd = CharacterDisplayST7032i::new(i2c, LcdDisplayType::Lcd16x2, delay);
58//! ```
59//! When creating the display object, you can choose the display type from the `LcdDisplayType` enum. The display type should match the physical
60//! display you are using. This display type configures the number of rows and columns, and the internal row offsets for the display.
61//!
62//! Initialize the display:
63//! ```rust
64//! if let Err(e) = lcd.init() {
65//!    panic!("Error initializing LCD: {}", e);
66//! }
67//! ```
68//! Use the display:
69//! ```rust
70//! // set up the display
71//! lcd.backlight(true)?.clear()?.home()?;
72//! // print a message
73//! lcd.print("Hello, world!")?;
74//! // can also use the `core::fmt::write!` macro
75//! use core::fmt::Write;
76//!
77//! write!(lcd, "Hello, world!")?;
78//! ```
79//! The optional `ufmt` feature enables the `ufmt` crate, which allows the `uwriteln!` and `uwrite!` macros to be used with the display:
80//! ```rust
81//! use ufmt::uwriteln;
82//!
83//! uwriteln!(lcd, "Hello, world!")?;
84//! ```
85//!
86//! The various methods for controlling the LCD are also available. Each returns a `Result` that wraps the display object in `Ok()`, allowing for easy chaining
87//! of commands. For example:
88//! ```rust
89//! lcd.backlight(true)?.clear()?.home()?.print("Hello, world!")?;
90//! ```
91//! ### Reading from the display
92//! Some I2C adapters support reading data from the HD44780 controller. For the I2C adapters that support it, the `read_device_data` method can be used to read
93//! from either the CGRAM or DDRAM at the current cursor position. The `read_address_counter` method can be used to read the address counter from the HD44780 controller.
94//! In both cases, the specific meaning of the data depends on the prior commands sent to the display. See the HD44780 datasheet for more information.
95//!
96//! ### Backlight control
97//! All HD44780 controllers support backlight control. The `backlight` method can be used to turn the backlight on or off. The AiP31068 controller does not support
98//! backlight control, and calling the `backlight` method with a AiP31068 controller will return an error.
99//!
100//! ### Multiple HD44780 controller character displays
101//! Some character displays, such as the 40x4 display, use two HD44780 controllers to drive the display. This library supports these displays by
102//! treating them as one logical display with multiple HD44780 controllers. The `CharacterDisplayDualHD44780` type is used to control these displays.
103//! Use the various methods to control the display as you would with a single HD44780 controller display. The `set_cursor` method sets the active HD44780
104//! controller device based on the row number you select.
105//!
106#![no_std]
107#![allow(dead_code, non_camel_case_types, non_upper_case_globals)]
108use core::{fmt::Display, marker::PhantomData};
109
110use embedded_hal::{delay::DelayNs, i2c};
111
112/// HD44780 based character display using a generic PCF8574T I2C adapter.
113pub type CharacterDisplayPCF8574T<I2C, DELAY> = BaseCharacterDisplay<
114    I2C,
115    DELAY,
116    crate::driver::hd44780::adapter::generic_pcf8574t::GenericPCF8574TAdapter<I2C, DELAY>,
117    crate::driver::hd44780::GenericHD44780PCF8574T<I2C, DELAY>,
118>;
119
120/// HD44780 based character display using an Adafruit I2C/SPI LCD backpack adapter.
121pub type AdafruitLCDBackpack<I2C, DELAY> = BaseCharacterDisplay<
122    I2C,
123    DELAY,
124    crate::driver::hd44780::adapter::adafruit_lcd_backpack::AdafruitLCDBackpackAdapter<I2C, DELAY>,
125    crate::driver::hd44780::AdafruitLCDBackpack<I2C, DELAY>,
126>;
127
128/// Character display using dual HD44780 I2C drivers connected using a generic PCF8574T I2C adapter with a pinout that
129/// has two enable pins, one for each HD44780 driver. Typically used for 40x4 character displays.
130pub type CharacterDisplayDualHD44780<I2C, DELAY> = BaseCharacterDisplay<
131    I2C,
132    DELAY,
133    crate::driver::hd44780::adapter::dual_controller_pcf8574t::DualHD44780_PCF8574TAdapter<
134        I2C,
135        DELAY,
136    >,
137    crate::driver::hd44780::DualHD44780PCF8574T<I2C, DELAY>,
138>;
139
140/// Character display using the AIP31068 controller with built-in I2C adapter.
141pub type CharacterDisplayAIP31068<I2C, DELAY> = BaseCharacterDisplay<
142    I2C,
143    DELAY,
144    crate::driver::aip31068::AIP31068<I2C, DELAY>,
145    crate::driver::standard::StandardCharacterDisplayHandler,
146>;
147
148/// Character display using the ST7032i controller with built-in I2C adapter.
149pub type CharacterDisplayST7032i<I2C, DELAY> = BaseCharacterDisplay<
150    I2C,
151    DELAY,
152    crate::driver::st7032i::ST7032i<I2C, DELAY>,
153    crate::driver::st7032i::ST7032iDisplayActions<I2C, DELAY>,
154>;
155
156// commands
157const LCD_CMD_CLEARDISPLAY: u8 = 0x01; //  Clear display, set cursor position to zero
158const LCD_CMD_RETURNHOME: u8 = 0x02; //  Set cursor position to zero
159const LCD_CMD_ENTRYMODESET: u8 = 0x04; //  Sets the entry mode
160const LCD_CMD_DISPLAYCONTROL: u8 = 0x08; //  Controls the display; does stuff like turning it off and on
161const LCD_CMD_CURSORSHIFT: u8 = 0x10; //  Lets you move the cursor
162const LCD_CMD_FUNCTIONSET: u8 = 0x20; //  Used to send the function to set to the display
163const LCD_CMD_SETCGRAMADDR: u8 = 0x40; //  Used to set the CGRAM (character generator RAM) with characters
164const LCD_CMD_SETDDRAMADDR: u8 = 0x80; //  Used to set the DDRAM (Display Data RAM)
165
166// flags for display entry mode
167const LCD_FLAG_ENTRYRIGHT: u8 = 0x00; //  Used to set text to flow from right to left
168const LCD_FLAG_ENTRYLEFT: u8 = 0x02; //  Uset to set text to flow from left to right
169const LCD_FLAG_ENTRYSHIFTINCREMENT: u8 = 0x01; //  Used to 'right justify' text from the cursor
170const LCD_FLAG_ENTRYSHIFTDECREMENT: u8 = 0x00; //  Used to 'left justify' text from the cursor
171
172// flags for display on/off control
173const LCD_FLAG_DISPLAYON: u8 = 0x04; //  Turns the display on
174const LCD_FLAG_DISPLAYOFF: u8 = 0x00; //  Turns the display off
175const LCD_FLAG_CURSORON: u8 = 0x02; //  Turns the cursor on
176const LCD_FLAG_CURSOROFF: u8 = 0x00; //  Turns the cursor off
177const LCD_FLAG_BLINKON: u8 = 0x01; //  Turns on the blinking cursor
178const LCD_FLAG_BLINKOFF: u8 = 0x00; //  Turns off the blinking cursor
179
180// flags for display/cursor shift
181const LCD_FLAG_DISPLAYMOVE: u8 = 0x08; //  Flag for moving the display
182const LCD_FLAG_CURSORMOVE: u8 = 0x00; //  Flag for moving the cursor
183const LCD_FLAG_MOVERIGHT: u8 = 0x04; //  Flag for moving right
184const LCD_FLAG_MOVELEFT: u8 = 0x00; //  Flag for moving left
185
186// flags for function set
187const LCD_FLAG_8BITMODE: u8 = 0x10; //  LCD 8 bit mode
188const LCD_FLAG_4BITMODE: u8 = 0x00; //  LCD 4 bit mode
189const LCD_FLAG_2LINE: u8 = 0x08; //  LCD 2 line mode
190const LCD_FLAG_1LINE: u8 = 0x00; //  LCD 1 line mode
191const LCD_FLAG_5x10_DOTS: u8 = 0x04; //  10 pixel high font mode
192const LCD_FLAG_5x8_DOTS: u8 = 0x00; //  8 pixel high font mode
193
194mod driver;
195
196const MAX_DEVICE_COUNT: usize = 2;
197
198#[derive(Debug, PartialEq, Copy, Clone)]
199/// Errors that can occur when using the LCD backpack
200pub enum CharacterDisplayError<I2C>
201where
202    I2C: i2c::I2c,
203{
204    /// I2C error returned from the underlying I2C implementation
205    I2cError(I2C::Error),
206    /// Row is out of range
207    RowOutOfRange,
208    /// Column is out of range
209    ColumnOutOfRange,
210    /// Formatting error
211    FormattingError(core::fmt::Error),
212    /// The discplay type is not compatible with specific adapter.
213    UnsupportedDisplayType,
214    /// The requested operation is not supported by the adapter or controller
215    UnsupportedOperation,
216    /// Read operation is not supported by the adapter
217    ReadNotSupported,
218    /// Internal error - bad device ID
219    BadDeviceId,
220    /// Internal error - buffer too small
221    BufferTooSmall,
222}
223
224impl<I2C> From<core::fmt::Error> for CharacterDisplayError<I2C>
225where
226    I2C: i2c::I2c,
227{
228    fn from(err: core::fmt::Error) -> Self {
229        CharacterDisplayError::FormattingError(err)
230    }
231}
232
233impl<I2C> From<&CharacterDisplayError<I2C>> for &'static str
234where
235    I2C: i2c::I2c,
236{
237    fn from(err: &CharacterDisplayError<I2C>) -> Self {
238        match err {
239            CharacterDisplayError::I2cError(_) => "I2C error",
240            CharacterDisplayError::RowOutOfRange => "Row out of range",
241            CharacterDisplayError::ColumnOutOfRange => "Column out of range",
242            CharacterDisplayError::FormattingError(_) => "Formatting error",
243            CharacterDisplayError::UnsupportedDisplayType => "Unsupported display type",
244            CharacterDisplayError::UnsupportedOperation => "Unsupported operation",
245            CharacterDisplayError::ReadNotSupported => "Read operation not supported",
246            CharacterDisplayError::BadDeviceId => "Bad device ID",
247            CharacterDisplayError::BufferTooSmall => "Buffer too small",
248        }
249    }
250}
251
252#[cfg(feature = "defmt")]
253impl<I2C> defmt::Format for CharacterDisplayError<I2C>
254where
255    I2C: i2c::I2c,
256{
257    fn format(&self, fmt: defmt::Formatter) {
258        let msg: &'static str = From::from(self);
259        defmt::write!(fmt, "{}", msg);
260    }
261}
262
263#[cfg(feature = "ufmt")]
264impl<I2C> ufmt::uDisplay for CharacterDisplayError<I2C>
265where
266    I2C: i2c::I2c,
267{
268    fn fmt<W>(&self, w: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
269    where
270        W: ufmt::uWrite + ?Sized,
271    {
272        let msg: &'static str = From::from(self);
273        ufmt::uwrite!(w, "{}", msg)
274    }
275}
276
277impl<I2C> Display for CharacterDisplayError<I2C>
278where
279    I2C: i2c::I2c,
280{
281    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        let msg: &'static str = From::from(self);
283        write!(f, "{}", msg)
284    }
285}
286
287#[derive(Debug, PartialEq, Clone, Copy)]
288/// The type of LCD display. This is used to determine the number of rows and columns, and the row offsets.
289pub enum LcdDisplayType {
290    /// 20x4 display
291    Lcd20x4,
292    /// 20x2 display
293    Lcd20x2,
294    /// 16x2 display
295    Lcd16x2,
296    /// 16x4 display
297    Lcd16x4,
298    /// 8x2 display
299    Lcd8x2,
300    /// 40x2 display
301    Lcd40x2,
302    /// 40x4 display. Should be used with a DualHD44780 adapter.
303    Lcd40x4,
304}
305
306impl From<&LcdDisplayType> for &'static str {
307    fn from(display_type: &LcdDisplayType) -> Self {
308        match display_type {
309            LcdDisplayType::Lcd20x4 => "20x4",
310            LcdDisplayType::Lcd20x2 => "20x2",
311            LcdDisplayType::Lcd16x2 => "16x2",
312            LcdDisplayType::Lcd16x4 => "16x4",
313            LcdDisplayType::Lcd8x2 => "8x2",
314            LcdDisplayType::Lcd40x2 => "40x2",
315            LcdDisplayType::Lcd40x4 => "40x4",
316        }
317    }
318}
319
320#[cfg(feature = "defmt")]
321impl defmt::Format for LcdDisplayType {
322    fn format(&self, fmt: defmt::Formatter) {
323        let msg: &'static str = From::from(self);
324        defmt::write!(fmt, "{}", msg);
325    }
326}
327
328#[cfg(feature = "ufmt")]
329impl ufmt::uDisplay for LcdDisplayType {
330    fn fmt<W>(&self, w: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
331    where
332        W: ufmt::uWrite + ?Sized,
333    {
334        let msg: &'static str = From::from(self);
335        ufmt::uwrite!(w, "{}", msg)
336    }
337}
338
339impl Display for LcdDisplayType {
340    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341        let msg: &'static str = From::from(self);
342        write!(f, "{}", msg)
343    }
344}
345
346impl LcdDisplayType {
347    /// Get the number of rows for the display type
348    const fn rows(&self) -> u8 {
349        match self {
350            LcdDisplayType::Lcd20x4 => 4,
351            LcdDisplayType::Lcd20x2 => 2,
352            LcdDisplayType::Lcd16x2 => 2,
353            LcdDisplayType::Lcd16x4 => 4,
354            LcdDisplayType::Lcd8x2 => 2,
355            LcdDisplayType::Lcd40x2 => 2,
356            LcdDisplayType::Lcd40x4 => 4,
357        }
358    }
359
360    /// Get the number of columns for the display type
361    const fn cols(&self) -> u8 {
362        match self {
363            LcdDisplayType::Lcd20x4 => 20,
364            LcdDisplayType::Lcd20x2 => 20,
365            LcdDisplayType::Lcd16x2 => 16,
366            LcdDisplayType::Lcd16x4 => 16,
367            LcdDisplayType::Lcd8x2 => 8,
368            LcdDisplayType::Lcd40x2 => 40,
369            LcdDisplayType::Lcd40x4 => 40,
370        }
371    }
372
373    /// Get the row offsets for the display type. This always returns an array of length 4.
374    /// For displays with less than 4 rows, the unused rows will be set to offsets offscreen.
375    const fn row_offsets(&self) -> [u8; 4] {
376        match self {
377            LcdDisplayType::Lcd20x4 => [0x00, 0x40, 0x14, 0x54],
378            LcdDisplayType::Lcd20x2 => [0x00, 0x40, 0x00, 0x40],
379            LcdDisplayType::Lcd16x2 => [0x00, 0x40, 0x10, 0x50],
380            LcdDisplayType::Lcd16x4 => [0x00, 0x40, 0x10, 0x50],
381            LcdDisplayType::Lcd8x2 => [0x00, 0x40, 0x00, 0x40],
382            LcdDisplayType::Lcd40x2 => [0x00, 0x40, 0x00, 0x40],
383            LcdDisplayType::Lcd40x4 => [0x00, 0x40, 0x00, 0x40],
384        }
385    }
386}
387
388pub struct DeviceSetupConfig<I2C, DELAY>
389where
390    I2C: i2c::I2c,
391    DELAY: DelayNs,
392{
393    lcd_type: LcdDisplayType,
394    i2c: I2C,
395    address: u8,
396    delay: DELAY,
397}
398
399pub struct BaseCharacterDisplay<I2C, DELAY, DEVICE, ACTIONS>
400where
401    I2C: i2c::I2c,
402    DELAY: DelayNs,
403    DEVICE: driver::DeviceHardwareTrait<I2C, DELAY>,
404    ACTIONS: driver::DisplayActionsTrait<I2C, DELAY, DEVICE>,
405{
406    device: DEVICE,
407    actions: ACTIONS,
408    _phantom_i2c: PhantomData<I2C>,
409    _phantom_delay: PhantomData<DELAY>,
410}
411
412impl<I2C, DELAY, DEVICE, ACTIONS> BaseCharacterDisplay<I2C, DELAY, DEVICE, ACTIONS>
413where
414    I2C: i2c::I2c,
415    DELAY: DelayNs,
416    DEVICE: driver::DeviceHardwareTrait<I2C, DELAY>,
417    ACTIONS: driver::DisplayActionsTrait<I2C, DELAY, DEVICE>,
418{
419    /// Create a new character display object with the default I2C address for the adapter.
420    pub fn new(i2c: I2C, lcd_type: LcdDisplayType, delay: DELAY) -> Self {
421        Self::new_with_address(i2c, DEVICE::default_i2c_address(), lcd_type, delay)
422    }
423
424    /// Create a new character display object with a specific I2C address for the adapter.
425    pub fn new_with_address(i2c: I2C, address: u8, lcd_type: LcdDisplayType, delay: DELAY) -> Self {
426        let config = DeviceSetupConfig {
427            lcd_type,
428            i2c,
429            address,
430            delay,
431        };
432        Self {
433            device: DEVICE::new(config),
434            actions: ACTIONS::default(),
435            _phantom_i2c: PhantomData,
436            _phantom_delay: PhantomData,
437        }
438    }
439
440    /// Initialize the display. This must be called before using the display.
441    pub fn init(&mut self) -> Result<(), CharacterDisplayError<I2C>> {
442        let (display_function, display_control, display_mode) = self.device.init()?;
443        self.actions
444            .init_display_state(display_function, display_control, display_mode)?;
445        Ok(())
446    }
447
448    /// returns a reference to the I2C peripheral. mostly needed for testing
449    fn i2c(&mut self) -> &mut I2C {
450        self.device.i2c()
451    }
452
453    /// returns the `LcdDisplayType` used to create the display
454    pub fn display_type(&self) -> LcdDisplayType {
455        self.device.lcd_type()
456    }
457
458    /// Supports the ability to read from the display.
459    pub fn supports_reads() -> bool {
460        DEVICE::supports_reads()
461    }
462
463    // /// Writes a data byte to the display. Normally users do not need to call this directly.
464    // /// For multiple devices, this writes the data to the currently active contoller device.
465    // fn write_data(&mut self, data: u8) -> Result<&mut Self, CharacterDisplayError<I2C>> {
466    //     self.device.write_data(&mut self.config, data)?;
467    //     Ok(self)
468    // }
469
470    /// Reads into the buffer data from the display device either the CGRAM or DDRAM at the current cursor position.
471    /// For multiple devices, this reads from the currently active device as set by the cursor position.
472    /// The amount of data read is determined by the length of the buffer.
473    /// Not all adapters support reads from the device. This will return an error if the adapter
474    /// does not support reads.
475    pub fn read_device_data(
476        &mut self,
477        buffer: &mut [u8],
478    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
479        self.actions.read_device_data(&mut self.device, buffer)?;
480
481        Ok(self)
482    }
483
484    /// Reads the address counter from the display device. The ready bit is masked off.
485    /// Not all adapters support reads from the device. This will return an error if the adapter
486    /// does not support reads.
487    pub fn read_address_counter(&mut self) -> Result<u8, CharacterDisplayError<I2C>> {
488        self.actions.read_address_counter(&mut self.device)
489    }
490
491    //--------------------------------------------------------------------------------------------------
492    // high level commands, for the user!
493    //--------------------------------------------------------------------------------------------------
494
495    /// Clear the display
496    pub fn clear(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
497        self.actions.clear(&mut self.device)?;
498        Ok(self)
499    }
500
501    /// Set the cursor to the home position.
502    pub fn home(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
503        self.actions.home(&mut self.device)?;
504        Ok(self)
505    }
506
507    /// Set the cursor position at specified column and row. Columns and rows are zero-indexed.
508    pub fn set_cursor(
509        &mut self,
510        col: u8,
511        row: u8,
512    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
513        self.actions.set_cursor(&mut self.device, col, row)?;
514        Ok(self)
515    }
516
517    /// Set the cursor visibility.
518    pub fn show_cursor(
519        &mut self,
520        show_cursor: bool,
521    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
522        self.actions.show_cursor(&mut self.device, show_cursor)?;
523        Ok(self)
524    }
525
526    /// Set the cursor blinking.
527    pub fn blink_cursor(
528        &mut self,
529        blink_cursor: bool,
530    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
531        self.actions.blink_cursor(&mut self.device, blink_cursor)?;
532        Ok(self)
533    }
534
535    /// Set the display visibility.
536    pub fn show_display(
537        &mut self,
538        show_display: bool,
539    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
540        self.actions.show_display(&mut self.device, show_display)?;
541        Ok(self)
542    }
543
544    /// Scroll the display to the left.
545    pub fn scroll_display_left(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
546        self.actions.scroll_left(&mut self.device)?;
547        Ok(self)
548    }
549
550    /// Scroll the display to the right.
551    pub fn scroll_display_right(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
552        self.actions.scroll_right(&mut self.device)?;
553        Ok(self)
554    }
555
556    /// Set the text flow direction to left to right.
557    pub fn left_to_right(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
558        self.actions.left_to_right(&mut self.device)?;
559        Ok(self)
560    }
561
562    /// Set the text flow direction to right to left.
563    pub fn right_to_left(&mut self) -> Result<&mut Self, CharacterDisplayError<I2C>> {
564        self.actions.right_to_left(&mut self.device)?;
565        Ok(self)
566    }
567
568    /// Set the auto scroll mode.
569    pub fn autoscroll(
570        &mut self,
571        autoscroll: bool,
572    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
573        self.actions.autoscroll(&mut self.device, autoscroll)?;
574        Ok(self)
575    }
576
577    /// Create a new custom character.
578    pub fn create_char(
579        &mut self,
580        location: u8,
581        charmap: [u8; 8],
582    ) -> Result<&mut Self, CharacterDisplayError<I2C>> {
583        self.actions
584            .create_char(&mut self.device, location, charmap)?;
585        Ok(self)
586    }
587
588    /// Prints a string to the LCD at the current cursor position of the active device.
589    pub fn print(&mut self, text: &str) -> Result<&mut Self, CharacterDisplayError<I2C>> {
590        self.actions.print(&mut self.device, text)?;
591        Ok(self)
592    }
593
594    /// Turn the backlight on or off.
595    /// Note that the AIP31068 controller does not support backlight control.
596    pub fn backlight(&mut self, on: bool) -> Result<&mut Self, CharacterDisplayError<I2C>> {
597        self.actions.backlight(&mut self.device, on)?;
598        Ok(self)
599    }
600
601    /// Set the contrast level of the display. This is only supported by the ST7032i controller.
602    pub fn set_contrast(&mut self, contrast: u8) -> Result<&mut Self, CharacterDisplayError<I2C>> {
603        self.actions.set_contrast(&mut self.device, contrast)?;
604        Ok(self)
605    }
606}
607
608/// Implement the `core::fmt::Write` trait, allowing it to be used with the `write!` macro.
609/// This is a convenience method for printing to the display. For multi-device, this will print to the active device as set by
610/// `set_cursor`.
611impl<I2C, DELAY, DEVICE, ACTIONS> core::fmt::Write
612    for BaseCharacterDisplay<I2C, DELAY, DEVICE, ACTIONS>
613where
614    I2C: i2c::I2c,
615    DELAY: DelayNs,
616    DEVICE: driver::DeviceHardwareTrait<I2C, DELAY>,
617    ACTIONS: driver::DisplayActionsTrait<I2C, DELAY, DEVICE>,
618{
619    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
620        if let Err(_e) = self.print(s) {
621            return Err(core::fmt::Error);
622        }
623        Ok(())
624    }
625}
626
627#[cfg(feature = "ufmt")]
628/// Implement the `ufmt::uWrite` trait, allowing it to be used with the `uwriteln!` and `uwrite!` macros.
629/// This is a convenience method for printing to the display. For multi-device, this will print to the active device as set by
630/// `set_cursor`.
631impl<I2C, DELAY, DEVICE, ACTIONS> ufmt::uWrite for BaseCharacterDisplay<I2C, DELAY, DEVICE, ACTIONS>
632where
633    I2C: i2c::I2c,
634    DELAY: DelayNs,
635    DEVICE: driver::DeviceHardwareTrait<I2C, DELAY>,
636    ACTIONS: driver::DisplayActionsTrait<I2C, DELAY, DEVICE>,
637{
638    fn write_str(&mut self, s: &str) -> Result<(), CharacterDisplayError<I2C>> {
639        if let Err(e) = self.print(s) {
640            return Err(e);
641        }
642        Ok(())
643    }
644
645    type Error = CharacterDisplayError<I2C>;
646}
647
648#[cfg(test)]
649mod lib_tests {
650    extern crate std;
651    use super::*;
652    use embedded_hal_mock::eh1::{
653        delay::NoopDelay,
654        i2c::{Mock as I2cMock, Transaction as I2cTransaction},
655    };
656
657    #[test]
658    fn test_character_display_pcf8574t_init() {
659        let i2c_address = 0x27_u8;
660        let expected_i2c_transactions = std::vec![
661            // the PCF8574T has no adapter init sequence, so nothing to prepend
662            // the LCD init sequence
663            // write low nibble of 0x03 3 times
664            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
665            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
666            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
667            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
668            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
669            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
670            // write high nibble of 0x02 one time
671            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1
672            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
673            // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]),    // backlight on
674            // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE
675            // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28
676            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1
677            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
678            I2cTransaction::write(i2c_address, std::vec![0b1000_0100]), // low nibble, rw=0, enable=1
679            I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0
680            // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF
681            // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C
682            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
683            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
684            I2cTransaction::write(i2c_address, std::vec![0b1100_0100]), // low nibble, rw=0, enable=1
685            I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0
686            // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT
687            // = 0x04 | 0x02 | 0x00 = 0x06
688            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
689            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
690            I2cTransaction::write(i2c_address, std::vec![0b0110_0100]), // low nibble, rw=0, enable=1
691            I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0
692            // LCD_CMD_CLEARDISPLAY
693            // = 0x01
694            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
695            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
696            I2cTransaction::write(i2c_address, std::vec![0b0001_0100]), // low nibble, rw=0, enable=1
697            I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0
698            // LCD_CMD_RETURNHOME
699            // = 0x02
700            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
701            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
702            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // low nibble, rw=0, enable=1
703            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0
704            // Set Backlight
705            I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // backlight on
706        ];
707
708        let i2c = I2cMock::new(&expected_i2c_transactions);
709        let mut lcd = CharacterDisplayPCF8574T::new(i2c, LcdDisplayType::Lcd16x2, NoopDelay::new());
710        let result = lcd.init();
711        assert!(result.is_ok());
712
713        // finish the i2c mock
714        lcd.i2c().done();
715    }
716
717    #[test]
718    fn test_adafruit_lcd_backpack_init() {
719        let i2c_address = 0x20_u8;
720        let expected_i2c_transactions = std::vec![
721            // the Adafruit Backpack need to init the adapter IC first
722            // write 0x00 to the MCP23008 IODIR register to set all pins as outputs
723            I2cTransaction::write(i2c_address, std::vec![0x00, 0x00]),
724            // the LCD init sequence
725            // write low nibble of 0x03 3 times
726            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_100]), // low nibble, rw=0, enable=1
727            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_000]), // low nibble, rw=0, enable=0
728            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_100]), // low nibble, rw=0, enable=1
729            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_000]), // low nibble, rw=0, enable=0
730            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_100]), // low nibble, rw=0, enable=1
731            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0011_000]), // low nibble, rw=0, enable=0
732            // write high nibble of 0x02 one time
733            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_100]), // high nibble, rw=0, enable=1
734            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_000]), // high nibble, rw=0, enable=0
735            // turn on the backlight
736            // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]),    // backlight on
737            // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE
738            // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28
739            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_100]), // high nibble, rw=0, enable=1
740            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_000]), // high nibble, rw=0, enable=0
741            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1000_100]), // low nibble, rw=0, enable=1
742            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1000_000]), // low nibble, rw=0, enable=0
743            // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF
744            // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C
745            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1
746            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0
747            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1100_100]), // low nibble, rw=0, enable=1
748            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_1100_000]), // low nibble, rw=0, enable=0
749            // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT
750            // = 0x04 | 0x02 | 0x00 = 0x06
751            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1
752            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0
753            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0110_100]), // low nibble, rw=0, enable=1
754            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0110_000]), // low nibble, rw=0, enable=0
755            // LCD_CMD_CLEARDISPLAY
756            // = 0x01
757            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1
758            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0
759            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0001_100]), // low nibble, rw=0, enable=1
760            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0001_000]), // low nibble, rw=0, enable=0
761            // LCD_CMD_RETURNHOME
762            // = 0x02
763            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_100]), // high nibble, rw=0, enable=1
764            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0000_000]), // high nibble, rw=0, enable=0
765            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_100]), // low nibble, rw=0, enable=1
766            I2cTransaction::write(i2c_address, std::vec![0x09, 0b0_0010_000]), // low nibble, rw=0, enable=0
767            // Set Backlight
768            I2cTransaction::write(i2c_address, std::vec![0x09, 0b1_0010_000]), // backlight on
769        ];
770
771        let i2c = I2cMock::new(&expected_i2c_transactions);
772        let mut lcd = AdafruitLCDBackpack::new(i2c, LcdDisplayType::Lcd16x2, NoopDelay::new());
773
774        let result = lcd.init();
775        assert!(result.is_ok());
776        assert!(lcd.display_type() == LcdDisplayType::Lcd16x2);
777
778        // finish the i2c mock
779        lcd.i2c().done();
780    }
781
782    #[test]
783    fn test_character_display_dual_hd44780_init() {
784        let i2c_address = 0x27_u8;
785        let expected_i2c_transactions = std::vec![
786            // the PCF8574T has no adapter init sequence, so nothing to prepend
787            // *** Device 0 ***
788            // the LCD init sequence for device 0
789            // write low nibble of 0x03 3 times
790            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
791            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
792            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
793            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
794            I2cTransaction::write(i2c_address, std::vec![0b0011_0100]), // low nibble, rw=0, enable=1
795            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
796            // write high nibble of 0x02 one time
797            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1
798            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
799            // turn on the backlight
800            // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]),    // backlight on
801            // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE
802            // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28
803            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // high nibble, rw=0, enable=1
804            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
805            I2cTransaction::write(i2c_address, std::vec![0b1000_0100]), // low nibble, rw=0, enable=1
806            I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0
807            // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF
808            // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C
809            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
810            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
811            I2cTransaction::write(i2c_address, std::vec![0b1100_0100]), // low nibble, rw=0, enable=1
812            I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0
813            // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT
814            // = 0x04 | 0x02 | 0x00 = 0x06
815            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
816            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
817            I2cTransaction::write(i2c_address, std::vec![0b0110_0100]), // low nibble, rw=0, enable=1
818            I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0
819            // LCD_CMD_CLEARDISPLAY
820            // = 0x01
821            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
822            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
823            I2cTransaction::write(i2c_address, std::vec![0b0001_0100]), // low nibble, rw=0, enable=1
824            I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0
825            // LCD_CMD_RETURNHOME
826            // = 0x02
827            I2cTransaction::write(i2c_address, std::vec![0b0000_0100]), // high nibble, rw=0, enable=1
828            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
829            I2cTransaction::write(i2c_address, std::vec![0b0010_0100]), // low nibble, rw=0, enable=1
830            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0
831            // *** Device 1 ***
832            // the LCD init sequence for device 0
833            // write low nibble of 0x03 3 times
834            I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1
835            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
836            I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1
837            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
838            I2cTransaction::write(i2c_address, std::vec![0b0011_0010]), // low nibble, rw=0, enable=1
839            I2cTransaction::write(i2c_address, std::vec![0b0011_0000]), // low nibble, rw=0, enable=0
840            // write high nibble of 0x02 one time
841            I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // high nibble, rw=0, enable=1
842            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
843            // turn on the backlight
844            // I2cTransaction::write(i2c_address, std::vec![0b0000_1000]),    // backlight on
845            // LCD_CMD_FUNCTIONSET | LCD_FLAG_4BITMODE | LCD_FLAG_5x8_DOTS | LCD_FLAG_2LINE
846            // = 0x20 | 0x00 | 0x00 | 0x08 = 0x28
847            I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // high nibble, rw=0, enable=1
848            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // high nibble, rw=0, enable=0
849            I2cTransaction::write(i2c_address, std::vec![0b1000_0010]), // low nibble, rw=0, enable=1
850            I2cTransaction::write(i2c_address, std::vec![0b1000_0000]), // low nibble, rw=0, enable=0
851            // LCD_CMD_DISPLAYCONTROL | LCD_FLAG_DISPLAYON | LCD_FLAG_CURSOROFF | LCD_FLAG_BLINKOFF
852            // = 0x08 | 0x04 | 0x00 | 0x00 = 0x0C
853            I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1
854            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
855            I2cTransaction::write(i2c_address, std::vec![0b1100_0010]), // low nibble, rw=0, enable=1
856            I2cTransaction::write(i2c_address, std::vec![0b1100_0000]), // low nibble, rw=0, enable=0
857            // LCD_CMD_ENTRYMODESET | LCD_FLAG_ENTRYLEFT | LCD_FLAG_ENTRYSHIFTDECREMENT
858            // = 0x04 | 0x02 | 0x00 = 0x06
859            I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1
860            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
861            I2cTransaction::write(i2c_address, std::vec![0b0110_0010]), // low nibble, rw=0, enable=1
862            I2cTransaction::write(i2c_address, std::vec![0b0110_0000]), // low nibble, rw=0, enable=0
863            // LCD_CMD_CLEARDISPLAY
864            // = 0x01
865            I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1
866            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
867            I2cTransaction::write(i2c_address, std::vec![0b0001_0010]), // low nibble, rw=0, enable=1
868            I2cTransaction::write(i2c_address, std::vec![0b0001_0000]), // low nibble, rw=0, enable=0
869            // LCD_CMD_RETURNHOME
870            // = 0x02
871            I2cTransaction::write(i2c_address, std::vec![0b0000_0010]), // high nibble, rw=0, enable=1
872            I2cTransaction::write(i2c_address, std::vec![0b0000_0000]), // high nibble, rw=0, enable=0
873            I2cTransaction::write(i2c_address, std::vec![0b0010_0010]), // low nibble, rw=0, enable=1
874            I2cTransaction::write(i2c_address, std::vec![0b0010_0000]), // low nibble, rw=0, enable=0
875            // Set Backlight
876            I2cTransaction::write(i2c_address, std::vec![0b0010_1000]), // backlight on
877        ];
878
879        let i2c = I2cMock::new(&expected_i2c_transactions);
880        let mut lcd =
881            CharacterDisplayDualHD44780::new(i2c, LcdDisplayType::Lcd40x4, NoopDelay::new());
882        let result = lcd.init();
883        assert!(result.is_ok());
884        assert!(lcd.display_type() == LcdDisplayType::Lcd40x4);
885
886        // finish the i2c mock
887        lcd.i2c().done();
888    }
889}