From af614480b82bca4685769f401f09fe5a51091feb Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 23 Aug 2020 20:00:04 +0100 Subject: [PATCH] Added LCD display integration (closes #145) --- docs/source/conf.py | 2 + platypush/plugins/lcd/__init__.py | 246 ++++++++++++++++++++++++++++++ platypush/plugins/lcd/gpio.py | 85 +++++++++++ platypush/plugins/lcd/i2c.py | 82 ++++++++++ requirements.txt | 4 + setup.py | 2 + 6 files changed, 421 insertions(+) create mode 100644 platypush/plugins/lcd/__init__.py create mode 100644 platypush/plugins/lcd/gpio.py create mode 100644 platypush/plugins/lcd/i2c.py diff --git a/docs/source/conf.py b/docs/source/conf.py index e5857f2740..74c3af3ff0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -260,6 +260,8 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'twilio', 'pytz', 'Adafruit_Python_DHT', + 'RPi.GPIO', + 'RPLCD', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/plugins/lcd/__init__.py b/platypush/plugins/lcd/__init__.py new file mode 100644 index 0000000000..275e9239a3 --- /dev/null +++ b/platypush/plugins/lcd/__init__.py @@ -0,0 +1,246 @@ +from abc import ABC, abstractmethod +from enum import Enum +from typing import List, Optional + +from platypush.plugins import Plugin, action + + +class PinMode(Enum): + import RPi.GPIO + BOARD = RPi.GPIO.BOARD + BCM = RPi.GPIO.BCM + + +class LcdPlugin(Plugin, ABC): + """ + Abstract class for plugins to communicate with LCD displays. + + Requires: + + * **RPLCD** (``pip install RPLCD``) + * **RPi.GPIO** (``pip install RPi.GPIO``) + + """ + import RPLCD.lcd + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.lcd = None + + @staticmethod + def _get_pin_mode(pin_mode: str) -> int: + pin_mode = pin_mode.upper() + assert hasattr(PinMode, pin_mode), \ + 'Invalid pin_mode: {}. Supported modes: {}'.format( + pin_mode, list([mode.name for mode in PinMode if mode.name != 'RPi'])) + + return getattr(PinMode, pin_mode).value + + @abstractmethod + def _get_lcd(self) -> RPLCD.lcd.BaseCharLCD: + pass + + def _init_lcd(self): + if self.lcd: + return + + self.lcd = self._get_lcd() + + @action + def close(self, clear: bool = False): + """ + Close the handler to the LCD display and release the GPIO resources. + + :param clear: Clear the display as well on close (default: False). + """ + if self.lcd: + self.lcd.close(clear=clear) + self.lcd = None + + @action + def clear(self): + """ + Clear the LCD display. + """ + self._init_lcd() + self.lcd.clear() + + @action + def home(self): + """ + Set cursor to initial position and reset any shifting. + """ + self._init_lcd() + self.lcd.home() + + @action + def shift_display(self, amount: int): + """ + Set cursor to initial position and reset any shifting. + """ + self._init_lcd() + self.lcd.shift_display(amount) + + @action + def write_string(self, value: str, position: Optional[List[int]] = None): + """ + Write a string to the display. + + :param value: String to be displayed. + :param position: String position on the display as a 2-int list. + """ + self._init_lcd() + if position: + self.lcd.cursor_pos = tuple(position) + + self.lcd.write_string(value) + + @action + def set_cursor_pos(self, position: List[int]): + """ + Change the position of the cursor on the display. + + :param position: New cursor position, as a list of two elements. + """ + self._init_lcd() + self.lcd.cursor_pos = tuple(position) + + @action + def set_text_align(self, mode: str): + """ + Change the text align mode. + + :param mode: Supported values: ``left``, ``right``. + """ + modes = ['left', 'right'] + mode = mode.lower() + assert mode in modes, 'Unsupported text mode: {}. Supported modes: {}'.format( + mode, modes) + + self._init_lcd() + self.lcd.text_align_mode = mode + + @action + def enable_display(self): + """ + Turn on the display. + """ + self._init_lcd() + self.lcd.display_enabled = True + + @action + def disable_display(self): + """ + Turn off the display. + """ + self._init_lcd() + self.lcd.display_enabled = False + + @action + def toggle_display(self): + """ + Toggle the display state. + """ + self._init_lcd() + self.lcd.display_enabled = not self.lcd.display_enabled + + @action + def enable_backlight(self): + """ + Enable the display backlight. + """ + self._init_lcd() + self.lcd.backlight_enabled = True + + @action + def disable_backlight(self): + """ + Disable the display backlight. + """ + self._init_lcd() + self.lcd.backlight_enabled = False + + @action + def toggle_backlight(self): + """ + Toggle the display backlight on/off. + """ + self._init_lcd() + self.lcd.backlight_enabled = not self.lcd.backlight_enabled + + @action + def create_char(self, location: int, bitmap: List[int]): + """ + Create a new character. + The HD44780 supports up to 8 custom characters (location 0-7). + + :param location: The place in memory where the character is stored. + Values need to be integers between 0 and 7. + :param bitmap: The bitmap containing the character. This should be a + list of 8 numbers, each representing a 5 pixel row. + + Example for the smiley character: + + .. code-block:: python + + [ + 0, # 0b00000 + 10, # 0b01010 + 10, # 0b01010 + 0, # 0b00000 + 17, # 0b10001 + 17, # 0b10001 + 14, # 0b01110 + 0 # 0b00000 + ] + + """ + self._init_lcd() + self.lcd.create_char(location=location, bitmap=tuple(bitmap)) + + @action + def command(self, value: int): + """ + Send a raw command to the LCD. + + :param value: Command to be sent. + """ + self._init_lcd() + self.lcd.command(value) + + @action + def write(self, value: int): + """ + Write a raw byte to the LCD. + + :param value: Byte to be sent. + """ + self._init_lcd() + self.lcd.write(value) + + @action + def cr(self): + """ + Write a carriage return (``\\r``) character to the LCD. + """ + self._init_lcd() + self.lcd.cr() + + @action + def lf(self): + """ + Write a line feed (``\\n``) character to the LCD. + """ + self._init_lcd() + self.lcd.lf() + + @action + def crlf(self): + """ + Write a carriage return + line feed (``\\r\\n``) sequence to the LCD. + """ + self._init_lcd() + self.lcd.crlf() + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/lcd/gpio.py b/platypush/plugins/lcd/gpio.py new file mode 100644 index 0000000000..2f17208702 --- /dev/null +++ b/platypush/plugins/lcd/gpio.py @@ -0,0 +1,85 @@ +from typing import List, Optional + +from platypush.plugins import action +from platypush.plugins.lcd import LcdPlugin + + +class LcdGpioPlugin(LcdPlugin): + """ + Plugin to write to an LCD display connected via GPIO. + + Requires: + + * **RPLCD** (``pip install RPLCD``) + * **RPi.GPIO** (``pip install RPi.GPIO``) + + """ + + def __init__(self, pin_rs: int, pin_e: int, pins_data: List[int], + pin_rw: Optional[int] = None, pin_mode: str = 'BOARD', + pin_backlight: Optional[int] = None, + cols: int = 16, rows: int = 2, + backlight_enabled: bool = True, + backlight_mode: str = 'active_low', + dotsize: int = 8, charmap: str = 'A02', + auto_linebreaks: bool = True, + compat_mode: bool = False, **kwargs): + """ + :param pin_rs: Pin for register select (RS). + :param pin_e: Pin to start data read or write (E). + :param pins_data: List of data bus pins in 8 bit mode (DB0-DB7) or in 4 + bit mode (DB4-DB7) in ascending order. + :param pin_mode: Which scheme to use for numbering of the GPIO pins, + either ``BOARD`` or ``BCM``. Default: ``BOARD``. + :param pin_rw: Pin for selecting read or write mode (R/W). Default: + ``None``, read only mode. + :param pin_backlight: Pin for controlling backlight on/off. Set this to + ``None`` for no backlight control. Default: ``None``. + :param cols: Number of columns per row (usually 16 or 20). Default: ``16``. + :param rows: Number of display rows (usually 1, 2 or 4). Default: ``2``. + :param backlight_enabled: Whether the backlight is enabled initially. + Default: ``True``. Has no effect if pin_backlight is ``None`` + :param backlight_mode: Set this to either ``active_high`` or ``active_low`` + to configure the operating control for the backlight. Has no effect if + pin_backlight is ``None`` + :param dotsize: Some 1 line displays allow a font height of 10px. + Allowed: ``8`` or ``10``. Default: ``8``. + :param charmap: The character map used. Depends on your LCD. This must + be either ``A00`` or ``A02`` or ``ST0B``. Default: ``A02``. + :param auto_linebreaks: Whether or not to automatically insert line + breaks. Default: ``True``. + :param compat_mode: Whether to run additional checks to support older LCDs + that may not run at the reference clock (or keep up with it). + Default: ``False``. + """ + super().__init__(**kwargs) + + self.pin_mode = self._get_pin_mode(pin_mode) + self.pin_rs = pin_rs + self.pin_e = pin_e + self.pin_rw = pin_rw + self.pin_backlight = pin_backlight + self.pins_data = pins_data + self.cols = cols + self.rows = rows + self.backlight_enabled = backlight_enabled + self.backlight_mode = backlight_mode + self.dotsize = dotsize + self.auto_linebreaks = auto_linebreaks + self.compat_mode = compat_mode + self.charmap = charmap + + def _get_lcd(self): + from RPLCD.gpio import CharLCD + return CharLCD(cols=self.cols, rows=self.rows, pin_rs=self.pin_rs, + pin_e=self.pin_e, pins_data=self.pins_data, + numbering_mode=self.pin_mode, pin_rw=self.pin_rw, + pin_backlight=self.pin_backlight, + backlight_enabled=self.backlight_enabled, + backlight_mode=self.backlight_mode, + dotsize=self.dotsize, charmap=self.charmap, + auto_linebreaks=self.auto_linebreaks, + compat_mode=self.compat_mode) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/lcd/i2c.py b/platypush/plugins/lcd/i2c.py new file mode 100644 index 0000000000..1b6b85cb68 --- /dev/null +++ b/platypush/plugins/lcd/i2c.py @@ -0,0 +1,82 @@ +from typing import List, Optional + +from platypush.plugins import action +from platypush.plugins.lcd import LcdPlugin + + +class LcdI2cPlugin(LcdPlugin): + """ + Plugin to write to an LCD display connected via I2C. + Adafruit I2C/SPI LCD Backback is supported. + + Warning: You might need a level shifter (that supports i2c) + between the SCL/SDA connections on the MCP chip / backpack and the Raspberry Pi. + Or you might damage the Pi and possibly any other 3.3V i2c devices + connected on the i2c bus. Or cause reliability issues. The SCL/SDA are rated 0.7*VDD + on the MCP23008, so it needs 3.5V on the SCL/SDA when 5V is applied to drive the LCD. + The MCP23008 and MCP23017 needs to be connected exactly the same way as the backpack. + For complete schematics see the adafruit page at: + https://learn.adafruit.com/i2c-spi-lcd-backpack/ + 4-bit operation. I2C only supported. + + Pin mapping:: + + 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 + BL | D7 | D6 | D5 | D4 | E | RS | - + + Requires: + + * **RPLCD** (``pip install RPLCD``) + * **RPi.GPIO** (``pip install RPi.GPIO``) + + """ + + def __init__(self, i2c_expander: str, address: int, + expander_params: Optional[dict] = None, + port: int = 1, cols: int = 16, rows: int = 2, + backlight_enabled: bool = True, + dotsize: int = 8, charmap: str = 'A02', + auto_linebreaks: bool = True, **kwargs): + """ + :param i2c_expander: Set your I²C chip type. Supported: "PCF8574", "MCP23008", "MCP23017". + :param address: The I2C address of your LCD. + :param expander_params: Parameters for expanders, in a dictionary. Only needed for MCP23017 + gpio_bank - This must be either ``A`` or ``B`` + If you have a HAT, A is usually marked 1 and B is 2. + Example: ``expander_params={'gpio_bank': 'A'}`` + :param port: The I2C port number. Default: ``1``. + :param cols: Number of columns per row (usually 16 or 20). Default: ``16``. + :param rows: Number of display rows (usually 1, 2 or 4). Default: ``2``. + :param backlight_enabled: Whether the backlight is enabled initially. + Default: ``True``. Has no effect if pin_backlight is ``None`` + :param dotsize: Some 1 line displays allow a font height of 10px. + Allowed: ``8`` or ``10``. Default: ``8``. + :param charmap: The character map used. Depends on your LCD. This must + be either ``A00`` or ``A02`` or ``ST0B``. Default: ``A02``. + :param auto_linebreaks: Whether or not to automatically insert line + breaks. Default: ``True``. + """ + super().__init__(**kwargs) + + self.i2c_expander = i2c_expander + self.address = address + self.expander_params = expander_params or {} + self.port = port + self.cols = cols + self.rows = rows + self.backlight_enabled = backlight_enabled + self.dotsize = dotsize + self.auto_linebreaks = auto_linebreaks + self.charmap = charmap + + def _get_lcd(self): + from RPLCD.i2c import CharLCD + return CharLCD(cols=self.cols, rows=self.rows, + i2c_expander=self.i2c_expander, + address=self.address, port=self.port, + backlight_enabled=self.backlight_enabled, + dotsize=self.dotsize, charmap=self.charmap, + auto_linebreaks=self.auto_linebreaks) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index b9ff637055..c2737dd3ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -289,3 +289,7 @@ croniter # Support for DHT11/DHT22/AM2302 temperature/humidity sensors # git+https://github.com/adafruit/Adafruit_Python_DHT +# Support for LCD display integration +# RPi.GPIO +# RPLCD + diff --git a/setup.py b/setup.py index 84f99a73a5..0371af90c2 100755 --- a/setup.py +++ b/setup.py @@ -328,5 +328,7 @@ setup( 'github': ['pytz'], # Support for DHT11/DHT22/AM2302 temperature/humidity sensors 'dht': ['Adafruit_Python_DHT @ git+https://github.com/adafruit/Adafruit_Python_DHT'], + # Support for LCD display integration + 'lcd': ['RPi.GPIO', 'RPLCD'], }, )