├── .gitignore ├── LICENSE ├── README.md ├── magicmirror-files ├── config │ └── config.js └── css │ ├── custom.css │ └── fonts │ ├── LICENSE │ ├── cherry-10-b.ttf │ ├── cherry-10-r.ttf │ ├── cherry-11-b.ttf │ ├── cherry-11-r.ttf │ ├── cherry-12-b.ttf │ ├── cherry-12-r.ttf │ ├── cherry-13-b.ttf │ └── cherry-13-r.ttf ├── main.py ├── onetime.sh ├── requirements.txt ├── rpi-magicmirror-eink.service ├── run.sh ├── screenshot.jpg └── waveshare ├── epd7in5_V2.py ├── epd7in5b.py ├── epdconfig.py └── epdif.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/venv,python,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=venv,python,intellij+all 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | ### Intellij+all Patch ### 76 | # Ignores the whole .idea folder and all .iml files 77 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 78 | 79 | .idea/ 80 | 81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 82 | 83 | *.iml 84 | modules.xml 85 | .idea/misc.xml 86 | *.ipr 87 | 88 | # Sonarlint plugin 89 | .idea/sonarlint 90 | 91 | ### Python ### 92 | # Byte-compiled / optimized / DLL files 93 | __pycache__/ 94 | *.py[cod] 95 | *$py.class 96 | 97 | # C extensions 98 | *.so 99 | 100 | # Distribution / packaging 101 | .Python 102 | build/ 103 | develop-eggs/ 104 | dist/ 105 | downloads/ 106 | eggs/ 107 | .eggs/ 108 | lib/ 109 | lib64/ 110 | parts/ 111 | sdist/ 112 | var/ 113 | wheels/ 114 | pip-wheel-metadata/ 115 | share/python-wheels/ 116 | *.egg-info/ 117 | .installed.cfg 118 | *.egg 119 | MANIFEST 120 | 121 | # PyInstaller 122 | # Usually these files are written by a python script from a template 123 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 124 | *.manifest 125 | *.spec 126 | 127 | # Installer logs 128 | pip-log.txt 129 | pip-delete-this-directory.txt 130 | 131 | # Unit test / coverage reports 132 | htmlcov/ 133 | .tox/ 134 | .nox/ 135 | .coverage 136 | .coverage.* 137 | .cache 138 | nosetests.xml 139 | coverage.xml 140 | *.cover 141 | .hypothesis/ 142 | .pytest_cache/ 143 | 144 | # Translations 145 | *.mo 146 | *.pot 147 | 148 | # Django stuff: 149 | *.log 150 | local_settings.py 151 | db.sqlite3 152 | db.sqlite3-journal 153 | 154 | # Flask stuff: 155 | instance/ 156 | .webassets-cache 157 | 158 | # Scrapy stuff: 159 | .scrapy 160 | 161 | # Sphinx documentation 162 | docs/_build/ 163 | 164 | # PyBuilder 165 | target/ 166 | 167 | # Jupyter Notebook 168 | .ipynb_checkpoints 169 | 170 | # IPython 171 | profile_default/ 172 | ipython_config.py 173 | 174 | # pyenv 175 | .python-version 176 | 177 | # pipenv 178 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 179 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 180 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 181 | # install all needed dependencies. 182 | #Pipfile.lock 183 | 184 | # celery beat schedule file 185 | celerybeat-schedule 186 | 187 | # SageMath parsed files 188 | *.sage.py 189 | 190 | # Environments 191 | .env 192 | .venv 193 | env/ 194 | venv/ 195 | ENV/ 196 | env.bak/ 197 | venv.bak/ 198 | 199 | # Spyder project settings 200 | .spyderproject 201 | .spyproject 202 | 203 | # Rope project settings 204 | .ropeproject 205 | 206 | # mkdocs documentation 207 | /site 208 | 209 | # mypy 210 | .mypy_cache/ 211 | .dmypy.json 212 | dmypy.json 213 | 214 | # Pyre type checker 215 | .pyre/ 216 | 217 | ### venv ### 218 | # Virtualenv 219 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 220 | [Bb]in 221 | [Ii]nclude 222 | [Ll]ib 223 | [Ll]ib64 224 | [Ll]ocal 225 | [Ss]cripts 226 | pyvenv.cfg 227 | pip-selfcheck.json 228 | 229 | # End of https://www.gitignore.io/api/venv,python,intellij+all 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Alexander Rashed 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](screenshot.jpg) 2 | 3 | ## How it works 4 | This script is inspired by [BenRoe's rpi-magic-mirror-eink](https://github.com/BenRoe/rpi-magicmirror-eink), but is fully implemented in python. 5 | 6 | The python script uses [pyppeteer (Python implementation of Chromium's instrumentation API)](https://github.com/miyakogi/pyppeteer) to take a screenshot of a website (usually a locally running instance of [MagicMirror²](https://magicmirror.builders)) and then renders the screenshot on Waveshare's 7.5" 3-color display. 7 | 8 | ## Requirements 9 | - Raspbian Buster (Python 3.6+) 10 | - Waveshare 7.5 3-color epaper display 11 | - Ikea RIBBA frame (13x18cm) 12 | 13 | ## Installation 14 | - Enable the SPI is enabled by uncommenting the following line in `/boot/config.txt`: 15 | ``` 16 | dtparam=spi=on 17 | ``` 18 | - Install some packages: 19 | ```bash 20 | sudo apt update && sudo apt install git python3.7-venv python3-venv libatlas-base-dev -y 21 | # Make sure to check the script before executing the next part! You are about to pipe a script from the world wide web to bash! At least it's not root... 22 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash 23 | nvm install --lts 24 | ``` 25 | - We need an older version of chrome (as there are problems with the current puppeteer version and Chromium 74): 26 | - Create a sources list for stretch: 27 | ```bash 28 | sudo nano /etc/apt/sources.list.d/stretch.list 29 | ``` 30 | - Add the following two lines: 31 | ``` 32 | deb http://archive.raspberrypi.org/debian/ stretch main ui 33 | deb-src http://archive.raspberrypi.org/debian/ stretch main ui 34 | ``` 35 | - Add a default apt settings file: 36 | ```bash 37 | sudo nano /etc/apt/apt.conf.d/default 38 | ``` 39 | - Set the default apt release to buster by adding the following line: 40 | ``` 41 | APT::Default-Release "buster"; 42 | ``` 43 | - Install the chromium version from the stretch repo: 44 | ```bash 45 | sudo apt update && sudo apt-get install -t stretch chromium-browser -y 46 | ``` 47 | - Install MagicMirror²: 48 | ```bash 49 | mkdir repos 50 | cd repos 51 | git clone https://github.com/MichMich/MagicMirror 52 | cd MagicMirror 53 | npm install 54 | # Ignore the electron install error (if it occurs) and install the vendor and font packages manually: 55 | npm run install 56 | npm run install-fonts 57 | ``` 58 | - Clone this repository: 59 | ```bash 60 | cd ~/repos 61 | git clone https://github.com/alexrashed/rpi-magicmirror-eink.git 62 | ``` 63 | - Setup the Python environment: 64 | ```bash 65 | cd rpi-magicmirror-eink 66 | python3.7 -m venv .venv 67 | source .venv/bin/activate 68 | pip install -r requirements 69 | ``` 70 | - Copy the MagicMirror² config, CSS and fonts into the MagicMirror directory: 71 | ```bash 72 | cp -r ./magicmirror-files/. ~/repos/MagicMirror 73 | ``` 74 | - Install the MagicMirror modules: 75 | ```bash 76 | cd ~/repos/MagicMirror/modules 77 | git clone https://github.com/alexrashed/calendar_monthly.git 78 | ``` 79 | - Adjust the config to your needs (set calendars, weather info,...): 80 | ```bash 81 | nano ~/repos/MagicMirror/config/config.js 82 | ``` 83 | - Intall PM2: 84 | - Install the package globally: 85 | ```bash 86 | npm install -g pm2 87 | pm2 startup 88 | ``` 89 | - Make sure to execute the command which is printed (DO NOT JUST COPY THIS, IT WILL BE DIFFERENT): 90 | ```bash 91 | sudo env PATH=$PATH:/home/pi/.nvm/versions/node/v10.16.3/bin /home/pi/.nvm/versions/node/v10.16.3/lib/node_modules/pm2/bin/pm2 startup systemd -u pi --hp /home/pi 92 | ``` 93 | - Setup the automatic startup on boot for MagicMirror: 94 | ```bash 95 | # Make sure to execute the command which is printed 96 | # Adjust the path (if MagicMirror is not directly in your home) 97 | nano ~/repos/MagicMirror/installers/pm2_MagicMirror.json 98 | # Adjust the path (if MagicMirror is not directly in your home) and replace the command with `node serveronly` 99 | nano ~/repos/MagicMirror/installers/mm.sh 100 | # If your user is not 'pi' checkout these lines before: 101 | # https://github.com/MichMich/MagicMirror/blob/master/installers/raspberry.sh#L223 102 | pm2 start ~/repos/MagicMirror/installers/pm2_MagicMirror.json 103 | pm2 save 104 | ``` 105 | - Setup the automatic startup on boot for the python script: 106 | ```bash 107 | sudo cp ~/repos/rpi-magicmirror-eink/rpi-magicmirror-eink.service /etc/systemd/system/rpi-magicmirror-eink.service 108 | sudo chmod 644 /etc/systemd/system/rpi-magicmirror-eink.service 109 | sudo systemctl enable rpi-magicmirror-eink 110 | sudo systemctl start rpi-magicmirror-eink 111 | sudo systemctl status rpi-magicmirror-eink 112 | ``` 113 | 114 | ## Configuration 115 | Currently the script is configured by modifying the global variables at the top of [main.py](main.py). 116 | The cron schedule can be adjusted in the [run.sh](run.sh) script. 117 | 118 | ## Resources 119 | - Reused the [Cherry bitmap font files](https://github.com/turquoise-hexagon/cherry) by [marin](https://github.com/turquoise-hexagon) converted to ttf with [Bits'N'Picas](https://github.com/kreativekorp/bitsnpicas) by [BenRoe](https://github.com/BenRoe/) 120 | - The waveshare epaper library 121 | - The project uses the Original Library written by Waveshare. It can be downloaded [here](https://www.waveshare.com/wiki/Main_Page#OLEDs_.2F_LCDs). 122 | 123 | -------------------------------------------------------------------------------- /magicmirror-files/config/config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | address: "localhost", 3 | port: 8080, 4 | ipWhitelist: [], 5 | language: "de", 6 | timeFormat: 24, 7 | units: "metric", 8 | modules: [ 9 | { 10 | module: "calendar_monthly", 11 | position: "top_left", 12 | config: { 13 | } 14 | }, 15 | { 16 | module: "weatherforecast", 17 | position: "top_right", 18 | header: "Wien", 19 | config: { 20 | appendLocationNameToHeader: false, 21 | animationSpeed: 0, 22 | fade: false, 23 | location: "Vienna", 24 | appid: "OPEN_WEATHER_API_KEY" 25 | } 26 | }, 27 | { 28 | module: "currentweather", 29 | position: "top_right", 30 | config: { 31 | appendLocationNameToHeader: false, 32 | animationSpeed: 0, 33 | fade: false, 34 | location: "Vienna", 35 | appid: "OPEN_WEATHER_API_KEY" 36 | } 37 | }, 38 | { 39 | module: "clock", 40 | position: "middle_center", 41 | config: { 42 | showWeek: true 43 | } 44 | }, 45 | { 46 | module: "compliments", 47 | position: "middle_center", 48 | config: { 49 | fadeSpeed: 0 50 | } 51 | } 52 | }, 53 | { 54 | module: "calendar", 55 | header: "Geburtstage", 56 | position: "bottom_bar", 57 | config: { 58 | maximumEntries: 10, 59 | maxTitleLength: 40, 60 | fetchInterval: 3600000, // every 60 min 61 | animationSpeed: 0, 62 | fade: false, 63 | dateFormat: "DD.MM.", 64 | fullDayEventDateFormat: "DD.MM.", 65 | timeFormat: "absolute", 66 | urgency: 7, 67 | wrapEvents: true, 68 | displayRepeatingCountTitle: false, 69 | calendars: [ 70 | { 71 | symbol: "calendar-check-o", 72 | url: "webcal://www.calendarlabs.com/ical-calendar/ics/46/Germany_Holidays.ics" 73 | } 74 | ] 75 | } 76 | }, 77 | ] 78 | }; 79 | 80 | /*************** DO NOT EDIT THE LINE BELOW ***************/ 81 | if (typeof module !== "undefined") {module.exports = config;} 82 | -------------------------------------------------------------------------------- /magicmirror-files/css/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 5px; 3 | height: calc(100% - 10px) !important; 4 | width: calc(100% - 10px) !important; 5 | } 6 | 7 | html, body, .bright, .light, .normal, .medium, .dimmed, .today, .calendar-header-day, header { 8 | color: black !important; 9 | background: white !important; 10 | -webkit-font-smoothing: none !important; 11 | font-smooth: never; 12 | -webkit-font-smoothing : none; 13 | font-family: "cherry13"; 14 | font-weight: 700; 15 | font-size: 13px !important; 16 | } 17 | 18 | .small, .xsmall, .medium, .large { 19 | line-height: normal !important; 20 | font-size: 13px !important; 21 | } 22 | 23 | .fa { 24 | -webkit-font-smoothing: none; 25 | } 26 | 27 | header { 28 | font-weight: bold; 29 | color: red !important; 30 | } 31 | 32 | .compliments > div > div { 33 | /*color: black !important;*/ 34 | color: red !important; 35 | font-weight: 400 !important; 36 | font-size: 40px !important; 37 | } 38 | 39 | .xlarge { 40 | line-height: 30px !important; 41 | font-size: 13px !important; 42 | } 43 | 44 | 45 | 46 | 47 | 48 | /** Clock **/ 49 | /* Only show the date, not the time */ 50 | .module.clock .time { 51 | display: none; 52 | } 53 | 54 | 55 | 56 | 57 | /** Calendar Monthly **/ 58 | .calendar_monthly { 59 | /* width: 280px; */ 60 | } 61 | 62 | .square-box { 63 | /* One day box */ 64 | width: 100%; 65 | } 66 | 67 | .monthPrev, .monthNext { 68 | /* Color of the days of the previous month */ 69 | color: black; 70 | } 71 | 72 | .today { 73 | /* Border of the current day */ 74 | border: 1px solid red !important; 75 | } 76 | 77 | .footer { 78 | display: none; 79 | } 80 | 81 | 82 | 83 | 84 | /** Weather **/ 85 | .currentweather { 86 | /* width: 280px; */ 87 | } 88 | 89 | .calendar-header-day { 90 | padding-top: 0px !important; 91 | } 92 | 93 | #calendar-table { 94 | padding: 0px !important; 95 | } 96 | 97 | .wi { 98 | font-weight: normal !important; 99 | font-family: "weathericons" !important; 100 | line-height: normal !important; 101 | } 102 | 103 | #module_2_currentweather > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > span:nth-child(4) { 104 | /* Space character between the weather icons */ 105 | font-weight: normal !important; 106 | font-family: "Roboto Condensed", sans-serif !important; 107 | } 108 | 109 | .module { 110 | margin-bottom: 10px !important; 111 | } 112 | 113 | .day { 114 | padding-right: 5px !important; 115 | } 116 | 117 | .weather-icon { 118 | padding-right: 5px !important; 119 | } 120 | 121 | .min-temp { 122 | padding-left: 5px !important; 123 | } 124 | 125 | 126 | 127 | 128 | /* Font Definitions */ 129 | @font-face { 130 | font-family: 'cherry10'; 131 | src: url('fonts/cherry-10-r.ttf') format('truetype'); 132 | font-weight: 400; 133 | font-style: normal; 134 | } 135 | 136 | @font-face { 137 | font-family: 'cherry11'; 138 | src: url('fonts/cherry-11-r.ttf') format('truetype'); 139 | font-weight: 400; 140 | font-style: normal; 141 | } 142 | 143 | @font-face { 144 | font-family: 'cherry13'; 145 | src: url('fonts/cherry-13-r.ttf') format('truetype'); 146 | font-weight: 400; 147 | font-style: normal; 148 | } 149 | 150 | @font-face { 151 | font-family: 'cherry13'; 152 | src: url('fonts/cherry-13-b.ttf') format('truetype'); 153 | font-weight: 700; 154 | font-style: normal; 155 | } 156 | 157 | 158 | -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 marin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-10-b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-10-b.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-10-r.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-10-r.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-11-b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-11-b.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-11-r.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-11-r.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-12-b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-12-b.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-12-r.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-12-r.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-13-b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-13-b.ttf -------------------------------------------------------------------------------- /magicmirror-files/css/fonts/cherry-13-r.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/magicmirror-files/css/fonts/cherry-13-r.ttf -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | import logging 4 | import tempfile 5 | import argparse 6 | import sys 7 | import numpy as np 8 | # from aiocron import crontab 9 | from pyppeteer import launch 10 | from PIL import Image 11 | import PIL.ImageOps 12 | 13 | # TODO fix ignored KeyboardInterrupt when running in the the event loop (run_forever) 14 | # TODO implement proper reset and shutdown of the screen when the systemd service is stopped or the raspberry pi is shut down 15 | # TODO maybe a "last updated" time (just a clock module with a different header) 16 | 17 | # Import the waveshare folder (containing the waveshare display drivers) without refactoring it to a module 18 | # TODO maybe switch to a git submodule here and upgrade to the latest version: 19 | # https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5_V2c.py 20 | sys.path.insert(0, './waveshare') 21 | import epd7in5_V2 22 | 23 | 24 | # Global config 25 | display_width = 800 # Width of the display 26 | display_height = 480 # Height of the display 27 | is_portrait = False # True of the display should be in landscape mode (make sure to adjust the width and height accordingly) 28 | is_topdown = True 29 | wait_to_load = 60 # Page load timeout 30 | wait_after_load = 30 # Time to evaluate the JS afte the page load (f.e. to lazy-load the calendar data) default=18 31 | url = 'http://localhost:8080' # URL to create the screenshot of 32 | 33 | def reset_screen(): 34 | epd = epd7in5_V2.EPD() 35 | epd.init() 36 | Limage = Image.new('1', (epd.height, epd.width), 255) # 255: clear the frame 37 | epd.display(epd.getbuffer(Limage)) 38 | 39 | 40 | async def create_screenshot(file_path): 41 | global display_width 42 | global display_height 43 | global wait_to_load 44 | global wait_after_load 45 | global url 46 | logging.debug('Creating screenshot') 47 | browser = await launch(headless=True, args=['--no-sandbox', '--disable-setuid-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], executablePath='/usr/bin/chromium-browser') 48 | page = await browser.newPage() 49 | await page.setViewport({ 50 | "width": display_width, 51 | "height": display_height 52 | }) 53 | await page.goto(url, timeout=wait_to_load * 1000) 54 | await page.waitFor(wait_after_load * 1000); 55 | await page.screenshot({'path': file_path}) 56 | await browser.close() 57 | logging.debug('Finished creating screenshot') 58 | 59 | 60 | def remove_aliasing_artefacts(image): 61 | red = (255,000,000) 62 | black = (000,000,000) 63 | white = (255,255,255) 64 | img = image.convert('RGB') 65 | data = np.array(img) 66 | # If the R value of the pixel is less than 50, make it black 67 | black_mask = np.bitwise_and(data[:,:,0] <= 230, data[:,:,1] <= 135, data[:,:,2] <= 135) 68 | # If the R value is higher than 69 | red_mask = np.bitwise_and(data[:,:,0] >= 230, data[:,:,1] <= 135, data[:,:,2] <= 135) 70 | # Everything else should be white 71 | white_mask = np.bitwise_not(np.bitwise_or(red_mask, black_mask)) 72 | data[black_mask] = black 73 | data[red_mask] = red 74 | data[white_mask] = white 75 | return Image.fromarray(data, mode='RGB') 76 | 77 | 78 | async def refresh(): 79 | logging.info('Starting refresh.') 80 | logging.debug('Initializing / waking screen.') 81 | epd = epd7in5_V2.EPD() 82 | epd.init() 83 | with tempfile.NamedTemporaryFile(suffix='.png') as tmp_file: 84 | logging.debug(f'Created temporary file at {tmp_file.name}.') 85 | await create_screenshot(tmp_file.name) 86 | logging.debug('Opening screenshot.') 87 | image = Image.open(tmp_file) 88 | # Replace all colors with are neither black nor red with white 89 | image = remove_aliasing_artefacts(image) 90 | image = PIL.ImageOps.invert(image) 91 | # Rotate the image by 90° 92 | if is_portrait: 93 | logging.debug('Rotating image (portrait mode).') 94 | image = image.rotate(90) 95 | if is_topdown: 96 | logging.debug('Rotating image (topdown mode).') 97 | image = image.rotate(180) 98 | logging.debug('Sending image to screen.') 99 | # epd.display_frame(epd.get_frame_buffer(image)) 100 | epd.display(epd.getbuffer(image)) 101 | logging.debug('Sending display back to sleep.') 102 | epd.sleep() 103 | logging.info('Refresh finished.') 104 | 105 | 106 | def main(): 107 | try: 108 | parser = argparse.ArgumentParser(description='Python EInk MagicMirror') 109 | parser.add_argument('-d', '--debug', action='store_true', dest='debug', 110 | help='Enable debug logs.', default=False) 111 | parser.add_argument('-c', '--cron', action='store', dest='cron', 112 | help='Sets a schedule using cron syntax') 113 | parser.add_argument('-r', '--reset', action='store_true', dest='reset', 114 | help='Ignore all other settings and just reset the screen.', default=False) 115 | args = parser.parse_args() 116 | level = logging.DEBUG if args.debug else logging.INFO 117 | logging.basicConfig(level=level, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 118 | 119 | if not args.reset: 120 | if args.cron: 121 | logging.info(f'Scheduling the refresh using the schedule "{args.cron}".') 122 | # crontab(args.cron, func=refresh) 123 | # Initially refresh the display before relying on the schedule 124 | asyncio.get_event_loop().run_until_complete(refresh()) 125 | asyncio.get_event_loop().run_forever() 126 | else: 127 | logging.info('Only running the refresh once.') 128 | asyncio.get_event_loop().run_until_complete(refresh()) 129 | except KeyboardInterrupt: 130 | logging.info('Shutting down after receiving a keyboard interrupt.') 131 | finally: 132 | logging.info('Resetting screen.') 133 | # reset_screen() 134 | 135 | 136 | if __name__ == '__main__': 137 | main() 138 | -------------------------------------------------------------------------------- /onetime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /home/pi/rpi-magicmirror-eink/.venv/bin/activate 3 | python3.7 main.py 4 | deactivate 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==2.1.0 2 | spidev==3.4 3 | pyppeteer==0.0.25 4 | aiocron==1.3 5 | websockets==6.0 6 | RPi.GPIO==0.7.0 7 | numpy==1.17.4 8 | -------------------------------------------------------------------------------- /rpi-magicmirror-eink.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=RPi MagicMirror EInk 3 | 4 | [Service] 5 | WorkingDirectory=/home/pi/rpi-magicmirror-eink/ 6 | ExecStart=/home/pi/rpi-magicmirror-eink/run.sh 7 | Restart=on-failure 8 | User=pi 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source .venv/bin/activate 3 | python main.py -c "*/5 * * * *" 4 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-kwl/rpi-magicmirror-eink/cfc7d3e218b76fb148a826af5eacaa646513347f/screenshot.jpg -------------------------------------------------------------------------------- /waveshare/epd7in5_V2.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd7in5.py 3 | # * | Author : Waveshare team 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20 9 | # # | Info : python demo 10 | # ----------------------------------------------------------------------------- 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | 31 | import logging 32 | import epdconfig 33 | 34 | # Display resolution 35 | EPD_WIDTH = 800 36 | EPD_HEIGHT = 480 37 | 38 | class EPD: 39 | def __init__(self): 40 | self.reset_pin = epdconfig.RST_PIN 41 | self.dc_pin = epdconfig.DC_PIN 42 | self.busy_pin = epdconfig.BUSY_PIN 43 | self.cs_pin = epdconfig.CS_PIN 44 | self.width = EPD_WIDTH 45 | self.height = EPD_HEIGHT 46 | 47 | # Hardware reset 48 | def reset(self): 49 | epdconfig.digital_write(self.reset_pin, 1) 50 | epdconfig.delay_ms(200) 51 | epdconfig.digital_write(self.reset_pin, 0) 52 | epdconfig.delay_ms(2) 53 | epdconfig.digital_write(self.reset_pin, 1) 54 | epdconfig.delay_ms(200) 55 | 56 | def send_command(self, command): 57 | epdconfig.digital_write(self.dc_pin, 0) 58 | epdconfig.digital_write(self.cs_pin, 0) 59 | epdconfig.spi_writebyte([command]) 60 | epdconfig.digital_write(self.cs_pin, 1) 61 | 62 | def send_data(self, data): 63 | epdconfig.digital_write(self.dc_pin, 1) 64 | epdconfig.digital_write(self.cs_pin, 0) 65 | epdconfig.spi_writebyte([data]) 66 | epdconfig.digital_write(self.cs_pin, 1) 67 | 68 | def ReadBusy(self): 69 | logging.debug("e-Paper busy") 70 | self.send_command(0x71) 71 | busy = epdconfig.digital_read(self.busy_pin) 72 | while(busy == 0): 73 | self.send_command(0x71) 74 | busy = epdconfig.digital_read(self.busy_pin) 75 | epdconfig.delay_ms(200) 76 | 77 | def init(self): 78 | if (epdconfig.module_init() != 0): 79 | return -1 80 | # EPD hardware init start 81 | self.reset() 82 | 83 | self.send_command(0x01) #POWER SETTING 84 | self.send_data(0x07) 85 | self.send_data(0x07) #VGH=20V,VGL=-20V 86 | self.send_data(0x3f) #VDH=15V 87 | self.send_data(0x3f) #VDL=-15V 88 | 89 | self.send_command(0x04) #POWER ON 90 | epdconfig.delay_ms(100) 91 | self.ReadBusy() 92 | 93 | self.send_command(0X00) #PANNEL SETTING 94 | self.send_data(0x1F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f 95 | 96 | self.send_command(0x61) #tres 97 | self.send_data(0x03) #source 800 98 | self.send_data(0x20) 99 | self.send_data(0x01) #gate 480 100 | self.send_data(0xE0) 101 | 102 | self.send_command(0X15) 103 | self.send_data(0x00) 104 | 105 | self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING 106 | self.send_data(0x10) 107 | self.send_data(0x07) 108 | 109 | self.send_command(0X60) #TCON SETTING 110 | self.send_data(0x22) 111 | 112 | # EPD hardware init end 113 | return 0 114 | 115 | def getbuffer(self, image): 116 | # logging.debug("bufsiz = ",int(self.width/8) * self.height) 117 | buf = [0xFF] * (int(self.width/8) * self.height) 118 | image_monocolor = image.convert('1') 119 | imwidth, imheight = image_monocolor.size 120 | pixels = image_monocolor.load() 121 | # logging.debug("imwidth = %d, imheight = %d",imwidth,imheight) 122 | if(imwidth == self.width and imheight == self.height): 123 | logging.debug("Vertical") 124 | for y in range(imheight): 125 | for x in range(imwidth): 126 | # Set the bits for the column of pixels at the current position. 127 | if pixels[x, y] == 0: 128 | buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) 129 | elif(imwidth == self.height and imheight == self.width): 130 | logging.debug("Horizontal") 131 | for y in range(imheight): 132 | for x in range(imwidth): 133 | newx = y 134 | newy = self.height - x - 1 135 | if pixels[x, y] == 0: 136 | buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8)) 137 | return buf 138 | 139 | def display(self, image): 140 | self.send_command(0x13) 141 | for i in range(0, int(self.width * self.height / 8)): 142 | self.send_data(~image[i]); 143 | 144 | self.send_command(0x12) 145 | epdconfig.delay_ms(100) 146 | self.ReadBusy() 147 | 148 | def Clear(self): 149 | self.send_command(0x10) 150 | for i in range(0, int(self.width * self.height / 8)): 151 | self.send_data(0x00) 152 | 153 | self.send_command(0x13) 154 | for i in range(0, int(self.width * self.height / 8)): 155 | self.send_data(0x00) 156 | 157 | self.send_command(0x12) 158 | epdconfig.delay_ms(100) 159 | self.ReadBusy() 160 | 161 | def sleep(self): 162 | self.send_command(0x02) # POWER_OFF 163 | self.ReadBusy() 164 | 165 | self.send_command(0x07) # DEEP_SLEEP 166 | self.send_data(0XA5) 167 | 168 | epdconfig.module_exit() 169 | ### END OF FILE ### 170 | 171 | -------------------------------------------------------------------------------- /waveshare/epd7in5b.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @filename : epd7in5.py 3 | # @brief : Implements for Dual-color e-paper library 4 | # @author : Yehui from Waveshare 5 | # 6 | # Copyright (C) Waveshare July 10 2017 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documnetation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | 27 | import epdif 28 | from PIL import Image 29 | import RPi.GPIO as GPIO 30 | 31 | # Display resolution 32 | EPD_WIDTH = 640 33 | EPD_HEIGHT = 384 34 | 35 | # EPD7IN5 commands 36 | PANEL_SETTING = 0x00 37 | POWER_SETTING = 0x01 38 | POWER_OFF = 0x02 39 | POWER_OFF_SEQUENCE_SETTING = 0x03 40 | POWER_ON = 0x04 41 | POWER_ON_MEASURE = 0x05 42 | BOOSTER_SOFT_START = 0x06 43 | DEEP_SLEEP = 0x07 44 | DATA_START_TRANSMISSION_1 = 0x10 45 | DATA_STOP = 0x11 46 | DISPLAY_REFRESH = 0x12 47 | IMAGE_PROCESS = 0x13 48 | LUT_FOR_VCOM = 0x20 49 | LUT_BLUE = 0x21 50 | LUT_WHITE = 0x22 51 | LUT_GRAY_1 = 0x23 52 | LUT_GRAY_2 = 0x24 53 | LUT_RED_0 = 0x25 54 | LUT_RED_1 = 0x26 55 | LUT_RED_2 = 0x27 56 | LUT_RED_3 = 0x28 57 | LUT_XON = 0x29 58 | PLL_CONTROL = 0x30 59 | TEMPERATURE_SENSOR_COMMAND = 0x40 60 | TEMPERATURE_CALIBRATION = 0x41 61 | TEMPERATURE_SENSOR_WRITE = 0x42 62 | TEMPERATURE_SENSOR_READ = 0x43 63 | VCOM_AND_DATA_INTERVAL_SETTING = 0x50 64 | LOW_POWER_DETECTION = 0x51 65 | TCON_SETTING = 0x60 66 | TCON_RESOLUTION = 0x61 67 | SPI_FLASH_CONTROL = 0x65 68 | REVISION = 0x70 69 | GET_STATUS = 0x71 70 | AUTO_MEASUREMENT_VCOM = 0x80 71 | READ_VCOM_VALUE = 0x81 72 | VCM_DC_SETTING = 0x82 73 | 74 | class EPD: 75 | def __init__(self): 76 | self.reset_pin = epdif.RST_PIN 77 | self.dc_pin = epdif.DC_PIN 78 | self.busy_pin = epdif.BUSY_PIN 79 | self.width = EPD_WIDTH 80 | self.height = EPD_HEIGHT 81 | 82 | def digital_write(self, pin, value): 83 | epdif.epd_digital_write(pin, value) 84 | 85 | def digital_read(self, pin): 86 | return epdif.epd_digital_read(pin) 87 | 88 | def delay_ms(self, delaytime): 89 | epdif.epd_delay_ms(delaytime) 90 | 91 | def send_command(self, command): 92 | self.digital_write(self.dc_pin, GPIO.LOW) 93 | # the parameter type is list but not int 94 | # so use [command] instead of command 95 | epdif.spi_transfer([command]) 96 | 97 | def send_data(self, data): 98 | self.digital_write(self.dc_pin, GPIO.HIGH) 99 | # the parameter type is list but not int 100 | # so use [data] instead of data 101 | epdif.spi_transfer([data]) 102 | 103 | def init(self): 104 | if (epdif.epd_init() != 0): 105 | return -1 106 | self.reset() 107 | self.send_command(POWER_SETTING) 108 | self.send_data(0x37) 109 | self.send_data(0x00) 110 | self.send_command(PANEL_SETTING) 111 | self.send_data(0xCF) 112 | self.send_data(0x08) 113 | self.send_command(BOOSTER_SOFT_START) 114 | self.send_data(0xc7) 115 | self.send_data(0xcc) 116 | self.send_data(0x28) 117 | self.send_command(POWER_ON) 118 | self.wait_until_idle() 119 | self.send_command(PLL_CONTROL) 120 | self.send_data(0x3c) 121 | self.send_command(TEMPERATURE_CALIBRATION) 122 | self.send_data(0x00) 123 | self.send_command(VCOM_AND_DATA_INTERVAL_SETTING) 124 | self.send_data(0x77) 125 | self.send_command(TCON_SETTING) 126 | self.send_data(0x22) 127 | self.send_command(TCON_RESOLUTION) 128 | self.send_data(0x02) #source 640 129 | self.send_data(0x80) 130 | self.send_data(0x01) #gate 384 131 | self.send_data(0x80) 132 | self.send_command(VCM_DC_SETTING) 133 | self.send_data(0x1E) #decide by LUT file 134 | self.send_command(0xe5) #FLASH MODE 135 | self.send_data(0x03) 136 | 137 | def wait_until_idle(self): 138 | while(self.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle 139 | self.delay_ms(100) 140 | 141 | def reset(self): 142 | self.digital_write(self.reset_pin, GPIO.LOW) # module reset 143 | self.delay_ms(200) 144 | self.digital_write(self.reset_pin, GPIO.HIGH) 145 | self.delay_ms(200) 146 | 147 | def get_frame_buffer(self, image): 148 | buf = [0x00] * int(self.width * self.height / 4) 149 | # Set buffer to value of Python Imaging Library image. 150 | # Image must be in mode L. 151 | image_grayscale = image.convert('L') 152 | imwidth, imheight = image_grayscale.size 153 | if imwidth != self.width or imheight != self.height: 154 | raise ValueError('Image must be same dimensions as display \ 155 | ({0}x{1}).' .format(self.width, self.height)) 156 | 157 | pixels = image_grayscale.load() 158 | for y in range(self.height): 159 | for x in range(self.width): 160 | # Set the bits for the column of pixels at the current position. 161 | if pixels[x, y] < 64: # black 162 | buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) 163 | elif pixels[x, y] < 192: # convert gray to red 164 | buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) 165 | buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2) 166 | else: # white 167 | buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2) 168 | return buf 169 | 170 | def display_frame(self, frame_buffer): 171 | self.send_command(DATA_START_TRANSMISSION_1) 172 | for i in range(0, int(self.width / 4 * self.height)): 173 | temp1 = frame_buffer[i] 174 | j = 0 175 | while (j < 4): 176 | if ((temp1 & 0xC0) == 0xC0): 177 | temp2 = 0x03 178 | elif ((temp1 & 0xC0) == 0x00): 179 | temp2 = 0x00 180 | else: 181 | temp2 = 0x04 182 | temp2 = (temp2 << 4) & 0xFF 183 | temp1 = (temp1 << 2) & 0xFF 184 | j += 1 185 | if((temp1 & 0xC0) == 0xC0): 186 | temp2 |= 0x03 187 | elif ((temp1 & 0xC0) == 0x00): 188 | temp2 |= 0x00 189 | else: 190 | temp2 |= 0x04 191 | temp1 = (temp1 << 2) & 0xFF 192 | self.send_data(temp2) 193 | j += 1 194 | self.send_command(DISPLAY_REFRESH) 195 | self.delay_ms(100) 196 | self.wait_until_idle() 197 | 198 | def sleep(self): 199 | self.send_command(POWER_OFF) 200 | self.wait_until_idle() 201 | self.send_command(DEEP_SLEEP) 202 | self.send_data(0xa5) 203 | 204 | ### END OF FILE ### 205 | 206 | -------------------------------------------------------------------------------- /waveshare/epdconfig.py: -------------------------------------------------------------------------------- 1 | # /***************************************************************************** 2 | # * | File : epdconfig.py 3 | # * | Author : Waveshare team 4 | # * | Function : Hardware underlying interface 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V1.0 8 | # * | Date : 2019-06-21 9 | # * | Info : 10 | # ****************************************************************************** 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | import os 31 | import logging 32 | import sys 33 | import time 34 | 35 | 36 | class RaspberryPi: 37 | # Pin definition 38 | RST_PIN = 17 39 | DC_PIN = 25 40 | CS_PIN = 8 41 | BUSY_PIN = 24 42 | 43 | def __init__(self): 44 | import spidev 45 | import RPi.GPIO 46 | 47 | self.GPIO = RPi.GPIO 48 | 49 | # SPI device, bus = 0, device = 0 50 | self.SPI = spidev.SpiDev(0, 0) 51 | 52 | def digital_write(self, pin, value): 53 | self.GPIO.output(pin, value) 54 | 55 | def digital_read(self, pin): 56 | return self.GPIO.input(pin) 57 | 58 | def delay_ms(self, delaytime): 59 | time.sleep(delaytime / 1000.0) 60 | 61 | def spi_writebyte(self, data): 62 | self.SPI.writebytes(data) 63 | 64 | def module_init(self): 65 | self.GPIO.setmode(self.GPIO.BCM) 66 | self.GPIO.setwarnings(False) 67 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 68 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 69 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 70 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 71 | self.SPI.max_speed_hz = 4000000 72 | self.SPI.mode = 0b00 73 | return 0 74 | 75 | def module_exit(self): 76 | logging.debug("spi end") 77 | self.SPI.close() 78 | 79 | logging.debug("close 5V, Module enters 0 power consumption ...") 80 | self.GPIO.output(self.RST_PIN, 0) 81 | self.GPIO.output(self.DC_PIN, 0) 82 | 83 | self.GPIO.cleanup() 84 | 85 | 86 | class JetsonNano: 87 | # Pin definition 88 | RST_PIN = 17 89 | DC_PIN = 25 90 | CS_PIN = 8 91 | BUSY_PIN = 24 92 | 93 | def __init__(self): 94 | import ctypes 95 | find_dirs = [ 96 | os.path.dirname(os.path.realpath(__file__)), 97 | '/usr/local/lib', 98 | '/usr/lib', 99 | ] 100 | self.SPI = None 101 | for find_dir in find_dirs: 102 | so_filename = os.path.join(find_dir, 'sysfs_software_spi.so') 103 | if os.path.exists(so_filename): 104 | self.SPI = ctypes.cdll.LoadLibrary(so_filename) 105 | break 106 | if self.SPI is None: 107 | raise RuntimeError('Cannot find sysfs_software_spi.so') 108 | 109 | import Jetson.GPIO 110 | self.GPIO = Jetson.GPIO 111 | 112 | def digital_write(self, pin, value): 113 | self.GPIO.output(pin, value) 114 | 115 | def digital_read(self, pin): 116 | return self.GPIO.input(self.BUSY_PIN) 117 | 118 | def delay_ms(self, delaytime): 119 | time.sleep(delaytime / 1000.0) 120 | 121 | def spi_writebyte(self, data): 122 | self.SPI.SYSFS_software_spi_transfer(data[0]) 123 | 124 | def module_init(self): 125 | self.GPIO.setmode(self.GPIO.BCM) 126 | self.GPIO.setwarnings(False) 127 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 128 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 129 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 130 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 131 | self.SPI.SYSFS_software_spi_begin() 132 | return 0 133 | 134 | def module_exit(self): 135 | logging.debug("spi end") 136 | self.SPI.SYSFS_software_spi_end() 137 | 138 | logging.debug("close 5V, Module enters 0 power consumption ...") 139 | self.GPIO.output(self.RST_PIN, 0) 140 | self.GPIO.output(self.DC_PIN, 0) 141 | 142 | self.GPIO.cleanup() 143 | 144 | 145 | if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'): 146 | implementation = RaspberryPi() 147 | else: 148 | implementation = JetsonNano() 149 | 150 | for func in [x for x in dir(implementation) if not x.startswith('_')]: 151 | setattr(sys.modules[__name__], func, getattr(implementation, func)) 152 | 153 | 154 | ### END OF FILE ### 155 | -------------------------------------------------------------------------------- /waveshare/epdif.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @filename : epdif.py 3 | # @brief : EPD hardware interface implements (GPIO, SPI) 4 | # @author : Yehui from Waveshare 5 | # 6 | # Copyright (C) Waveshare July 10 2017 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documnetation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | 27 | import spidev 28 | import RPi.GPIO as GPIO 29 | import time 30 | 31 | # Pin definition 32 | RST_PIN = 17 33 | DC_PIN = 25 34 | CS_PIN = 8 35 | BUSY_PIN = 24 36 | 37 | # SPI device, bus = 0, device = 0 38 | SPI = spidev.SpiDev(0, 0) 39 | 40 | def epd_digital_write(pin, value): 41 | GPIO.output(pin, value) 42 | 43 | def epd_digital_read(pin): 44 | return GPIO.input(pin) 45 | 46 | def epd_delay_ms(delaytime): 47 | time.sleep(delaytime / 1000.0) 48 | 49 | def spi_transfer(data): 50 | SPI.writebytes(data) 51 | 52 | def epd_init(): 53 | GPIO.setmode(GPIO.BCM) 54 | GPIO.setwarnings(False) 55 | GPIO.setup(RST_PIN, GPIO.OUT) 56 | GPIO.setup(DC_PIN, GPIO.OUT) 57 | GPIO.setup(CS_PIN, GPIO.OUT) 58 | GPIO.setup(BUSY_PIN, GPIO.IN) 59 | SPI.max_speed_hz = 2000000 60 | SPI.mode = 0b00 61 | return 0; 62 | 63 | ### END OF FILE ### 64 | --------------------------------------------------------------------------------