├── .github └── workflows │ ├── flake8.yml │ └── pytest.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── PIA03519_small.jpg ├── basic_example │ └── draw_image.py ├── ini_file_example │ ├── draw_image.py │ └── omni-epd.ini ├── mode_example │ ├── draw_image.py │ └── omni-epd.ini └── photo_attribution.txt ├── pyproject.toml ├── setup.py ├── src └── omni_epd │ ├── __init__.py │ ├── conf.py │ ├── didder │ ├── displayfactory.py │ ├── displays │ ├── __init__.py │ ├── inky_display.py │ ├── mock_display.py │ └── waveshare_display.py │ ├── errors.py │ ├── test_utility.py │ └── virtualepd.py └── tests ├── __init__.py ├── constants.py ├── ini ├── all_options.ini ├── bad_conf.ini ├── basic_dither.ini ├── custom_dither.ini ├── custom_dither_json.ini ├── omni-epd.ini └── omni_epd.mock.ini ├── json └── custom_diffusion_matrix.json ├── master_bw_output.png ├── test_display_manual.py ├── test_epd_loading.py ├── test_image_processing.py └── test_omni_epd_config.py /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: Python Code Check 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | flake8-lint: 7 | runs-on: ubuntu-latest 8 | name: flake8 Linter 9 | steps: 10 | - name: Check out source repository 11 | uses: actions/checkout@v3 12 | - name: Set up Python environment 13 | uses: actions/setup-python@v3 14 | with: 15 | python-version: "3.11" 16 | - name: flake8 Lint 17 | uses: py-actions/flake8@v2 18 | with: 19 | max-line-length: "150" 20 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test Check 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | unittest-pytest: 7 | runs-on: ubuntu-latest 8 | name: Unit Tests 9 | steps: 10 | - name: Check out source repository 11 | uses: actions/checkout@v3 12 | - name: Set up Python environment 13 | uses: actions/setup-python@v3 14 | with: 15 | python-version: "3.11" 16 | - name: Set up ARM runner 17 | uses: pguyot/arm-runner-action@v2 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip build wheel 21 | pip install .[dev] 22 | - name: Test with pytest 23 | run: | 24 | pytest 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/* 3 | build/* 4 | dist/* 5 | *.egg-info 6 | .venv 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | 7 | ## Version 0.4.2 8 | 9 | ## Added 10 | 11 | - support for [Waveshare 7.3in E](https://www.waveshare.com/7.3inch-e-paper-hat-e.htm) display - thanks @antoinecellerier 12 | 13 | ## Changed 14 | 15 | - replaced `importlib.resources` with `importlib_resources` per the [migration information](https://importlib-resources.readthedocs.io/en/latest/using.html#migrating-from-legacy) 16 | - Waveshare libraries now loaded from [official repo](https://github.com/waveshareteam/e-Paper) again. 17 | 18 | ### Fixed 19 | 20 | - Waveshare 7.3 inch was loading the wrong driver - thanks @reinharty 21 | - Waveshare 1.54in V2 had wrong init() method - thanks @W11T 22 | - fixed GitHub actions Pytests not working 23 | 24 | ## Version 0.4.1 25 | 26 | ### Added 27 | 28 | - `--list` argument for the `omni-epd-test` utility. This will list all valid display options and exit. 29 | - Added [PiArtFrame](https://github.com/runezor/PiArtFrame) to projects list 30 | 31 | ### Changed 32 | 33 | - added development mode install instructions to README 34 | 35 | ### Fixed 36 | 37 | - waveshare 1in displays had wrong device name in README - thanks @rpdom 38 | - palette filter processing was not working with modes other than black/white - thanks @fardage for the issue 39 | 40 | ## [Version 0.4.0](https://github.com/robweber/omni-epd/compare/v0.3.5...robweber:v0.4.0) 41 | 42 | ### Added 43 | 44 | - support for Raspberry Pi OS 12 (Bookworm) and Python 3.11 through some dependency fixes 45 | - information regarding Python virtual environment usage 46 | - disclaimer on status of official Waveshare repo support with Bookworm 47 | 48 | ### Changed 49 | 50 | - updated IT8951 repo link to build from source with RPI dependencies 51 | - Waveshare drivers now loaded from [forked repo](https://github.com/mendhak/waveshare-epaper-sample) to fix Bookworm Rpi detection 52 | - `setup.cfg` information merged into `pyproject.toml` 53 | 54 | ## [Version 0.3.5](https://github.com/robweber/omni-epd/compare/v0.3.4...robweber:v0.3.5) 55 | 56 | ### Changed 57 | 58 | - changed valid Python versions from 3.7 < 3.11. Issues currently on 3.11 installs running on Raspberry Pi OS 12 (Bookworm) 59 | - pinned Waveshare repo to [4822c07](https://github.com/waveshareteam/e-Paper/commit/4822c075f5df714f88b02e10c336b4eeff7e603e). This is known to work with Python 3.7 < 3.10. 60 | - pinned IT8951 repo to [6721516](https://github.com/GregDMeyer/IT8951/commit/67215164a7fc471bc6904f72ad55e51030905a97). This is known to work with Python 3.7 < 3.10. 61 | - all build information moved from `setup.cfg` to `pyproject.toml` 62 | 63 | ### Fixed 64 | 65 | - Updated Waveshare GitHub URL to https://github.com/waveshareteam/e-Paper/ in README and documentation 66 | - Fixed Waveshare 1.02in as the `display()` method [has changed](https://github.com/waveshareteam/e-Paper/pull/283). 67 | 68 | ## [Version 0.3.4](https://github.com/robweber/omni-epd/compare/v0.3.2...robweber:v0.3.4) 69 | 70 | ### Added 71 | 72 | - `palette_filter` advanced option now supports [color names](https://github.com/python-pillow/Pillow/blob/e3cb4bb8e00fcaf4c3e0783f7c02e51372595659/src/PIL/ImageColor.py#L153-L305) or hex values in addition to RGB colors. Thanks @missionfloyd 73 | - IT8951 devices now support `gray16` mode for grayscale 74 | - [Waveshare 10.3 IT8951](https://www.waveshare.com/10.3inch-e-paper-hat.htm) device marked as tested. Thanks @simonjowett 75 | 76 | ## Version 0.3.3 77 | 78 | ### Added 79 | 80 | - added link to [pycasso](https://github.com/jezs00/pycasso) in the README. Thanks @jezs00 81 | - new displays, [Waveshare 7.3in 7 Color](https://www.waveshare.com/7.3inch-e-paper-hat-f.htm) - thanks @evelyndooley, [Waveshare 2.13in V3](https://www.waveshare.com/2.13inch-e-paper-hat.htm) 82 | 83 | ## [Version 0.3.2](https://github.com/robweber/omni-epd/compare/v0.3.1...robweber:v0.3.2) 84 | 85 | ### Changed 86 | 87 | - updated Pillow min version to 9.1+ 88 | - code cleanup for proper style standards - thanks @missionfloyd 89 | 90 | ### Added 91 | 92 | - added new 4 color waveshare displays (epd1in64g, epd2in36g, epd3in0g, epd4in37g, epd7in3g). Thanks @missionfloyd 93 | 94 | ## [Version 0.3.1](https://github.com/robweber/omni-epd/compare/v0.3.0...robweber:v0.3.1) 95 | 96 | ### Added 97 | 98 | - `omni_epd.mock` can now set both the width and height values within the `.ini` file. Thanks @missionfloyd 99 | 100 | ### Fixed 101 | 102 | - `omni_epd.mock` device now returns a palette filter when using the color mode. Previously this returned only b/w and resulted in image processing enhancements resulting in a black and white only image, even when color was selected. Hardcoded palette based on web safe colors. 103 | 104 | - Waveshare device `epd2in13_V2` should use the alternate clear method which requires a color parameter. Thanks @ThatIsAPseudo for pointing this out 105 | 106 | ### Changed 107 | 108 | - dithering is now done with [didder](https://github.com/makeworld-the-better-one/didder) - this is a massive improvement both in scope and speed to hitherdither. Thanks @missionfloyd 109 | 110 | ### Removed 111 | 112 | - removed `hitherdither` as a dependency 113 | 114 | ## [0.3.0](https://github.com/robweber/omni-epd/compare/v0.2.6...robweber:v0.3.0) 115 | 116 | ### Added 117 | 118 | - added `force_palette` argument to the `virtualepd._filter()` function. Will force palette based conversion if wanted, default is False 119 | - added additional tested displays per #63 comments 120 | - new unit tests to make sure image processing components run without error 121 | - support for `inky.auto` as a valid EPD device. This will auto detect Inky devices and load the correct driver. Thanks @donbing 122 | - support for IT8951 devices, such as the WaveShare 6in display 123 | 124 | ### Fixed 125 | 126 | - calls to `Image.quantize` require an RGB or L mode Image object, convert any loaded image before applying new palettes 127 | - when filling palette too many colors were being set (< 256), wrong length variable was being used 128 | - fixed regression where Inky `bw` mode was causing colors to be inverted 129 | - universal fix for Waveshare Tri-color displays as original fixes broke some displays - thanks @aaron8684 130 | 131 | ### Changed 132 | 133 | - make sure Pillow and Inky packages are known working versions or above - thanks @donbing 134 | - `bw` standardized as the consistent naming for the default black/white device mode. `black` will throw a warning, affects Inky devices - thanks @missionfloyd 135 | 136 | ## 0.2.6 137 | 138 | ### Added 139 | 140 | - added `version` identifier for Waveshare devices so that V2 and V3 boards can be identified from the others 141 | 142 | ### Fixed 143 | 144 | - fixed typos in 5.65in Waveshare implementation - thanks @aaronr8684 145 | - fixed issues with BW display on 7.5 tri-color screens - thanks @aaronr8684 146 | 147 | ### Removed 148 | 149 | - removed dependency inky[fonts], this is not needed. Thanks @missionfloyd 150 | 151 | ## Version 0.2.5 152 | 153 | ### Fixed 154 | 155 | - fixed overlay colors in epd5in83c tri-color display (thanks @dr-boehmerie) 156 | 157 | ### Added 158 | 159 | - `waveshare_epd.epd2in9` and `waveshare_epd.epd5in83c` are now tested (thanks @dr-boehmerie) 160 | 161 | ## Version 0.2.4 162 | 163 | ### Fixed 164 | 165 | - fixed issues with Waveshare 3.7in devices not working properly. 166 | 167 | ## Version 0.2.3 168 | 169 | ### Added 170 | 171 | - dithering options in `ini` file added using the [hitherdither](https://github.com/hbldh/hitherdither) library, thanks @missionfloyd 172 | 173 | ## Version 0.2.2 174 | 175 | ### Added 176 | 177 | - added [SlowMovie](https://github.com/TomWhitwell/SlowMovie) to list of implementing projects 178 | 179 | ### Changed 180 | 181 | - modified several of the Waveshare devices to make sure `init()`, `display()`, and `clear()` methods are all being called correctly based on device specifics 182 | 183 | ## Version 0.2.1 184 | 185 | ### Changed 186 | 187 | - restructured object inheritance to reduce duplicate code, this is especially true for waveshare devices 188 | 189 | ### Fixed 190 | 191 | - fixed issue where test utility would fail on inky displays. made the `draw()` function more universal between devices 192 | 193 | ## Version 0.2.0 194 | 195 | ### Added 196 | 197 | - added device specific modes to INI file 198 | - updated device types to use multiple color modes when available 199 | - added many device specific options, loaded within INI files. [Documented on wiki](https://github.com/robweber/omni-epd/wiki/Device-Specific-Options) 200 | - Inky Impression is tested - thanks @missionfloyd 201 | 202 | ### Changed 203 | 204 | - updated device table to show available device modes for each supported type 205 | - dynamically load class files instead of using import where possible 206 | - changed example image to use one from NASA with more colors for better color display test 207 | - updated tests to better handle INI file cleanup 208 | 209 | ### Removed 210 | 211 | - removed Image Enhancements from INI having to do with colors, moved to device specific configurations 212 | 213 | ## Version 0.1.7 214 | 215 | ### Added 216 | 217 | - The mock display driver, `omni_epd.mock`, now writes the image file to a jpg in the local directory for better testing 218 | 219 | ### Fixed 220 | 221 | - EPD config section didn't have corresponding var in `conf.py` 222 | - fixed issues with some Waveshare displays not working due to differences in individual drivers #8 for more details 223 | 224 | ## Version 0.1.6 225 | 226 | ### Added 227 | 228 | - Added some notes on contributing 229 | - unit test build badge to README 230 | 231 | ### Changed 232 | 233 | - Rebrand! `vsmp-epd` renamed to `omni-epd`, subsequent commands and documentation also updated 234 | 235 | ## Version 0.1.5 236 | 237 | ### Added 238 | 239 | - support for Inky type displays (pHAT, wHAT, and Impression) 240 | - added instructions for installing direct from repo 241 | 242 | ### Removed 243 | 244 | - removed PyPi setup instructions, more important to allow installing of waveshare libs 245 | 246 | ## Version 0.1.4 247 | 248 | ### Added 249 | 250 | - added ability to create `vsmp-epd.ini` file to manually set display options for epd that always get applied 251 | - added device level ini file using `devicename.ini` for syntax 252 | - automatic pytest checks for PRs on Github Actions 253 | - added working code examples 254 | - `vsmp-epd-test` now accepts the `-i` flag to load an image in addition to the default display pattern 255 | 256 | ### Changed 257 | 258 | - don't use the root logger 259 | - added additional VirtualEPD class logging 260 | - modified `setup.cfg` to add additional [Classifiers](https://pypi.org/classifiers/) and correct dependencies (waveshare from git) 261 | 262 | ## Version 0.1.3 263 | 264 | ### Added 265 | 266 | - added `clear()` functionality to waveshare display class 267 | - added `EPDTestUtility` class for basic display troubleshooting 268 | - added `vsmp-epd-test` console script for quick user testing 269 | 270 | ### Fixed 271 | 272 | - fixed issue when waveshare lib not installed throwing error due to import ordering 273 | 274 | ### Changed 275 | 276 | - moved `EPDNotFoundError` class so it's easier to import 277 | - updated README with better individual display and testing instructions 278 | 279 | ## Version 0.1.2 280 | 281 | ### Fixed 282 | 283 | - missed some debug messages and syntax errors 284 | 285 | ## Version 0.1.1 286 | 287 | ### Added 288 | 289 | - added some license notices per gnu.org 290 | - added some unit tests 291 | 292 | ### Changed 293 | 294 | - invalid device now throws `EPDNotFoundError` instead of calling exit - let the user deal with it 295 | 296 | ## Version 0.1.0 - 2021-04-15 297 | 298 | ### Added 299 | 300 | - Pypi badge with most current version 301 | 302 | ### Changed 303 | 304 | - small tweaks to create a decent release version for PyPi 305 | 306 | ## Version 0.0.3 - 2021-04-15 307 | 308 | ### Changed 309 | 310 | - Added information on supported displays and usage information to README 311 | 312 | ### Fixed 313 | 314 | - fixed waveshare `close()` behavior 315 | 316 | ## Version 0.0.2 - 2021-04-14 317 | 318 | ### Added 319 | 320 | - added project config files like .gitignore, README, License, etc 321 | - added python project build files (setup.py, setup.cfg, etc) 322 | 323 | ### Changed 324 | 325 | - updated legacy class files for better package management 326 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omni-EPD 2 | ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Frobweber%2Fomni-epd%2Fmain%2Fpyproject.toml) 3 | [![build-status](https://img.shields.io/github/actions/workflow/status/robweber/omni-epd/pytest.yml?branch=main)](https://github.com/robweber/omni-epd/actions/workflows/pytest.yml?query=branch%3Amain) 4 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg)](https://github.com/RichardLitt/standard-readme) 5 | 6 | An EPD (electronic paper display) class abstraction to simplify communications across multiple display types. 7 | 8 | There are several great EPD projects all over the internet, many in written in Python. The problem with a lot of these is that the code is often for one specific type of display, or perhaps a family of displays. This project abstracts the EPD communications into a common interface so a variety of displays can be interchangeably used in the same project. It also adds a lot of [helpful conveniences](#advanced-epd-control) such as the ability to automatically rotate, add contrast, or dither images on their way to the display. This gives more control to end users without having to add extra features in your upstream project. 9 | 10 | For EPD project builders this expands the number of displays you can use for your project without having to code around each one. To utilize this in your project read the usage instructions. For a list of (known) projects that use this abstraction see the [list below](#displays-implmented). 11 | 12 | ## Table Of Contents 13 | 14 | - [Install](#install) 15 | - [Python Virtual Environments](#python-virtual-environments) 16 | - [Usage](#usage) 17 | - [VirtualEPD Object](#virtualepd-object) 18 | - [Display Testing](#display-testing) 19 | - [Advanced EPD Control](#advanced-epd-control) 20 | - [Dithering](#dithering) 21 | - [Displays Implemented](#displays-implemented) 22 | - [Display Driver Installation](#display-driver-installation) 23 | - [Implementing Projects](#implementing-projects) 24 | - [Acknowledgements](#acknowledgements) 25 | - [Contributing](#contributing) 26 | - [Contributors](#contributors) 27 | - [License](#license) 28 | 29 | ## Install 30 | 31 | Installing this module installs any required _Python_ library files. Refer to instructions for your specific display for any [additional requirements](#display-driver-installation) that may need to be satisfied. A common requirement is [enabling SPI support](https://www.raspberrypi-spy.co.uk/2014/08/enabling-the-spi-interface-on-the-raspberry-pi/) on a Raspberry Pi. Install any required libraries or setup files and then run: 32 | 33 | ``` 34 | 35 | pip3 install --upgrade pip setuptools wheel 36 | pip3 install git+https://github.com/robweber/omni-epd.git#egg=omni-epd 37 | 38 | ``` 39 | 40 | This will install the abstraction library. The [test utility](#display-testing) can be used to test your display and ensure everything is working properly. You can also clone this repo and install from source with: 41 | 42 | ``` 43 | 44 | git clone https://github.com/robweber/omni-epd.git 45 | cd omni-epd 46 | pip3 install --upgrade pip setuptools wheel 47 | pip3 install --prefer-binary . 48 | 49 | ``` 50 | 51 | ### Python Virtual Environments 52 | 53 | It is best practice to install inside a [virtual environment]( http://rptl.io/venv). For implementing projects you may experience errors installing outside of a virtual environment. 54 | 55 | The [numpy](https://numpy.org/) package, required by Pillow, needs access to the system installed version of _numpy_ in order to work properly. When setting up your virtual environment be sure to pass in the `--system-site-packages` argument to enable using system packages if they're available. An example would be: 56 | 57 | ``` 58 | # create the environment 59 | python3 -m venv --system-site-packages .venv 60 | 61 | # activate the environment 62 | source .venv/bin/activate 63 | 64 | # deactivate the environment 65 | deactivate 66 | ``` 67 | 68 | ## Usage 69 | 70 | Usage in this case refers to EPD project implementers that wish to abstract their code with this library. In general, this is pretty simple. This library is meant to be very close to a 1:1 replacement for existing EPD code you may have in your project. Function names may vary slightly but most calls are very similar. Refer to the [examples folder](https://github.com/robweber/omni-epd/tree/main/examples) for some working code examples you can run. In general, once the `VirtualEPD` object is loaded it can interact with your display using the methods described below. For testing, the device `omni_epd.mock` can be used to write output to a PNG file instead of to a display. 71 | 72 | ### VirtualEPD Object 73 | 74 | Objects returned by the `displayfactory` class all inherit methods from the `VirtualEPD` class. The following methods are available to be implemented once the object is loaded. Be aware that not all displays may implement all methods but `display` is required. 75 | 76 | * `width` and `height` - these are convenience attributes to get the width and height of the display in your code. 77 | * `prepare()` - does any initializing information on the display. This is waking up from sleep or doing anything else prior to a new image being drawn. 78 | * `display(image)` - draws an image on the display. The image must be a [Pillow Image](https://pillow.readthedocs.io/en/stable/reference/Image.html) object. 79 | * `sleep()` - puts the display into sleep mode, if available for that device. Generally this is lower power consumption and maintains longer life of the display. 80 | * `clear()` - clears the display 81 | * `close()` - performs any cleanup operations and closes access to the display. Use at the end of a program or when the object is no longer needed. 82 | 83 | If the display you're using supports any advanced features, like multiple colors, these can be handled by setting some additional variables. See [advanced display control](#advanced-epd-control) for a better idea of how to additional options. 84 | 85 | * `modes_available` - a tuple containing the names of valid modes, __BW__ available by default 86 | * `max_colors` - The maximum number of colors supported (up to 256 RGB) 87 | * `palette_filter` - a tuple of RGB values for valid colors an `Image` can send to the display 88 | 89 | ### Display Testing 90 | 91 | There is a utility, `omni-epd-test` to verify the display. This is useful to provide users with a way to test that their hardware is working properly. Many displays have specific library requirements that need to be installed with OS level package utilities and may throw errors until they are resolved. The test utility helps confirm all requirements are met before doing more advanced work with the display. This can be run from the command line, specifying the device from the table below. 92 | 93 | ``` 94 | # this will draw a series of rectangles 95 | user@server:~ $ omni-epd-test -e omni_epd.mock 96 | 97 | # this will draw the specified image 98 | user@server:~ $ omni-epd-test -e omni_epd.mock -i /path/to/image.jpg 99 | 100 | # print a list of all valid EPD options 101 | user@server:~ $ omni-epd-test --list 102 | 103 | ``` 104 | 105 | ### Advanced EPD Control 106 | 107 | There are scenarios where additional post-processing needs to be done for a particular project, or a particular display. An example of this might be to rotate the display 180 degrees to account for how the physical hardware is mounted. Another might be always adjusting the image with brightness or contrast settings. These are modifications that are specific to display requirements or user preferences and can be applied by use of a .ini file instead of having to modify code or allow for options via implementing scripts. 108 | 109 | Two types of __ini__ files can be used in these situations. A global file, named `omni-epd.ini`, or a device specific file; which is the device name from the table below with a `.ini` suffix. These must exist in the root directory where the calling script is run. This is the directory given by the `os.getcwd()` method call. Valid options for this file are listed below. These will be applied on top of any processing done to the passed in image object. For example, if the implementing script is already modifying the image object to rotate 90 degrees, adding a rotate command will rotate an additional X degrees. For precedence device specific configurations trump any global configurations. Some displays also have options specific to them only. [Consult with that list](https://github.com/robweber/omni-epd/wiki/Device-Specific-Options) if these additional options are needed in your situation. 110 | 111 | ``` 112 | # file shown with default values 113 | [EPD] 114 | type=none # only valid in the global configuration file, will load this display if none given to displayfactor.load_display_driver() 115 | mode=bw # the mode of the display, typically b+w by default. See list of supported modes for each display below 116 | 117 | [Display] 118 | rotate=0 # rotate final image written to display by X degrees [0-360] 119 | flip_horizontal=False # flip image horizontally 120 | flip_vertical=False # flip image vertically 121 | dither=FloydSteinberg # apply a dithering algorithm to the image 122 | 123 | [Image Enhancements] 124 | palette_filter=[[R,G,B], [R,G,B]] # for multi color displays the palette filter used to determine colors passed to the display, must be less than or equal to max colors the display supports 125 | contrast=1 # adjust image contrast, 1 = no adjustment 126 | brightness=1 # adjust image brightness, 1 = no adjustment 127 | sharpness=1 # adjust image sharpness, 1 = no adjustment 128 | ``` 129 | 130 | __Palette Filtering__ 131 | 132 | The `palette_filter` option controls what colors are passed to multi color displays by filtering the image so only the listed colors remain. The total number of colors must be less than or equal to the max number of colors the display supports. Colors can be specified as an array of RGB values (`[[R,G,B], [R,G,B]]`), hexidecimal values (`#ff0000, #00ff00`), or [color names](https://github.com/python-pillow/Pillow/blob/e3cb4bb8e00fcaf4c3e0783f7c02e51372595659/src/PIL/ImageColor.py#L153-L305) (`blue, maroon`). Combinations of these can also be given as long as each color specified is separated by a comma. 133 | 134 | ### Dithering 135 | 136 | When using the `dither` option many algorithms are available. Please read the [full instructions](https://github.com/robweber/omni-epd/wiki/Image-Dithering-Options) for dithering and how it can be used. 137 | 138 | ## Displays Implemented 139 | Below is a list of displays currently implemented in the library. The Omni Device Name is what you'd pass to `displaymanager.load_display_driver(deviceName)` to load the correct device driver. Generally this is the `packagename.devicename` Devices in __bold__ have been tested on actual hardware while others have been implemented but not verified. This often happens when multiple displays use the same libraries but no physical verification has happened for all models. The color modes are available modes that can be set on the device. 140 | 141 | | Device Library | Device Name | Omni Device Name | Color Modes | 142 | |:---------------|:------------|:-----------------|-------------| 143 | | [Inky](https://github.com/pimoroni/inky) | [Inky AutoDetect](https://shop.pimoroni.com/search?q=inky) (try this first) | inky.auto | bw, yellow, red, color | 144 | | | [Inky Impression 7 Color](https://shop.pimoroni.com/products/inky-impression) | __inky.impression__ | bw, color | 145 | | | [Inky pHAT Red/Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254217811) - 212x104 | __inky.phat_red__ | bw, red | 146 | | | [Inky pHAT Yellow/Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254905939) - 212x104 | inky.phat_yellow | bw, yellow | 147 | | | [Inky pHAT Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254938707) - 212x104 | inky.phat_black | bw | 148 | | | [Inky pHAT Red/Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254217811) - 250x122 | inky.phat1608_red | bw, red | 149 | | | [Inky pHAT Yellow/Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254905939) - 250x122 | inky.phat1608_yellow | bw, yellow | 150 | | | [Inky pHAT Black/White](https://shop.pimoroni.com/products/inky-phat?variant=12549254938707) - 250x122 | inky.phat1608_black | bw | 151 | | | [Inky wHAT Red/Black/White](https://shop.pimoroni.com/products/inky-what?variant=13590497624147) | __inky.what_red__ | bw, red | 152 | | | [Inky wHAT Yellow/Black/White](https://shop.pimoroni.com/products/inky-what?variant=21441988558931) | inky.what_yellow | bw, yellow | 153 | | | [Inky wHAT Black/White](https://shop.pimoroni.com/products/inky-what?variant=21214020436051) | inky.what_black | bw | 154 | | Omni-EPD | Mock Display (emulates EPD with no hardware) | __omni_epd.mock__ | bw, color, palette | 155 | | [Waveshare](https://github.com/waveshareteam/e-Paper) | [1.02inch E-Ink display module](https://www.waveshare.com/1.02inch-e-Paper-Module.htm) | waveshare_epd.epd1in02 | bw | 156 | | | [1.54inch E-Ink display module](https://www.waveshare.com/1.54inch-e-Paper-Module.htm) | waveshare_epd.epd1in54
waveshare_epd.epd1in54_V2 | bw | 157 | | | [1.54inch e-Paper Module B](https://www.waveshare.com/1.54inch-e-Paper-Module-B.htm) | waveshare_epd.epd1in54b
waveshare_epd.epd1in54b_V2 | bw, red | 158 | | | [1.54inch e-Paper Module C ](https://www.waveshare.com/1.54inch-e-Paper-Module-C.htm) | waveshare_epd.epd1in54c | bw, yellow | 159 | | | [1.64inch e-Paper Module G ](https://www.waveshare.com/1.64inch-e-paper-module-g.htm) | waveshare_epd.epd1in64g | bw, red, yellow, 4color | 160 | | | [2.13inch e-Paper HAT](https://www.waveshare.com/2.13inch-e-Paper-HAT.htm) | waveshare_epd.epd2in13
waveshare_epd.epd2in13_V2
__waveshare_epd.epd2in13_V3__ | bw | 161 | | | [2.13inch e-Paper HAT B](https://www.waveshare.com/2.13inch-e-Paper-HAT-B.htm) | waveshare_epd.epd2in13b
waveshare_epd.epd2in13b_V3 | bw, red | 162 | | | [2.13inch e-Paper HAT C ](https://www.waveshare.com/2.13inch-e-Paper-HAT-C.htm) | waveshare_epd.epd2in13c | bw, yellow | 163 | | | [2.13inch e-Paper HAT D](https://www.waveshare.com/2.13inch-e-Paper-HAT-D.htm) | waveshare_epd.epd2in13d | bw | 164 | | | [2.36inch e-Paper Module G](https://www.waveshare.com/2.36inch-e-paper-module-g.htm) | waveshare_epd.epd2in36g | bw, red, yellow, 4color | 165 | | | [2.66inch e-Paper Module](https://www.waveshare.com/2.66inch-e-Paper-Module.htm) | waveshare_epd.epd2in66 | bw | 166 | | | [2.66inch e-Paper Module B](https://www.waveshare.com/2.66inch-e-Paper-Module-B.htm) | waveshare_epd.epd2in66b | bw, red | 167 | | | [2.7inch e-Paper HAT](https://www.waveshare.com/2.7inch-e-Paper-HAT.htm) | __waveshare_epd.epd2in7__ | bw | 168 | | | [2.7inch e-Paper HAT B](https://www.waveshare.com/2.7inch-e-Paper-HAT-B.htm) | waveshare_epd.epd2in7b
__waveshare_epd.epd2in7b_V2__ | bw, red | 169 | | | [2.9inch e-Paper Module](https://www.waveshare.com/2.9inch-e-Paper-Module.htm) | __waveshare_epd.epd2in9__
waveshare_epd.epd2in9_V2 | bw | 170 | | | [2.9inch e-Paper Module B](https://www.waveshare.com/2.9inch-e-Paper-Module-B.htm) | waveshare_epd.epd2in9b
waveshare_epd.epd2in9b_V3 | bw, red | 171 | | | [2.9inch e-Paper Module C](https://www.waveshare.com/2.9inch-e-Paper-Module-C.htm) | waveshare_epd.epd2in9c | bw, yellow | 172 | | | [2.9inch e-Paper HAT D](https://www.waveshare.com/2.9inch-e-Paper-HAT-D.htm) | waveshare_epd.epd2in9d | bw | 173 | | | [3inch e-Paper Module G](https://www.waveshare.com/3inch-e-paper-module-g.htm) | __waveshare_epd.epd3in0g__ | bw, red, yellow, 4color | 174 | | | [3.7inch e-Paper HAT](https://www.waveshare.com/3.7inch-e-Paper-HAT.htm) | __waveshare_epd.epd3in7__ | gray4 | 175 | | | [4.01inch 7 color e-Paper HAT](https://www.waveshare.com/4.01inch-e-paper-hat-f.htm) | waveshare_epd.epd4in01f | bw, color | 176 | | | [4.2inch e-Paper Module](https://www.waveshare.com/4.2inch-e-Paper-Module.htm) |waveshare_epd.epd4in2 | bw | 177 | | | [4.2inch e-Paper Module B](https://www.waveshare.com/4.2inch-e-Paper-Module-B.htm) |waveshare_epd.epd4in2b
waveshare_epd.epd4in2b_V2 | bw, red | 178 | | | [4.2inch e-Paper Module C](https://www.waveshare.com/4.2inch-e-Paper-Module-C.htm) | __waveshare_epd.epd4in2c__ | bw, yellow | 179 | | | [4.37inch e-Paper Module G](https://www.waveshare.com/4.37inch-e-paper-module-g.htm) | waveshare_epd.epd4in37g | bw, red, yellow, 4color | 180 | | | [5.65inch e-Paper Module F](https://www.waveshare.com/5.65inch-e-Paper-Module-F.htm) |__waveshare_epd.epd5in65f__ | bw, color | 181 | | | [5.83inch e-Paper HAT](https://www.waveshare.com/5.83inch-e-Paper-HAT.htm) |waveshare_epd.epd5in83
waveshare_epd.epd5in83_V2 | bw | 182 | | | [5.83inch e-Paper HAT B](https://www.waveshare.com/5.83inch-e-Paper-HAT-B.htm) |waveshare_epd.epd5in83b
waveshare_epd.epd5in83b_V2 | bw, red | 183 | | | [5.83inch e-Paper HAT C](https://www.waveshare.com/5.83inch-e-Paper-HAT-C.htm) | __waveshare_epd.epd5in83c__ | bw, yellow | 184 | | | [6inch e-Ink Display](https://www.waveshare.com/6inch-e-paper-hat.htm) | waveshare_epd.it8951 | bw, gray16 | 185 | | | [7.3inch e-Paper HAT E](https://www.waveshare.com/7.3inch-e-paper-hat-e.htm) | __waveshare_epd.epd7in3e__ | bw, color | 186 | | | [7.3inch e-Paper HAT G](https://www.waveshare.com/7.3inch-e-paper-hat-g.htm) | __waveshare_epd.epd7in3g__ | bw, red, yellow, 4color | 187 | | | [7.3inch e-Paper HAT F](https://www.waveshare.com/7.3inch-e-paper-hat-f.htm) | waveshare_epd.epd7in3f | bw, color | 188 | | | [7.5inch e-Paper HAT](https://www.waveshare.com/7.5inch-e-Paper-HAT.htm) | waveshare_epd.epd7in5 | bw | 189 | | | [7.5inch e-Paper HAT V2](https://www.waveshare.com/7.5inch-e-Paper-HAT.htm) | __waveshare_epd.epd7in5_V2__ | bw | 190 | | | [7.5inch HD e-Paper HAT](https://www.waveshare.com/7.5inch-HD-e-Paper-HAT.htm) |waveshare_epd.epd7in5_HD | bw | 191 | | | [7.5inch HD e-Paper HAT B](https://www.waveshare.com/7.5inch-HD-e-Paper-HAT-B.htm) |waveshare_epd.epd7in5b_HD | bw, red | 192 | | | [7.5inch e-Paper HAT B](https://www.waveshare.com/7.5inch-HD-e-Paper-HAT-B.htm)| waveshare_epd.epd7in5b | bw, red | 193 | | | [7.5inch e-Paper HAT B V2](https://www.waveshare.com/7.5inch-HD-e-Paper-HAT-B.htm)| __waveshare_epd.epd7in5b_V2__ | bw, red | 194 | | | [7.5inch e-Paper HAT C](https://www.waveshare.com/7.5inch-e-Paper-HAT-C.htm) | waveshare_epd.epd7in5c | bw, yellow | 195 | | | [7.8inch e-Ink Display](https://www.waveshare.com/7.8inch-e-paper-hat.htm) | waveshare_epd.it8951 | bw, gray16 | 196 | | | [9.7inch e-Ink Display](https://www.waveshare.com/9.7inch-e-paper-hat.htm) | waveshare_epd.it8951 | bw, gray16 | 197 | | | [10.3inch e-Ink Display](https://www.waveshare.com/10.3inch-e-paper-hat.htm) | __waveshare_epd.it8951__ | bw, gray16 | 198 | 199 | ### Display Driver Installation 200 | 201 | Each display type has different install requirements depending on the platform. While loading this module will install any required _Python_ libraries for supported displays; specific OS level configuration may need to be done. Basic instructions are below for each library type. Refer to instructions for your specific display to make sure you've satisfied these requirements. The `omni-epd-test` utility can be used to verify things are working properly. 202 | 203 | __Inky__ 204 | 205 | Inky makes things pretty easy with a one-line installer. This makes the necessary OS level changes and pulls in the [Inky library](https://github.com/pimoroni/inky/). Using the `inky.auto` device type uses Inky library's auto detect method and is the most surefire way of loading the proper driver. 206 | 207 | ``` 208 | curl https://get.pimoroni.com/inky | bash 209 | ``` 210 | 211 | If installing Inky manually be sure that SPI and I2C are enabled via `sudo raspi-config`. 212 | 213 | __Waveshare__ 214 | 215 | The [Waveshare device library](https://github.com/waveshareteam/e-Paper) requires that [SPI support](https://www.raspberrypi-spy.co.uk/2014/08/enabling-the-spi-interface-on-the-raspberry-pi/) be enabled on your system prior to use. The `waveshare-epd` module is automatically downloaded and installed as a dependency of this module. 216 | 217 | __IT8951__ 218 | 219 | IT8951 devices, such as the [Waveshare 6in EPD](https://www.waveshare.com/6inch-e-paper-hat.htm), are supported via a [separately maintained Python module](https://github.com/GregDMeyer/IT8951) from Greg Kahanamoku-Meyer. This module and it's requirements are downloaded as part of omni-epd setup. 220 | 221 | ## Implementing Projects 222 | Below is a list of known projects currently utilizing `omni-epd`. If you're interested in building a very small media player, check them out. 223 | 224 | * [SlowMovie](https://github.com/TomWhitwell/SlowMovie) - A very popular VSMP player with lots of options for playing files and an easy install process. 225 | * [VSMP+](https://github.com/robweber/vsmp-plus) - My own VSMP project with a built in web server for easy administration. 226 | * [pycasso](https://github.com/jezs00/pycasso) - System to send AI generated art to an E-Paper display through a Raspberry PI unit. 227 | * [PiArtFrame](https://github.com/runezor/PiArtFrame) - EPD project that displays randomly generated fractal art. 228 | 229 | ## Acknowledgements 230 | 231 | Dithering support provided by the __didder__ Tool - https://github.com/makeworld-the-better-one/didder 232 | 233 | ## Contributing 234 | 235 | PRs accepted! If there a fix for any of the documentation or something is not quite clear, please [point it out](https://github.com/robweber/omni-epd/issues). If you test one of the listed displays, please mark it as verified by __bolding__ it in the [Displays Implemented](#displays-implemented) section. If you want to extend this framework by adding a new display type; a good place to start is one of the [existing display classes](https://github.com/robweber/omni-epd/tree/main/src/omni_epd/displays) for an example. Installing the library in [development mode](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode) will also make live testing easier. This will allow you test your changes quickly without re-installation. 236 | 237 | ``` 238 | 239 | pip3 install -e .[dev] --prefer-binary 240 | 241 | ``` 242 | 243 | ### Contributors 244 | 245 | * [@missionfloyd](https://github.com/missionfloyd) 246 | * [@qubist](https://github.com/qubist) 247 | * [@dr-boehmerie](https://github.com/dr-boehmerie) 248 | * [@aaronr8684](https://github.com/aaronr8684) 249 | * [@donbing](https://github.com/donbing) 250 | * [@evelyndooley](https://github.com/evelyndooley) 251 | * [@reinharty](https://github.com/reinharty) 252 | * [@antoinecellerier](https://github.com/antoinecellerier) 253 | 254 | ## License 255 | [GPLv3](/LICENSE) 256 | -------------------------------------------------------------------------------- /examples/PIA03519_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robweber/omni-epd/6bfcb2e8857e4ba33714dff05ae21489541a549a/examples/PIA03519_small.jpg -------------------------------------------------------------------------------- /examples/basic_example/draw_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | import sys 21 | from omni_epd import displayfactory, EPDNotFoundError 22 | from PIL import Image 23 | 24 | """ 25 | This basic example will load your device (modify string below), 26 | load an image, and then write it to the display 27 | 28 | """ 29 | # load your particular display using the displayfactory 30 | displayName = "omni_epd.mock" 31 | 32 | print('Loading display') 33 | try: 34 | epd = displayfactory.load_display_driver(displayName) 35 | except EPDNotFoundError: 36 | print(f"Couldn't find {displayName}") 37 | sys.exit() 38 | 39 | # if now load an image file using the Pillow lib 40 | print('Loading image') 41 | image = Image.open('../PIA03519_small.jpg') 42 | 43 | # resize for your display 44 | image = image.resize((epd.width, epd.height)) 45 | 46 | # prepare the epd, write the image, and close 47 | print('Writing to display') 48 | epd.prepare() 49 | 50 | epd.display(image) 51 | 52 | epd.close() 53 | -------------------------------------------------------------------------------- /examples/ini_file_example/draw_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | import sys 21 | from omni_epd import displayfactory, EPDNotFoundError 22 | from PIL import Image 23 | 24 | """ 25 | This example will load your display from the INI file in this directory. 26 | It will then load the image and write it to the display, applying the image 27 | post-processing rules also defined in the INI file, these are: 28 | 29 | * rotate 180 degrees 30 | * apply contrast 31 | * sharpen image 32 | 33 | """ 34 | # load your particular display using the displayfactory, driver specified in INI file 35 | print('Loading display') 36 | try: 37 | epd = displayfactory.load_display_driver() 38 | except EPDNotFoundError: 39 | print("Couldn't find your display") 40 | sys.exit() 41 | 42 | # if now load an image file using the Pillow lib 43 | print('Loading image') 44 | image = Image.open('../PIA03519_small.jpg') 45 | 46 | # resize for your display 47 | image = image.resize((epd.width, epd.height)) 48 | 49 | # prepare the epd, write the image, and close 50 | print('Writing to display') 51 | print("Rotating image 180 degrees, adjusting sharpness and contrast") 52 | epd.prepare() 53 | 54 | epd.display(image) 55 | 56 | epd.close() 57 | -------------------------------------------------------------------------------- /examples/ini_file_example/omni-epd.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | type=omni_epd.mock 3 | 4 | [Display] 5 | rotate=180 6 | 7 | [Image Enhancements] 8 | contrast=1.5 9 | sharpness=2 10 | -------------------------------------------------------------------------------- /examples/mode_example/draw_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | import sys 21 | from omni_epd import displayfactory, EPDNotFoundError 22 | from PIL import Image 23 | 24 | """ 25 | This basic example will load your device (modify string below), 26 | load an image, and then write it to the display. Additionally the 27 | omni-epd.ini file will change the mode on the image from bw (default) 28 | to palette and filter based on a color array. This is dependant on your display, 29 | see https://github.com/robweber/omni-epd/blob/main/README.md#displays-implemented for 30 | modes that each display supports 31 | 32 | """ 33 | # load your particular display using the displayfactory 34 | displayName = "omni_epd.mock" 35 | 36 | print('Loading display') 37 | try: 38 | epd = displayfactory.load_display_driver(displayName) 39 | except EPDNotFoundError: 40 | print(f"Couldn't find {displayName}") 41 | sys.exit() 42 | 43 | # if now load an image file using the Pillow lib 44 | print('Loading image') 45 | image = Image.open('../PIA03519_small.jpg') 46 | 47 | # resize for your display 48 | image = image.resize((epd.width, epd.height)) 49 | 50 | # prepare the epd, write the image, and close 51 | print('Writing to display using the palette filter') 52 | epd.prepare() 53 | 54 | epd.display(image) 55 | 56 | epd.close() 57 | -------------------------------------------------------------------------------- /examples/mode_example/omni-epd.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | mode=palette 3 | # filter image to show only show b+w plus red and blue 4 | palette_filter=[[0,0,0], [255,255,255], [255,0,0], [0,0,255]] 5 | -------------------------------------------------------------------------------- /examples/photo_attribution.txt: -------------------------------------------------------------------------------- 1 | Cassiopeia photo via NASA images: https://images.nasa.gov/details-PIA03519 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "omni_epd" 11 | version = "0.4.2" 12 | requires-python = ">=3.7" 13 | authors = [ 14 | {name = "Rob Weber", email = "robweberjr@gmail.com"} 15 | ] 16 | description = "An EPD class abstraction to simplify communications across multiple display types." 17 | readme = "README.md" 18 | license = {file = "LICENSE"} 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 23 | "Operating System :: POSIX :: Linux" 24 | ] 25 | 26 | dependencies = [ 27 | "importlib-resources", 28 | "Pillow>=9.1.0", 29 | "waveshare-epd @ git+https://github.com/waveshareteam/e-Paper.git#subdirectory=RaspberryPi_JetsonNano/python&egg=waveshare-epd", 30 | "inky[rpi]>=1.3.1", 31 | "IT8951[rpi] @ git+https://github.com/GregDMeyer/IT8951.git" 32 | ] 33 | 34 | [project.optional-dependencies] 35 | dev = [ 36 | "flake8", 37 | "Flake8-pyproject", 38 | "pytest" 39 | ] 40 | 41 | [project.scripts] 42 | omni-epd-test = "omni_epd.test_utility:main" 43 | 44 | [project.urls] 45 | "Homepage" = "https://github.com/robweber/omni-epd" 46 | "Bug Reports" = "https://github.com/robweber/omni-epd/issues" 47 | 48 | [tool.setuptools.packages.find] 49 | where = ["src"] 50 | 51 | [tool.setuptools.package-data] 52 | omni_epd = ["didder"] 53 | 54 | [tool.flake8] 55 | max-line-length = 150 56 | exclude = [".venv"] 57 | 58 | [tool.pytest.ini_options] 59 | filterwarnings = [ 60 | "ignore::DeprecationWarning" 61 | ] 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /src/omni_epd/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | from . errors import EPDNotFoundError, EPDConfigurationError # noqa: F401 22 | from . test_utility import EPDTestUtility # noqa: F401 23 | -------------------------------------------------------------------------------- /src/omni_epd/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | import importlib.util 22 | import sys 23 | 24 | # config file name 25 | CONFIG_FILE = "omni-epd.ini" 26 | 27 | # config option sections 28 | EPD_CONFIG = "EPD" 29 | IMAGE_DISPLAY = "Display" 30 | IMAGE_ENHANCEMENTS = "Image Enhancements" 31 | 32 | 33 | # helper method to check if a module is (or can be) installed 34 | def check_module_installed(moduleName): 35 | result = False 36 | 37 | # check if the module is already loaded, or can be loaded 38 | if (moduleName in sys.modules or (importlib.util.find_spec(moduleName)) is not None): 39 | result = True 40 | 41 | return result 42 | -------------------------------------------------------------------------------- /src/omni_epd/didder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robweber/omni-epd/6bfcb2e8857e4ba33714dff05ae21489541a549a/src/omni_epd/didder -------------------------------------------------------------------------------- /src/omni_epd/displayfactory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | import configparser 22 | import importlib 23 | import os 24 | import logging 25 | from . errors import EPDNotFoundError, EPDConfigurationError 26 | from . conf import CONFIG_FILE, EPD_CONFIG 27 | from . virtualepd import VirtualEPD 28 | from . displays.mock_display import MockDisplay # noqa: F401 29 | from . displays.waveshare_display import WaveshareDisplay # noqa: F401 30 | from . displays.inky_display import InkyDisplay # noqa: F401 31 | 32 | 33 | def __loadConfig(deviceName): 34 | logger = logging.getLogger(__name__) 35 | 36 | config = configparser.ConfigParser() 37 | 38 | # check for global ini file 39 | if (os.path.exists(os.path.join(os.getcwd(), CONFIG_FILE))): 40 | config.read(os.path.join(os.getcwd(), CONFIG_FILE)) 41 | logger.debug(f"Loading {CONFIG_FILE}") 42 | 43 | # possible device name exists in global configuration file 44 | if (not deviceName and config.has_option(EPD_CONFIG, 'type')): 45 | deviceName = config.get(EPD_CONFIG, 'type') 46 | 47 | # check for device specific ini file 48 | if (deviceName and os.path.exists(os.path.join(os.getcwd(), f"{deviceName}.ini"))): 49 | config.read(os.path.join(os.getcwd(), f"{deviceName}.ini")) 50 | logger.debug(f"Loading {deviceName}.ini") 51 | 52 | return config 53 | 54 | 55 | def __get_subclasses(cName): 56 | """ 57 | Can be used to recursively find classes that implement 58 | a given class resursively (ie, subclass of a subclass) 59 | """ 60 | result = [] 61 | 62 | for sub in cName.__subclasses__(): 63 | result.append(sub) 64 | result.extend(__get_subclasses(sub)) 65 | 66 | return result 67 | 68 | 69 | def list_supported_displays(as_dict=False): 70 | result = [] 71 | 72 | # get a list of display classes extending VirtualEPD 73 | displayClasses = [(cls.__module__, cls.__name__) for cls in __get_subclasses(VirtualEPD)] 74 | 75 | for modName, className in displayClasses: 76 | # load the module the class belongs to 77 | mod = importlib.import_module(modName) 78 | # get the class 79 | classObj = getattr(mod, className) 80 | 81 | if (as_dict): 82 | result.append({'package': modName, 'class': className, 'devices': classObj.get_supported_devices()}) 83 | else: 84 | # add supported devices of this class 85 | result = sorted(result + classObj.get_supported_devices()) 86 | 87 | return result 88 | 89 | 90 | def load_display_driver(displayName='', configDict={}): 91 | result = None 92 | 93 | # load any config files and merge passed in configs 94 | config = __loadConfig(displayName) 95 | config.read_dict(configDict) 96 | 97 | # possible device name is part of global conf 98 | if (not displayName and config.has_option(EPD_CONFIG, 'type')): 99 | displayName = config.get(EPD_CONFIG, 'type') 100 | 101 | # get a dict of all valid display device classes 102 | displayClasses = list_supported_displays(True) 103 | foundClass = list(filter(lambda d: displayName in d['devices'], displayClasses)) 104 | 105 | if (len(foundClass) == 1): 106 | # split on the pkg.classname 107 | deviceType = displayName.split('.') 108 | 109 | # create the class and initialize 110 | mod = importlib.import_module(foundClass[0]['package']) 111 | classObj = getattr(mod, foundClass[0]['class']) 112 | 113 | result = classObj(deviceType[1], config) 114 | 115 | # check that the display mode is valid - must be done after class loaded 116 | if (result.mode not in result.modes_available): 117 | raise EPDConfigurationError(displayName, "mode", result.mode) 118 | 119 | else: 120 | # we have a problem 121 | raise EPDNotFoundError(displayName) 122 | 123 | return result 124 | -------------------------------------------------------------------------------- /src/omni_epd/displays/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | -------------------------------------------------------------------------------- /src/omni_epd/displays/inky_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | from inky.inky_uc8159 import DESATURATED_PALETTE 21 | from PIL import Image 22 | from .. virtualepd import VirtualEPD 23 | from .. conf import check_module_installed 24 | 25 | INKY_PKG = "inky" 26 | 27 | 28 | class InkyDisplay(VirtualEPD): 29 | """ 30 | This is an abstraction for Pimoroni Inky pHat and wHat devices 31 | https://github.com/pimoroni/inky 32 | """ 33 | 34 | pkg_name = INKY_PKG 35 | mode = "bw" # default mode is black 36 | modes_available = ("bw") 37 | deviceList = ["phat_black", "phat_red", "phat_yellow", 38 | "phat1608_black", "phat1608_red", "phat1608_yellow", 39 | "what_black", "what_red", "what_yellow", "auto", 40 | "impression"] 41 | 42 | def __init__(self, deviceName, config): 43 | super().__init__(deviceName, config) 44 | 45 | self._device, self.clear_color, dColor = self.load_device(deviceName) 46 | 47 | # set mode to black + any other color supported 48 | if (self.mode != "bw"): 49 | self.modes_available = ('bw', dColor) 50 | 51 | # phat and what devices expect colors in the order white, black, other 52 | if (self.mode == dColor == "red"): 53 | self.palette_filter.append([255, 0, 0]) 54 | elif (self.mode == dColor == "yellow"): 55 | self.palette_filter.append([255, 255, 0]) 56 | elif (self.mode == dColor == "color"): 57 | self.palette_filter = DESATURATED_PALETTE 58 | 59 | # set the width and height 60 | self.width = self._device.width 61 | self.height = self._device.height 62 | self.max_colors = len(self.palette_filter) 63 | 64 | def load_device(self, deviceName): 65 | # need to figure out what type of device we have 66 | dType, dColor, *_ = deviceName.split('_') + [None] 67 | if (dType == 'phat'): 68 | deviceObj = self.load_display_driver(self.pkg_name, 'phat') 69 | device = deviceObj.InkyPHAT(dColor) 70 | elif (dType == 'phat1608'): 71 | deviceObj = self.load_display_driver(self.pkg_name, 'phat') 72 | device = deviceObj.InkyPHAT_SSD1608(dColor) 73 | elif (dType == 'what'): 74 | deviceObj = self.load_display_driver(self.pkg_name, 'what') 75 | device = deviceObj.InkyWHAT(dColor) 76 | elif (dType == 'impression'): 77 | deviceObj = self.load_display_driver(self.pkg_name, 'inky_uc8159') 78 | device = deviceObj.Inky() 79 | elif (dType == 'auto'): 80 | deviceObj = self.load_display_driver(self.pkg_name, 'auto') 81 | device = deviceObj.auto() 82 | 83 | if (device.colour == 'multi'): 84 | return device, device.CLEAN, 'color' 85 | else: 86 | return device, device.WHITE, device.colour 87 | 88 | @staticmethod 89 | def get_supported_devices(): 90 | return [] if not check_module_installed(INKY_PKG) else [f"{INKY_PKG}.{n}" for n in InkyDisplay.deviceList] 91 | 92 | # set the image and display 93 | def _display(self, image): 94 | # apply any needed conversions to this image based on the mode - force palette based conversion 95 | if (self.mode != 'color'): 96 | image = self._filterImage(image, force_palette=True) 97 | 98 | # set border 99 | self._device.set_border(getattr(self._device, self._get_device_option('border', '').upper(), self._device.border_colour)) 100 | 101 | # apply any needed conversions to this image based on the mode 102 | if (self._device.colour == 'multi'): 103 | saturation = self._getfloat_device_option('saturation', .5) # .5 is default from Inky lib 104 | self._device.set_image(image.convert("RGB"), saturation=saturation) 105 | else: 106 | self._device.set_image(image) 107 | 108 | self._device.show() 109 | 110 | def clear(self): 111 | clear_image = Image.new("P", (self.width, self.height), self.clear_color) 112 | self._device.set_image(clear_image) 113 | self._device.show() 114 | -------------------------------------------------------------------------------- /src/omni_epd/displays/mock_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | import logging 22 | import os.path 23 | from .. virtualepd import VirtualEPD 24 | 25 | 26 | class MockDisplay(VirtualEPD): 27 | """ 28 | This is a reference implementation of a display extending VirtualEPD 29 | it can write images to a testing file for use as a mock testing device 30 | """ 31 | 32 | pkg_name = 'omni_epd' 33 | output_file = 'mock_output.png' 34 | max_colors = 256 35 | modes_available = ('bw', 'color', 'palette') 36 | 37 | def __init__(self, deviceName, config): 38 | super().__init__(deviceName, config) 39 | 40 | self.logger = logging.getLogger(__name__) 41 | 42 | # this is normally where you'd load actual device class but nothing to load here 43 | 44 | # set location to write test image - can be set in ini file 45 | self.output_file = self._get_device_option("file", os.path.join(os.getcwd(), self.output_file)) 46 | 47 | # set the width and height - can be set in ini file 48 | self.width = self._getint_device_option("width", 400) 49 | self.height = self._getint_device_option("height", 200) 50 | 51 | if (self.mode == 'color'): 52 | self.palette_filter = self.__generate_colors() 53 | 54 | def __generate_colors(self): 55 | """returns a list of 216 "web safe" colors, each color is represented with 6 shades 56 | https://en.wikipedia.org/wiki/Web_colors#Web-safe_colors 57 | """ 58 | # 6 shades per color 59 | shades = (0, 51, 102, 153, 204, 255) 60 | 61 | # 216 colors total 62 | result = [] 63 | for i in range(0, 216): 64 | row = int(i / 6) 65 | # red = row/6, green=row%6, blue=current column 66 | result.append([shades[int(row / 6)], shades[int(row % 6)], shades[i - (row * 6)]]) 67 | 68 | return result 69 | 70 | @staticmethod 71 | def get_supported_devices(): 72 | # only one display supported, the test display 73 | return [f"{MockDisplay.pkg_name}.mock"] 74 | 75 | def prepare(self): 76 | self.logger.info(f"preparing {self.__str__()}") 77 | 78 | def _display(self, image): 79 | 80 | if (self.mode != 'color'): 81 | image = self._filterImage(image) 82 | 83 | if (self._getboolean_device_option('write_file', True)): 84 | self.logger.info(f"{self.__str__()} writing image to {self.output_file}") 85 | 86 | image.save(self.output_file, "PNG") 87 | else: 88 | self.logger.info(f"{self.__str__()} display() called, skipping output") 89 | 90 | def sleep(self): 91 | self.logger.info(f"{self.__str__()} is sleeping") 92 | 93 | def clear(self): 94 | self.logger.info(f"clearing {self.__str__()}") 95 | 96 | def close(self): 97 | self.logger.info(f"closing {self.__str__()}") 98 | -------------------------------------------------------------------------------- /src/omni_epd/displays/waveshare_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | from .. virtualepd import VirtualEPD 22 | from .. conf import check_module_installed 23 | from PIL import Image 24 | 25 | 26 | WAVESHARE_PKG = "waveshare_epd" 27 | 28 | 29 | class WaveshareDisplay(VirtualEPD): 30 | """ 31 | This is a generic class for all Waveshare devices to encapsulate common functions 32 | """ 33 | 34 | pkg_name = WAVESHARE_PKG 35 | 36 | def __init__(self, deviceName, config): 37 | """ 38 | All classes should perform these steps when initialized 39 | individual classes can perform additional functions once class is loaded 40 | """ 41 | super().__init__(deviceName, config) 42 | 43 | # load the module 44 | deviceObj = self.load_display_driver(self.pkg_name, deviceName) 45 | 46 | # create the epd object 47 | self._device = deviceObj.EPD() 48 | 49 | # set the width and height 50 | self.width = self._device.width 51 | self.height = self._device.height 52 | 53 | @staticmethod 54 | def get_supported_devices(): 55 | # this class is meant to be abstract but will be called by displayfactory, return nothing 56 | return [] 57 | 58 | def sleep(self): 59 | """ 60 | Most devices utilize the same sleep function 61 | """ 62 | self._device.sleep() 63 | 64 | def clear(self): 65 | """ 66 | Most devices utilize the same clear function 67 | """ 68 | self._device.Clear() 69 | 70 | def close(self): 71 | """ 72 | Most devices utilize the same close function 73 | """ 74 | epdconfig = self.load_display_driver(self.pkg_name, 'epdconfig') 75 | epdconfig.module_init() 76 | epdconfig.module_exit() 77 | 78 | 79 | class WaveshareBWDisplay(WaveshareDisplay): 80 | """ 81 | This is an abstraction for Waveshare EPD devices that are single color only 82 | https://github.com/waveshareteam/e-Paper 83 | """ 84 | 85 | # devices that use alternate init methods 86 | deviceMap = {"epd1in54": {"alt_init": True, "lut_init": True, "alt_clear": True, "version": 1}, 87 | "epd1in54_V2": {"alt_init": True, "lut_init": False, "alt_clear": True, "version": 3}, 88 | "epd2in9": {"alt_init": True, "lut_init": True, "alt_clear": True, "version": 1}, 89 | "epd2in9_V2": {"alt_init": False, "lut_init": False, "alt_clear": True, "version": 2}, 90 | "epd2in9d": {"alt_init": False, "lut_init": False, "alt_clear": True, "version": 1}, 91 | "epd2in13": {"alt_init": True, "lut_init": True, "alt_clear": True, "version": 1}, 92 | "epd2in13_V2": {"alt_init": True, "lut_init": False, "alt_clear": True, "version": 2}, 93 | "epd2in13_V3": {"alt_init": False, "lut_init": False, "alt_clear": True, "version": 1}, 94 | "epd2in13d": {"alt_init": False, "lut_init": False, "alt_clear": True, "version": 1}, 95 | "epd2in66": {"alt_init": True, "lut_init": False, "alt_clear": False, "version": 1}, 96 | "epd5in83": {"alt_init": False, "lut_init": False, "alt_clear": False, "version": 1}, 97 | "epd5in83_V2": {"alt_init": False, "lut_init": False, "alt_clear": False, "version": 2}, 98 | "epd7in5": {"alt_init": False, "lut_init": False, "alt_clear": False, "version": 1}, 99 | "epd7in5_HD": {"alt_init": False, "lut_init": False, "alt_clear": False, "version": 1}, 100 | "epd7in5_V2": {"alt_init": False, "lut_init": False, "alt_clear": False, "version": 2}} 101 | 102 | alt_init_param = 0 # the parameter to pass to init - specifies update mode (full vs partial) 103 | 104 | def __init__(self, deviceName, config): 105 | super().__init__(deviceName, config) 106 | 107 | # device object loaded in parent class 108 | 109 | # some devices set the full instruction as the param 110 | if (self.deviceMap[deviceName]['lut_init']): 111 | self.alt_init_param = self._device.lut_full_update 112 | 113 | @staticmethod 114 | def get_supported_devices(): 115 | result = [] 116 | 117 | # python libs for this might not be installed - that's ok, return nothing 118 | if (check_module_installed(WAVESHARE_PKG)): 119 | # return a list of all submodules (device types) 120 | result = [f"{WAVESHARE_PKG}.{n}" for n in WaveshareBWDisplay.deviceMap] 121 | 122 | return result 123 | 124 | def prepare(self): 125 | # if device needs an init param 126 | if (self.deviceMap[self._device_name]['alt_init']): 127 | self._device.init(self.alt_init_param) 128 | else: 129 | self._device.init() 130 | 131 | def _display(self, image): 132 | # no need to adjust palette, done in waveshare driver 133 | self._device.display(self._device.getbuffer(image)) 134 | 135 | def clear(self): 136 | if (self.deviceMap[self._device_name]['alt_clear']): 137 | # device needs color parameter, hardcode white 138 | self._device.Clear(0xFF) 139 | else: 140 | self._device.Clear() 141 | 142 | 143 | class WaveshareTriColorDisplay(WaveshareDisplay): 144 | """ 145 | This class is for the Waveshare displays that support 3 colors 146 | typically white/black/red or white/black/yellow 147 | https://github.com/waveshareteam/e-Paper 148 | """ 149 | 150 | max_colors = 3 151 | 152 | # list of all devices - some drivers cover more than one device 153 | deviceMap = {"epd1in54b": {"driver": "epd1in54b", "modes": ("bw", "red"), "version": 1}, 154 | "epd1in54b_V2": {"driver": "epd1in54b_V2", "modes": ("bw", "red"), "version": 2}, 155 | "epd1in54c": {"driver": "epd1in54c", "modes": ("bw", "yellow"), "version": 1}, 156 | "epd2in13b": {"driver": "epd2in13bc", "modes": ("bw", "red"), "version": 1}, 157 | "epd2in13b_V3": {"driver": "epd2in13b_V3", "modes": ("bw", "red"), "version": 3}, 158 | "epd2in13c": {"driver": "epd2in13bc", "modes": ("bw", "yellow"), "version": 1}, 159 | "epd2in66b": {"driver": "epd2in66b", "modes": ("bw", "red"), "version": 1}, 160 | "epd2in7b": {"driver": "epd2in7b", "modes": ("bw", "red"), "version": 1}, 161 | "epd2in7b_V2": {"driver": "epd2in7b_V2", "modes": ("bw", "red"), "version": 2}, 162 | "epd2in9b": {"driver": "epd2in9bc", "modes": ("bw", "red"), "version": 1}, 163 | "epd2in9b_V3": {"driver": "epd2in9b_V3", "modes": ("bw", "red"), "version": 3}, 164 | "epd2in9c": {"driver": "epd2in9bc", "modes": ("bw", "yellow"), "version": 1}, 165 | "epd4in2b": {"driver": "epd4in2bc", "modes": ("bw", "red"), "version": 1}, 166 | "epd4in2c": {"driver": "epd4in2bc", "modes": ("bw", "yellow"), "version": 1}, 167 | "epd4in2b_V2": {"driver": "epd4in2b_V2", "modes": ("bw", "red"), "version": 2}, 168 | "epd5in83b": {"driver": "epd5in83bc", "modes": ("bw", "red"), "version": 1}, 169 | "epd5in83c": {"driver": "epd5in83bc", "modes": ("bw", "yellow"), "version": 1}, 170 | "epd5in83b_V2": {"driver": "epd5in83b_V2", "modes": ("bw", "red"), "version": 2}, 171 | "epd7in5b": {"driver": "epd7in5bc", "modes": ("bw", "red"), "version": 1}, 172 | "epd7in5c": {"driver": "epd7in5bc", "modes": ("bw", "yellow"), "version": 1}, 173 | "epd7in5b_V2": {"driver": "epd7in5b_V2", "modes": ("bw", "red"), "version": 2}, 174 | "epd7in5b_HD": {"driver": "epd7in5b_HD", "modes": ("bw", "red"), "version": 1}} 175 | 176 | def __init__(self, deviceName, config): 177 | driverName = self.deviceMap[deviceName]['driver'] 178 | super().__init__(driverName, config) 179 | 180 | # device object loaded in parent class 181 | 182 | # set the allowed modes 183 | self.modes_available = self.deviceMap[deviceName]['modes'] 184 | 185 | if (self.mode == 'red'): 186 | self.palette_filter.append([255, 0, 0]) 187 | elif (self.mode == 'yellow'): 188 | self.palette_filter.append([255, 255, 0]) 189 | 190 | @staticmethod 191 | def get_supported_devices(): 192 | result = [] 193 | 194 | # python libs for this might not be installed - that's ok, return nothing 195 | if (check_module_installed(WAVESHARE_PKG)): 196 | result = [f"{WAVESHARE_PKG}.{n}" for n in WaveshareTriColorDisplay.deviceMap] 197 | 198 | return result 199 | 200 | def prepare(self): 201 | self._device.init() 202 | 203 | def _display(self, image): 204 | 205 | if (self.mode == 'bw'): 206 | # send the black/white image and blank second image (safer since some drivers require data) 207 | img_white = Image.new('1', (self._device.height, self._device.width), 255) 208 | self._device.display(self._device.getbuffer(image), self._device.getbuffer(img_white)) 209 | else: 210 | # apply the color filter to get a 3 color image 211 | image = self._filterImage(image) 212 | 213 | # convert to greyscale 214 | image = image.convert('L') 215 | 216 | # convert greys to black or white based on threshold of 20 217 | # https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.point 218 | img_black = image.point(lambda p: 255 if p >= 20 else 0) 219 | 220 | # convert greys to third color (represented as black in the image) or white based on threshold of 20 221 | img_color = image.point(lambda p: 0 if 20 < p < 235 else 255) 222 | 223 | # send to display 224 | self._device.display(self._device.getbuffer(img_black), self._device.getbuffer(img_color)) 225 | 226 | 227 | class WaveshareQuadColorDisplay(WaveshareDisplay): 228 | """ 229 | This class is for the Waveshare displays that support 4 colors 230 | white/black/yellow/red 231 | https://github.com/waveshareteam/e-Paper 232 | """ 233 | modes_available = ("bw", "red", "yellow", "4color") 234 | max_colors = 4 235 | mode = "4color" 236 | 237 | # list of all devices - some drivers cover more than one device 238 | deviceMap = {"epd1in64g": {"driver": "epd1in64g", "version": 1}, 239 | "epd2in36g": {"driver": "epd2in36g", "version": 1}, 240 | "epd3in0g": {"driver": "epd3in0g", "version": 1}, 241 | "epd4in37g": {"driver": "epd4in37g", "version": 1}, 242 | "epd7in3g": {"driver": "epd7in3g", "version": 1}} 243 | 244 | def __init__(self, deviceName, config): 245 | driverName = self.deviceMap[deviceName]['driver'] 246 | super().__init__(driverName, config) 247 | 248 | # device object loaded in parent class 249 | 250 | # set the palette 251 | if (self.mode == '4color' or self.mode == 'yellow'): 252 | self.palette_filter.append([255, 255, 0]) 253 | 254 | if (self.mode == '4color' or self.mode == 'red'): 255 | self.palette_filter.append([255, 0, 0]) 256 | 257 | @staticmethod 258 | def get_supported_devices(): 259 | result = [] 260 | 261 | # python libs for this might not be installed - that's ok, return nothing 262 | if (check_module_installed(WAVESHARE_PKG)): 263 | result = [f"{WAVESHARE_PKG}.{n}" for n in WaveshareQuadColorDisplay.deviceMap] 264 | 265 | return result 266 | 267 | def prepare(self): 268 | self._device.init() 269 | 270 | def _display(self, image): 271 | # driver takes care of filtering when in 4color mode 272 | if (self.mode != '4color'): 273 | image = self._filterImage(image) 274 | 275 | # send to display 276 | self._device.display(self._device.getbuffer(image)) 277 | 278 | 279 | class WaveshareGrayscaleDisplay(WaveshareDisplay): 280 | """ 281 | This class is for the Waveshare displays that support 4 shade grayscale 282 | 283 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7.py 284 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd4in2.py 285 | """ 286 | 287 | modes_available = ("bw", "gray4") 288 | max_colors = 4 289 | 290 | deviceMap = {"epd2in7": {"alt_clear": False, "version": 1}, 291 | "epd4in2": {"alt_clear": False, "version": 1}} # devices that support 4 shade grayscale 292 | 293 | def __init__(self, deviceName, config): 294 | super().__init__(deviceName, config) 295 | 296 | # device object created in parent class 297 | 298 | @staticmethod 299 | def get_supported_devices(): 300 | result = [] 301 | 302 | if (check_module_installed(WAVESHARE_PKG)): 303 | result = [f"{WAVESHARE_PKG}.{n}" for n in WaveshareGrayscaleDisplay.deviceMap] 304 | 305 | return result 306 | 307 | def prepare(self): 308 | 309 | if (self.mode == "gray4"): 310 | self._device.Init_4Gray() 311 | else: 312 | self._device.init() 313 | 314 | def _display(self, image): 315 | # no need to adjust image, done in waveshare lib 316 | if (self.mode == "gray4"): 317 | self._device.display_4Gray(self._device.getbuffer_4Gray(image)) 318 | else: 319 | self._device.display(self._device.getbuffer(image)) 320 | 321 | def clear(self): 322 | if (self.deviceMap[self._device_name]['alt_clear']): 323 | self._device.Clear(0xFF) # use white for clear 324 | else: 325 | self._device.Clear() 326 | 327 | 328 | class Waveshare3in7Display(WaveshareDisplay): 329 | """ 330 | This class is for the Waveshare displays that support 4 shade grayscale 331 | 332 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd3in7.py 333 | """ 334 | 335 | modes_available = ("gray4") 336 | mode = "gray4" 337 | max_colors = 4 338 | 339 | deviceMap = {"epd3in7": {"alt_clear": True, "version": 1}} # devices that support 4 shade grayscale 340 | 341 | def __init__(self, deviceName, config): 342 | super().__init__(deviceName, config) 343 | 344 | # for this device the height/width are reversed in the official files 345 | self.width = self._device.height 346 | self.height = self._device.width 347 | 348 | @staticmethod 349 | def get_supported_devices(): 350 | result = [] 351 | 352 | if (check_module_installed(WAVESHARE_PKG)): 353 | result = [f"{WAVESHARE_PKG}.epd3in7"] 354 | 355 | return result 356 | 357 | def prepare(self): 358 | # 3.7 in has different init methods 359 | self._device.init(0) 360 | 361 | def _display(self, image): 362 | # no need to adjust image, done in waveshare lib 363 | self._device.display_4Gray(self._device.getbuffer_4Gray(image)) 364 | 365 | def clear(self): 366 | # 3.7 in needs mode and color to clear 367 | self._device.Clear(0xFF, 0) 368 | 369 | 370 | class Waveshare102inDisplay(WaveshareDisplay): 371 | """ 372 | This class is for the Waveshare 1.02 in display only as it has some method calls that are different 373 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd1in02.py 374 | """ 375 | 376 | def __init__(self, deviceName, config): 377 | super().__init__(deviceName, config) 378 | 379 | # device object loaded in parent class 380 | 381 | @staticmethod 382 | def get_supported_devices(): 383 | result = [] 384 | 385 | if (check_module_installed(WAVESHARE_PKG)): 386 | result = [f"{WAVESHARE_PKG}.epd1in02"] 387 | 388 | return result 389 | 390 | def prepare(self): 391 | self._device.Init() 392 | 393 | def _display(self, image): 394 | self._device.display(self._device.getbuffer(image)) 395 | 396 | def sleep(self): 397 | # this differs from parent 398 | self._device.Sleep() 399 | 400 | def clear(self): 401 | # this differs from parent 402 | self._device.Clear() 403 | 404 | 405 | class WaveshareMultiColorDisplay(WaveshareDisplay): 406 | """ 407 | This class is for the Waveshare 7 color displays 408 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd5in65f.py 409 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd4in01f.py 410 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in3e.py 411 | https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in3f.py 412 | """ 413 | 414 | max_colors = 7 415 | modes_available = ('bw', 'color') 416 | 417 | deviceList = ["epd5in65f", "epd4in01f", "epd7in3e", "epd7in3f"] 418 | 419 | def __init__(self, deviceName, config): 420 | super().__init__(deviceName, config) 421 | 422 | # device object loaded in parent class 423 | 424 | @staticmethod 425 | def get_supported_devices(): 426 | result = [] 427 | 428 | if (check_module_installed(WAVESHARE_PKG)): 429 | result = [f"{WAVESHARE_PKG}.{n}" for n in WaveshareMultiColorDisplay.deviceList] 430 | 431 | return result 432 | 433 | def prepare(self): 434 | self._device.init() 435 | 436 | def _display(self, image): 437 | # driver takes care of filtering when in color mode 438 | if (self.mode == 'bw'): 439 | image = self._filterImage(image) 440 | 441 | self._device.display(self._device.getbuffer(image)) 442 | 443 | 444 | class IT8951Display(VirtualEPD): 445 | """ 446 | This will communicate with IT8951 type displays utilzing the driver built by GregDMeyer 447 | https://github.com/GregDMeyer/IT8951 448 | """ 449 | 450 | pkg_name = WAVESHARE_PKG 451 | 452 | max_colors = 16 453 | modes_available = ('bw', 'gray16') 454 | 455 | it8951_pkg_name = 'IT8951' 456 | it8951_constants = None 457 | 458 | def __init__(self, deviceName, config): 459 | super().__init__(deviceName, config) 460 | 461 | # load the IT8951.display package and create the object 462 | deviceObj = self.load_display_driver(self.it8951_pkg_name, "display") 463 | self._device = deviceObj.AutoEPDDisplay(vcom=self._getfloat_device_option('vcom', -2.06), 464 | spi_hz=self._getint_device_option('spi_hz', 24000000), 465 | rotate=self._get_device_option('rotate', None)) 466 | 467 | # set the width and height 468 | self.width = self._device.width 469 | self.height = self._device.height 470 | 471 | # load the from IT8951.constants 472 | self.it8951_constants = self.load_display_driver(self.it8951_pkg_name, "constants") 473 | 474 | @staticmethod 475 | def get_supported_devices(): 476 | # same type for all it8951 displays 477 | return [f"{WAVESHARE_PKG}.it8951"] 478 | 479 | def prepare(self): 480 | self._device.epd.run() 481 | 482 | def _display(self, image): 483 | if (self.mode == 'bw'): 484 | image = self._filterImage(image) 485 | 486 | self.clear() # not sure if this is needed, was part of example 487 | 488 | dims = (self.width, self.height) 489 | image.thumbnail(dims) 490 | 491 | paste_coords = [dims[i] - image.size[i] for i in (0, 1)] # align image with bottom of display 492 | 493 | # write image to display 494 | self._device.frame_buf.paste(image, paste_coords) 495 | self._device.draw_full(self.it8951_constants.DisplayModes.GC16) 496 | 497 | def sleep(self): 498 | self._device.epd.sleep() 499 | 500 | def clear(self): 501 | self._device.clear() 502 | -------------------------------------------------------------------------------- /src/omni_epd/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | 22 | class EPDNotFoundError(Exception): 23 | """ 24 | An EPDNotFoundError is thrown when no display can be loaded for the given device name 25 | """ 26 | 27 | def __init__(self, deviceName): 28 | super().__init__(f"A display device for device name {deviceName} cannot be loaded") 29 | 30 | 31 | class EPDConfigurationError(Exception): 32 | """ 33 | EPDConfigurationError is thrown when an invalid configuration option is given for a display 34 | this could be an invalid display mode, color option, or other issue 35 | """ 36 | 37 | def __init__(self, deviceName, optionName, optionValue): 38 | super().__init__(f"'{optionValue}' for '{optionName}' is not a valid configuration value for {deviceName}") 39 | -------------------------------------------------------------------------------- /src/omni_epd/test_utility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | import argparse 22 | from . import displayfactory 23 | from . errors import EPDNotFoundError 24 | from PIL import Image, ImageColor, ImageDraw 25 | 26 | 27 | class EPDTestUtility: 28 | """ 29 | A simple test utility to make sure all display pre-reqs are met. 30 | Can test draw and clear capabilities for a given display 31 | """ 32 | epd = None 33 | 34 | def __init__(self, displayName): 35 | 36 | # attempt to load the EPD with the given name 37 | try: 38 | self.epd = displayfactory.load_display_driver(displayName) 39 | print(f"Loaded {self.epd} with width {self.epd.width} and height {self.epd.height}") 40 | 41 | except EPDNotFoundError: 42 | print(f"{displayName} is not a valid display. Valid options are:") 43 | list_displays() 44 | 45 | def __draw_rectangle(self, imgObj, width, height, x, y, percent, step): 46 | # draw recursively until we go below 0 47 | if (percent > 0): 48 | # calculate the dimensions of the rectangle 49 | rWidth = width * percent 50 | rHeight = height * percent 51 | 52 | # calculate the starting position to center it 53 | rX = x + (width - rWidth) / 2 54 | rY = y + (height - rHeight) / 2 55 | 56 | print(f"Drawing rectangle of width {rWidth} and height {rHeight}") 57 | imgObj.rectangle((rX, rY, rWidth + rX, rHeight + rY), outline=ImageColor.getrgb("black"), width=2) 58 | 59 | return self.__draw_rectangle(imgObj, rWidth, rHeight, rX, rY, percent - step, step) 60 | else: 61 | return imgObj 62 | 63 | def __draw_on_display(self, image): 64 | self.epd.prepare() 65 | 66 | self.epd.display(image) 67 | 68 | self.epd.close() 69 | 70 | print("Display closed - testing complete") 71 | 72 | def isReady(self): 73 | return self.epd is not None 74 | 75 | def draw(self): 76 | 77 | # create a blank image 78 | im = Image.new('RGB', (self.epd.width, self.epd.height), color=ImageColor.getrgb("white")) 79 | draw = ImageDraw.Draw(im) 80 | 81 | # draw a series of rectangles 82 | draw = self.__draw_rectangle(draw, self.epd.width, self.epd.height, 0, 0, .75, .25) 83 | 84 | self.__draw_on_display(im) 85 | 86 | def draw_image(self, file): 87 | 88 | # load the image 89 | im = Image.open(file) 90 | 91 | # resize for display 92 | im = im.resize((self.epd.width, self.epd.height)) 93 | 94 | # write to the display 95 | self.__draw_on_display(im) 96 | 97 | def clear(self): 98 | print("Clearing display") 99 | self.epd.prepare() 100 | 101 | self.epd.clear() 102 | 103 | self.epd.close() 104 | 105 | print("Display closed - testing complete") 106 | 107 | 108 | def list_displays(): 109 | validDisplays = displayfactory.list_supported_displays() 110 | print("\n".join(map(str, validDisplays))) 111 | 112 | 113 | def main(): 114 | 115 | # parse args 116 | parser = argparse.ArgumentParser(description='EPD Test Utility') 117 | mutex_group = parser.add_mutually_exclusive_group(required=True) 118 | mutex_group.add_argument('-l', '--list', action='store_true', 119 | help="List valid EPD display options") 120 | mutex_group.add_argument('-e', '--epd', 121 | help="The type of EPD driver to test") 122 | parser.add_argument('-i', '--image', required=False, type=str, 123 | help="Path to an image file to draw on the display") 124 | 125 | args = parser.parse_args() 126 | 127 | if (args.list): 128 | # list valid displays and exist 129 | list_displays() 130 | else: 131 | test = EPDTestUtility(args.epd) 132 | 133 | if (test.isReady()): 134 | if (args.image): 135 | test.draw_image(args.image) 136 | else: 137 | # this will draw a rectangle in the center of the display 138 | test.draw() 139 | -------------------------------------------------------------------------------- /src/omni_epd/virtualepd.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2021 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | 21 | import json 22 | import importlib 23 | import logging 24 | import subprocess 25 | import io 26 | import re 27 | import itertools 28 | from importlib_resources import path 29 | from PIL import Image, ImageEnhance, ImageColor 30 | from . conf import EPD_CONFIG, IMAGE_DISPLAY, IMAGE_ENHANCEMENTS 31 | from . errors import EPDConfigurationError 32 | 33 | 34 | class VirtualEPD: 35 | """ 36 | VirtualEPD is a wrapper class for a device, or family of devices, that all use the same display code 37 | New devices should extend this class and implement the, at a minimum, the following: 38 | 39 | pkg_name = set this to the package name of the concrete class 40 | width = width of display, can set in __init__ 41 | height = height of display, can set in __init__ 42 | get_supported_devices() = must return a list of supported devices for this class in the format {pkgname.devicename} 43 | _display() = performs the action of writing the image to the display 44 | """ 45 | 46 | pkg_name = "virtualdevice" # the package name of the concrete class 47 | width = 0 # width of display 48 | height = 0 # height of display 49 | mode = "bw" # mode of the display, bw by default, others defined by display class 50 | modes_available = ("bw") # modes this display supports, set in __init__ 51 | 52 | # only used by displays that need palette filtering before sending to display driver 53 | max_colors = 2 # assume only b+w supported by default, set in __init__ 54 | palette_filter = [[255, 255, 255], [0, 0, 0]] # assume only b+w supported by default, set in __init__ 55 | 56 | _device = None # concrete device class, initialize in __init__ 57 | _config = None # configuration options passed in via dict at runtime or .ini file 58 | _device_name = "" # name of this device 59 | 60 | def __init__(self, deviceName, config): 61 | self._config = config 62 | self._device_name = deviceName 63 | 64 | self._logger = logging.getLogger(self.__str__()) 65 | 66 | # set the display mode 67 | self.mode = self._get_device_option('mode', self.mode) 68 | 69 | if (self.mode == 'black'): 70 | self._logger.warn("The mode 'black' is deprecated, 'bw' should be used instead. This will be removed in a future release.") 71 | self.mode = 'bw' 72 | 73 | def __str__(self): 74 | return f"{self.pkg_name}.{self._device_name}" 75 | 76 | def __parse_palette(self, color_str): 77 | """ parse the color infomration to return a RGB color from a color string 78 | :param color_str: the color as either a hex value (#000000), RGB list [R,G,B] or color name (blue, red) 79 | :raises ValueError: if the color string is in an invalid format 80 | 81 | :returns: the color_str converted to a list of RGB values 82 | """ 83 | if re.match(r'#[a-fA-F0-9]{6}', color_str) or color_str.lower() in ImageColor.colormap: 84 | return ImageColor.getrgb(color_str) 85 | elif re.match(r'\[?(\d{1,3}),(\d{1,3}),(\d{1,3})\]?', color_str): 86 | return list(map(int, re.findall(r'\d{1,3}', color_str))) 87 | else: 88 | raise ValueError(f"Invalid color format: {color_str}") 89 | 90 | def __generate_palette(self, colors): 91 | """ generate a palette given the colors available for this display 92 | :param colors: a list of valid colors as a string 93 | 94 | :returns: a list of tuples representing RGB values 95 | """ 96 | result = colors.replace(" ", "") 97 | result = re.findall(fr'#[a-fA-F0-9]{{6}}|\[?\d{{1,3}},\d{{1,3}},\d{{1,3}}\]?|{"|".join(ImageColor.colormap.keys())}', result, re.IGNORECASE) 98 | result = list(map(self.__parse_palette, result)) 99 | 100 | return result 101 | 102 | def __applyConfig(self, image): 103 | """ 104 | Apply any values passed in from the global configuration that should 105 | apply to all images before writing to the epd 106 | 107 | :param image: an Image object 108 | 109 | :returns: the modified image 110 | """ 111 | 112 | if (self._config.has_option(IMAGE_DISPLAY, "rotate")): 113 | image = image.rotate(self._config.getfloat(IMAGE_DISPLAY, "rotate")) 114 | self._logger.debug(f"Rotating image {self._config.getfloat(IMAGE_DISPLAY, 'rotate')}") 115 | 116 | if (self._config.has_option(IMAGE_DISPLAY, "flip_horizontal") and self._config.getboolean(IMAGE_DISPLAY, "flip_horizontal")): 117 | image = image.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) 118 | self._logger.debug("Flipping image horizontally") 119 | 120 | if (self._config.has_option(IMAGE_DISPLAY, "flip_vertical") and self._config.getboolean(IMAGE_DISPLAY, "flip_vertical")): 121 | image = image.transpose(method=Image.Transpose.FLIP_TOP_BOTTOM) 122 | self._logger.debug("Flipping image vertically") 123 | 124 | if (self._config.has_option(IMAGE_ENHANCEMENTS, "contrast")): 125 | enhancer = ImageEnhance.Contrast(image) 126 | image = enhancer.enhance(self._config.getfloat(IMAGE_ENHANCEMENTS, "contrast")) 127 | self._logger.debug(f"Applying contrast: {self._config.getfloat(IMAGE_ENHANCEMENTS, 'contrast')}") 128 | 129 | if (self._config.has_option(IMAGE_ENHANCEMENTS, "brightness")): 130 | enhancer = ImageEnhance.Brightness(image) 131 | image = enhancer.enhance(self._config.getfloat(IMAGE_ENHANCEMENTS, "brightness")) 132 | self._logger.debug(f"Applying brightness: {self._config.getfloat(IMAGE_ENHANCEMENTS, 'brightness')}") 133 | 134 | if (self._config.has_option(IMAGE_ENHANCEMENTS, "sharpness")): 135 | enhancer = ImageEnhance.Sharpness(image) 136 | image = enhancer.enhance(self._config.getfloat(IMAGE_ENHANCEMENTS, "sharpness")) 137 | self._logger.debug(f"Applying sharpness: {self._config.getfloat(IMAGE_ENHANCEMENTS, 'sharpness')}") 138 | 139 | if (self._config.has_option(IMAGE_DISPLAY, "dither") and self._config.get(IMAGE_DISPLAY, "dither")): 140 | dither = self._config.get(IMAGE_DISPLAY, "dither").lower().replace("sierra-2-4a", "sierralite").replace("-", "") 141 | image = self._ditherImage(image, dither) 142 | self._logger.debug(f"Applying dither: {dither}") 143 | 144 | return image 145 | 146 | """ 147 | helper methods to get custom config options, providing a fallback if needed 148 | avoids having to do constant has_option(), get() calls within device class 149 | """ 150 | def _get_device_option(self, option, fallback): 151 | # if exists in local config use that, otherwise check EPD section 152 | if (self._config.has_option(self.getName(), option)): 153 | return self._config.get(self.getName(), option) 154 | else: 155 | return self._config.get(EPD_CONFIG, option, fallback=fallback) 156 | 157 | def _getint_device_option(self, option, fallback): 158 | # if exists in local config use that, otherwise check EPD section 159 | if (self._config.has_option(self.getName(), option)): 160 | return self._config.getint(self.getName(), option) 161 | else: 162 | return self._config.getint(EPD_CONFIG, option, fallback=fallback) 163 | 164 | def _getfloat_device_option(self, option, fallback): 165 | # if exists in local config use that, otherwise check EPD section 166 | if (self._config.has_option(self.getName(), option)): 167 | return self._config.getfloat(self.getName(), option) 168 | else: 169 | return self._config.getfloat(EPD_CONFIG, option, fallback=fallback) 170 | 171 | def _getboolean_device_option(self, option, fallback): 172 | # if exists in local config use that, otherwise check EPD section 173 | if (self._config.has_option(self.getName(), option)): 174 | return self._config.getboolean(self.getName(), option) 175 | else: 176 | return self._config.getboolean(EPD_CONFIG, option, fallback=fallback) 177 | 178 | def _filterImage(self, image, dither=Image.Dither.FLOYDSTEINBERG, force_palette=False): 179 | """ Converts image to b/w or attempts a palette filter based on allowed colors in the display 180 | :param image: an Image object 181 | :param dither: a valid dither technique, default is FLOYDSTEINBERG 182 | 183 | :raises EPDConfigurationError: if more colors are given in the palette than the display can support 184 | :returns: the image with the palette filtering applied 185 | """ 186 | if (self.mode == 'bw' and not force_palette): 187 | image = image.convert("1", dither=dither) 188 | else: 189 | # load palette as string - this is a catch in case it was changed by the user 190 | colors = self._get_device_option('palette_filter', json.dumps(self.palette_filter)) 191 | colors = self.__generate_palette(colors) 192 | 193 | # check if we have too many colors in the palette 194 | if (len(colors) > self.max_colors): 195 | raise EPDConfigurationError(self.getName(), "palette_filter", f"{len(colors)} colors") 196 | 197 | # create a new image to define the palette 198 | palette_image = Image.new("P", (1, 1)) 199 | 200 | # set the palette, set all other colors to 0 201 | palette = list(itertools.chain.from_iterable(colors)) 202 | palette_image.putpalette(palette + [0, 0, 0] * (256 - len(colors))) 203 | 204 | if (image.mode != 'RGB'): 205 | # convert to RGB as quantize requires it 206 | image = image.convert(mode='RGB') 207 | 208 | # apply the palette 209 | image = image.quantize(palette=palette_image, dither=dither) 210 | 211 | return image 212 | 213 | def _ditherImage(self, image, dither): 214 | """ apply a dithering effect to the image using the didder library 215 | https://github.com/robweber/omni-epd/wiki/Image-Dithering-Options 216 | :param image: an Image object 217 | :param dither: dithering effect as a string 218 | 219 | :raises EPDConfigurationError: if more colors are given in the palette than the display can support 220 | :returns: the image with the effect applied 221 | """ 222 | dither_modes_ordered = ("clustereddot4x4", "clustereddotdiagonal8x8", "vertical5x3", "horizontal3x5", 223 | "clustereddotdiagonal6x6", "clustereddotdiagonal8x8_2", "clustereddotdiagonal16x16", 224 | "clustereddot6x6", "clustereddotspiral5x5", "clustereddothorizontalline", 225 | "clustereddotverticalline", "clustereddot8x8", "clustereddot6x6_2", 226 | "clustereddot6x6_3", "clustereddotdiagonal8x8_3") 227 | 228 | dither_modes_diffusion = ("simple2d", "floydsteinberg", "falsefloydsteinberg", "jarvisjudiceninke", "atkinson", 229 | "stucki", "burkes", "sierra", "tworowsierra", "sierralite", "stevenpigeon", "sierra3", 230 | "sierra2", "sierra2_4a") 231 | 232 | if (self.mode == 'bw'): 233 | colors = [[255, 255, 255], [0, 0, 0]] 234 | else: 235 | # load palette - this is a catch in case it was changed by the user 236 | colors = self._get_device_option('palette_filter', json.dumps(self.palette_filter)) 237 | colors = self.__generate_palette(colors) 238 | 239 | # check if we have too many colors in the palette 240 | if (len(colors) > self.max_colors): 241 | raise EPDConfigurationError(self.getName(), "palette_filter", f"{len(colors)} colors") 242 | 243 | # format palette the way didder expects it 244 | palette = [",".join(map(str, x)) for x in colors] 245 | palette = " ".join(palette) 246 | with path("omni_epd", "didder") as p: 247 | didder = p 248 | 249 | cmd = [didder, "--in", "-", "--out", "-", "--palette", palette] 250 | cmd += ["--strength", self._config.get(IMAGE_DISPLAY, 'dither_strength', raw=True, fallback='1.0')] 251 | 252 | if (dither == "none"): 253 | return self._filterImage(image, Image.Dither.NONE) 254 | elif (dither in dither_modes_ordered): 255 | cmd += ["odm", dither] 256 | elif (dither in dither_modes_diffusion): 257 | cmd += ["edm", dither] 258 | elif (dither == "bayer"): 259 | # dither_args: X,Y dimensions of bayer matrix - powers of two, 3x3, 3x5, or 5x3 260 | cmd += ["bayer", self._config.get(IMAGE_DISPLAY, 'dither_args', fallback='4,4')] 261 | elif (dither == "random"): 262 | # dither_args: min,max or min_r,max_r,min_g,max_g,min_b,max_b 263 | cmd += ["random", self._config.get(IMAGE_DISPLAY, 'dither_args', fallback='-0.5,0.5')] 264 | elif (dither == "customordered"): 265 | # dither_args: JSON file or string 266 | cmd += ["odm", self._config.get(IMAGE_DISPLAY, 'dither_args', fallback='')] 267 | elif (dither == "customdiffusion"): 268 | # dither_args: JSON file or string 269 | cmd += ["edm", self._config.get(IMAGE_DISPLAY, 'dither_args', fallback='')] 270 | 271 | if (cmd[-2] == "edm" and self._config.getboolean(IMAGE_DISPLAY, 'dither_serpentine', fallback=False)): 272 | cmd.insert(-1, "--serpentine") 273 | 274 | with io.BytesIO() as buf: 275 | image.save(buf, "PNG") 276 | proc = subprocess.run(cmd, input=buf.getvalue(), capture_output=True) 277 | 278 | if (proc.returncode): 279 | self._logger.error(proc.stdout.decode().strip()) 280 | return image 281 | 282 | with io.BytesIO(proc.stdout) as buf: 283 | image = Image.open(buf).convert("RGB") 284 | 285 | return image 286 | 287 | def load_display_driver(self, packageName, className): 288 | """helper method to load a concrete display object based on the package and class name""" 289 | try: 290 | # load the given driver module 291 | driver = importlib.import_module(f"{packageName}.{className}") 292 | except ModuleNotFoundError: 293 | # hard stop if driver not 294 | print(f"{packageName}.{className} not found, refer to install instructions") 295 | exit(2) 296 | 297 | return driver 298 | 299 | def getName(self): 300 | """ returns package.device name """ 301 | return self.__str__() 302 | 303 | @staticmethod 304 | def get_supported_devices(): 305 | """ REQUIRED - a list of devices supported by this class, format is {pkgname.devicename} 306 | :raises NotImplementedError: if not implemented by child class 307 | """ 308 | raise NotImplementedError 309 | 310 | def _display(self, image): 311 | """ REQUIRED - actual display code, PIL image given 312 | :raises NotImplementedError: if not implemented by child class 313 | """ 314 | raise NotImplementedError 315 | 316 | def prepare(self): 317 | """ OPTIONAL - run at the top of each update to do required pre-work """ 318 | return True 319 | 320 | def display(self, image): 321 | """ Called to draw an image on the display, this applies configured effects 322 | DON'T override this method directly, use _display() in child classes 323 | 324 | :param image: an Image object 325 | """ 326 | self._display(self.__applyConfig(image)) 327 | 328 | def sleep(self): 329 | """ OPTIONAL - put the display to sleep after each update, if device supports """ 330 | return True 331 | 332 | def clear(self): 333 | """ OPTIONAL - clear the display, if device supports """ 334 | return True 335 | 336 | def close(self): 337 | """ OPTIONAL close out the device, called when the program ends """ 338 | return True 339 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # add local path to load libraries 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', "src"))) 6 | -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2022 Rob Weber 3 | 4 | This file is part of omni-epd 5 | 6 | omni-epd is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | """ 20 | import os.path 21 | 22 | GOOD_EPD_NAME = "omni_epd.mock" # this should always be a valid EPD 23 | BAD_EPD_NAME = "omni_epd.bad" # this is not a valid EPD 24 | 25 | # INI files 26 | BAD_CONFIG_FILE = 'bad_conf.ini' # name of invalid configuration file 27 | ALL_IMAGE_OPTIONS = "all_options.ini" # ini file that attempt to run all base options 28 | BASIC_DITHER = "basic_dither.ini" # in file with basic dither applied 29 | CUSTOM_DITHER_INI = "custom_dither.ini" 30 | CUSTOM_DITHER_JSON = "custom_dither_json.ini" 31 | 32 | # Testing Images 33 | MOCK_EPD_OUTPUT = os.path.join(os.getcwd(), 'mock_output.png') # path to where output will be generated 34 | GALAXY_IMAGE = os.path.join(os.getcwd(), "examples", "PIA03519_small.jpg") 35 | MASTER_IMAGE = os.path.join(os.getcwd(), "tests", "master_bw_output.png") 36 | -------------------------------------------------------------------------------- /tests/ini/all_options.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | type=omni_epd.mock 3 | mode=color 4 | 5 | [Display] 6 | rotate = 90 7 | flip_horizontal=True 8 | flip_vertical=True 9 | 10 | [Image Enhancements] 11 | contrast=2 12 | brightness=2 13 | sharpness=2 14 | -------------------------------------------------------------------------------- /tests/ini/bad_conf.ini: -------------------------------------------------------------------------------- 1 | [omni_epd.mock] 2 | mode=bad_mode 3 | -------------------------------------------------------------------------------- /tests/ini/basic_dither.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | mode=bw 3 | 4 | [Display] 5 | dither=ClusteredDot4x4 6 | -------------------------------------------------------------------------------- /tests/ini/custom_dither.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | mode=bw 3 | 4 | [Display] 5 | dither=CustomOrdered 6 | dither_args={"matrix": [[12, 5, 6, 13], [4, 0, 1, 7], [11, 3, 2, 8], [15, 10, 9, 14]], "max": 16} 7 | -------------------------------------------------------------------------------- /tests/ini/custom_dither_json.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | mode=bw 3 | 4 | [Display] 5 | dither=CustomDiffusion 6 | dither_args=tests/json/custom_diffusion_matrix.json 7 | -------------------------------------------------------------------------------- /tests/ini/omni-epd.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | type=omni_epd.mock 3 | 4 | [Display] 5 | rotate = 90 6 | flip_horizontal=True 7 | 8 | [Image Enhancements] 9 | color=1 10 | contrast=2 11 | -------------------------------------------------------------------------------- /tests/ini/omni_epd.mock.ini: -------------------------------------------------------------------------------- 1 | [EPD] 2 | mode=palette 3 | palette_filter=[[0,0,0],[255,255,255],[255,0,0],[0,255,0],[0,0,255]] 4 | 5 | [Display] 6 | flip_horizontal=False 7 | -------------------------------------------------------------------------------- /tests/json/custom_diffusion_matrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | [0, 0, 0.4375], 3 | [0.1875, 0.3125, 0.0625] 4 | ] 5 | -------------------------------------------------------------------------------- /tests/master_bw_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robweber/omni-epd/6bfcb2e8857e4ba33714dff05ae21489541a549a/tests/master_bw_output.png -------------------------------------------------------------------------------- /tests/test_display_manual.py: -------------------------------------------------------------------------------- 1 | 2 | from omni_epd import displayfactory 3 | from PIL import Image 4 | import unittest 5 | import pytest 6 | import os 7 | 8 | image_path = os.path.dirname(os.path.realpath(__file__)) + '/../examples/PIA03519_small.jpg' 9 | 10 | inky_impression = 'inky.impression' 11 | inky_auto = 'inky.auto' 12 | inky_what_red = 'inky.what_red' 13 | waveshare_27bV2 = 'waveshare_epd.epd2in7b_V2' 14 | waveshare_42bV2 = 'waveshare_epd.epd4in2b_V2' 15 | 16 | empty_config = {} 17 | color_config = {'EPD': {'mode': 'color'}} 18 | red_config = {'EPD': {'mode': 'red'}} 19 | 20 | test_params = [ 21 | ('inky impression in color mode', inky_impression, color_config), 22 | ('inky impression in default mode', inky_impression, empty_config), 23 | ('inky auto in color mode', inky_auto, color_config), 24 | ('inky auto in default mode', inky_auto, empty_config), 25 | ('inky auto in red mode', inky_auto, red_config), 26 | ('inky what_red in red mode', inky_what_red, red_config), 27 | ('inky what_red in default mode', inky_what_red, empty_config), 28 | ('epd2in7b_V2 in red mode', waveshare_27bV2, red_config), 29 | ('epd2in7b_V2 in default mode', waveshare_27bV2, empty_config), 30 | ('epd4in2b_V2 in red mode', waveshare_42bV2, red_config), 31 | ('epd4in2b_V2 in default mode', waveshare_42bV2, empty_config), 32 | ] 33 | 34 | 35 | class DeviceMetaTest(type): 36 | def __new__(mcs, name, bases, dict): 37 | def gen_test(name, device, config_dict): 38 | def test(self): 39 | epd = displayfactory.load_display_driver(device, config_dict) 40 | epd.prepare() 41 | # display image 42 | epd.display(Image.open(image_path).resize((epd.width, epd.height))) 43 | # wait while you verify the image and then clear 44 | # import time 45 | # time.sleep(5) 46 | # clear the image after 47 | # epd.clear() 48 | epd.close() 49 | return test 50 | 51 | for test_param in test_params: 52 | test_name = "test %s" % test_param[0] 53 | dict[test_name] = gen_test(*test_param) 54 | 55 | return type.__new__(mcs, name, bases, dict) 56 | 57 | 58 | @pytest.mark.skip("requires a connected epd") 59 | class TestInkyDeviceWithConfigs(unittest.TestCase, metaclass=DeviceMetaTest): 60 | __metaclass__ = DeviceMetaTest 61 | -------------------------------------------------------------------------------- /tests/test_epd_loading.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import time 4 | import json 5 | import glob 6 | import pytest 7 | from . import constants as constants 8 | from shutil import copyfile 9 | from omni_epd import EPDNotFoundError, EPDConfigurationError 10 | from omni_epd import displayfactory 11 | from omni_epd.virtualepd import VirtualEPD 12 | from omni_epd.conf import IMAGE_DISPLAY, CONFIG_FILE 13 | 14 | 15 | class TestEpdLoading(unittest.TestCase): 16 | 17 | def _delete_ini(self): 18 | fileList = glob.glob(os.path.join(os.getcwd(), "*.ini")) 19 | 20 | for f in fileList: 21 | # don't bother catching errors - just let it fail out 22 | os.remove(f) 23 | 24 | @pytest.fixture(autouse=True) 25 | def run_before_and_after_tests(self): 26 | # clean up any files left over from previous tests 27 | self._delete_ini() 28 | 29 | yield 30 | 31 | # clean up any files made during this test 32 | self._delete_ini() 33 | 34 | def test_supported_diplays(self): 35 | """ 36 | Test that displays can be loaded 37 | """ 38 | drivers = displayfactory.list_supported_displays() 39 | 40 | assert len(drivers) > 0 41 | 42 | def test_loading_error(self): 43 | """ 44 | Confirm error thrown if an invalid name passed to load function 45 | """ 46 | self.assertRaises(EPDNotFoundError, displayfactory.load_display_driver, constants.BAD_EPD_NAME) 47 | 48 | def test_loading_success(self): 49 | """ 50 | Confirm a good display can be loaded and extends VirtualEPD 51 | """ 52 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 53 | 54 | assert isinstance(epd, VirtualEPD) 55 | 56 | def test_global_conf(self): 57 | """ 58 | Test loading of omni-epd.ini config file 59 | Once loaded confirm options from file exist within display class config 60 | Also confirm values not in the config file aren't changed from defaults 61 | """ 62 | # set up a global config file 63 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', CONFIG_FILE), os.path.join(os.getcwd(), CONFIG_FILE)) 64 | time.sleep(1) 65 | 66 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 67 | 68 | assert epd._config.has_option(IMAGE_DISPLAY, 'rotate') 69 | assert epd._config.getfloat(IMAGE_DISPLAY, 'rotate') == 90 70 | 71 | # test that mode is default 72 | assert epd.mode == 'bw' 73 | 74 | def test_device_config(self): 75 | """ 76 | Test that when both omni-epd.ini file is present and device specific INI present 77 | that the device specific config overrides options in global config 78 | """ 79 | deviceConfig = constants.GOOD_EPD_NAME + ".ini" 80 | 81 | # set up a global config file and device config 82 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', CONFIG_FILE), os.path.join(os.getcwd(), CONFIG_FILE)) 83 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', deviceConfig), os.path.join(os.getcwd(), deviceConfig)) 84 | time.sleep(1) 85 | 86 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 87 | 88 | # device should override global 89 | assert epd._config.has_option(IMAGE_DISPLAY, 'flip_horizontal') 90 | self.assertFalse(epd._config.getboolean(IMAGE_DISPLAY, 'flip_horizontal')) 91 | 92 | # test mode and palette configurations 93 | assert epd.mode == 'palette' 94 | assert len(json.loads(epd._get_device_option('palette_filter', "[]"))) == 5 # confirms custom palette will be loaded 95 | 96 | def test_load_device_from_conf(self): 97 | """ 98 | Test that a device will load when given the type= option in the omni-epd.ini file 99 | and no args to load_display_driver() 100 | """ 101 | deviceConfig = constants.GOOD_EPD_NAME + ".ini" 102 | 103 | # set up a global config file 104 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', CONFIG_FILE), os.path.join(os.getcwd(), CONFIG_FILE)) 105 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', deviceConfig), os.path.join(os.getcwd(), deviceConfig)) 106 | time.sleep(1) 107 | 108 | # should load driver from ini file without error 109 | epd = displayfactory.load_display_driver() 110 | 111 | # test that driver specific file also loaded 112 | assert epd._config.has_option(IMAGE_DISPLAY, 'flip_horizontal') 113 | self.assertFalse(epd._config.getboolean(IMAGE_DISPLAY, 'flip_horizontal')) 114 | 115 | # should attempt to load passed in driver, and fail, instead of one in conf file 116 | self.assertRaises(EPDNotFoundError, displayfactory.load_display_driver, constants.BAD_EPD_NAME) 117 | 118 | def test_configuration_error(self): 119 | """ 120 | Confirm that an EPDConfigurationError is thrown by passing a bad mode value 121 | to a display 122 | """ 123 | deviceConfig = constants.GOOD_EPD_NAME + ".ini" 124 | 125 | # copy bad config file to be loaded 126 | copyfile(os.path.join(os.getcwd(), "tests", 'ini', constants.BAD_CONFIG_FILE), os.path.join(os.getcwd(), deviceConfig)) 127 | 128 | # load the display driver, shoudl throw EPDConfigurationError 129 | self.assertRaises(EPDConfigurationError, displayfactory.load_display_driver, constants.GOOD_EPD_NAME) 130 | -------------------------------------------------------------------------------- /tests/test_image_processing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import time 4 | import glob 5 | import pytest 6 | from . import constants as constants 7 | from PIL import Image, ImageChops 8 | from shutil import copyfile 9 | from omni_epd import displayfactory 10 | from omni_epd.conf import CONFIG_FILE 11 | 12 | TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__))) 13 | 14 | 15 | class TestImageProcessing(unittest.TestCase): 16 | 17 | def _delete_files(self, file_type='ini'): 18 | fileList = glob.glob(os.path.join(os.getcwd(), "*." + file_type)) 19 | 20 | for f in fileList: 21 | # don't bother catching errors - just let it fail out 22 | os.remove(f) 23 | 24 | @pytest.fixture(autouse=True) 25 | def run_before_and_after_tests(self): 26 | # clean up any files left over from previous tests 27 | self._delete_files() 28 | self._delete_files('png') 29 | yield 30 | 31 | # clean up any files made during this test 32 | self._delete_files() 33 | self._delete_files('png') 34 | 35 | def setup_config(self, source_config_file_name, target_config_file_name): 36 | copyfile(os.path.join(TEST_PATH, 'ini', source_config_file_name), os.path.join(os.getcwd(), target_config_file_name)) 37 | time.sleep(1) 38 | 39 | def open_image(self, image, w, h): 40 | """Open an image and resize it for EPD display""" 41 | result = Image.open(image) 42 | 43 | return result.resize((w, h)) 44 | 45 | def compare_images(self, image_one, image_two): 46 | """compare if two images are equal, return true/false """ 47 | im1 = Image.open(image_one) 48 | im2 = Image.open(image_two) 49 | 50 | diff = ImageChops.difference(im1, im2) 51 | 52 | if diff.getbbox() is None: 53 | # same 54 | return True 55 | else: 56 | return False 57 | 58 | def test_image_processing_options(self): 59 | """ 60 | Test all common image processing options (rotating, contrast, etc) 61 | https://github.com/robweber/omni-epd#advanced-epd-control 62 | """ 63 | 64 | self.setup_config(constants.ALL_IMAGE_OPTIONS, CONFIG_FILE) 65 | 66 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 67 | 68 | # write the image 69 | image = self.open_image(constants.GALAXY_IMAGE, epd.width, epd.height) 70 | 71 | epd.display(image) 72 | 73 | def test_basic_dither(self): 74 | """ 75 | Test that a basic dither algorithm can be applied - tests that result image is different than master (non-modified) image 76 | Dithering will return same image if not applied or dither algorithm does not exist 77 | """ 78 | self.setup_config(constants.BASIC_DITHER, CONFIG_FILE) 79 | 80 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 81 | 82 | # write the image 83 | image = self.open_image(constants.GALAXY_IMAGE, epd.width, epd.height) 84 | epd.display(image) 85 | 86 | # compare the two images should be different (dither applied) 87 | assert not self.compare_images(constants.MOCK_EPD_OUTPUT, constants.MASTER_IMAGE) 88 | 89 | def test_custom_dither(self): 90 | """ 91 | Tests that custom dithering can be applied via either the INI file 92 | Tests that generated images are not the same as a master (non-modified image) 93 | """ 94 | self.setup_config(constants.CUSTOM_DITHER_INI, CONFIG_FILE) 95 | 96 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 97 | 98 | # write the image 99 | image = self.open_image(constants.GALAXY_IMAGE, epd.width, epd.height) 100 | epd.display(image) 101 | 102 | # compare the two images should be different (dither applied) 103 | assert not self.compare_images(constants.MOCK_EPD_OUTPUT, constants.MASTER_IMAGE) 104 | 105 | def test_custom_dither_json(self): 106 | """ 107 | Tests that custom dithering can be applied from a JSON file 108 | Tests that generated images are not the same as a master (non-modified image) 109 | This will fail if JSON file can't be loaded, the same image will be returned by didder 110 | """ 111 | self.setup_config(constants.CUSTOM_DITHER_JSON, CONFIG_FILE) 112 | 113 | epd = displayfactory.load_display_driver(constants.GOOD_EPD_NAME) 114 | 115 | # write the image 116 | image = self.open_image(constants.GALAXY_IMAGE, epd.width, epd.height) 117 | epd.display(image) 118 | 119 | # compare the two images should be different (dither applied) 120 | assert not self.compare_images(constants.MOCK_EPD_OUTPUT, constants.MASTER_IMAGE) 121 | -------------------------------------------------------------------------------- /tests/test_omni_epd_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import time 4 | import json 5 | import glob 6 | import pytest 7 | from shutil import copyfile 8 | from omni_epd import EPDNotFoundError, EPDConfigurationError 9 | from omni_epd import displayfactory 10 | from omni_epd.virtualepd import VirtualEPD 11 | from omni_epd.conf import IMAGE_DISPLAY, CONFIG_FILE 12 | 13 | TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__))) 14 | 15 | 16 | class TestomniEpd(unittest.TestCase): 17 | goodEpd = "omni_epd.mock" # this should always be a valid EPD 18 | badEpd = "omni_epd.bad" # this is not a valid EPD 19 | badConfig = 'bad_conf.ini' # name of invalid configuration file 20 | 21 | def _delete_ini(self): 22 | fileList = glob.glob(os.path.join(os.getcwd(), "*.ini")) 23 | 24 | for f in fileList: 25 | # don't bother catching errors - just let it fail out 26 | os.remove(f) 27 | 28 | @pytest.fixture(autouse=True) 29 | def run_before_and_after_tests(self): 30 | # clean up any files left over from previous tests 31 | self._delete_ini() 32 | 33 | yield 34 | 35 | # clean up any files made during this test 36 | self._delete_ini() 37 | 38 | def setup_config(self, source_config_file_name, target_config_file_name): 39 | copyfile(os.path.join(TEST_PATH, 'ini', source_config_file_name), os.path.join(os.getcwd(), target_config_file_name)) 40 | time.sleep(1) 41 | 42 | def test_supported_diplays(self): 43 | """ 44 | Test that displays can be loaded 45 | """ 46 | drivers = displayfactory.list_supported_displays() 47 | 48 | assert len(drivers) > 0 49 | 50 | def test_loading_error(self): 51 | """ 52 | Confirm error thrown if an invalid name passed to load function 53 | """ 54 | self.assertRaises(EPDNotFoundError, displayfactory.load_display_driver, self.badEpd) 55 | 56 | def test_loading_success(self): 57 | """ 58 | Confirm a good display can be loaded and extends VirtualEPD 59 | """ 60 | epd = displayfactory.load_display_driver(self.goodEpd) 61 | 62 | assert isinstance(epd, VirtualEPD) 63 | 64 | def test_global_conf(self): 65 | """ 66 | Test loading of omni-epd.ini config file 67 | Once loaded confirm options from file exist within display class config 68 | Also confirm values not in the config file aren't changed from defaults 69 | """ 70 | # set up a global config file 71 | self.setup_config(CONFIG_FILE, CONFIG_FILE) 72 | 73 | epd = displayfactory.load_display_driver(self.goodEpd) 74 | 75 | assert epd._config.has_option(IMAGE_DISPLAY, 'rotate') 76 | assert epd._config.getfloat(IMAGE_DISPLAY, 'rotate') == 90 77 | 78 | # test that mode is default 79 | assert epd.mode == 'bw' 80 | 81 | def test_device_config(self): 82 | """ 83 | Test that when both omni-epd.ini file is present and device specific INI present 84 | that the device specific config overrides options in global config 85 | """ 86 | deviceConfig = self.goodEpd + ".ini" 87 | 88 | # set up a global config file and device config 89 | self.setup_config(CONFIG_FILE, CONFIG_FILE) 90 | self.setup_config(deviceConfig, deviceConfig) 91 | 92 | epd = displayfactory.load_display_driver(self.goodEpd) 93 | 94 | # device should override global 95 | assert epd._config.has_option(IMAGE_DISPLAY, 'flip_horizontal') 96 | self.assertFalse(epd._config.getboolean(IMAGE_DISPLAY, 'flip_horizontal')) 97 | 98 | # test mode and palette configurations 99 | assert epd.mode == 'palette' 100 | assert len(json.loads(epd._get_device_option('palette_filter', "[]"))) == 5 # confirms custom palette will be loaded 101 | 102 | def test_load_device_from_conf(self): 103 | """ 104 | Test that a device will load when given the type= option in the omni-epd.ini file 105 | and no args to load_display_driver() 106 | """ 107 | deviceConfig = self.goodEpd + ".ini" 108 | 109 | # set up a global config file 110 | self.setup_config(CONFIG_FILE, CONFIG_FILE) 111 | self.setup_config(deviceConfig, deviceConfig) 112 | 113 | # should load driver from ini file without error 114 | epd = displayfactory.load_display_driver() 115 | 116 | # test that driver specific file also loaded 117 | assert epd._config.has_option(IMAGE_DISPLAY, 'flip_horizontal') 118 | self.assertFalse(epd._config.getboolean(IMAGE_DISPLAY, 'flip_horizontal')) 119 | 120 | # should attempt to load passed in driver, and fail, instead of one in conf file 121 | self.assertRaises(EPDNotFoundError, displayfactory.load_display_driver, self.badEpd) 122 | 123 | def test_configuration_error(self): 124 | """ 125 | Confirm that an EPDConfigurationError is thrown by passing a bad mode value 126 | to a display 127 | """ 128 | deviceConfig = self.goodEpd + ".ini" 129 | 130 | # copy bad config file to be loaded 131 | self.setup_config(self.badConfig, deviceConfig) 132 | 133 | # load the display driver, shoudl throw EPDConfigurationError 134 | self.assertRaises(EPDConfigurationError, displayfactory.load_display_driver, self.goodEpd) 135 | --------------------------------------------------------------------------------