├── .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 | 
3 | [](https://github.com/robweber/omni-epd/actions/workflows/pytest.yml?query=branch%3Amain)
4 | [](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 |
--------------------------------------------------------------------------------