commit 2e354de75fdbe74451a283c53d8de8b09cdfbf14 Author: Olli Graf Date: Thu Dec 11 10:14:23 2025 +0100 erster Commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f13fbba --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.log +__pycache__ +HEAD +config +description +hooks/ +info/ + diff --git a/epd4in0e.py b/epd4in0e.py new file mode 100644 index 0000000..6c62e07 --- /dev/null +++ b/epd4in0e.py @@ -0,0 +1,232 @@ +# ***************************************************************************** +# * | File : epd7in3f.py +# * | Author : Waveshare team +# * | Function : Electronic paper driver +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2022-10-20 +# # | 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 +import epdconfig + +import PIL +from PIL import Image +import io + +# Display resolution +EPD_WIDTH = 400 +EPD_HEIGHT = 600 + +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 + self.BLACK = 0x000000 # 0000 BGR + self.WHITE = 0xffffff # 0001 + self.YELLOW = 0x00ffff # 0010 + self.RED = 0x0000ff # 0011 + self.BLUE = 0xff0000 # 0101 + self.GREEN = 0x00ff00 # 0110 + + + # Hardware reset + def reset(self): + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + epdconfig.digital_write(self.reset_pin, 0) # module reset + epdconfig.delay_ms(2) + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + + 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) + + 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) + + def ReadBusyH(self): + logger.debug("e-Paper busy H") + while(epdconfig.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle + epdconfig.delay_ms(5) + epdconfig.delay_ms(200) + logger.debug("e-Paper busy H release") + + def TurnOnDisplay(self): + self.send_command(0x04) # POWER_ON + self.ReadBusyH() + + self.send_command(0x06) + self.send_data(0x6F) + self.send_data(0x1F) + self.send_data(0x17) + self.send_data(0x27) + epdconfig.delay_ms(200) + + self.send_command(0x12) # DISPLAY_REFRESH + self.send_data(0X00) + self.ReadBusyH() + + self.send_command(0x02) # POWER_OFF + self.send_data(0X00) + self.ReadBusyH() + + def init(self): + if (epdconfig.module_init() != 0): + return -1 + # EPD hardware init start + self.reset() + self.ReadBusyH() + epdconfig.delay_ms(30) + + self.send_command(0xAA) + self.send_data(0x49) + self.send_data(0x55) + self.send_data(0x20) + self.send_data(0x08) + self.send_data(0x09) + self.send_data(0x18) + + self.send_command(0x01) + self.send_data(0x3F) + + self.send_command(0x00) + self.send_data(0x5F) + self.send_data(0x69) + + self.send_command(0x05) + self.send_data(0x40) + self.send_data(0x1F) + self.send_data(0x1F) + self.send_data(0x2C) + + self.send_command(0x08) + self.send_data(0x6F) + self.send_data(0x1F) + self.send_data(0x1F) + self.send_data(0x22) + + self.send_command(0x06) + self.send_data(0x6F) + self.send_data(0x1F) + self.send_data(0x17) + self.send_data(0x17) + + self.send_command(0x03) + self.send_data(0x00) + self.send_data(0x54) + self.send_data(0x00) + self.send_data(0x44) + + self.send_command(0x60) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x30) + self.send_data(0x08) + + self.send_command(0x50) + self.send_data(0x3F) + + self.send_command(0x61) + self.send_data(0x01) + self.send_data(0x90) + self.send_data(0x02) + self.send_data(0x58) + + self.send_command(0xE3) + self.send_data(0x2F) + + self.send_command(0x84) + self.send_data(0x01) + self.ReadBusyH() + return 0 + + def getbuffer(self, image): + # Create a pallette with the 7 colors supported by the panel + pal_image = Image.new("P", (1,1)) + pal_image.putpalette( (0,0,0, 255,255,255, 255,255,0, 255,0,0, 0,0,0, 0,0,255, 0,255,0) + (0,0,0)*249) + + # Check if we need to rotate the image + imwidth, imheight = image.size + if(imwidth == self.width and imheight == self.height): + image_temp = image + elif(imwidth == self.height and imheight == self.width): + image_temp = image.rotate(90, expand=True) + else: + logger.warning("Invalid image dimensions: %d x %d, expected %d x %d" % (imwidth, imheight, self.width, self.height)) + + # Convert the soruce image to the 7 colors, dithering if needed + image_6color = image_temp.convert("RGB").quantize(palette=pal_image) + buf_6color = bytearray(image_6color.tobytes('raw')) + + # PIL does not support 4 bit color, so pack the 4 bits of color + # into a single byte to transfer to the panel + buf = [0x00] * int(self.width * self.height / 2) + idx = 0 + for i in range(0, len(buf_6color), 2): + buf[idx] = (buf_6color[i] << 4) + buf_6color[i+1] + idx += 1 + + return buf + + def display(self, image): + self.send_command(0x10) + self.send_data2(image) + + self.TurnOnDisplay() + + def Clear(self, color=0x11): + self.send_command(0x10) + self.send_data2([color] * int(self.height) * int(self.width/2)) + + self.TurnOnDisplay() + + def sleep(self): + self.send_command(0x07) # DEEP_SLEEP + self.send_data(0XA5) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() +### END OF FILE ### + diff --git a/epd_4in0e_test.py b/epd_4in0e_test.py new file mode 100644 index 0000000..df7b99e --- /dev/null +++ b/epd_4in0e_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +import sys +import os +picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic') +libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib') +if os.path.exists(libdir): + sys.path.append(libdir) + +import logging +import epd4in0e +import time +from PIL import Image,ImageDraw,ImageFont +import traceback + +logging.basicConfig(level=logging.DEBUG) + +try: + logging.info("epd7in3f Demo") + + epd = epd4in0e.EPD() + logging.info("init and Clear") + epd.init() + epd.Clear() + font24 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 24) + font18 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 18) + font40 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 40) + + # Drawing on the image + logging.info("1.Drawing on the image...") + Himage = Image.new('RGB', (epd.width, epd.height), epd.WHITE) # 255: clear the frame + draw = ImageDraw.Draw(Himage) + draw.text((5, 0), 'hello world', font = font18, fill = epd.RED) + draw.text((5, 20), '4inch e-Paper (e)', font = font24, fill = epd.YELLOW) + draw.text((5, 45), u'微雪电子', font = font40, fill = epd.GREEN) + draw.text((5, 85), u'微雪电子', font = font40, fill = epd.BLUE) + draw.text((5, 125), u'微雪电子', font = font40, fill = epd.BLACK) + + draw.line((5, 170, 80, 245), fill = epd.BLUE) + draw.line((80, 170, 5, 245), fill = epd.YELLOW) + draw.rectangle((5, 170, 80, 245), outline = epd.BLACK) + draw.rectangle((90, 170, 165, 245), fill = epd.GREEN) + draw.arc((5, 250, 80, 325), 0, 360, fill = epd.RED) + draw.chord((90, 250, 165, 325), 0, 360, fill = epd.YELLOW) + epd.display(epd.getbuffer(Himage)) + time.sleep(3) + + # read bmp file + logging.info("2.read bmp file") + Himage = Image.open(os.path.join(picdir, '01.bmp')) + epd.display(epd.getbuffer(Himage)) + time.sleep(3) + + logging.info("Clear...") + epd.Clear() + + logging.info("Goto Sleep...") + epd.sleep() + +except IOError as e: + logging.info(e) + +except KeyboardInterrupt: + logging.info("ctrl + c:") + epd4in0e.epdconfig.module_exit(cleanup=True) + exit() diff --git a/epdconfig.py b/epdconfig.py new file mode 100644 index 0000000..b390252 --- /dev/null +++ b/epdconfig.py @@ -0,0 +1,322 @@ +# /***************************************************************************** +# * | 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 +import subprocess + +from ctypes import * + +logger = logging.getLogger(__name__) + + +class RaspberryPi: + # Pin definition + RST_PIN = 17 + DC_PIN = 25 + CS_PIN = 8 + BUSY_PIN = 24 + PWR_PIN = 18 + MOSI_PIN = 10 + SCLK_PIN = 11 + + def __init__(self): + import spidev + import gpiozero + + self.SPI = spidev.SpiDev() + self.GPIO_RST_PIN = gpiozero.LED(self.RST_PIN) + self.GPIO_DC_PIN = gpiozero.LED(self.DC_PIN) + # self.GPIO_CS_PIN = gpiozero.LED(self.CS_PIN) + self.GPIO_PWR_PIN = gpiozero.LED(self.PWR_PIN) + self.GPIO_BUSY_PIN = gpiozero.Button(self.BUSY_PIN, pull_up = False) + + + + def digital_write(self, pin, value): + if pin == self.RST_PIN: + if value: + self.GPIO_RST_PIN.on() + else: + self.GPIO_RST_PIN.off() + elif pin == self.DC_PIN: + if value: + self.GPIO_DC_PIN.on() + else: + self.GPIO_DC_PIN.off() + # elif pin == self.CS_PIN: + # if value: + # self.GPIO_CS_PIN.on() + # else: + # self.GPIO_CS_PIN.off() + elif pin == self.PWR_PIN: + if value: + self.GPIO_PWR_PIN.on() + else: + self.GPIO_PWR_PIN.off() + + def digital_read(self, pin): + if pin == self.BUSY_PIN: + return self.GPIO_BUSY_PIN.value + elif pin == self.RST_PIN: + return self.RST_PIN.value + elif pin == self.DC_PIN: + return self.DC_PIN.value + # elif pin == self.CS_PIN: + # return self.CS_PIN.value + elif pin == self.PWR_PIN: + return self.PWR_PIN.value + + 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 DEV_SPI_write(self, data): + self.DEV_SPI.DEV_SPI_SendData(data) + + def DEV_SPI_nwrite(self, data): + self.DEV_SPI.DEV_SPI_SendnData(data) + + def DEV_SPI_read(self): + return self.DEV_SPI.DEV_SPI_ReadData() + + def module_init(self, cleanup=False): + self.GPIO_PWR_PIN.on() + + if cleanup: + find_dirs = [ + os.path.dirname(os.path.realpath(__file__)), + '/usr/local/lib', + '/usr/lib', + ] + self.DEV_SPI = None + for find_dir in find_dirs: + val = int(os.popen('getconf LONG_BIT').read()) + logging.debug("System is %d bit"%val) + if val == 64: + so_filename = os.path.join(find_dir, 'DEV_Config_64.so') + else: + so_filename = os.path.join(find_dir, 'DEV_Config_32.so') + if os.path.exists(so_filename): + self.DEV_SPI = CDLL(so_filename) + break + if self.DEV_SPI is None: + RuntimeError('Cannot find DEV_Config.so') + + self.DEV_SPI.DEV_Module_Init() + + else: + # 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, cleanup=False): + logger.debug("spi end") + self.SPI.close() + + self.GPIO_RST_PIN.off() + self.GPIO_DC_PIN.off() + self.GPIO_PWR_PIN.off() + logger.debug("close 5V, Module enters 0 power consumption ...") + + if cleanup: + self.GPIO_RST_PIN.close() + self.GPIO_DC_PIN.close() + # self.GPIO_CS_PIN.close() + self.GPIO_PWR_PIN.close() + self.GPIO_BUSY_PIN.close() + + + + + +class JetsonNano: + # Pin definition + RST_PIN = 17 + DC_PIN = 25 + CS_PIN = 8 + BUSY_PIN = 24 + PWR_PIN = 18 + + 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.PWR_PIN, self.GPIO.OUT) + self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) + + self.GPIO.output(self.PWR_PIN, 1) + + 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.output(self.PWR_PIN, 0) + + self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN, self.PWR_PIN]) + + +class SunriseX3: + # Pin definition + RST_PIN = 17 + DC_PIN = 25 + CS_PIN = 8 + BUSY_PIN = 24 + PWR_PIN = 18 + 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.PWR_PIN, self.GPIO.OUT) + self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) + + self.GPIO.output(self.PWR_PIN, 1) + + # 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.output(self.PWR_PIN, 0) + + self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN], self.PWR_PIN) + + +if sys.version_info[0] == 2: + process = subprocess.Popen("cat /proc/cpuinfo | grep Raspberry", shell=True, stdout=subprocess.PIPE) +else: + process = subprocess.Popen("cat /proc/cpuinfo | grep Raspberry", shell=True, stdout=subprocess.PIPE, text=True) +output, _ = process.communicate() +if sys.version_info[0] == 2: + output = output.decode(sys.stdout.encoding) + +if "Raspberry" in output: + 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 ### diff --git a/logging_config.json b/logging_config.json new file mode 100644 index 0000000..6b29248 --- /dev/null +++ b/logging_config.json @@ -0,0 +1,28 @@ +{ + "version":1, + "formatters":{ + "std_out":{ + "format": "%(asctime)s : %(levelname)s : %(module)s : %(funcName)s : %(lineno)d : (Process Details : (%(process)d, %(processName)s), Thread Details : (%(thread)d, %(threadName)s): %(message)s", + "datefmt":"%d-%m-%Y %I:%M:%S" + } + }, + "handlers":{ + "console":{ + "formatter": "std_out", + "class": "logging.StreamHandler", + "level": "DEBUG" + }, + "file":{ + "formatter":"std_out", + "class":"logging.FileHandler", + "level":"DEBUG", + "filename" : "spi_demo.log" + } + }, + "root":{ + "handlers":["console","file"], + "level": "DEBUG" + } + +} + diff --git a/spi_demo.py b/spi_demo.py new file mode 100644 index 0000000..3e8ca07 --- /dev/null +++ b/spi_demo.py @@ -0,0 +1,48 @@ +import os +import time +import logging +from logging import config +import json +import epd4in0e + +from PIL import Image,ImageDraw,ImageFont + +# Auflösung des ePaper +EPD_WIDTH = 400 +EPD_HEIGHT = 600 + +#Verzeichnis für Grafikdaten +picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic') + +# Logging Konfiguration laden. +with open('logging_config.json') as file_config: + config.dictConfig(json.load(file_config)) + +logger = logging.getLogger(__name__) + +def main(): + logger.info('initalisiere Display') + + epd = epd4in0e.EPD() + epd.init() + epd.Clear() +# font18 = ImageFont.truetype(os.path.join(picdir, 'Font.ttc'), 18) + Himage = Image.new('RGB', (epd.width, epd.height), epd.WHITE) # 255: clear the frame + draw = ImageDraw.Draw(Himage) +# draw.text((5, 0), 'hello world', font = font18, fill = epd.RED) + + logger.info('zeichne blaue Linie') + draw.line((0,0, 80, 245), fill = epd.BLUE) + epd.display(epd.getbuffer(Himage)) + + logger.info('zeichne gefülltes Rechteck') + draw.rectangle((90, 170, 165, 245), fill = epd.RED) + epd.display(epd.getbuffer(Himage)) + + time.sleep(5) + logger.info('cleanup') + epd4in0e.epdconfig.module_exit(cleanup=True) + exit() + +if __name__ == '__main__': + main()