Thomascountz / micropython_i2c_lcd

This MicroPython library provides a framework for interacting with HD44780-based LCD displays through a PCF8574 I/O expander. It offers a high-level API for LCD control, including text display, cursor manipulation, and backlight settings, while also providing lower-level access to the GPIO operations on the PCF8574.
MIT License
3 stars 3 forks source link

Introduce `HD44780Controller` interface #1

Closed Thomascountz closed 1 year ago

Thomascountz commented 1 year ago

The motivation behind introducing the HD44780Controller interface is to abstract away the specific communication mechanism with the HD44780 LCD controller.

The HD44780 class, which encapsulates the logic of controlling the HD44780 LCD, should ideally not be tightly coupled with a specific I/O expander class such as PCF8574. Instead, it should work with any class that provides the necessary methods for sending commands and data to the HD44780.

In other words, the HD44780 class needs an object that can "write commands" and "write data" (and eventually read) to the HD44780, but it doesn't need to know the specifics of how this is accomplished. This could be done via the PCF8574, another type of I/O expander, or even directly via GPIO pins on the microcontroller.

This is where the HD44780Controller interface comes into play. By programming to this interface, the HD44780 class is decoupled from the specific class and can work with any class that implements the HD44780Controller interface.

  1. Create a Payload class to represent the data and control signals that need to be sent to the HD44780 controller. This class is immutable and simply holds the values:

    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Payload:
        e: int
        rs: int
        rw: int
        data: int
  2. Define an interface for writing command/data payloads. This could be implemented by any class that controls the HD44780, whether it's an I/O expander like the PCF8574 or a direct connection to the microcontroller:

    from abc import ABC, abstractmethod
    
    class HD44780Controller(ABC):
        @abstractmethod
        def write(self, payload: Payload):
            pass
  3. Move the logic for arranging the byte array into the PCF8574 class. The PCF8574 class implements the HD44780Controller interface, and its write method takes a Payload object, arranges the bits into the correct positions for the PCF8574's pins, and sends the result to the I2C bus:

    class PCF8574(HD44780Controller):
        # ...
    
        def write(self, payload: Payload):
            # Prepare the byte to be sent to the hardware
            byte = (self.backlight << self.BACKLIGHT_SHIFT) | (payload.e << self.PIN_ENABLE_SHIFT) | \
                   (payload.rs << self.PIN_RS_SHIFT) | (payload.rw << self.PIN_RW_SHIFT) | (payload.data << self.DATA_SHIFT)
            self._write_byte(byte)
    
        def _write_byte(self, byte: int):
            self._gpio_state[0] = byte & 0xFF
            self.i2c.writeto(self.address, self._gpio_state)
  4. Change the HD44780 class to use the HD44780Controller interface for writing payloads. This way, it doesn't need to know about the specifics of the PCF8574 or any other controller:

    class HD44780:
        def __init__(self, controller: HD44780Controller, num_lines: int, num_columns: int):
            self.controller = controller
            # ...
    
        def _write_command(self, cmd: int):
            # Prepare high order bits, set E pin high, and write
            payload = Payload(e=1, rs=0, rw=0, data=(cmd >> 4) & 0x0F)
            self.controller.write(payload)
    
            # Toggle E pin low and write
            payload = Payload(e=0, rs=0, rw=0, data=(cmd >> 4) & 0x0F)
            self.controller.write(payload)
    
            # Prepare low order bits, set E pin high, and write
            payload = Payload(e=1, rs=0, rw=0, data=cmd & 0x0F)
            self.controller.write(payload)
    
            # Toggle E pin low and write
            payload = Payload(e=0, rs=0, rw=0, data=cmd & 0x0F)
            self.controller.write(payload)
    
            utime.sleep_ms(2)  # commands need > 37us to settle
Thomascountz commented 1 year ago

In MicroPython, which, to the best of my knowledge, does not support the abc module for defining abstract base classes, we should be able to achieve a similar effect by defining methods in a base class that raise a NotImplementedError, signaling that subclasses are expected to provide an implementation for these methods.

class HD44780Controller:
    def write(self, payload):
        raise NotImplementedError