feat: Add in support for Waveshare 2.13inb V4 e-ink display

Signed-off-by: Rocco Augusto <rocco@nerdofsteel.com>

(cherry picked from commit 28c0adbb08ca5fcc6c36dc11d00f666eaecdc4cb)
Signed-off-by: Jeroen Oudshoorn <oudshoorn.jeroen@gmail.com>
This commit is contained in:
Rocco Augusto
2023-01-18 09:27:49 +01:00
committed by Jeroen Oudshoorn
parent 796255c75b
commit d68fccb6e9
6 changed files with 513 additions and 8 deletions

View File

@ -70,6 +70,9 @@ class Display(View):
def is_waveshare213bc(self):
return self._implementation.name == 'waveshare213bc'
def is_waveshare213inb_v4(self):
return self._implementation.name == 'waveshare213inb_v4'
def is_spotpear24inch(self):
return self._implementation.name == 'spotpear24inch'

View File

@ -13,9 +13,9 @@ from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
from pwnagotchi.ui.hw.waveshare213bc import Waveshare213bc
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
from pwnagotchi.ui.hw.displayhatmini import DisplayHatMini
from pwnagotchi.ui.hw.waveshare35lcd import Waveshare35lcd
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
def display_for(config):
# config has been normalized already in utils.load_config
@ -64,11 +64,8 @@ def display_for(config):
elif config['ui']['display']['type'] == 'waveshare213bc':
return Waveshare213bc(config)
elif config['ui']['display']['type'] == 'spotpear24inch':
return Spotpear24inch(config)
elif config['ui']['display']['type'] == 'displayhatmini':
return DisplayHatMini(config)
elif config['ui']['display']['type'] == 'waveshare35lcd':
return Waveshare35lcd(config)
elif config['ui']['display']['type'] == 'spotpear24inch':
return Spotpear24inch(config)

View File

@ -0,0 +1,204 @@
# *****************************************************************************
# * | File : epd2in13b_V4.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2022-04-21
# # | Info : python demo
# -----------------------------------------------------------------------------
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation 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
# furished 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 OR 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 logging
from . import epdconfig
# Display resolution
EPD_WIDTH = 122
EPD_HEIGHT = 250
logger = logging.getLogger(__name__)
class EPD:
def __init__(self):
self.reset_pin = epdconfig.RST_PIN
self.dc_pin = epdconfig.DC_PIN
self.busy_pin = epdconfig.BUSY_PIN
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(20)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(2)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(20)
# send 1 byte command
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([command])
epdconfig.digital_write(self.cs_pin, 1)
# send 1 byte data
def send_data(self, data):
epdconfig.digital_write(self.dc_pin, 1)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
# send a lot of data
def send_data2(self, data):
epdconfig.digital_write(self.dc_pin, 1)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte2(data)
epdconfig.digital_write(self.cs_pin, 1)
# judge e-Paper whether is busy
def busy(self):
logger.debug("e-Paper busy")
while (epdconfig.digital_read(self.busy_pin) != 0):
epdconfig.delay_ms(10)
logger.debug("e-Paper busy release")
# set the display window
def set_windows(self, xstart, ystart, xend, yend):
self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
self.send_data((xstart >> 3) & 0xff)
self.send_data((xend >> 3) & 0xff)
self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
self.send_data(ystart & 0xff)
self.send_data((ystart >> 8) & 0xff)
self.send_data(yend & 0xff)
self.send_data((yend >> 8) & 0xff)
# set the display cursor(origin)
def set_cursor(self, xstart, ystart):
self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
self.send_data(xstart & 0xff)
self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
self.send_data(ystart & 0xff)
self.send_data((ystart >> 8) & 0xff)
# initialize
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.busy()
self.send_command(0x12) # SWRESET
self.busy()
self.send_command(0x01) # Driver output control
self.send_data(0xf9)
self.send_data(0x00)
self.send_data(0x00)
self.send_command(0x11) # data entry mode
self.send_data(0x03)
self.set_windows(0, 0, self.width - 1, self.height - 1)
self.set_cursor(0, 0)
self.send_command(0x3C) # BorderWavefrom
self.send_data(0x05)
self.send_command(0x18) # Read built-in temperature sensor
self.send_data(0x80)
self.send_command(0x21) # Display update control
self.send_data(0x80)
self.send_data(0x80)
self.busy()
return 0
# turn on display
def ondisplay(self):
self.send_command(0x20)
self.busy()
# image converted to bytearray
def getbuffer(self, image):
img = image
imwidth, imheight = img.size
if (imwidth == self.width and imheight == self.height):
img = img.convert('1')
elif (imwidth == self.height and imheight == self.width):
# image has correct dimensions, but needs to be rotated
img = img.rotate(90, expand=True).convert('1')
else:
logger.warning("Wrong image dimensions: must be " +
str(self.width) + "x" + str(self.height))
# return a blank buffer
return [0x00] * (int(self.width/8) * self.height)
buf = bytearray(img.tobytes('raw'))
return buf
# display image
def display(self, imageblack, imagered):
self.send_command(0x24)
self.send_data2(imageblack)
self.send_command(0x26)
self.send_data2(imagered)
self.ondisplay()
# display white image
def clear(self):
if self.width % 8 == 0:
linewidth = int(self.width/8)
else:
linewidth = int(self.width/8) + 1
buf = [0xff] * (int(linewidth * self.height))
self.send_command(0x24)
self.send_data2(buf)
self.send_command(0x26)
self.send_data2(buf)
self.ondisplay()
# Compatible with older version functions
def Clear(self):
self.clear()
# sleep
def sleep(self):
self.send_command(0x10) # DEEP_SLEEP
self.send_data(0x01) # check code
epdconfig.delay_ms(2000)
epdconfig.module_exit()
### END OF FILE ###

View File

@ -0,0 +1,227 @@
# /*****************************************************************************
# * | File : epdconfig.py
# * | Author : Waveshare team
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.2
# * | Date : 2022-10-29
# * | Info :
# ******************************************************************************
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation 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
# furished 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 OR 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 os
import logging
import sys
import time
logger = logging.getLogger(__name__)
class RaspberryPi:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import spidev
import RPi.GPIO
self.GPIO = RPi.GPIO
self.SPI = spidev.SpiDev()
def digital_write(self, pin, value):
self.GPIO.output(pin, value)
def digital_read(self, pin):
return self.GPIO.input(pin)
def delay_ms(self, delaytime):
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
self.SPI.writebytes(data)
def spi_writebyte2(self, data):
self.SPI.writebytes2(data)
def module_init(self):
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
# SPI device, bus = 0, device = 0
self.SPI.open(0, 0)
self.SPI.max_speed_hz = 4000000
self.SPI.mode = 0b00
return 0
def module_exit(self):
logger.debug("spi end")
self.SPI.close()
logger.debug("close 5V, Module enters 0 power consumption ...")
self.GPIO.output(self.RST_PIN, 0)
self.GPIO.output(self.DC_PIN, 0)
self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN])
class JetsonNano:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import ctypes
find_dirs = [
os.path.dirname(os.path.realpath(__file__)),
'/usr/local/lib',
'/usr/lib',
]
self.SPI = None
for find_dir in find_dirs:
so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
if os.path.exists(so_filename):
self.SPI = ctypes.cdll.LoadLibrary(so_filename)
break
if self.SPI is None:
raise RuntimeError('Cannot find sysfs_software_spi.so')
import Jetson.GPIO
self.GPIO = Jetson.GPIO
def digital_write(self, pin, value):
self.GPIO.output(pin, value)
def digital_read(self, pin):
return self.GPIO.input(self.BUSY_PIN)
def delay_ms(self, delaytime):
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
self.SPI.SYSFS_software_spi_transfer(data[0])
def spi_writebyte2(self, data):
for i in range(len(data)):
self.SPI.SYSFS_software_spi_transfer(data[i])
def module_init(self):
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
self.SPI.SYSFS_software_spi_begin()
return 0
def module_exit(self):
logger.debug("spi end")
self.SPI.SYSFS_software_spi_end()
logger.debug("close 5V, Module enters 0 power consumption ...")
self.GPIO.output(self.RST_PIN, 0)
self.GPIO.output(self.DC_PIN, 0)
self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN])
class SunriseX3:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
Flag = 0
def __init__(self):
import spidev
import Hobot.GPIO
self.GPIO = Hobot.GPIO
self.SPI = spidev.SpiDev()
def digital_write(self, pin, value):
self.GPIO.output(pin, value)
def digital_read(self, pin):
return self.GPIO.input(pin)
def delay_ms(self, delaytime):
time.sleep(delaytime / 1000.0)
def spi_writebyte(self, data):
self.SPI.writebytes(data)
def spi_writebyte2(self, data):
# for i in range(len(data)):
# self.SPI.writebytes([data[i]])
self.SPI.xfer3(data)
def module_init(self):
if self.Flag == 0:
self.Flag = 1
self.GPIO.setmode(self.GPIO.BCM)
self.GPIO.setwarnings(False)
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
# SPI device, bus = 0, device = 0
self.SPI.open(2, 0)
self.SPI.max_speed_hz = 4000000
self.SPI.mode = 0b00
return 0
else:
return 0
def module_exit(self):
logger.debug("spi end")
self.SPI.close()
logger.debug("close 5V, Module enters 0 power consumption ...")
self.Flag = 0
self.GPIO.output(self.RST_PIN, 0)
self.GPIO.output(self.DC_PIN, 0)
self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN])
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
implementation = RaspberryPi()
elif os.path.exists('/sys/bus/platform/drivers/gpio-x3'):
implementation = SunriseX3()
else:
implementation = JetsonNano()
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))
### END OF FILE ###

View File

@ -0,0 +1,71 @@
import logging
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl
from PIL import Image
class Waveshare213bV4(DisplayImpl):
def __init__(self, config):
super(Waveshare213bV4, self).__init__(config, 'waveshare213inb_v4')
self._display = None
def layout(self):
if self.config['color'] == 'black':
fonts.setup(10, 9, 10, 35, 25, 9)
self._layout['width'] = 250
self._layout['height'] = 122
self._layout['face'] = (0, 40)
self._layout['name'] = (5, 20)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (28, 0)
self._layout['uptime'] = (185, 0)
self._layout['line1'] = [0, 14, 250, 14]
self._layout['line2'] = [0, 108, 250, 108]
self._layout['friend_face'] = (0, 92)
self._layout['friend_name'] = (40, 94)
self._layout['shakes'] = (0, 109)
self._layout['mode'] = (225, 109)
self._layout['status'] = {
'pos': (125, 20),
'font': fonts.status_font(fonts.Medium),
'max': 20
}
else:
fonts.setup(10, 8, 10, 25, 25, 9)
self._layout['width'] = 212
self._layout['height'] = 104
self._layout['face'] = (0, 26)
self._layout['name'] = (5, 15)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (28, 0)
self._layout['status'] = (91, 15)
self._layout['uptime'] = (147, 0)
self._layout['line1'] = [0, 12, 212, 12]
self._layout['line2'] = [0, 92, 212, 92]
self._layout['friend_face'] = (0, 76)
self._layout['friend_name'] = (40, 78)
self._layout['shakes'] = (0, 93)
self._layout['mode'] = (187, 93)
self._layout['status'] = {
'pos': (125, 20),
'font': fonts.status_font(fonts.Medium),
'max': 14
}
return self._layout
def initialize(self):
logging.info("initializing waveshare 2.13inb v4 display")
from pwnagotchi.ui.hw.libs.waveshare.v213inb_v4.epd2in13b_V4 import EPD
self._display = EPD()
self._display.init()
self._display.Clear()
def render(self, canvasBlack = None, canvasRed = None):
buffer = self._display.getbuffer
image = Image.new('1', (self._layout['height'], self._layout['width']))
imageBlack = image if canvasBlack is None else canvasBlack
imageRed = image if canvasRed is None else canvasRed
self._display.display(buffer(imageBlack), buffer(imageRed))
def clear(self):
self._display.Clear()

View File

@ -278,6 +278,9 @@ def load_config(args):
elif config['ui']['display']['type'] in ('ws_213bc', 'ws213bc', 'waveshare_213bc', 'waveshare213bc'):
config['ui']['display']['type'] = 'waveshare213bc'
elif config['ui']['display']['type'] in ('ws_213bv4', 'ws213bv4', 'waveshare_213bv4', 'waveshare213inb_v4'):
config['ui']['display']['type'] = 'waveshare213inb_v4'
elif config['ui']['display']['type'] in ('spotpear24inch'):
config['ui']['display']['type'] = 'spotpear24inch'