├── .gitignore ├── epdconfig.py ├── status_display.py └── epd2in13_V2.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | -------------------------------------------------------------------------------- /epdconfig.py: -------------------------------------------------------------------------------- 1 | # /***************************************************************************** 2 | # * | File : epdconfig.py 3 | # * | Author : Waveshare team, edited by João Calado 4 | # * | Function : Hardware underlying interface 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V1.2 8 | # * | Date : 2022-10-29, updated in 21/04/2025 9 | # * | Info : 10 | # ****************************************************************************** 11 | 12 | import os 13 | import logging 14 | import sys 15 | import time 16 | 17 | from ctypes import * 18 | 19 | LOG = logging.getLogger(__name__) 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | class RaspberryPi: 23 | # Pin definition 24 | RST_PIN = 17 25 | DC_PIN = 25 26 | CS_PIN = 8 27 | BUSY_PIN = 24 28 | PWR_PIN = 18 29 | MOSI_PIN = 10 30 | SCLK_PIN = 11 31 | 32 | def __init__(self): 33 | import spidev 34 | import gpiozero 35 | 36 | self.SPI = spidev.SpiDev() 37 | self.GPIO_RST_PIN = gpiozero.LED(self.RST_PIN) 38 | self.GPIO_DC_PIN = gpiozero.LED(self.DC_PIN) 39 | # self.GPIO_CS_PIN = gpiozero.LED(self.CS_PIN) 40 | self.GPIO_PWR_PIN = gpiozero.LED(self.PWR_PIN) 41 | self.GPIO_BUSY_PIN = gpiozero.Button(self.BUSY_PIN, pull_up = False) 42 | 43 | def digital_write(self, pin, value): 44 | if pin == self.RST_PIN: 45 | if value: 46 | self.GPIO_RST_PIN.on() 47 | else: 48 | self.GPIO_RST_PIN.off() 49 | elif pin == self.DC_PIN: 50 | if value: 51 | self.GPIO_DC_PIN.on() 52 | else: 53 | self.GPIO_DC_PIN.off() 54 | # elif pin == self.CS_PIN: 55 | # if value: 56 | # self.GPIO_CS_PIN.on() 57 | # else: 58 | # self.GPIO_CS_PIN.off() 59 | elif pin == self.PWR_PIN: 60 | if value: 61 | self.GPIO_PWR_PIN.on() 62 | else: 63 | self.GPIO_PWR_PIN.off() 64 | 65 | def digital_read(self, pin): 66 | if pin == self.BUSY_PIN: 67 | return self.GPIO_BUSY_PIN.value 68 | elif pin == self.RST_PIN: 69 | return self.RST_PIN.value 70 | elif pin == self.DC_PIN: 71 | return self.DC_PIN.value 72 | # elif pin == self.CS_PIN: 73 | # return self.CS_PIN.value 74 | elif pin == self.PWR_PIN: 75 | return self.PWR_PIN.value 76 | 77 | def delay_ms(self, delaytime): 78 | time.sleep(delaytime / 1000.0) 79 | 80 | def spi_writebyte(self, data): 81 | self.SPI.writebytes(data) 82 | 83 | def spi_writebyte2(self, data): 84 | self.SPI.writebytes2(data) 85 | 86 | def DEV_SPI_write(self, data): 87 | self.DEV_SPI.DEV_SPI_SendData(data) 88 | 89 | def DEV_SPI_nwrite(self, data): 90 | self.DEV_SPI.DEV_SPI_SendnData(data) 91 | 92 | def DEV_SPI_read(self): 93 | return self.DEV_SPI.DEV_SPI_ReadData() 94 | 95 | def module_init(self, cleanup=False): 96 | self.GPIO_PWR_PIN.on() 97 | 98 | if cleanup: 99 | find_dirs = [ 100 | os.path.dirname(os.path.realpath(__file__)), 101 | '/usr/local/lib', 102 | '/usr/lib', 103 | ] 104 | self.DEV_SPI = None 105 | for find_dir in find_dirs: 106 | val = int(os.popen('getconf LONG_BIT').read()) 107 | LOG.debug("System is %d bit"%val) 108 | if val == 64: 109 | so_filename = os.path.join(find_dir, 'DEV_Config_64.so') 110 | else: 111 | so_filename = os.path.join(find_dir, 'DEV_Config_32.so') 112 | if os.path.exists(so_filename): 113 | self.DEV_SPI = CDLL(so_filename) 114 | break 115 | if self.DEV_SPI is None: 116 | RuntimeError('Cannot find DEV_Config.so') 117 | 118 | self.DEV_SPI.DEV_Module_Init() 119 | 120 | else: 121 | # SPI device, bus = 0, device = 0 122 | self.SPI.open(0, 0) 123 | self.SPI.max_speed_hz = 4000000 124 | self.SPI.mode = 0b00 125 | return 0 126 | 127 | def module_exit(self, cleanup=False): 128 | LOG.debug("SPI End") 129 | self.SPI.close() 130 | 131 | self.GPIO_RST_PIN.off() 132 | self.GPIO_DC_PIN.off() 133 | self.GPIO_PWR_PIN.off() 134 | LOG.debug("Closed 5V, Module enters 0 power consumption...") 135 | 136 | if cleanup: 137 | self.GPIO_RST_PIN.close() 138 | self.GPIO_DC_PIN.close() 139 | # self.GPIO_CS_PIN.close() 140 | self.GPIO_PWR_PIN.close() 141 | self.GPIO_BUSY_PIN.close() 142 | 143 | implementation = RaspberryPi() 144 | 145 | for func in [x for x in dir(implementation) if not x.startswith('_')]: 146 | setattr(sys.modules[__name__], func, getattr(implementation, func)) 147 | 148 | ### END OF FILE ### 149 | -------------------------------------------------------------------------------- /status_display.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import datetime 4 | import subprocess 5 | import requests 6 | from PIL import Image, ImageDraw, ImageFont, ImageOps 7 | import epd2in13_V2 8 | 9 | # === Logger === 10 | LOG = logging.getLogger(__name__) 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | # === Paths and Limits === 14 | USER_PATH = '/home/secrets/pihole_user' 15 | PW_PATH = '/home/secrets/pihole_pw' 16 | LOW_BATTERY_THRESHOLD = 20 17 | 18 | # === Aux funcs === 19 | def readFromFile(path): 20 | try: 21 | with open(path, 'r') as f: 22 | return f.read().strip() 23 | except Exception as e: 24 | LOG.error(f"readFromFile - Error loading file content: {e}") 25 | return None 26 | 27 | def getBattery(): 28 | try: 29 | LOG.debug("getBattery - Getting battery info via netcat") 30 | command = 'echo "get battery" | nc -q 0 127.0.0.1 8423' 31 | output = subprocess.check_output(command, shell=True, text=True) 32 | for line in output.splitlines(): 33 | if line.lower().startswith("battery:"): 34 | battery_value = float(line.split(":")[1].strip()) 35 | LOG.debug(f"getBattery - Battery % is: {battery_value}") 36 | return int(round(battery_value)) 37 | return "N/A" 38 | except Exception as e: 39 | LOG.error(f"getBattery - Error getting battery status via netcat: {e}") 40 | return "N/A" 41 | 42 | # === PiHole funcs === 43 | def authenticate(ip, password): 44 | url = f"http://{ip}/api/auth" 45 | payload = {"password": password} 46 | try: 47 | response = requests.post(url, json=payload, timeout=5) 48 | data = response.json() 49 | if not data.get("session", {}).get("valid"): 50 | raise Exception("Session is not valid") 51 | return data.get("session", {}).get("sid") 52 | except Exception as e: 53 | LOG.error(f"authenticate - Authentication failed for {ip}: {e}") 54 | return None 55 | 56 | def logout(ip, sid): 57 | url = f"http://{ip}/api/auth" 58 | try: 59 | requests.delete(url, headers={"sid": sid}, timeout=5) 60 | except Exception as e: 61 | LOG.error(f"logout - Logout failed for {ip}: {e}") 62 | 63 | def getPiHoleData(ip, password): 64 | sid = authenticate(ip, password) 65 | if not sid: 66 | return "N/A", 0.0 67 | try: 68 | summary_url = f"http://{ip}/api/stats/summary" 69 | response = requests.get(summary_url, headers={"sid": sid}, timeout=5) 70 | resquestsNumber = response.json()["queries"].get("total") 71 | blockedPercentage = response.json()["queries"].get("percent_blocked", 0.0) 72 | return resquestsNumber, float(f"{blockedPercentage:.2f}") 73 | except Exception as e: 74 | LOG.error(f"getPiHoleData - Error getting PiHole data from {ip}: {e}") 75 | return "N/A", 0.0 76 | finally: 77 | logout(ip, sid) 78 | 79 | # === System === 80 | def getLocalUpTime(): 81 | try: 82 | with open("/proc/uptime", "r") as f: 83 | uptimeSeconds = float(f.readline().split()[0]) 84 | hours = int(uptimeSeconds // 3600) 85 | minutes = int((uptimeSeconds % 3600) // 60) 86 | return f"{hours}h {minutes}m" 87 | except Exception as e: 88 | LOG.error(f"getLocalUpTime - Error reading local uptime: {e}") 89 | return "N/A" 90 | 91 | def getLocalCPUTemp(): 92 | try: 93 | with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: 94 | return round(int(f.read()) / 1000, 1) 95 | except Exception as e: 96 | LOG.error(f"getLocalCPUTemp - Error reading local CPU temp: {e}") 97 | return "N/A" 98 | 99 | def getRemoteUpTime(ip, user, password): 100 | try: 101 | LOG.debug(f"getRemoteUpTime - Reading remote uptime on: {ip}") 102 | command = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no {user}@{ip} cat /proc/uptime" 103 | output = subprocess.getoutput(command) 104 | uptimeSeconds = float(output.split()[0]) 105 | hours = int(uptimeSeconds // 3600) 106 | minutes = int((uptimeSeconds % 3600) // 60) 107 | return f"{hours}h {minutes}m" 108 | except Exception as e: 109 | LOG.error(f"getRemoteUpTime - Error reading remote uptime: {e}") 110 | return "N/A" 111 | 112 | def getRemoteCPUTemp(ip, user, password): 113 | try: 114 | LOG.debug(f"getRemoteCPUTemp - Reading remote CPU Temp on: {ip}") 115 | command = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no {user}@{ip} cat /sys/class/thermal/thermal_zone0/temp" 116 | output = subprocess.getoutput(command) 117 | return round(int(output.strip()) / 1000, 1) 118 | except Exception as e: 119 | LOG.error(f"getRemoteCPUTemp - Error reading remote CPU temp: {e}") 120 | return "N/A" 121 | 122 | # === Main === 123 | def main(): 124 | LOG.debug("main - Starting execution") 125 | pihole1Ip = '192.168.50.135' 126 | pihole2Ip = '127.0.0.1' 127 | 128 | user = readFromFile(USER_PATH) 129 | password = readFromFile(PW_PATH) 130 | 131 | pihole1RequestsNumber, pihole1Blocked = getPiHoleData(pihole1Ip, password) 132 | pihole2RequestsNumber, pihole2Blocked = getPiHoleData(pihole2Ip, password) 133 | 134 | pihole1UpTime = getRemoteUpTime(pihole1Ip, user, password) 135 | pihole1Temp = getRemoteCPUTemp(pihole1Ip, user, password) 136 | 137 | pihole2UpTime = getLocalUpTime() 138 | pihole2Temp = getLocalCPUTemp() 139 | 140 | battery = getBattery() 141 | 142 | now_str = datetime.datetime.now().strftime("%d/%m %H:%M") 143 | 144 | epd = epd2in13_V2.EPD() 145 | epd.init(epd.PART_UPDATE) 146 | width, height = epd.height, epd.width 147 | image = Image.new('1', (width, height), 255) 148 | draw = ImageDraw.Draw(image) 149 | 150 | fontSmall = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14) 151 | fontSmallBold = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 14) 152 | fontBigBold = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 16) 153 | 154 | draw.text((10, 4), "PiHole .135", font=fontBigBold, fill=0) 155 | draw.text((135, 4), "PiHole .136", font=fontBigBold, fill=0) 156 | 157 | draw.text((10, 25), "REQ:", font=fontSmallBold, fill=0) 158 | draw.text((50, 25), f"{pihole1RequestsNumber}", font=fontSmall, fill=0) 159 | draw.text((10, 44), "BLKD:", font=fontSmallBold, fill=0) 160 | draw.text((60, 44), f"{pihole1Blocked}%", font=fontSmall, fill=0) 161 | draw.text((10, 63), "UP:", font=fontSmallBold, fill=0) 162 | draw.text((40, 63), f"{pihole1UpTime}", font=fontSmall, fill=0) 163 | draw.text((10, 82), "TEMP:", font=fontSmallBold, fill=0) 164 | draw.text((60, 82), f"{pihole1Temp}°C", font=fontSmall, fill=0) 165 | 166 | draw.text((135, 25), "REQ:", font=fontSmallBold, fill=0) 167 | draw.text((175, 25), f"{pihole2RequestsNumber}", font=fontSmall, fill=0) 168 | draw.text((135, 44), "BLKD:", font=fontSmallBold, fill=0) 169 | draw.text((185, 44), f"{pihole2Blocked}%", font=fontSmall, fill=0) 170 | draw.text((135, 63), "UP:", font=fontSmallBold, fill=0) 171 | draw.text((165, 63), f"{pihole2UpTime}", font=fontSmall, fill=0) 172 | draw.text((135, 82), "TEMP:", font=fontSmallBold, fill=0) 173 | draw.text((185, 82), f"{pihole2Temp}°C", font=fontSmall, fill=0) 174 | 175 | # Separator lines 176 | draw.line((127.5, 0, 127.5, 100), fill=0) # Vertical separator 177 | draw.line((0, 101, width, 101), fill=0) # Horizontal footer separator 178 | 179 | # Battery 180 | if isinstance(battery, int): 181 | batteryBarLength = int(60 * battery / 100) 182 | draw.text((85, 105), f"{round(battery)}%", font=fontSmall, fill=0) 183 | else: 184 | batteryBarLength = 0 185 | draw.text((85, 105), "N/A", font=fontSmall, fill=0) 186 | draw.rectangle((10, 107, 10 + batteryBarLength, 117), fill=0) 187 | draw.rectangle((10, 107, 70, 117), outline=0) 188 | 189 | # Date-time 190 | draw.text((155, 105), now_str, font=fontSmall, fill=0) 191 | 192 | rotatedImage = image.rotate(180) 193 | colorInvertedImage = ImageOps.invert(rotatedImage.convert("L")).convert("1") 194 | epd.display(epd.getbuffer(colorInvertedImage)) 195 | epd.sleep() 196 | 197 | if __name__ == "__main__": 198 | main() 199 | -------------------------------------------------------------------------------- /epd2in13_V2.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd2in13_V2.py 3 | # * | Author : Waveshare team, edited by João Calado 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20, edited in 21/04/2025 9 | # # | Info : python demo 10 | # ----------------------------------------------------------------------------- 11 | 12 | import logging 13 | import epdconfig 14 | 15 | # Display resolution 16 | EPD_WIDTH = 122 17 | EPD_HEIGHT = 250 18 | 19 | LOG = logging.getLogger(__name__) 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | class EPD: 23 | LOG.debug("Initializing epd2in13_V2") 24 | def __init__(self): 25 | self.reset_pin = epdconfig.RST_PIN 26 | self.dc_pin = epdconfig.DC_PIN 27 | self.busy_pin = epdconfig.BUSY_PIN 28 | self.cs_pin = epdconfig.CS_PIN 29 | self.width = EPD_WIDTH 30 | self.height = EPD_HEIGHT 31 | 32 | FULL_UPDATE = 0 33 | PART_UPDATE = 1 34 | lut_full_update= [ 35 | 0x80,0x60,0x40,0x00,0x00,0x00,0x00, #LUT0: BB: VS 0 ~7 36 | 0x10,0x60,0x20,0x00,0x00,0x00,0x00, #LUT1: BW: VS 0 ~7 37 | 0x80,0x60,0x40,0x00,0x00,0x00,0x00, #LUT2: WB: VS 0 ~7 38 | 0x10,0x60,0x20,0x00,0x00,0x00,0x00, #LUT3: WW: VS 0 ~7 39 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00, #LUT4: VCOM: VS 0 ~7 40 | 41 | 0x03,0x03,0x00,0x00,0x02, # TP0 A~D RP0 42 | 0x09,0x09,0x00,0x00,0x02, # TP1 A~D RP1 43 | 0x03,0x03,0x00,0x00,0x02, # TP2 A~D RP2 44 | 0x00,0x00,0x00,0x00,0x00, # TP3 A~D RP3 45 | 0x00,0x00,0x00,0x00,0x00, # TP4 A~D RP4 46 | 0x00,0x00,0x00,0x00,0x00, # TP5 A~D RP5 47 | 0x00,0x00,0x00,0x00,0x00, # TP6 A~D RP6 48 | 49 | 0x15,0x41,0xA8,0x32,0x30,0x0A, 50 | ] 51 | 52 | lut_partial_update = [ #20 bytes 53 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00, #LUT0: BB: VS 0 ~7 54 | 0x80,0x00,0x00,0x00,0x00,0x00,0x00, #LUT1: BW: VS 0 ~7 55 | 0x40,0x00,0x00,0x00,0x00,0x00,0x00, #LUT2: WB: VS 0 ~7 56 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00, #LUT3: WW: VS 0 ~7 57 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00, #LUT4: VCOM: VS 0 ~7 58 | 59 | 0x0A,0x00,0x00,0x00,0x00, # TP0 A~D RP0 60 | 0x00,0x00,0x00,0x00,0x00, # TP1 A~D RP1 61 | 0x00,0x00,0x00,0x00,0x00, # TP2 A~D RP2 62 | 0x00,0x00,0x00,0x00,0x00, # TP3 A~D RP3 63 | 0x00,0x00,0x00,0x00,0x00, # TP4 A~D RP4 64 | 0x00,0x00,0x00,0x00,0x00, # TP5 A~D RP5 65 | 0x00,0x00,0x00,0x00,0x00, # TP6 A~D RP6 66 | 67 | 0x15,0x41,0xA8,0x32,0x30,0x0A, 68 | ] 69 | 70 | # Hardware reset 71 | def reset(self): 72 | epdconfig.digital_write(self.reset_pin, 1) 73 | epdconfig.delay_ms(200) 74 | epdconfig.digital_write(self.reset_pin, 0) 75 | epdconfig.delay_ms(5) 76 | epdconfig.digital_write(self.reset_pin, 1) 77 | epdconfig.delay_ms(200) 78 | 79 | def send_command(self, command): 80 | epdconfig.digital_write(self.dc_pin, 0) 81 | epdconfig.digital_write(self.cs_pin, 0) 82 | epdconfig.spi_writebyte([command]) 83 | epdconfig.digital_write(self.cs_pin, 1) 84 | 85 | def send_data(self, data): 86 | epdconfig.digital_write(self.dc_pin, 1) 87 | epdconfig.digital_write(self.cs_pin, 0) 88 | epdconfig.spi_writebyte([data]) 89 | epdconfig.digital_write(self.cs_pin, 1) 90 | 91 | # send a lot of data 92 | def send_data2(self, data): 93 | epdconfig.digital_write(self.dc_pin, 1) 94 | epdconfig.digital_write(self.cs_pin, 0) 95 | epdconfig.spi_writebyte2(data) 96 | epdconfig.digital_write(self.cs_pin, 1) 97 | 98 | def ReadBusy(self): 99 | while(epdconfig.digital_read(self.busy_pin) == 1): # 0: idle, 1: busy 100 | epdconfig.delay_ms(100) 101 | 102 | def TurnOnDisplay(self): 103 | self.send_command(0x22) 104 | self.send_data(0xC7) 105 | self.send_command(0x20) 106 | self.ReadBusy() 107 | 108 | def TurnOnDisplayPart(self): 109 | self.send_command(0x22) 110 | self.send_data(0x0c) 111 | self.send_command(0x20) 112 | self.ReadBusy() 113 | 114 | def init(self, update): 115 | if (epdconfig.module_init() != 0): 116 | return -1 117 | # EPD hardware init start 118 | self.reset() 119 | if(update == self.FULL_UPDATE): 120 | self.ReadBusy() 121 | self.send_command(0x12) # soft reset 122 | self.ReadBusy() 123 | 124 | self.send_command(0x74) #set analog block control 125 | self.send_data(0x54) 126 | self.send_command(0x7E) #set digital block control 127 | self.send_data(0x3B) 128 | 129 | self.send_command(0x01) #Driver output control 130 | self.send_data(0xF9) 131 | self.send_data(0x00) 132 | self.send_data(0x00) 133 | 134 | self.send_command(0x11) #data entry mode 135 | self.send_data(0x01) 136 | 137 | self.send_command(0x44) #set Ram-X address start/end position 138 | self.send_data(0x00) 139 | self.send_data(0x0F) #0x0C-->(15+1)*8=128 140 | 141 | self.send_command(0x45) #set Ram-Y address start/end position 142 | self.send_data(0xF9) #0xF9-->(249+1)=250 143 | self.send_data(0x00) 144 | self.send_data(0x00) 145 | self.send_data(0x00) 146 | 147 | self.send_command(0x3C) #BorderWavefrom 148 | self.send_data(0x03) 149 | 150 | self.send_command(0x2C) #VCOM Voltage 151 | self.send_data(0x55) # 152 | 153 | self.send_command(0x03) 154 | self.send_data(self.lut_full_update[70]) 155 | 156 | self.send_command(0x04) # 157 | self.send_data(self.lut_full_update[71]) 158 | self.send_data(self.lut_full_update[72]) 159 | self.send_data(self.lut_full_update[73]) 160 | 161 | self.send_command(0x3A) #Dummy Line 162 | self.send_data(self.lut_full_update[74]) 163 | self.send_command(0x3B) #Gate time 164 | self.send_data(self.lut_full_update[75]) 165 | 166 | self.send_command(0x32) 167 | for count in range(70): 168 | self.send_data(self.lut_full_update[count]) 169 | 170 | self.send_command(0x4E) # set RAM x address count to 0 171 | self.send_data(0x00) 172 | self.send_command(0x4F) # set RAM y address count to 0X127 173 | self.send_data(0xF9) 174 | self.send_data(0x00) 175 | self.ReadBusy() 176 | else: 177 | self.send_command(0x2C) #VCOM Voltage 178 | self.send_data(0x26) 179 | 180 | self.ReadBusy() 181 | 182 | self.send_command(0x32) 183 | for count in range(70): 184 | self.send_data(self.lut_partial_update[count]) 185 | 186 | self.send_command(0x37) 187 | self.send_data(0x00) 188 | self.send_data(0x00) 189 | self.send_data(0x00) 190 | self.send_data(0x00) 191 | self.send_data(0x40) 192 | self.send_data(0x00) 193 | self.send_data(0x00) 194 | 195 | self.send_command(0x22) 196 | self.send_data(0xC0) 197 | self.send_command(0x20) 198 | self.ReadBusy() 199 | 200 | self.send_command(0x3C) #BorderWavefrom 201 | self.send_data(0x01) 202 | return 0 203 | 204 | def getbuffer(self, image): 205 | if self.width%8 == 0: 206 | linewidth = int(self.width/8) 207 | else: 208 | linewidth = int(self.width/8) + 1 209 | 210 | buf = [0xFF] * (linewidth * self.height) 211 | image_monocolor = image.convert('1') 212 | imwidth, imheight = image_monocolor.size 213 | pixels = image_monocolor.load() 214 | 215 | if(imwidth == self.width and imheight == self.height): 216 | for y in range(imheight): 217 | for x in range(imwidth): 218 | if pixels[x, y] == 0: 219 | x = imwidth - x 220 | buf[int(x / 8) + y * linewidth] &= ~(0x80 >> (x % 8)) 221 | elif(imwidth == self.height and imheight == self.width): 222 | for y in range(imheight): 223 | for x in range(imwidth): 224 | newx = y 225 | newy = self.height - x - 1 226 | if pixels[x, y] == 0: 227 | newy = imwidth - newy - 1 228 | buf[int(newx / 8) + newy*linewidth] &= ~(0x80 >> (y % 8)) 229 | return buf 230 | 231 | 232 | def display(self, image): 233 | self.send_command(0x24) 234 | self.send_data2(image) 235 | self.TurnOnDisplay() 236 | 237 | def displayPartial(self, image): 238 | if self.width%8 == 0: 239 | linewidth = int(self.width/8) 240 | else: 241 | linewidth = int(self.width/8) + 1 242 | 243 | buf = [0x00] * self.height * linewidth 244 | for j in range(0, self.height): 245 | for i in range(0, linewidth): 246 | buf[i + j * linewidth] = ~image[i + j * linewidth] 247 | 248 | self.send_command(0x24) 249 | self.send_data2(image) 250 | 251 | 252 | self.send_command(0x26) 253 | self.send_data2(buf) 254 | self.TurnOnDisplayPart() 255 | 256 | def displayPartBaseImage(self, image): 257 | self.send_command(0x24) 258 | self.send_data2(image) 259 | 260 | self.send_command(0x26) 261 | self.send_data2(image) 262 | self.TurnOnDisplay() 263 | 264 | def Clear(self, color=0xFF): 265 | if self.width%8 == 0: 266 | linewidth = int(self.width/8) 267 | else: 268 | linewidth = int(self.width/8) + 1 269 | # LOG.debug(linewidth) 270 | 271 | buf = [0x00] * self.height * linewidth 272 | for j in range(0, self.height): 273 | for i in range(0, linewidth): 274 | buf[i + j * linewidth] = color 275 | 276 | self.send_command(0x24) 277 | self.send_data2(buf) 278 | 279 | # self.send_command(0x26) 280 | # for j in range(0, self.height): 281 | # for i in range(0, linewidth): 282 | # self.send_data(color) 283 | 284 | self.TurnOnDisplay() 285 | 286 | def sleep(self): 287 | # self.send_command(0x22) #POWER OFF 288 | # self.send_data(0xC3) 289 | # self.send_command(0x20) 290 | 291 | self.send_command(0x10) #enter deep sleep 292 | self.send_data(0x03) 293 | epdconfig.delay_ms(2000) 294 | epdconfig.module_exit() 295 | 296 | ### END OF FILE ### 297 | 298 | --------------------------------------------------------------------------------