diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index 15f03553..d7ec5b1f 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -260,9 +260,21 @@ class Display(View): def is_pirateaudio(self): return self._implementation.name == 'pirateaudio' + def gfxhat(self): + return self._implementation.name == 'gfxhat' + + def is_argonpod(self): + return self._implementation.name == 'argonpod' + def is_pitft(self): return self._implementation.name == 'pitft' + def is_minipitft(self): + return self._implementation.name == 'minipitft' + + def is_minipitft2(self): + return self._implementation.name == 'minipitft2' + def is_tftbonnet(self): return self._implementation.name == 'tftbonnet' diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index e7699c77..1937599e 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -96,10 +96,26 @@ def display_for(config): from pwnagotchi.ui.hw.pirateaudio import PirateAudio return PirateAudio(config) + elif config['ui']['display']['type'] == 'gfxhat': + from pwnagotchi.ui.hw.gfxhat import GfxHat + return GfxHat(config) + + elif config['ui']['display']['type'] == 'argonpod': + from pwnagotchi.ui.hw.argonpod import ArgonPod + return ArgonPod(config) + elif config['ui']['display']['type'] == 'pitft': from pwnagotchi.ui.hw.pitft import Pitft return Pitft(config) + elif config['ui']['display']['type'] == 'minipitft': + from pwnagotchi.ui.hw.minipitft import MiniPitft + return MiniPitft(config) + + elif config['ui']['display']['type'] == 'minipitft2': + from pwnagotchi.ui.hw.minipitft2 import MiniPitft2 + return MiniPitft2(config) + elif config['ui']['display']['type'] == 'tftbonnet': from pwnagotchi.ui.hw.tftbonnet import TftBonnet return TftBonnet(config) diff --git a/pwnagotchi/ui/hw/argonpod.py b/pwnagotchi/ui/hw/argonpod.py new file mode 100644 index 00000000..1749df34 --- /dev/null +++ b/pwnagotchi/ui/hw/argonpod.py @@ -0,0 +1,54 @@ +# board GPIO: +# Key1: +# Key2: +# Key3: +# Key4: +# +# Touch chipset: +# HW info: https://argon40.com/products/pod-display-2-8inch +# HW datasheet: + +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class ArgonPod(DisplayImpl): + def __init__(self, config): + super(ArgonPod, self).__init__(config, 'argonpod') + self._display = None + + def layout(self): + fonts.setup(12, 10, 12, 70, 25, 9) + self._layout['width'] = 320 + self._layout['height'] = 240 + self._layout['face'] = (0, 36) + self._layout['name'] = (150, 36) + self._layout['channel'] = (0, 0) + self._layout['aps'] = (40, 0) + self._layout['uptime'] = (240, 0) + self._layout['line1'] = [0, 14, 320, 14] + self._layout['line2'] = [0, 220, 320, 220] + self._layout['friend_face'] = (0, 130) + self._layout['friend_name'] = (40, 135) + self._layout['shakes'] = (0, 220) + self._layout['mode'] = (280, 220) + self._layout['status'] = { + 'pos': (150, 48), + 'font': fonts.status_font(fonts.Medium), + 'max': 20 + } + + return self._layout + + def initialize(self): + logging.info("Initializing Argon Pod display") + from pwnagotchi.ui.hw.libs.argon.argonpod.ILI9341 import ILI9341 + self._display = ILI9341(0, 0, 22, 18) + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index d404f962..55cb86ef 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -7,7 +7,7 @@ from pwnagotchi.ui.hw.base import DisplayImpl class DisplayHatMini(DisplayImpl): def __init__(self, config): super(DisplayHatMini, self).__init__(config, 'displayhatmini') - self.mode = "RGB" # its actually BGR;16 5,6,5 bit, but display lib converts it + self._display = None def layout(self): fonts.setup(12, 10, 12, 70, 25, 9) @@ -35,10 +35,10 @@ class DisplayHatMini(DisplayImpl): def initialize(self): logging.info("initializing Display Hat Mini") from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 - self._display = ST7789(0, 1, 9, 13, width=self._layout['width'], height=self._layout['height'], rotation=0) + self._display = ST7789(0,1,9,13) def render(self, canvas): self._display.display(canvas) def clear(self): - pass + self._display.clear() diff --git a/pwnagotchi/ui/hw/gfxhat.py b/pwnagotchi/ui/hw/gfxhat.py new file mode 100644 index 00000000..ced3d5ea --- /dev/null +++ b/pwnagotchi/ui/hw/gfxhat.py @@ -0,0 +1,63 @@ +# Created for the Pwnagotchi project by RasTacsko +# HW libraries are based on the pimoroni gfx-hat repo: +# https://github.com/pimoroni/gfx-hat/tree/master +# +# Contrast and Backlight color are imported from config.toml +# +# ui.display.contrast = 40 +# ui.display.blcolor = "olive" +# +# Contrast should be between 30-50, default is 40 +# Backlight are predefined in the epd.py +# Available backlight colors: +# white, grey, maroon, red, purple, fuchsia, green, +# lime, olive, yellow, navy, blue, teal, aqua + +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + +class GfxHat(DisplayImpl): + def __init__(self, config): + self._config = config['ui']['display'] + super(GfxHat, self).__init__(config, 'gfxhat') + + def layout(self): + fonts.setup(8, 8, 8, 10, 10, 8) + self._layout['width'] = 128 + self._layout['height'] = 64 + self._layout['face'] = (0, 30) + self._layout['name'] = (0, 10) + self._layout['channel'] = (72, 10) + self._layout['aps'] = (0, 0) + self._layout['uptime'] = (87, 0) + self._layout['line1'] = [0, 9, 128, 9] + self._layout['line2'] = [0, 54, 128, 54] + self._layout['friend_face'] = (0, 41) + self._layout['friend_name'] = (40, 43) + self._layout['shakes'] = (0, 55) + self._layout['mode'] = (107, 10) + self._layout['status'] = { + 'pos': (37, 19), + 'font': fonts.status_font(fonts.Small), + 'max': 18 + } + return self._layout + + def initialize(self): + contrast = self._config['contrast'] if 'contrast' in self._config else 40 + blcolor = self._config['blcolor'] if 'blcolor' in self._config else 'OLIVE' + logging.info("initializing Pimoroni GfxHat") + logging.info("initializing Pimoroni GfxHat - Contrast: %d Backlight color: %s" % (contrast, blcolor)) + from pwnagotchi.ui.hw.libs.pimoroni.gfxhat.epd import EPD + self._display = EPD(contrast=contrast) + self._display.Init(color_name=blcolor) + self._display.Clear() + + + def render(self, canvas): + self._display.Display(canvas) + + def clear(self): + self._display.Clear() diff --git a/pwnagotchi/ui/hw/libs/adafruit/minipitft/ST7789.py b/pwnagotchi/ui/hw/libs/adafruit/minipitft/ST7789.py new file mode 100644 index 00000000..71e2988a --- /dev/null +++ b/pwnagotchi/ui/hw/libs/adafruit/minipitft/ST7789.py @@ -0,0 +1,360 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import numbers +import time +import numpy as np + +import spidev +import RPi.GPIO as GPIO + + +__version__ = '0.0.4' + +BG_SPI_CS_BACK = 0 +BG_SPI_CS_FRONT = 1 + +SPI_CLOCK_HZ = 16000000 + +ST7789_NOP = 0x00 +ST7789_SWRESET = 0x01 +ST7789_RDDID = 0x04 +ST7789_RDDST = 0x09 + +ST7789_SLPIN = 0x10 +ST7789_SLPOUT = 0x11 +ST7789_PTLON = 0x12 +ST7789_NORON = 0x13 + +ST7789_INVOFF = 0x20 +ST7789_INVON = 0x21 +ST7789_DISPOFF = 0x28 +ST7789_DISPON = 0x29 + +ST7789_CASET = 0x2A +ST7789_RASET = 0x2B +ST7789_RAMWR = 0x2C +ST7789_RAMRD = 0x2E + +ST7789_PTLAR = 0x30 +ST7789_MADCTL = 0x36 +ST7789_COLMOD = 0x3A + +ST7789_FRMCTR1 = 0xB1 +ST7789_FRMCTR2 = 0xB2 +ST7789_FRMCTR3 = 0xB3 +ST7789_INVCTR = 0xB4 +ST7789_DISSET5 = 0xB6 + +ST7789_GCTRL = 0xB7 +ST7789_GTADJ = 0xB8 +ST7789_VCOMS = 0xBB + +ST7789_LCMCTRL = 0xC0 +ST7789_IDSET = 0xC1 +ST7789_VDVVRHEN = 0xC2 +ST7789_VRHS = 0xC3 +ST7789_VDVS = 0xC4 +ST7789_VMCTR1 = 0xC5 +ST7789_FRCTRL2 = 0xC6 +ST7789_CABCCTRL = 0xC7 + +ST7789_RDID1 = 0xDA +ST7789_RDID2 = 0xDB +ST7789_RDID3 = 0xDC +ST7789_RDID4 = 0xDD + +ST7789_GMCTRP1 = 0xE0 +ST7789_GMCTRN1 = 0xE1 + +ST7789_PWCTR6 = 0xFC + + +class ST7789(object): + """Representation of an ST7789 TFT LCD.""" + + def __init__(self, port, cs, dc, backlight, rst=None, width=240, + height=240, rotation=90, invert=True, spi_speed_hz=60 * 1000 * 1000, + offset_left=0, + offset_top=0): + """Create an instance of the display using SPI communication. + + Must provide the GPIO pin number for the D/C pin and the SPI driver. + + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + + :param port: SPI port number + :param cs: SPI chip-select number (0 or 1 for BCM + :param backlight: Pin for controlling backlight + :param rst: Reset pin for ST7789 + :param width: Width of display connected to ST7789 + :param height: Height of display connected to ST7789 + :param rotation: Rotation of display connected to ST7789 + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + + """ + if rotation not in [0, 90, 180, 270]: + raise ValueError("Invalid rotation {}".format(rotation)) + + if width != height and rotation in [90, 270]: + raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height)) + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + self._spi = spidev.SpiDev(port, cs) + self._spi.mode = 0 + self._spi.lsbfirst = False + self._spi.max_speed_hz = spi_speed_hz + + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = offset_left + self._offset_top = offset_top + + # Set DC as output. + GPIO.setup(dc, GPIO.OUT) + + # Setup backlight as output (if provided). + self._backlight = backlight + if backlight is not None: + GPIO.setup(backlight, GPIO.OUT) + GPIO.output(backlight, GPIO.LOW) + time.sleep(0.1) + GPIO.output(backlight, GPIO.HIGH) + + # Setup reset as output (if provided). + if rst is not None: + GPIO.setup(self._rst, GPIO.OUT) + self.reset() + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + GPIO.output(self._dc, is_data) + # Convert scalar argument to list so either can be passed as parameter. + if isinstance(data, numbers.Number): + data = [data & 0xFF] + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start + chunk_size, len(data)) + self._spi.xfer(data[start:end]) + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + GPIO.output(self._backlight, value) + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + GPIO.output(self._rst, 1) + time.sleep(0.500) + GPIO.output(self._rst, 0) + time.sleep(0.500) + GPIO.output(self._rst, 1) + time.sleep(0.500) + + def _init(self): + # Initialize the display. + + self.command(ST7789_SWRESET) # Software reset + time.sleep(0.150) # delay 150 ms + + self.command(ST7789_MADCTL) + self.data(0x70) + + self.command(ST7789_FRMCTR2) # Frame rate ctrl - idle mode + self.data(0x0C) + self.data(0x0C) + self.data(0x00) + self.data(0x33) + self.data(0x33) + + self.command(ST7789_COLMOD) + self.data(0x05) + + self.command(ST7789_GCTRL) + self.data(0x14) + + self.command(ST7789_VCOMS) + self.data(0x37) + + self.command(ST7789_LCMCTRL) # Power control + self.data(0x2C) + + self.command(ST7789_VDVVRHEN) # Power control + self.data(0x01) + + self.command(ST7789_VRHS) # Power control + self.data(0x12) + + self.command(ST7789_VDVS) # Power control + self.data(0x20) + + self.command(0xD0) + self.data(0xA4) + self.data(0xA1) + + self.command(ST7789_FRCTRL2) + self.data(0x0F) + + self.command(ST7789_GMCTRP1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0D) + self.data(0x11) + self.data(0x13) + self.data(0x2B) + self.data(0x3F) + self.data(0x54) + self.data(0x4C) + self.data(0x18) + self.data(0x0D) + self.data(0x0B) + self.data(0x1F) + self.data(0x23) + + self.command(ST7789_GMCTRN1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0C) + self.data(0x11) + self.data(0x13) + self.data(0x2C) + self.data(0x3F) + self.data(0x44) + self.data(0x51) + self.data(0x2F) + self.data(0x1F) + self.data(0x1F) + self.data(0x20) + self.data(0x23) + + if self._invert: + self.command(ST7789_INVON) # Invert display + else: + self.command(ST7789_INVOFF) # Don't invert display + + self.command(ST7789_SLPOUT) + + self.command(ST7789_DISPON) # Display on + time.sleep(0.100) # 100 ms + + def begin(self): + """Set up the display + + Deprecated. Included in __init__. + + """ + pass + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to width-1,height-1. + """ + if x1 is None: + x1 = self._width - 1 + + if y1 is None: + y1 = self._height - 1 + + y0 += self._offset_top + y1 += self._offset_top + + x0 += self._offset_left + x1 += self._offset_left + + self.command(ST7789_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ST7789_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ST7789_RAMWR) # write to RAM + + def display(self, image): + """Write the provided image to the hardware. + + :param image: Should be RGB format and the same dimensions as the display hardware. + + """ + # Set address bounds to entire display. + self.set_window() + + # Convert image to 16bit RGB565 format and + # flatten into bytes. + pixelbytes = self.image_to_data(image, self._rotation) + + # Write data to hardware. + for i in range(0, len(pixelbytes), 4096): + self.data(pixelbytes[i:i + 4096]) + + def image_to_data(self, image, rotation=0): + if not isinstance(image, np.ndarray): + image = np.array(image.convert('RGB')) + + # Rotate the image + pb = np.rot90(image, rotation // 90).astype('uint16') + + # Mask and shift the 888 RGB into 565 RGB + red = (pb[..., [0]] & 0xf8) << 8 + green = (pb[..., [1]] & 0xfc) << 3 + blue = (pb[..., [2]] & 0xf8) >> 3 + + # Stick 'em together + result = red | green | blue + + # Output the raw bytes + return result.byteswap().tobytes() diff --git a/pwnagotchi/ui/hw/libs/adafruit/minipitft2/ST7789.py b/pwnagotchi/ui/hw/libs/adafruit/minipitft2/ST7789.py new file mode 100644 index 00000000..4d2ae0b6 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/adafruit/minipitft2/ST7789.py @@ -0,0 +1,360 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import numbers +import time +import numpy as np + +import spidev +import RPi.GPIO as GPIO + + +__version__ = '0.0.4' + +BG_SPI_CS_BACK = 0 +BG_SPI_CS_FRONT = 1 + +SPI_CLOCK_HZ = 16000000 + +ST7789_NOP = 0x00 +ST7789_SWRESET = 0x01 +ST7789_RDDID = 0x04 +ST7789_RDDST = 0x09 + +ST7789_SLPIN = 0x10 +ST7789_SLPOUT = 0x11 +ST7789_PTLON = 0x12 +ST7789_NORON = 0x13 + +ST7789_INVOFF = 0x20 +ST7789_INVON = 0x21 +ST7789_DISPOFF = 0x28 +ST7789_DISPON = 0x29 + +ST7789_CASET = 0x2A +ST7789_RASET = 0x2B +ST7789_RAMWR = 0x2C +ST7789_RAMRD = 0x2E + +ST7789_PTLAR = 0x30 +ST7789_MADCTL = 0x36 +ST7789_COLMOD = 0x3A + +ST7789_FRMCTR1 = 0xB1 +ST7789_FRMCTR2 = 0xB2 +ST7789_FRMCTR3 = 0xB3 +ST7789_INVCTR = 0xB4 +ST7789_DISSET5 = 0xB6 + +ST7789_GCTRL = 0xB7 +ST7789_GTADJ = 0xB8 +ST7789_VCOMS = 0xBB + +ST7789_LCMCTRL = 0xC0 +ST7789_IDSET = 0xC1 +ST7789_VDVVRHEN = 0xC2 +ST7789_VRHS = 0xC3 +ST7789_VDVS = 0xC4 +ST7789_VMCTR1 = 0xC5 +ST7789_FRCTRL2 = 0xC6 +ST7789_CABCCTRL = 0xC7 + +ST7789_RDID1 = 0xDA +ST7789_RDID2 = 0xDB +ST7789_RDID3 = 0xDC +ST7789_RDID4 = 0xDD + +ST7789_GMCTRP1 = 0xE0 +ST7789_GMCTRN1 = 0xE1 + +ST7789_PWCTR6 = 0xFC + + +class ST7789(object): + """Representation of an ST7789 TFT LCD.""" + + def __init__(self, port, cs, dc, backlight, rst=None, width=240, + height=135, rotation=0, invert=True, spi_speed_hz=60 * 1000 * 1000, + offset_left=0, + offset_top=0): + """Create an instance of the display using SPI communication. + + Must provide the GPIO pin number for the D/C pin and the SPI driver. + + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + + :param port: SPI port number + :param cs: SPI chip-select number (0 or 1 for BCM + :param backlight: Pin for controlling backlight + :param rst: Reset pin for ST7789 + :param width: Width of display connected to ST7789 + :param height: Height of display connected to ST7789 + :param rotation: Rotation of display connected to ST7789 + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + + """ + if rotation not in [0, 90, 180, 270]: + raise ValueError("Invalid rotation {}".format(rotation)) + + if width != height and rotation in [90, 270]: + raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height)) + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + self._spi = spidev.SpiDev(port, cs) + self._spi.mode = 0 + self._spi.lsbfirst = False + self._spi.max_speed_hz = spi_speed_hz + + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = offset_left + self._offset_top = offset_top + + # Set DC as output. + GPIO.setup(dc, GPIO.OUT) + + # Setup backlight as output (if provided). + self._backlight = backlight + if backlight is not None: + GPIO.setup(backlight, GPIO.OUT) + GPIO.output(backlight, GPIO.LOW) + time.sleep(0.1) + GPIO.output(backlight, GPIO.HIGH) + + # Setup reset as output (if provided). + if rst is not None: + GPIO.setup(self._rst, GPIO.OUT) + self.reset() + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + GPIO.output(self._dc, is_data) + # Convert scalar argument to list so either can be passed as parameter. + if isinstance(data, numbers.Number): + data = [data & 0xFF] + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start + chunk_size, len(data)) + self._spi.xfer(data[start:end]) + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + GPIO.output(self._backlight, value) + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + GPIO.output(self._rst, 1) + time.sleep(0.500) + GPIO.output(self._rst, 0) + time.sleep(0.500) + GPIO.output(self._rst, 1) + time.sleep(0.500) + + def _init(self): + # Initialize the display. + + self.command(ST7789_SWRESET) # Software reset + time.sleep(0.150) # delay 150 ms + + self.command(ST7789_MADCTL) + self.data(0x70) + + self.command(ST7789_FRMCTR2) # Frame rate ctrl - idle mode + self.data(0x0C) + self.data(0x0C) + self.data(0x00) + self.data(0x33) + self.data(0x33) + + self.command(ST7789_COLMOD) + self.data(0x05) + + self.command(ST7789_GCTRL) + self.data(0x14) + + self.command(ST7789_VCOMS) + self.data(0x37) + + self.command(ST7789_LCMCTRL) # Power control + self.data(0x2C) + + self.command(ST7789_VDVVRHEN) # Power control + self.data(0x01) + + self.command(ST7789_VRHS) # Power control + self.data(0x12) + + self.command(ST7789_VDVS) # Power control + self.data(0x20) + + self.command(0xD0) + self.data(0xA4) + self.data(0xA1) + + self.command(ST7789_FRCTRL2) + self.data(0x0F) + + self.command(ST7789_GMCTRP1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0D) + self.data(0x11) + self.data(0x13) + self.data(0x2B) + self.data(0x3F) + self.data(0x54) + self.data(0x4C) + self.data(0x18) + self.data(0x0D) + self.data(0x0B) + self.data(0x1F) + self.data(0x23) + + self.command(ST7789_GMCTRN1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0C) + self.data(0x11) + self.data(0x13) + self.data(0x2C) + self.data(0x3F) + self.data(0x44) + self.data(0x51) + self.data(0x2F) + self.data(0x1F) + self.data(0x1F) + self.data(0x20) + self.data(0x23) + + if self._invert: + self.command(ST7789_INVON) # Invert display + else: + self.command(ST7789_INVOFF) # Don't invert display + + self.command(ST7789_SLPOUT) + + self.command(ST7789_DISPON) # Display on + time.sleep(0.100) # 100 ms + + def begin(self): + """Set up the display + + Deprecated. Included in __init__. + + """ + pass + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to width-1,height-1. + """ + if x1 is None: + x1 = self._width - 1 + + if y1 is None: + y1 = self._height - 1 + + y0 += self._offset_top + y1 += self._offset_top + + x0 += self._offset_left + x1 += self._offset_left + + self.command(ST7789_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ST7789_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ST7789_RAMWR) # write to RAM + + def display(self, image): + """Write the provided image to the hardware. + + :param image: Should be RGB format and the same dimensions as the display hardware. + + """ + # Set address bounds to entire display. + self.set_window() + + # Convert image to 16bit RGB565 format and + # flatten into bytes. + pixelbytes = self.image_to_data(image, self._rotation) + + # Write data to hardware. + for i in range(0, len(pixelbytes), 4096): + self.data(pixelbytes[i:i + 4096]) + + def image_to_data(self, image, rotation=0): + if not isinstance(image, np.ndarray): + image = np.array(image.convert('RGB')) + + # Rotate the image + pb = np.rot90(image, rotation // 90).astype('uint16') + + # Mask and shift the 888 RGB into 565 RGB + red = (pb[..., [0]] & 0xf8) << 8 + green = (pb[..., [1]] & 0xfc) << 3 + blue = (pb[..., [2]] & 0xf8) >> 3 + + # Stick 'em together + result = red | green | blue + + # Output the raw bytes + return result.byteswap().tobytes() diff --git a/pwnagotchi/ui/hw/libs/argon/argonpod/ILI9341.py b/pwnagotchi/ui/hw/libs/argon/argonpod/ILI9341.py new file mode 100644 index 00000000..6ef1c074 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/argon/argonpod/ILI9341.py @@ -0,0 +1,362 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Modified for Pwnagotchi by RasTacsko +# Based on ST7899 driver for pimoroni displayhatmini by Do-Ki + +import numbers +import time +import numpy as np + +from PIL import Image +from PIL import ImageDraw + +import spidev +import RPi.GPIO as GPIO + +__version__ = '0.0.1' + +# Constants for interacting with display registers. +ILI9341_TFTWIDTH = 320 +ILI9341_TFTHEIGHT = 240 + +ILI9341_NOP = 0x00 +ILI9341_SWRESET = 0x01 +ILI9341_RDDID = 0x04 +ILI9341_RDDST = 0x09 + +ILI9341_SLPIN = 0x10 +ILI9341_SLPOUT = 0x11 +ILI9341_PTLON = 0x12 +ILI9341_NORON = 0x13 + +ILI9341_RDMODE = 0x0A +ILI9341_RDMADCTL = 0x0B +ILI9341_RDPIXFMT = 0x0C +ILI9341_RDIMGFMT = 0x0A +ILI9341_RDSELFDIAG = 0x0F + +ILI9341_INVOFF = 0x20 +ILI9341_INVON = 0x21 +ILI9341_GAMMASET = 0x26 +ILI9341_DISPOFF = 0x28 +ILI9341_DISPON = 0x29 + +ILI9341_CASET = 0x2A +ILI9341_PASET = 0x2B +ILI9341_RAMWR = 0x2C +ILI9341_RAMRD = 0x2E + +ILI9341_PTLAR = 0x30 +ILI9341_MADCTL = 0x36 +ILI9341_PIXFMT = 0x3A + +ILI9341_FRMCTR1 = 0xB1 +ILI9341_FRMCTR2 = 0xB2 +ILI9341_FRMCTR3 = 0xB3 +ILI9341_INVCTR = 0xB4 +ILI9341_DFUNCTR = 0xB6 + +ILI9341_PWCTR1 = 0xC0 +ILI9341_PWCTR2 = 0xC1 +ILI9341_PWCTR3 = 0xC2 +ILI9341_PWCTR4 = 0xC3 +ILI9341_PWCTR5 = 0xC4 +ILI9341_VMCTR1 = 0xC5 +ILI9341_VMCTR2 = 0xC7 + +ILI9341_RDID1 = 0xDA +ILI9341_RDID2 = 0xDB +ILI9341_RDID3 = 0xDC +ILI9341_RDID4 = 0xDD + +ILI9341_GMCTRP1 = 0xE0 +ILI9341_GMCTRN1 = 0xE1 + +ILI9341_PWCTR6 = 0xFC + + +class ILI9341(object): + """Representation of an ILI9341 TFT LCD.""" + + def __init__(self, port, cs, dc, backlight, rst=None, + width=ILI9341_TFTWIDTH, height=ILI9341_TFTHEIGHT, + rotation=270, invert=False, spi_speed_hz=64000000, + offset_left=0, offset_top=0): + """Create an instance of the display using SPI communication. + Must provide the GPIO pin number for the D/C pin and the SPI driver. + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + :param port: SPI port number -> 0 + :param cs: SPI chip-select number (0 or 1 for BCM) -> 1 + :param backlight: Pin for controlling backlight -> 18 + :param rst: Reset pin for ILI9341 -> 24? + :param width: Width of display connected to ILI9341 -> 240 + :param height: Height of display connected to ILI9341 -> 320 + :param rotation: Rotation of display connected to ILI9341 + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + """ + + if rotation not in [0, 90, 180, 270]: + raise ValueError("Invalid rotation {}".format(rotation)) + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + self._spi = spidev.SpiDev(port, cs) + self._spi.mode = 0 + self._spi.lsbfirst = False + self._spi.max_speed_hz = spi_speed_hz + + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = offset_left + self._offset_top = offset_top + + # Set DC as output. + GPIO.setup(dc, GPIO.OUT) + + # Setup backlight as output (if provided). + self._backlight = backlight + if backlight is not None: + GPIO.setup(backlight, GPIO.OUT) + GPIO.output(backlight, GPIO.LOW) + time.sleep(0.05) + GPIO.output(backlight, GPIO.HIGH) + + # Setup reset as output (if provided). + if rst is not None: + GPIO.setup(self._rst, GPIO.OUT) + self.reset() + + # Create an image buffer. + self.buffer = Image.new('RGB', (width, height)) + + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + GPIO.output(self._dc, is_data) + # Convert scalar argument to list so either can be passed as parameter. + if isinstance(data, numbers.Number): + data = [data & 0xFF] + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start+chunk_size, len(data)) + self._spi.xfer(data[start:end]) + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + GPIO.output(self._backlight, value) + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + GPIO.output(self._rst, 1) + time.sleep(0.005) + GPIO.output(self._rst, 0) + time.sleep(0.02) + GPIO.output(self._rst, 1) + time.sleep(0.150) + + def _init(self): + # Initialize the display. Broken out as a separate function so it can + # be overridden by other displays in the future. + self.command(0xEF) + self.data(0x03) + self.data(0x80) + self.data(0x02) + self.command(0xCF) + self.data(0x00) + self.data(0XC1) + self.data(0X30) + self.command(0xED) + self.data(0x64) + self.data(0x03) + self.data(0X12) + self.data(0X81) + self.command(0xE8) + self.data(0x85) + self.data(0x00) + self.data(0x78) + self.command(0xCB) + self.data(0x39) + self.data(0x2C) + self.data(0x00) + self.data(0x34) + self.data(0x02) + self.command(0xF7) + self.data(0x20) + self.command(0xEA) + self.data(0x00) + self.data(0x00) + self.command(ILI9341_PWCTR1) # Power control + self.data(0x23) # VRH[5:0] + self.command(ILI9341_PWCTR2) # Power control + self.data(0x10) # SAP[2:0];BT[3:0] + self.command(ILI9341_VMCTR1) # VCM control + self.data(0x3e) + self.data(0x28) + self.command(ILI9341_VMCTR2) # VCM control2 + self.data(0x86) # -- + self.command(ILI9341_MADCTL) # Memory Access Control + self.data(0x48) + self.command(ILI9341_PIXFMT) + self.data(0x55) + self.command(ILI9341_FRMCTR1) + self.data(0x00) + self.data(0x18) + self.command(ILI9341_DFUNCTR) # Display Function Control + self.data(0x08) + self.data(0x82) + self.data(0x27) + self.command(0xF2) # 3Gamma Function Disable + self.data(0x00) + self.command(ILI9341_GAMMASET) # Gamma curve selected + self.data(0x01) + self.command(ILI9341_GMCTRP1) # Set Gamma + self.data(0x0F) + self.data(0x31) + self.data(0x2B) + self.data(0x0C) + self.data(0x0E) + self.data(0x08) + self.data(0x4E) + self.data(0xF1) + self.data(0x37) + self.data(0x07) + self.data(0x10) + self.data(0x03) + self.data(0x0E) + self.data(0x09) + self.data(0x00) + self.command(ILI9341_GMCTRN1) # Set Gamma + self.data(0x00) + self.data(0x0E) + self.data(0x14) + self.data(0x03) + self.data(0x11) + self.data(0x07) + self.data(0x31) + self.data(0xC1) + self.data(0x48) + self.data(0x08) + self.data(0x0F) + self.data(0x0C) + self.data(0x31) + self.data(0x36) + self.data(0x0F) + if self._invert: + self.command(ILI9341_INVON) # Invert display + else: + self.command(ILI9341_INVOFF) # Don't invert display + self.command(ILI9341_SLPOUT) # Exit Sleep + time.sleep(0.120) + self.command(ILI9341_DISPON) # Display on + + def begin(self): + """Set up the display deprecated. + Included in __init__. """ + pass + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to 239,319. + """ + if x1 is None: + x1 = self.width-1 + if y1 is None: + y1 = self.height-1 + + self.command(ILI9341_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ILI9341_PASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ILI9341_RAMWR) # write to RAM + + def display(self, image): + """Write the provided image to the hardware. + :param image: Should be RGB format and the same dimensions as the display hardware. + """ + # Set address bounds to entire display. + self.set_window() + + # Convert image to 16bit RGB565 format and + # flatten into bytes. + pixelbytes = self.image_to_data(image, self._rotation) + + # Write data to hardware. + for i in range(0, len(pixelbytes), 4096): + self.data(pixelbytes[i:i + 4096]) + + def image_to_data(self, image, rotation=0): + if not isinstance(image, np.ndarray): + image = np.array(image.convert('RGB')) + + # Rotate the image + pb = np.rot90(image, rotation // 90).astype('uint16') + + # Mask and shift the 888 RGB into 565 RGB + red = (pb[..., [0]] & 0xf8) << 8 + green = (pb[..., [1]] & 0xfc) << 3 + blue = (pb[..., [2]] & 0xf8) >> 3 + + # Stick 'em together + result = red | green | blue + + # Output the raw bytes + return result.byteswap().tobytes() \ No newline at end of file diff --git a/pwnagotchi/ui/hw/libs/i2coled/epd.py b/pwnagotchi/ui/hw/libs/i2coled/epd.py index c35ee8c9..12023664 100644 --- a/pwnagotchi/ui/hw/libs/i2coled/epd.py +++ b/pwnagotchi/ui/hw/libs/i2coled/epd.py @@ -11,7 +11,7 @@ EPD_HEIGHT = 64 class EPD(object): - def __init__(self, address=0x3D, width=EPD_WIDTH, height=EPD_HEIGHT): + def __init__(self, address=0x3C, width=EPD_WIDTH, height=EPD_HEIGHT): self.width = width self.height = height diff --git a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py index cc554cdc..37a53e12 100644 --- a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py +++ b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py @@ -90,8 +90,8 @@ ST7789_PWCTR6 = 0xFC class ST7789(object): """Representation of an ST7789 TFT LCD.""" - def __init__(self, port, cs, dc, backlight=None, rst=None, width=240, - height=240, rotation=90, invert=True, spi_speed_hz=4000000, + def __init__(self, port, cs, dc, backlight, rst=None, width=320, + height=240, rotation=0, invert=True, spi_speed_hz=60 * 1000 * 1000, offset_left=0, offset_top=0): """Create an instance of the display using SPI communication. @@ -357,4 +357,4 @@ class ST7789(object): result = red | green | blue # Output the raw bytes - return result.byteswap().tobytes() \ No newline at end of file + return result.byteswap().tobytes() diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/backlight.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/backlight.py new file mode 100644 index 00000000..98ccc885 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/backlight.py @@ -0,0 +1,84 @@ +"""Library for the GFX HAT SN3218 backlight.""" +_sn3218 = None + +_buf = [0 for x in range(18)] + +LED_MAP = [2, 1, 0, 5, 4, 3] + + +def setup(): + """Set up the backlight on GFX HAT.""" + global _sn3218 + import sn3218 as _sn3218 + + _sn3218.enable() + _sn3218.enable_leds(0b111111111111111111) + _sn3218.output(_buf) + + +def set_pixel(x, r, g, b): + """Set a single backlight zone. + + :param x: pixel index (0 = left most, 5 = right most) + :param r: amount of red from 0 to 255 + :param g: amount of green from 0 to 255 + :param b: amount of blue from 0 to 255 + + """ + global _buf + if x > 5 or x < 0: + raise ValueError('x should be in the range 0 to 5') + + x = LED_MAP[x] + x *= 3 + _buf[x:x + 3] = b, g, r + + +def set_all(r, g, b): + """Set all backlight zones. + + :param r: amount of red from 0 to 255 + :param g: amount of green from 0 to 255 + :param b: amount of blue from 0 to 255 + + """ + for p in range(6): + set_pixel(p, r, g, b) + + +def show(): + """Show changes to the backlight.""" + setup() + _sn3218.output(_buf) + + +if __name__ == '__main__': # pragma: no cover + import time + import colorsys + + def wipe(r, g, b): # noqa D103 + for x in range(6): + set_pixel(x, r, g, b) + show() + time.sleep(0.1) + set_pixel(x, 0, 0, 0) + + wipe(255, 0, 0) + wipe(0, 255, 0) + wipe(0, 0, 255) + wipe(0, 0, 0) + + try: + while True: + t = time.time() + for x in range(6): + offset = (t * 250) + (x * 30) + r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(offset / 360.0, 1.0, 1.0)] + g = int(g * 0.8) + b = int(b * 0.8) + set_pixel(x, r, g, b) + show() + time.sleep(1.0 / 60) + + except KeyboardInterrupt: + pass diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/cap1xxx.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/cap1xxx.py new file mode 100644 index 00000000..a3fc68fe --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/cap1xxx.py @@ -0,0 +1,669 @@ +"""Cap-touch Driver Library for Microchip CAP1xxx ICs +Supports communication over i2c only. + +Currently supported ICs: +CAP1208 - 8 Inputs +CAP1188 - 8 Inputs, 8 LEDs +CAP1166 - 6 Inputs, 6 LEDs +""" + +import atexit +import signal +import threading +import time +from sys import version_info + +try: + from smbus import SMBus +except ImportError: + if version_info[0] < 3: + raise ImportError("This library requires python-smbus\nInstall with: sudo apt-get install python-smbus") + elif version_info[0] == 3: + raise ImportError("This library requires python3-smbus\nInstall with: sudo apt-get install python3-smbus") + +try: + import RPi.GPIO as GPIO +except ImportError: + raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo pip install RPi.GPIO") + +__version__ = '0.1.4' + +# DEVICE MAP +DEFAULT_ADDR = 0x28 + +# Supported devices +PID_CAP1208 = 0b01101011 +PID_CAP1188 = 0b01010000 +PID_CAP1166 = 0b01010001 + +# REGISTER MAP + +R_MAIN_CONTROL = 0x00 +R_GENERAL_STATUS = 0x02 +R_INPUT_STATUS = 0x03 +R_LED_STATUS = 0x04 +R_NOISE_FLAG_STATUS = 0x0A + +# Read-only delta counts for all inputs +R_INPUT_1_DELTA = 0x10 +R_INPUT_2_DELTA = 0x11 +R_INPUT_3_DELTA = 0x12 +R_INPUT_4_DELTA = 0x13 +R_INPUT_5_DELTA = 0x14 +R_INPUT_6_DELTA = 0x15 +R_INPUT_7_DELTA = 0x16 +R_INPUT_8_DELTA = 0x17 + +R_SENSITIVITY = 0x1F +# B7 = N/A +# B6..B4 = Sensitivity +# B3..B0 = Base Shift +SENSITIVITY = {128: 0b000, 64:0b001, 32:0b010, 16:0b011, 8:0b100, 4:0b100, 2:0b110, 1:0b111} + +R_GENERAL_CONFIG = 0x20 +# B7 = Timeout +# B6 = Wake Config ( 1 = Wake pin asserted ) +# B5 = Disable Digital Noise ( 1 = Noise threshold disabled ) +# B4 = Disable Analog Noise ( 1 = Low frequency analog noise blocking disabled ) +# B3 = Max Duration Recalibration ( 1 = Enable recalibration if touch is held longer than max duration ) +# B2..B0 = N/A + +R_INPUT_ENABLE = 0x21 + + +R_INPUT_CONFIG = 0x22 + +R_INPUT_CONFIG2 = 0x23 # Default 0x00000111 + +# Values for bits 3 to 0 of R_INPUT_CONFIG2 +# Determines minimum amount of time before +# a "press and hold" event is detected. + +# Also - Values for bits 3 to 0 of R_INPUT_CONFIG +# Determines rate at which interrupt will repeat +# +# Resolution of 35ms, max = 35 + (35 * 0b1111) = 560ms + +R_SAMPLING_CONFIG = 0x24 # Default 0x00111001 +R_CALIBRATION = 0x26 # Default 0b00000000 +R_INTERRUPT_EN = 0x27 # Default 0b11111111 +R_REPEAT_EN = 0x28 # Default 0b11111111 +R_MTOUCH_CONFIG = 0x2A # Default 0b11111111 +R_MTOUCH_PAT_CONF = 0x2B +R_MTOUCH_PATTERN = 0x2D +R_COUNT_O_LIMIT = 0x2E +R_RECALIBRATION = 0x2F + +# R/W Touch detection thresholds for inputs +R_INPUT_1_THRESH = 0x30 +R_INPUT_2_THRESH = 0x31 +R_INPUT_3_THRESH = 0x32 +R_INPUT_4_THRESH = 0x33 +R_INPUT_5_THRESH = 0x34 +R_INPUT_6_THRESH = 0x35 +R_INPUT_7_THRESH = 0x36 +R_INPUT_8_THRESH = 0x37 + +# R/W Noise threshold for all inputs +R_NOISE_THRESH = 0x38 + +# R/W Standby and Config Registers +R_STANDBY_CHANNEL = 0x40 +R_STANDBY_CONFIG = 0x41 +R_STANDBY_SENS = 0x42 +R_STANDBY_THRESH = 0x43 + +R_CONFIGURATION2 = 0x44 +# B7 = Linked LED Transition Controls ( 1 = LED trigger is !touch ) +# B6 = Alert Polarity ( 1 = Active Low Open Drain, 0 = Active High Push Pull ) +# B5 = Reduce Power ( 1 = Do not power down between poll ) +# B4 = Link Polarity/Mirror bits ( 0 = Linked, 1 = Unlinked ) +# B3 = Show RF Noise ( 1 = Noise status registers only show RF, 0 = Both RF and EMI shown ) +# B2 = Disable RF Noise ( 1 = Disable RF noise filter ) +# B1..B0 = N/A + +# Read-only reference counts for sensor inputs +R_INPUT_1_BCOUNT = 0x50 +R_INPUT_2_BCOUNT = 0x51 +R_INPUT_3_BCOUNT = 0x52 +R_INPUT_4_BCOUNT = 0x53 +R_INPUT_5_BCOUNT = 0x54 +R_INPUT_6_BCOUNT = 0x55 +R_INPUT_7_BCOUNT = 0x56 +R_INPUT_8_BCOUNT = 0x57 + +# LED Controls - For CAP1188 and similar +R_LED_OUTPUT_TYPE = 0x71 +R_LED_LINKING = 0x72 +R_LED_POLARITY = 0x73 +R_LED_OUTPUT_CON = 0x74 +R_LED_LTRANS_CON = 0x77 +R_LED_MIRROR_CON = 0x79 + +# LED Behaviour +R_LED_BEHAVIOUR_1 = 0x81 # For LEDs 1-4 +R_LED_BEHAVIOUR_2 = 0x82 # For LEDs 5-8 +R_LED_PULSE_1_PER = 0x84 +R_LED_PULSE_2_PER = 0x85 +R_LED_BREATHE_PER = 0x86 +R_LED_CONFIG = 0x88 +R_LED_PULSE_1_DUT = 0x90 +R_LED_PULSE_2_DUT = 0x91 +R_LED_BREATHE_DUT = 0x92 +R_LED_DIRECT_DUT = 0x93 +R_LED_DIRECT_RAMP = 0x94 +R_LED_OFF_DELAY = 0x95 + +# R/W Power buttonc ontrol +R_POWER_BUTTON = 0x60 +R_POW_BUTTON_CONF = 0x61 + +# Read-only upper 8-bit calibration values for sensors +R_INPUT_1_CALIB = 0xB1 +R_INPUT_2_CALIB = 0xB2 +R_INPUT_3_CALIB = 0xB3 +R_INPUT_4_CALIB = 0xB4 +R_INPUT_5_CALIB = 0xB5 +R_INPUT_6_CALIB = 0xB6 +R_INPUT_7_CALIB = 0xB7 +R_INPUT_8_CALIB = 0xB8 + +# Read-only 2 LSBs for each sensor input +R_INPUT_CAL_LSB1 = 0xB9 +R_INPUT_CAL_LSB2 = 0xBA + +# Product ID Registers +R_PRODUCT_ID = 0xFD +R_MANUFACTURER_ID = 0xFE +R_REVISION = 0xFF + +# LED Behaviour settings +LED_BEHAVIOUR_DIRECT = 0b00 +LED_BEHAVIOUR_PULSE1 = 0b01 +LED_BEHAVIOUR_PULSE2 = 0b10 +LED_BEHAVIOUR_BREATHE = 0b11 + +LED_OPEN_DRAIN = 0 # Default, LED is open-drain output with ext pullup +LED_PUSH_PULL = 1 # LED is driven HIGH/LOW with logic 1/0 + +LED_RAMP_RATE_2000MS = 7 +LED_RAMP_RATE_1500MS = 6 +LED_RAMP_RATE_1250MS = 5 +LED_RAMP_RATE_1000MS = 4 +LED_RAMP_RATE_750MS = 3 +LED_RAMP_RATE_500MS = 2 +LED_RAMP_RATE_250MS = 1 +LED_RAMP_RATE_0MS = 0 + +## Basic stoppable thread wrapper +# +# Adds Event for stopping the execution loop +# and exiting cleanly. +class StoppableThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.stop_event = threading.Event() + self.daemon = True + + def alive(self): + try: + return self.isAlive() + except AttributeError: + # Python >= 3.9 + return self.is_alive() + + def start(self): + if self.alive() == False: + self.stop_event.clear() + threading.Thread.start(self) + + def stop(self): + if self.alive() == True: + # set event to signal thread to terminate + self.stop_event.set() + # block calling thread until thread really has terminated + self.join() + +## Basic thread wrapper class for asyncronously running functions +# +# Basic thread wrapper class for running functions +# asyncronously. Return False from your function +# to abort looping. +class AsyncWorker(StoppableThread): + def __init__(self, todo): + StoppableThread.__init__(self) + self.todo = todo + + def run(self): + while self.stop_event.is_set() == False: + if self.todo() == False: + self.stop_event.set() + break + + +class CapTouchEvent(): + def __init__(self, channel, event, delta): + self.channel = channel + self.event = event + self.delta = delta + +class Cap1xxx(): + supported = [PID_CAP1208, PID_CAP1188, PID_CAP1166] + number_of_inputs = 8 + number_of_leds = 8 + + def __init__(self, i2c_addr=DEFAULT_ADDR, i2c_bus=1, alert_pin=-1, reset_pin=-1, on_touch=None, skip_init=False): + if on_touch == None: + on_touch = [None] * self.number_of_inputs + + self.async_poll = None + self.i2c_addr = i2c_addr + self.i2c = SMBus(i2c_bus) + self.alert_pin = alert_pin + self.reset_pin = reset_pin + self._delta = 50 + + GPIO.setmode(GPIO.BCM) + if not self.alert_pin == -1: + GPIO.setup(self.alert_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + if not self.reset_pin == -1: + GPIO.setup(self.reset_pin, GPIO.OUT) + GPIO.setup(self.reset_pin, GPIO.LOW) + GPIO.output(self.reset_pin, GPIO.HIGH) + time.sleep(0.01) + GPIO.output(self.reset_pin, GPIO.LOW) + + self.handlers = { + 'press' : [None] * self.number_of_inputs, + 'release' : [None] * self.number_of_inputs, + 'held' : [None] * self.number_of_inputs + } + + self.touch_handlers = on_touch + self.last_input_status = [False] * self.number_of_inputs + self.input_status = ['none'] * self.number_of_inputs + self.input_delta = [0] * self.number_of_inputs + self.input_pressed = [False] * self.number_of_inputs + self.repeat_enabled = 0b00000000 + self.release_enabled = 0b11111111 + + self.product_id = self._get_product_id() + + if not self.product_id in self.supported: + raise Exception("Product ID {} not supported!".format(self.product_id)) + + if skip_init: + return + + # Enable all inputs with interrupt by default + self.enable_inputs(0b11111111) + self.enable_interrupts(0b11111111) + + # Disable repeat for all channels, but give + # it sane defaults anyway + self.enable_repeat(0b00000000) + self.enable_multitouch(True) + + self.set_hold_delay(210) + self.set_repeat_rate(210) + + # Tested sane defaults for various configurations + self._write_byte(R_SAMPLING_CONFIG, 0b00001000) # 1sample per measure, 1.28ms time, 35ms cycle + self._write_byte(R_SENSITIVITY, 0b01100000) # 2x sensitivity + self._write_byte(R_GENERAL_CONFIG, 0b00111000) + self._write_byte(R_CONFIGURATION2, 0b01100000) + self.set_touch_delta(10) + + atexit.register(self.stop_watching) + + def get_input_status(self): + """Get the status of all inputs. + Returns an array of 8 boolean values indicating + whether an input has been triggered since the + interrupt flag was last cleared.""" + touched = self._read_byte(R_INPUT_STATUS) + threshold = self._read_block(R_INPUT_1_THRESH, self.number_of_inputs) + delta = self._read_block(R_INPUT_1_DELTA, self.number_of_inputs) + #status = ['none'] * 8 + for x in range(self.number_of_inputs): + if (1 << x) & touched: + status = 'none' + _delta = self._get_twos_comp(delta[x]) + #threshold = self._read_byte(R_INPUT_1_THRESH + x) + # We only ever want to detect PRESS events + # If repeat is disabled, and release detect is enabled + if _delta >= threshold[x]: # self._delta: + self.input_delta[x] = _delta + # Touch down event + if self.input_status[x] in ['press','held']: + if self.repeat_enabled & (1 << x): + status = 'held' + if self.input_status[x] in ['none','release']: + if self.input_pressed[x]: + status = 'none' + else: + status = 'press' + else: + # Touch release event + if self.release_enabled & (1 << x) and not self.input_status[x] == 'release': + status = 'release' + else: + status = 'none' + + self.input_status[x] = status + self.input_pressed[x] = status in ['press','held','none'] + else: + self.input_status[x] = 'none' + self.input_pressed[x] = False + return self.input_status + + def _get_twos_comp(self,val): + if ( val & (1<< (8 - 1))) != 0: + val = val - (1 << 8) + return val + + def clear_interrupt(self): + """Clear the interrupt flag, bit 0, of the + main control register""" + main = self._read_byte(R_MAIN_CONTROL) + main &= ~0b00000001 + self._write_byte(R_MAIN_CONTROL, main) + + def _interrupt_status(self): + if self.alert_pin == -1: + return self._read_byte(R_MAIN_CONTROL) & 1 + else: + return not GPIO.input(self.alert_pin) + + def wait_for_interrupt(self, timeout=100): + """Wait for, interrupt, bit 0 of the main + control register to be set, indicating an + input has been triggered.""" + start = self._millis() + while True: + status = self._interrupt_status() # self._read_byte(R_MAIN_CONTROL) + if status: + return True + if self._millis() > start + timeout: + return False + time.sleep(0.005) + + def on(self, channel=0, event='press', handler=None): + self.handlers[event][channel] = handler + self.start_watching() + return True + + def start_watching(self): + if not self.alert_pin == -1: + try: + GPIO.add_event_detect(self.alert_pin, GPIO.FALLING, callback=self._handle_alert, bouncetime=1) + self.clear_interrupt() + except: + pass + return True + + if self.async_poll == None: + self.async_poll = AsyncWorker(self._poll) + self.async_poll.start() + return True + return False + + def stop_watching(self): + if not self.alert_pin == -1: + GPIO.remove_event_detect(self.alert_pin) + + if not self.async_poll == None: + self.async_poll.stop() + self.async_poll = None + return True + return False + + def set_touch_delta(self, delta): + self._delta = delta + + def auto_recalibrate(self, value): + self._change_bit(R_GENERAL_CONFIG, 3, value) + + def filter_analog_noise(self, value): + self._change_bit(R_GENERAL_CONFIG, 4, not value) + + def filter_digital_noise(self, value): + self._change_bit(R_GENERAL_CONFIG, 5, not value) + + def set_hold_delay(self, ms): + """Set time before a press and hold is detected, + Clamps to multiples of 35 from 35 to 560""" + repeat_rate = self._calc_touch_rate(ms) + input_config = self._read_byte(R_INPUT_CONFIG2) + input_config = (input_config & ~0b1111) | repeat_rate + self._write_byte(R_INPUT_CONFIG2, input_config) + + def set_repeat_rate(self, ms): + """Set repeat rate in milliseconds, + Clamps to multiples of 35 from 35 to 560""" + repeat_rate = self._calc_touch_rate(ms) + input_config = self._read_byte(R_INPUT_CONFIG) + input_config = (input_config & ~0b1111) | repeat_rate + self._write_byte(R_INPUT_CONFIG, input_config) + + def _calc_touch_rate(self, ms): + ms = min(max(ms,0),560) + scale = int((round(ms / 35.0) * 35) - 35) / 35 + return int(scale) + + def _handle_alert(self, pin=-1): + inputs = self.get_input_status() + self.clear_interrupt() + for x in range(self.number_of_inputs): + self._trigger_handler(x, inputs[x]) + + def _poll(self): + """Single polling pass, should be called in + a loop, preferably threaded.""" + if self.wait_for_interrupt(): + self._handle_alert() + + def _trigger_handler(self, channel, event): + if event == 'none': + return + if callable(self.handlers[event][channel]): + try: + self.handlers[event][channel](CapTouchEvent(channel, event, self.input_delta[channel])) + except TypeError: + self.handlers[event][channel](channel, event) + + def _get_product_id(self): + return self._read_byte(R_PRODUCT_ID) + + def enable_multitouch(self, en=True): + """Toggles multi-touch by toggling the multi-touch + block bit in the config register""" + ret_mt = self._read_byte(R_MTOUCH_CONFIG) + if en: + self._write_byte(R_MTOUCH_CONFIG, ret_mt & ~0x80) + else: + self._write_byte(R_MTOUCH_CONFIG, ret_mt | 0x80 ) + + def enable_repeat(self, inputs): + self.repeat_enabled = inputs + self._write_byte(R_REPEAT_EN, inputs) + + def enable_interrupts(self, inputs): + self._write_byte(R_INTERRUPT_EN, inputs) + + def enable_inputs(self, inputs): + self._write_byte(R_INPUT_ENABLE, inputs) + + def _write_byte(self, register, value): + self.i2c.write_byte_data(self.i2c_addr, register, value) + + def _read_byte(self, register): + return self.i2c.read_byte_data(self.i2c_addr, register) + + def _read_block(self, register, length): + return self.i2c.read_i2c_block_data(self.i2c_addr, register, length) + + def _millis(self): + return int(round(time.time() * 1000)) + + def _set_bit(self, register, bit): + self._write_byte( register, self._read_byte(register) | (1 << bit) ) + + def _clear_bit(self, register, bit): + self._write_byte( register, self._read_byte(register) & ~(1 << bit ) ) + + def _change_bit(self, register, bit, state): + if state: + self._set_bit(register, bit) + else: + self._clear_bit(register, bit) + + def _change_bits(self, register, offset, size, bits): + original_value = self._read_byte(register) + for x in range(size): + original_value &= ~(1 << (offset+x)) + original_value |= (bits << offset) + self._write_byte(register, original_value) + + def __del__(self): + self.stop_watching() + +class Cap1xxxLeds(Cap1xxx): + def set_led_linking(self, led_index, state): + if led_index >= self.number_of_leds: + return False + self._change_bit(R_LED_LINKING, led_index, state) + + def set_led_output_type(self, led_index, state): + if led_index >= self.number_of_leds: + return False + self._change_bit(R_LED_OUTPUT_TYPE, led_index, state) + + def set_led_state(self, led_index, state): + if led_index >= self.number_of_leds: + return False + self._change_bit(R_LED_OUTPUT_CON, led_index, state) + + def set_led_polarity(self, led_index, state): + if led_index >= self.number_of_leds: + return False + self._change_bit(R_LED_POLARITY, led_index, state) + + def set_led_behaviour(self, led_index, value): + '''Set the behaviour of a LED''' + offset = (led_index * 2) % 8 + register = led_index / 4 + value &= 0b00000011 + self._change_bits(R_LED_BEHAVIOUR_1 + register, offset, 2, value) + + def set_led_pulse1_period(self, period_in_seconds): + '''Set the overall period of a pulse from 32ms to 4.064 seconds''' + period_in_seconds = min(period_in_seconds, 4.064) + value = int(period_in_seconds * 1000.0 / 32.0) & 0b01111111 + self._change_bits(R_LED_PULSE_1_PER, 0, 7, value) + + def set_led_pulse2_period(self, period_in_seconds): + '''Set the overall period of a pulse from 32ms to 4.064 seconds''' + period_in_seconds = min(period_in_seconds, 4.064) + value = int(period_in_seconds * 1000.0 / 32.0) & 0b01111111 + self._change_bits(R_PULSE_LED_2_PER, 0, 7, value) + + def set_led_breathe_period(self, period_in_seconds): + period_in_seconds = min(period_in_seconds, 4.064) + value = int(period_in_seconds * 1000.0 / 32.0) & 0b01111111 + self._change_bits(R_LED_BREATHE_PER, 0, 7, value) + + def set_led_pulse1_count(self, count): + count -= 1 + count &= 0b111 + self._change_bits(R_LED_CONFIG, 0, 3, count) + + def set_led_pulse2_count(self, count): + count -= 1 + count &= 0b111 + self._change_bits(R_LED_CONFIG, 3, 3, count) + + def set_led_ramp_alert(self, value): + self._change_bit(R_LED_CONFIG, 6, value) + + def set_led_direct_ramp_rate(self, rise_rate=0, fall_rate=0): + '''Set the rise/fall rate in ms, max 2000. + + Rounds input to the nearest valid value. + + Valid values are 0, 250, 500, 750, 1000, 1250, 1500, 2000 + + ''' + rise_rate = int(round(rise_rate / 250.0)) + fall_rate = int(round(fall_rate / 250.0)) + + rise_rate = min(7, rise_rate) + fall_rate = min(7, fall_rate) + + rate = (rise_rate << 4) | fall_rate + self._write_byte(R_LED_DIRECT_RAMP, rate) + + def set_led_direct_duty(self, duty_min, duty_max): + value = (duty_max << 4) | duty_min + self._write_byte(R_LED_DIRECT_DUT, value) + + def set_led_pulse1_duty(self, duty_min, duty_max): + value = (duty_max << 4) | duty_min + self._write_byte(R_LED_PULSE_1_DUT, value) + + def set_led_pulse2_duty(self, duty_min, duty_max): + value = (duty_max << 4) | duty_min + self._write_byte(R_LED_PULSE_2_DUT, value) + + def set_led_breathe_duty(self, duty_min, duty_max): + value = (duty_max << 4) | duty_min + self._write_byte(R_LED_BREATHE_DUT, value) + + def set_led_direct_min_duty(self, value): + self._change_bits(R_LED_DIRECT_DUT, 0, 4, value) + + def set_led_direct_max_duty(self, value): + self._change_bits(R_LED_DIRECT_DUT, 4, 4, value) + + def set_led_breathe_min_duty(self, value): + self._change_bits(R_LED_BREATHE_DUT, 0, 4, value) + + def set_led_breathe_max_duty(self, value): + self._change_bits(R_LED_BREATHE_DUT, 4, 4, value) + + def set_led_pulse1_min_duty(self, value): + self._change_bits(R_LED_PULSE_1_DUT, 0, 4, value) + + def set_led_pulse1_max_duty(self, value): + self._change_bits(R_LED_PULSE_1_DUT, 4, 4, value) + + def set_led_pulse2_min_duty(self, value): + self._change_bits(R_LED_PULSE_2_DUT, 0, 4, value) + + def set_led_pulse2_max_duty(self, value): + self._change_bits(R_LED_PULSE_2_DUT, 4, 4, value) + +class Cap1208(Cap1xxx): + supported = [PID_CAP1208] + +class Cap1188(Cap1xxxLeds): + number_of_leds = 8 + supported = [PID_CAP1188] + +class Cap1166(Cap1xxxLeds): + number_of_inputs = 6 + number_of_leds = 6 + supported = [PID_CAP1166] + +def DetectCap(i2c_addr, i2c_bus, product_id): + bus = SMBus(i2c_bus) + + try: + if bus.read_byte_data(i2c_addr, R_PRODUCT_ID) == product_id: + return True + else: + return False + except IOError: + return False + \ No newline at end of file diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/epd.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/epd.py new file mode 100644 index 00000000..48768e0f --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/epd.py @@ -0,0 +1,55 @@ +from . import st7567 +from . import backlight +CONTRAST = 40 + +# Define RGB colors +WHITE = (255, 255, 255) +GREY = (255, 255, 255) +MAROON = (128, 0, 0) +RED = (255, 0, 0) +PURPLE = (128, 0, 128) +FUCHSIA = (255, 0, 255) +GREEN = (0, 128, 0) +LIME = (0, 255, 0) +OLIVE = (128, 128, 0) +YELLOW = (255, 255, 0) +NAVY = (0, 0, 128) +BLUE = (0, 0, 255) +TEAL = (0, 128, 128) +AQUA = (0, 255, 255) + +# Map color names to RGB values +color_map = { + 'WHITE': WHITE, + 'GREY' : GREY, + 'MAROON': MAROON, + 'RED': RED, + 'PURPLE': PURPLE, + 'FUCHSIA': FUCHSIA, + 'GREEN' : GREEN, + 'LIME' : LIME, + 'OLIVE' : OLIVE, + 'YELLOW' : YELLOW, + 'NAVY' : NAVY, + 'BLUE' : BLUE, + 'TEAL' : TEAL, + 'AQUA' : AQUA +} + +class EPD(object): + + def __init__(self, contrast=CONTRAST, blcolor=('OLIVE')): + self.disp = st7567.ST7567() + self.disp.contrast(contrast) + + def Init(self, color_name): + self.disp.setup() + blcolor = color_map.get(color_name.upper(), OLIVE) # Default to olive if color not found + backlight.set_all(*blcolor) + backlight.show() + + def Clear(self): + self.disp.clear() + + def Display(self, image): + self.disp.show(image) \ No newline at end of file diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/lcd.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/lcd.py new file mode 100644 index 00000000..9ba0e4aa --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/lcd.py @@ -0,0 +1,57 @@ +"""Library for the GFX HAT ST7567 SPI LCD.""" +from .st7567 import ST7567 + +st7567 = ST7567() + +dimensions = st7567.dimensions + + +def clear(): + """Clear GFX HAT's display buffer.""" + st7567.clear() + + +def set_pixel(x, y, value): + """Set a single pixel in GTX HAT's display buffer. + + :param x: X position (from 0 to 127) + :param y: Y position (from 0 to 63) + :param value: pixel state 1 = On, 0 = Off + + """ + st7567.set_pixel(x, y, value) + + +def show(): + """Update GFX HAT with the current buffer contents.""" + st7567.show() + + +def contrast(value): + """Change GFX HAT LCD contrast.""" + st7567.contrast(value) + + +def rotation(r=0): + """Set the display rotation. + + :param r: Specify the rotation in degrees: 0, or 180 + + """ + if r == 0: + st7567.rotated = False + + elif r == 180: + st7567.rotated = True + + else: + raise ValueError('Rotation must be 0 or 180 degrees') + + +def get_rotation(): + """Get the display rotation value. + + Returns an integer, either 0, or 180 + + """ + return 180 if st7567.rotated else 0 diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/st7567.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/st7567.py new file mode 100644 index 00000000..0857db14 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/st7567.py @@ -0,0 +1,210 @@ +"""Library for the ST7567 128x64 SPI LCD.""" +import RPi.GPIO as GPIO +import spidev +import time +import random + +SPI_SPEED_HZ = 1000000 + +WIDTH = 128 +HEIGHT = 64 + +PIN_CS = 8 +PIN_RST = 5 +PIN_DC = 6 + +ST7567_PAGESIZE = 128 + +ST7567_DISPOFF = 0xae # 0xae: Display OFF (sleep mode) */ +ST7567_DISPON = 0xaf # 0xaf: Display ON in normal mode */ + +ST7567_SETSTARTLINE = 0x40 # 0x40-7f: Set display start line */ +ST7567_STARTLINE_MASK = 0x3f + +ST7567_REG_RATIO = 0x20 + +ST7567_SETPAGESTART = 0xb0 # 0xb0-b7: Set page start address */ +ST7567_PAGESTART_MASK = 0x07 + +ST7567_SETCOLL = 0x00 # 0x00-0x0f: Set lower column address */ +ST7567_COLL_MASK = 0x0f +ST7567_SETCOLH = 0x10 # 0x10-0x1f: Set higher column address */ +ST7567_COLH_MASK = 0x0f + +ST7567_SEG_DIR_NORMAL = 0xa0 # 0xa0: Column address 0 is mapped to SEG0 */ +ST7567_SEG_DIR_REV = 0xa1 # 0xa1: Column address 128 is mapped to SEG0 */ + +ST7567_DISPNORMAL = 0xa6 # 0xa6: Normal display */ +ST7567_DISPINVERSE = 0xa7 # 0xa7: Inverse display */ + +ST7567_DISPRAM = 0xa4 # 0xa4: Resume to RAM content display */ +ST7567_DISPENTIRE = 0xa5 # 0xa5: Entire display ON */ + +ST7567_BIAS_1_9 = 0xa2 # 0xa2: Select BIAS setting 1/9 */ +ST7567_BIAS_1_7 = 0xa3 # 0xa3: Select BIAS setting 1/7 */ + +ST7567_ENTER_RMWMODE = 0xe0 # 0xe0: Enter the Read Modify Write mode */ +ST7567_EXIT_RMWMODE = 0xee # 0xee: Leave the Read Modify Write mode */ +ST7567_EXIT_SOFTRST = 0xe2 # 0xe2: Software RESET */ + +ST7567_SETCOMNORMAL = 0xc0 # 0xc0: Set COM output direction, normal mode */ +ST7567_SETCOMREVERSE = 0xc8 # 0xc8: Set COM output direction, reverse mode */ + +ST7567_POWERCTRL_VF = 0x29 # 0x29: Control built-in power circuit */ +ST7567_POWERCTRL_VR = 0x2a # 0x2a: Control built-in power circuit */ +ST7567_POWERCTRL_VB = 0x2c # 0x2c: Control built-in power circuit */ +ST7567_POWERCTRL = 0x2f # 0x2c: Control built-in power circuit */ + +ST7567_REG_RES_RR0 = 0x21 # 0x21: Regulation Resistior ratio */ +ST7567_REG_RES_RR1 = 0x22 # 0x22: Regulation Resistior ratio */ +ST7567_REG_RES_RR2 = 0x24 # 0x24: Regulation Resistior ratio */ + +ST7567_SETCONTRAST = 0x81 # 0x81: Set contrast control */ + +ST7567_SETBOOSTER = 0xf8 # Set booster level */ +ST7567_SETBOOSTER4X = 0x00 # Set booster level */ +ST7567_SETBOOSTER5X = 0x01 # Set booster level */ + +ST7567_NOP = 0xe3 # 0xe3: NOP Command for no operation */ + +ST7565_STARTBYTES = 0 + + +class ST7567(object): + """Class to drive the ST7567 128x64 SPI LCD.""" + + def __init__(self, pin_rst=PIN_RST, pin_dc=PIN_DC, spi_bus=0, spi_cs=0, spi_speed=SPI_SPEED_HZ): + """Initialise the ST7567 class. + + :param pin_rst: BCM GPIO pin number for reset + :param pin_dc: BCM GPIO pin number for data/command + :param spi_bus: SPI bus ID + :param spi_cs: SPI chipselect ID (0/1 not BCM pin number) + :param spi_speed: SPI speed (hz) + + """ + self._is_setup = False + self.pin_rst = pin_rst + self.pin_dc = pin_dc + self.spi_bus = spi_bus + self.spi_cs = spi_cs + self.spi_speed = spi_speed + + self.rotated = False + + self.clear() + + def setup(self): + """Set up GPIO and initialise the ST7567 device.""" + if self._is_setup: + return True + + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + GPIO.setup(self.pin_rst, GPIO.OUT) + GPIO.setup(self.pin_dc, GPIO.OUT) + + self.spi = spidev.SpiDev() + self.spi.open(self.spi_bus, self.spi_cs) + self.spi.max_speed_hz = self.spi_speed + + self._reset() + self._init() + + self._is_setup = True + + def dimensions(self): + """Return the ST7567 display dimensions.""" + return (WIDTH, HEIGHT) + + def clear(self): + """Clear the python display buffer.""" + self.buf = [0 for _ in range(128 * 64 // 8)] + + def _command(self, data): + GPIO.output(self.pin_dc, 0) + self.spi.writebytes(data) + + def _data(self, data): + GPIO.output(self.pin_dc, 1) + self.spi.writebytes(data) + + def _reset(self): + GPIO.output(self.pin_rst, 0) + time.sleep(0.01) + GPIO.output(self.pin_rst, 1) + time.sleep(0.1) + + def _init(self): + self._command([ + ST7567_BIAS_1_7, # Bais 1/7 (0xA2 = Bias 1/9) + ST7567_SEG_DIR_NORMAL, + ST7567_SETCOMREVERSE, # Reverse COM - vertical flip + ST7567_DISPNORMAL, # Inverse display (0xA6 normal) + ST7567_SETSTARTLINE | 0, # Start at line 0 + ST7567_POWERCTRL, + ST7567_REG_RATIO | 3, + ST7567_DISPON, + ST7567_SETCONTRAST, # Set contrast + 40 # Contrast value + ]) + + def set_pixel(self, x, y, value): + """Set a single pixel in the python display buffer. + + :param x: X position (from 0 to 127) + :param y: Y position (from 0 to 63) + :param value: pixel state 1 = On, 0 = Off + + """ + if self.rotated: + x = (WIDTH - 1) - x + y = (HEIGHT - 1) - y + offset = ((y // 8) * WIDTH) + x + bit = y % 8 + self.buf[offset] &= ~(1 << bit) + self.buf[offset] |= (value & 1) << bit + + def show(self, image): + """Update the ST7567 display with the buffer contents.""" + width, height = self.dimensions() + + for x in range(width): + for y in range(height): + pixel = image.getpixel((x, y)) + self.set_pixel(x, y, pixel) + self.setup() + self._command([ST7567_ENTER_RMWMODE]) + for page in range(8): + offset = page * ST7567_PAGESIZE + self._command([ST7567_SETPAGESTART | page, ST7567_SETCOLL, ST7567_SETCOLH]) + self._data(self.buf[offset:offset + ST7567_PAGESIZE]) + self._command([ST7567_EXIT_RMWMODE]) + + def contrast(self, value): + """Update the ST7568 display contrast.""" + self.setup() + self._command([ST7567_SETCONTRAST, value]) + + +if __name__ == '__main__': # pragma: no cover + st7567 = ST7567() + st7567.setup() + + for x in range(64): + st7567.set_pixel(x, x, 1) + st7567.set_pixel(64 - x, x, 1) + st7567.set_pixel(x + 2, x, 1) + st7567.show() + + time.sleep(2.0) + + try: + while True: + for x in range(128): + for y in range(64): + st7567.set_pixel(x, y, random.randint(0, 1)) + st7567.show() + + except KeyboardInterrupt: + pass diff --git a/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/touch.py b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/touch.py new file mode 100644 index 00000000..417cfa99 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/gfxhat/touch.py @@ -0,0 +1,129 @@ +"""Library for the GFX HAT Cap1166 touch controller.""" +from . import cap1xxx + +_cap1166 = None +is_setup = False + +I2C_ADDR = 0x2c + +UP = 0 +DOWN = 1 +BACK = 2 +MINUS = LEFT = 3 +SELECT = ENTER = 4 +PLUS = RIGHT = 5 + +LED_MAPPING = [5, 4, 3, 2, 1, 0] + +NAME_MAPPING = ['up', 'down', 'back', 'minus', 'select', 'plus'] + + +def setup(): + """Set up the touch input on GFX HAT.""" + global _cap1166, is_setup + + if is_setup: + return + + _cap1166 = cap1xxx.Cap1166(i2c_addr=I2C_ADDR) + + for x in range(6): + _cap1166.set_led_linking(x, 0) + + # Force recalibration + _cap1166._write_byte(0x26, 0b00111111) + _cap1166._write_byte(0x1F, 0b01000000) + + is_setup = True + + +def get_name(index): + """Get the name of a touch pad from its channel index. + + :param index: Index of touch pad from 0 to 5 + + """ + return NAME_MAPPING[index] + + +def set_led(index, state): + """Set LED state. + + :param index: LED index + :param state: LED state (1 = on, 0 = off) + + """ + setup() + + _cap1166.set_led_state(LED_MAPPING[index], state) + + +def high_sensitivity(): + """Switch to high sensitivity mode. + + This predetermined high sensitivity mode is for using + touch through 3mm perspex or similar materials. + + """ + setup() + + _cap1166._write_byte(0x00, 0b11000000) + _cap1166._write_byte(0x1f, 0b00000000) + + +def enable_repeat(enable): + """Enable touch hold repeat. + + If enable is true, repeat will be enabled. This will + trigger new touch events at the set repeat_rate when + a touch input is held. + + :param enable: enable/disable repeat: True/False + + """ + setup() + + if enable: + _cap1166.enable_repeat(0b11111111) + else: + _cap1166.enable_repeat(0b00000000) + + +def set_repeat_rate(rate): + """Set hold repeat rate. + + Repeat rate values are clamped to the nearest 35ms, + values from 35 to 560 are valid. + + :param rate: time in ms from 35 to 560 + + """ + setup() + + _cap1166.set_repeat_rate(rate) + + +def on(buttons, handler=None): + """Handle a press of one or more buttons. + + Decorator. Use with @captouch.on(UP) + + :param buttons: List, or single instance of cap touch button constant + :param bounce: Maintained for compatibility with Dot3k joystick, unused + + """ + setup() + + buttons = buttons if isinstance(buttons, list) else [buttons] + + def register(handler): + for button in buttons: + _cap1166.on(channel=button, event='press', handler=handler) + _cap1166.on(channel=button, event='release', handler=handler) + _cap1166.on(channel=button, event='held', handler=handler) + + if handler is not None: + register(handler) + return + + return register diff --git a/pwnagotchi/ui/hw/minipitft.py b/pwnagotchi/ui/hw/minipitft.py new file mode 100644 index 00000000..02efff78 --- /dev/null +++ b/pwnagotchi/ui/hw/minipitft.py @@ -0,0 +1,49 @@ +# board GPIO: +# A: GPIO22 +# B: GPIO23 +# +# HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview + +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class MiniPitft(DisplayImpl): + def __init__(self, config): + super(MiniPitft, self).__init__(config, 'minipitft') + + def layout(self): + fonts.setup(10, 9, 10, 35, 25, 9) + self._layout['width'] = 240 + self._layout['height'] = 240 + self._layout['face'] = (0, 40) + self._layout['name'] = (5, 20) + self._layout['channel'] = (0, 0) + self._layout['aps'] = (28, 0) + self._layout['uptime'] = (175, 0) + self._layout['line1'] = [0, 14, 240, 14] + self._layout['line2'] = [0, 108, 240, 108] + self._layout['friend_face'] = (0, 92) + self._layout['friend_name'] = (40, 94) + self._layout['shakes'] = (0, 109) + self._layout['mode'] = (215, 109) + self._layout['status'] = { + 'pos': (125, 20), + 'font': fonts.status_font(fonts.Medium), + 'max': 20 + } + + return self._layout + + def initialize(self): + logging.info("initializing Adafruit Mini Pi Tft 240x240") + from pwnagotchi.ui.hw.libs.adafruit.minipitft.ST7789 import ST7789 + self._display = ST7789(0,0,25,22) + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() \ No newline at end of file diff --git a/pwnagotchi/ui/hw/minipitft2.py b/pwnagotchi/ui/hw/minipitft2.py new file mode 100644 index 00000000..d8905d05 --- /dev/null +++ b/pwnagotchi/ui/hw/minipitft2.py @@ -0,0 +1,49 @@ +# board GPIO: +# A: GPIO22 +# B: GPIO23 +# +# HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview + +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class MiniPitft2(DisplayImpl): + def __init__(self, config): + super(MiniPitft2, self).__init__(config, 'minipitft2') + + def layout(self): + fonts.setup(10, 9, 10, 35, 25, 9) + self._layout['width'] = 240 + self._layout['height'] = 135 + self._layout['face'] = (0, 40) + self._layout['name'] = (5, 20) + self._layout['channel'] = (0, 0) + self._layout['aps'] = (28, 0) + self._layout['uptime'] = (175, 0) + self._layout['line1'] = [0, 14, 240, 14] + self._layout['line2'] = [0, 108, 240, 108] + self._layout['friend_face'] = (0, 92) + self._layout['friend_name'] = (40, 94) + self._layout['shakes'] = (0, 109) + self._layout['mode'] = (215, 109) + self._layout['status'] = { + 'pos': (125, 20), + 'font': fonts.status_font(fonts.Medium), + 'max': 20 + } + + return self._layout + + def initialize(self): + logging.info("initializing Adafruit Mini Pi Tft 135x240") + from pwnagotchi.ui.hw.libs.adafruit.minipitft2.ST7789 import ST7789 + self._display = ST7789(0,0,25,22) + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() \ No newline at end of file diff --git a/pwnagotchi/ui/hw/pitft.py b/pwnagotchi/ui/hw/pitft.py index 6d761256..dbf6c358 100644 --- a/pwnagotchi/ui/hw/pitft.py +++ b/pwnagotchi/ui/hw/pitft.py @@ -3,7 +3,6 @@ # Key2 (for2,8"): GPIO22 # Key3 (for2,8"): GPIO23 # Key4 (for2,8"): GPIO27 -# Key5 (for2,4"): GPIO27 # # Key1 (for2,4"): GPIO16 # Key2 (for2,4"): GPIO13 diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index 0ae0c4c0..abb5a744 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -6,6 +6,7 @@ import time from threading import Lock from PIL import ImageDraw +from PIL import ImageColor as colors import pwnagotchi import pwnagotchi.plugins as plugins @@ -21,20 +22,98 @@ from pwnagotchi.voice import Voice WHITE = 0x00 # white is actually black on jays image BLACK = 0xFF # black is actually white on jays image +BACKGROUND_1 = 0 +FOREGROUND_1 = 1 + +BACKGROUND_L = 0 +FOREGROUND_L = 255 + +BACKGROUND_BGR_16 = (0,0,0) +FOREGROUND_BGR_16 = (31,63,31) + +BACKGROUND_RGB = (0,0,0) +FOREGROUND_RGB = (255,255,255) + + +ROOT = None + + + + +#1 (1-bit pixels, black and white, stored with one pixel per byte) + +#L (8-bit pixels, grayscale) + +#P (8-bit pixels, mapped to any other mode using a color palette) + +#BGR;16 (5,6,5 bits, for 65k color) + +#RGB (3x8-bit pixels, true color) + +#RGBA (4x8-bit pixels, true color with transparency mask) + +#CMYK (4x8-bit pixels, color separation) + +#YCbCr (3x8-bit pixels, color video format) + +#self.FOREGROUND is the main color +#self.BACKGROUNDGROUND is the 2ndary color, used for background + + + class View(object): def __init__(self, config, impl, state=None): global ROOT, BLACK, WHITE + + #values/code for display color mode + + self.mode = '1' # 1 = (1-bit pixels, black and white, stored with one pixel per byte) + if hasattr(impl, 'mode'): + self.mode = impl.mode + + + + match self.mode: + case '1': + self.BACKGROUND = BACKGROUND_1 + self.FOREGROUND = FOREGROUND_1 + # do stuff is color mode is 1 when View object is created. + case 'L': + self.BACKGROUND = BACKGROUND_L # black 0 to 255 + self.FOREGROUND = FOREGROUND_L + # do stuff is color mode is L when View object is created. + case 'P': + pass + # do stuff is color mode is P when View object is created. + case 'BGR;16': + self.BACKGROUND = BACKGROUND_BGR_16 #black tuple + self.FOREGROUND = FOREGROUND_BGR_16 #white tuple + case 'RGB': + self.BACKGROUND = BACKGROUND_RGB #black tuple + self.FOREGROUND = FOREGROUND_RGB #white tuple + # do stuff is color mode is RGB when View object is created. + case 'RGBA': + # do stuff is color mode is RGBA when View object is created. + pass + case 'CMYK': + # do stuff is color mode is CMYK when View object is created. + pass + case 'YCbCr': + # do stuff is color mode is YCbCr when View object is created. + pass + case _: + # do stuff when color mode doesnt exist for display + self.BACKGROUND = BACKGROUND_1 + self.FOREGROUND = FOREGROUND_1 + self.invert = 0 - self._black = 0xFF - self._white = 0x00 if 'invert' in config['ui'] and config['ui']['invert'] == True: - logging.debug("INVERT BLACK/WHITES:" + str(config['ui']['invert'])) + logging.debug("INVERT:" + str(config['ui']['invert'])) self.invert = 1 - BLACK = 0x00 - WHITE = 0xFF - self._black = 0x00 - self._white = 0xFF + tmp = self.FOREGROUND + self.FOREGROUND = self.FOREGROUND + self.FOREGROUND = tmp # setup faces from the configuration in case the user customized them faces.load_from_config(config['ui']['faces']) @@ -51,40 +130,40 @@ class View(object): self._width = self._layout['width'] self._height = self._layout['height'] self._state = State(state={ - 'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'], + 'channel': LabeledValue(color=self.FOREGROUND, label='CH', value='00', position=self._layout['channel'], label_font=fonts.Bold, text_font=fonts.Medium), - 'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=self._layout['aps'], + 'aps': LabeledValue(color=self.FOREGROUND, label='APS', value='0 (00)', position=self._layout['aps'], label_font=fonts.Bold, text_font=fonts.Medium), - 'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=self._layout['uptime'], + 'uptime': LabeledValue(color=self.FOREGROUND, label='UP', value='00:00:00', position=self._layout['uptime'], label_font=fonts.Bold, text_font=fonts.Medium), - 'line1': Line(self._layout['line1'], color=BLACK), - 'line2': Line(self._layout['line2'], color=BLACK), + 'line1': Line(self._layout['line1'], color=self.FOREGROUND), + 'line2': Line(self._layout['line2'], color=self.FOREGROUND), - 'face': Text(value=faces.SLEEP, position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']), color=BLACK, font=fonts.Huge, png=config['ui']['faces']['png']), + 'face': Text(value=faces.SLEEP, position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']), color=self.FOREGROUND, font=fonts.Huge, png=config['ui']['faces']['png']), - # 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK), - 'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=BLACK), + # 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=self.FOREGROUND), + 'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=self.FOREGROUND), - 'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=BLACK, font=fonts.Bold), + 'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=self.FOREGROUND, font=fonts.Bold), 'status': Text(value=self._voice.default(), position=self._layout['status']['pos'], - color=BLACK, + color=self.FOREGROUND, font=self._layout['status']['font'], wrap=True, # the current maximum number of characters per line, assuming each character is 6 pixels wide max_length=self._layout['status']['max']), - 'shakes': LabeledValue(label='PWND ', value='0 (00)', color=BLACK, + 'shakes': LabeledValue(label='PWND ', value='0 (00)', color=self.FOREGROUND, position=self._layout['shakes'], label_font=fonts.Bold, text_font=fonts.Medium), 'mode': Text(value='AUTO', position=self._layout['mode'], - font=fonts.Bold, color=BLACK), + font=fonts.Bold, color=self.FOREGROUND), }) if state: @@ -110,7 +189,7 @@ class View(object): self._state.has_element(key) def add_element(self, key, elem): - if self.invert is 1 and elem.color: + if self.invert is 1 and hasattr(elem, 'color'): if elem.color == 0xff: elem.color = 0x00 elif elem.color == 0x00: @@ -389,8 +468,8 @@ class View(object): state = self._state changes = state.changes(ignore=self._ignore_changes) if force or len(changes): - self._canvas = Image.new('1', (self._width, self._height), self._white) - drawer = ImageDraw.Draw(self._canvas) + self._canvas = Image.new(self.mode, (self._width, self._height), self.BACKGROUND) + drawer = ImageDraw.Draw(self._canvas, self.mode) plugins.on('ui_update', self) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index a8b4f72c..b2a9bcf2 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -309,9 +309,21 @@ def load_config(args): elif config['ui']['display']['type'] in ('pirateaudio'): config['ui']['display']['type'] = 'pirateaudio' + elif config['ui']['display']['type'] in ('gfxhat'): + config['ui']['display']['type'] = 'gfxhat' + elif config['ui']['display']['type'] in ('pitft'): config['ui']['display']['type'] = 'pitft' + elif config['ui']['display']['type'] in ('argonpod'): + config['ui']['display']['type'] = 'argonpod' + + elif config['ui']['display']['type'] in ('minipitft'): + config['ui']['display']['type'] = 'minipitft' + + elif config['ui']['display']['type'] in ('minipitft2'): + config['ui']['display']['type'] = 'minipitft2' + elif config['ui']['display']['type'] in ('tftbonnet'): config['ui']['display']['type'] = 'tftbonnet'