From 6b43a5e59200b6fe7164beaaf48755d7ca5d0d33 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 11 Aug 2020 14:43:54 +0200 Subject: [PATCH] Added luma.oled display support --- docs/source/backends.rst | 6 - docs/source/conf.py | 1 + docs/source/platypush/plugins/luma.oled.rst | 5 + docs/source/plugins.rst | 4 +- platypush/plugins/luma/__init__.py | 0 platypush/plugins/luma/oled.py | 322 ++++++++++++++++++++ requirements.txt | 3 + setup.py | 2 + 8 files changed, 334 insertions(+), 9 deletions(-) create mode 100644 docs/source/platypush/plugins/luma.oled.rst create mode 100644 platypush/plugins/luma/__init__.py create mode 100644 platypush/plugins/luma/oled.py diff --git a/docs/source/backends.rst b/docs/source/backends.rst index 49cdeac9..e1e61606 100644 --- a/docs/source/backends.rst +++ b/docs/source/backends.rst @@ -11,11 +11,6 @@ Backends platypush/backend/assistant.rst platypush/backend/assistant.google.rst platypush/backend/assistant.snowboy.rst - platypush/backend/bluetooth.rst - platypush/backend/bluetooth.fileserver.rst - platypush/backend/bluetooth.pushserver.rst - platypush/backend/bluetooth.scanner.rst - platypush/backend/bluetooth.scanner.ble.rst platypush/backend/button.flic.rst platypush/backend/camera.pi.rst platypush/backend/chat.telegram.rst @@ -53,7 +48,6 @@ Backends platypush/backend/sensor.distance.vl53l1x.rst platypush/backend/sensor.envirophat.rst platypush/backend/sensor.ir.zeroborg.rst - platypush/backend/sensor.leap.rst platypush/backend/sensor.ltr559.rst platypush/backend/sensor.mcp3008.rst platypush/backend/sensor.motion.pwm3901.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 73777139..f788f230 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -252,6 +252,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'pandas', 'samsungtvws', 'paramiko', + 'luma', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/docs/source/platypush/plugins/luma.oled.rst b/docs/source/platypush/plugins/luma.oled.rst new file mode 100644 index 00000000..77cb94e1 --- /dev/null +++ b/docs/source/platypush/plugins/luma.oled.rst @@ -0,0 +1,5 @@ +``platypush.plugins.luma.oled`` +=============================== + +.. automodule:: platypush.plugins.luma.oled + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index b8f9c893..93074dc3 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -8,7 +8,6 @@ Plugins platypush/plugins/adafruit.io.rst platypush/plugins/alarm.rst - platypush/plugins/arduino.rst platypush/plugins/assistant.rst platypush/plugins/assistant.echo.rst platypush/plugins/assistant.google.rst @@ -66,6 +65,7 @@ Plugins platypush/plugins/light.hue.rst platypush/plugins/linode.rst platypush/plugins/logger.rst + platypush/plugins/luma.oled.rst platypush/plugins/media.rst platypush/plugins/media.chromecast.rst platypush/plugins/media.ctrl.rst @@ -85,7 +85,6 @@ Plugins platypush/plugins/music.mpd.rst platypush/plugins/music.snapcast.rst platypush/plugins/nmap.rst - platypush/plugins/otp.rst platypush/plugins/pihole.rst platypush/plugins/ping.rst platypush/plugins/printer.cups.rst @@ -98,7 +97,6 @@ Plugins platypush/plugins/sound.rst platypush/plugins/ssh.rst platypush/plugins/stt.rst - platypush/plugins/stt.deepspeech.rst platypush/plugins/stt.picovoice.hotword.rst platypush/plugins/stt.picovoice.speech.rst platypush/plugins/switch.rst diff --git a/platypush/plugins/luma/__init__.py b/platypush/plugins/luma/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/platypush/plugins/luma/oled.py b/platypush/plugins/luma/oled.py new file mode 100644 index 00000000..3a7d15eb --- /dev/null +++ b/platypush/plugins/luma/oled.py @@ -0,0 +1,322 @@ +import enum +import os +from typing import Optional, Union, Tuple, List + +import luma.core.interface.serial +import luma.oled.device +from luma.core.render import canvas +from PIL import Image, ImageFont + +from platypush.plugins import Plugin, action + + +class DeviceInterface(enum.Enum): + I2C = 'i2c' + SPI = 'spi' + + +class DeviceSlot(enum.IntEnum): + BACK = 0 + FRONT = 1 + + +class DeviceRotation(enum.IntEnum): + ROTATE_0 = 0 + ROTATE_90 = 1 + ROTATE_180 = 2 + ROTATE_270 = 3 + + +class LumaOledPlugin(Plugin): + """ + Plugin to interact with small OLED-based RaspberryPi displays through the luma.oled driver. + + Requires: + + * **luma.oled** (``pip install git+https://github.com/rm-hull/luma.oled``) + + """ + + def __init__(self, + interface: str, + device: str, + port: int = 0, + slot: int = DeviceSlot.BACK.value, + width: int = 128, + height: int = 64, + rotate: int = DeviceRotation.ROTATE_0.value, + gpio_DC: int = 24, + gpio_RST: int = 25, + bus_speed_hz: int = 8000000, + address: int = 0x3c, + cs_high: bool = False, + transfer_size: int = 4096, + spi_mode: Optional[int] = None, + font: Optional[str] = None, + font_size: int = 10, + **kwargs): + """ + :param interface: Serial interface the display is connected to (``spi`` or ``i2c``). + :param device: Display chipset type (supported: ssd1306 ssd1309, ssd1322, ssd1325, ssd1327, ssd1331, ssd1351, ssd1362, sh1106). + :param port: Device port (usually 0 or 1). + :param slot: Device slot (0 for back, 1 for front). + :param width: Display width. + :param height: Display height. + :param rotate: Display rotation (0 for no rotation, 1 for 90 degrees, 2 for 180 degrees, 3 for 270 degrees). + :param gpio_DC: [SPI only] GPIO PIN used for data (default: 24). + :param gpio_RST: [SPI only] GPIO PIN used for RST (default: 25). + :param bus_speed_hz: [SPI only] Bus speed in Hz (default: 8 MHz). + :param address: [I2C only] Device address (default: 0x3c). + :param cs_high: [SPI only] Set to True if the SPI chip select is high. + :param transfer_size: [SPI only] Maximum amount of bytes to transfer in one go (default: 4096). + :param spi_mode: [SPI only] SPI mode as two bit pattern of clock polarity and phase [CPOL|CPHA], 0-3 (default:None). + :param font: Path to a default TTF font used to display the text. + :param font_size: Font size - it only applies if ``font`` is set. + """ + super().__init__(**kwargs) + + iface_name = interface + interface = getattr(luma.core.interface.serial, DeviceInterface(interface).value) + + if iface_name == DeviceInterface.SPI.value: + self.serial = interface(port=port, device=slot, cs_high=cs_high, gpio_DC=gpio_DC, + gpio_RST=gpio_RST, bus_speed_hz=bus_speed_hz, + transfer_size=transfer_size, spi_mode=spi_mode) + else: + self.serial = interface(port=port, address=address) + + device = getattr(luma.oled.device, device) + self.device = device(self.serial, width=width, height=height, rotate=rotate) + self.canvas = canvas(self.device) + self.font = None + self.font_size = font_size + self.font = self._get_font(font, font_size) + + def _get_font(self, font: Optional[str] = None, font_size: Optional[int] = None): + if font: + return ImageFont.truetype(os.path.abspath(os.path.expanduser(font)), font_size or self.font_size) + + return self.font + + @action + def clear(self): + """ + clear the display canvas. + """ + self.device.clear() + del self.canvas + self.canvas = canvas(self.device) + + @action + def text(self, text: str, pos: Union[Tuple[int], List[int]] = (0, 0), + fill: str = 'white', font: Optional[str] = None, font_size: Optional[int] = None, clear: bool = False): + """ + Draw text on the canvas. + + :param text: Text to be drawn. + :param pos: Position of the text. + :param fill: Text color (default: ``white``). + :param font: ``font`` type override. + :param font_size: ``font_size`` override. + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + font = self._get_font(font, font_size) + + with self.canvas as draw: + draw.text(pos, text, fill=fill, font=font) + + @action + def rectangle(self, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, clear: bool = False): + """ + Draw a rectangle on the canvas. + + :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.rectangle(xy, outline=outline, fill=fill, width=width) + + @action + def arc(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, clear: bool = False): + """ + Draw an arc on the canvas. + + :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.arc(xy, start=start, end=end, outline=outline, fill=fill, width=width) + + @action + def chord(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, clear: bool = False): + """ + Same as ``arc``, but it connects the end points with a straight line. + + :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.chord(xy, start=start, end=end, outline=outline, fill=fill, width=width) + + @action + def pieslice(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, clear: bool = False): + """ + Same as ``arc``, but it also draws straight lines between the end points and the center of the bounding box. + + :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise). + :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.pieslice(xy, start=start, end=end, outline=outline, fill=fill, width=width) + + @action + def ellipse(self, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, clear: bool = False): + """ + Draw an ellipse on the canvas. + + :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.ellipse(xy, outline=outline, fill=fill, width=width) + + @action + def line(self, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + width: int = 1, curve: bool = False, clear: bool = False): + """ + Draw a line on the canvas. + + :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...]. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param width: Figure width in pixels (default: 1). + :param curve: Set to True for rounded edges (default: False). + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.line(xy, outline=outline, fill=fill, width=width, joint='curve' if curve else None) + + @action + def point(self, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, clear: bool = False): + """ + Draw one or more points on the canvas. + + :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...]. + :param fill: Fill color - can be ``black`` or ``white``. + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.point(xy, fill=fill) + + @action + def polygon(self, xy: Optional[Union[Tuple[int], List[int]]] = None, + fill: Optional[str] = None, outline: Optional[str] = None, + clear: bool = False): + """ + Draw a polygon on the canvas. + + :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...]. + :param fill: Fill color - can be ``black`` or ``white``. + :param outline: Outline color - can be ``black`` or ``white``. + :param clear: Set to True if you want to clear the canvas before writing the text (default: False). + """ + if clear: + self.clear() + + if not xy: + xy = self.device.bounding_box + + with self.canvas as draw: + draw.polygon(xy, outline=outline, fill=fill) + + @action + def image(self, image: str): + """ + Draws an image to the canvas (this will clear the existing canvas). + + :param image: Image path. + """ + image = Image.open(os.path.abspath(os.path.expanduser(image))) + self.clear() + self.device.display(image) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index b1e2ae1b..aa0db579 100644 --- a/requirements.txt +++ b/requirements.txt @@ -270,3 +270,6 @@ croniter # Support for Google Translate # google-cloud-translate + +# Support for luma.oled +# git+https://github.com/rm-hull/luma.oled diff --git a/setup.py b/setup.py index 45d887cd..ea60e3be 100755 --- a/setup.py +++ b/setup.py @@ -316,5 +316,7 @@ setup( 'ssh': ['paramiko'], # Support for clipboard integration 'clipboard': ['pyperclip'], + # Support for luma.oled display drivers + 'luma-oled': ['luma.oled @ git+https://github.com/rm-hull/luma.oled'], }, )