Display drivers for pwnagotchi

<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->
Display support for the following screens:
- [Pimoroni GFX Hat](https://shop.pimoroni.com/products/gfx-hat?variant=12828343631955):
ui.display.type = "gfxhat"
Contrast and Backlight color can be imported from config.toml:
ui.display.contrast = 40
ui.display.blcolor = "olive"
Available backlight colors:
white, grey, maroon, red, purple, fuchsia, green,
lime, olive, yellow, navy, blue, teal, aqua
- [Adafruit miniPiTFT](https://www.adafruit.com/product/4484):
ui.display.type = "minipitft"
- [Adafruit miniPiTFT2](https://www.adafruit.com/product/4393):
ui.display.type = "minipitft2"
- [ArgonPod](https://argon40.com/products/pod-display-2-8inch):
ui.display.type = "argonpod"
- [DisplayHatMini](https://shop.pimoroni.com/products/display-hat-mini?variant=39496084717651):
Driver updated to fix issues
- I2C Oled:
Default I2C address changed to 0x3C, because most boards are coming with it by default.
Can be modified in config
ui.display.type = "i2coled"
ui.display.i2c_addr = 0x3C
ui.display.width = 128
ui.display.height = 64

## Motivation and Context
Future plan for LCD and OLED screens:
Change from the pwnagotchis hw libraries for drivers to LumaOLED LumaLCD packages.
Luma Core: https://github.com/rm-hull/luma.core
Luma LCD: https://github.com/rm-hull/luma.lcd
Luma OLED: https://github.com/rm-hull/luma.oled
It has the most used LCD and OLED drivers ready, and adding new screens could be easier in the future.

## How Has This Been Tested?
Except the argonpod and minipitft2 all screens were tested on previous builds,
should work in 2.9.2, but before release I would like to test it with an image.

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I've read the [CONTRIBUTION](https://github.com/evilsocket/pwnagotchi/blob/master/CONTRIBUTING.md) guide
- [x] I have signed-off my commits with `git commit -s`
This commit is contained in:
RasTacsko
2024-11-25 17:37:21 +01:00
parent d21c2cb30c
commit 131bfc4c1c
21 changed files with 2649 additions and 30 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()
return result.byteswap().tobytes()

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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'