Skip to content

Commit 7e557ab

Browse files
author
Jouko Strömmer
committed
Major refactoring
- Unified and reimplemented most of the Waveshare SPI drivers as driver classes - Switched to Python 3 - Use Pillow instead of PIL - Use virtualenv - CLI changes - Doc fixes - License added: GPL 3.0 *for the drivers* - Main program is still CC0
1 parent 097cc29 commit 7e557ab

File tree

11 files changed

+3099
-278
lines changed

11 files changed

+3099
-278
lines changed

drivers/LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

drivers/README.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Display drivers for PaperTTY
2+
3+
**Important note**: **while the PaperTTY program itself is in the public domain (CC0)**, these drivers are based on Waveshare's reference drivers (https://github.com/soonuse/epd-library-python) which are licensed under GPL 3.0 (*the files in the repo also contain MIT and BSD license notices, but I verified with Waveshare that GPL is intended*).
4+
5+
**Thus, the files in this `drivers` directory respect Waveshare's license and are also GPL 3.0 licensed.**
6+
7+
### Supported SPI displays
8+
9+
**Nothing is guaranteed - I don't own the hardware to test all of these. Use at your own risk!**
10+
11+
- **Supported models (SPI)**
12+
- **EPD 1.54" (monochrome) - [probably works, with partial refresh]**
13+
- **EPD 1.54" B (black/white/red)**
14+
- **EPD 1.54" C (black/white/yellow)**
15+
- **EPD 2.13" (monochrome) - [TESTED, with partial refresh]**
16+
- **EPD 2.13" B (black/white/red)**
17+
- **EPD 2.13" C (black/white/yellow)** - should work with `EPD2in13b`
18+
- **EPD 2.7" (monochrome)**
19+
- **EPD 2.7" B (black/white/red)**
20+
- **EPD 2.9" (monochrome) - [probably works, with partial refresh]**
21+
- **EPD 2.9" B (black/white/red)**
22+
- **EPD 2.9" C (black/white/yellow)** - should work with `EPD2in9b`
23+
- **EPD 4.2" (monochrome)**
24+
- **EPD 4.2" B (black/white/red)**
25+
- **EPD 4.2" C (black/white/yellow)** - should work with `EPD4in2b`
26+
- **EPD 7.5" (monochrome)**
27+
- **EPD 7.5" B (black/white/red)**
28+
- **EPD 7.5" C (black/white/yellow)** - should work with `EPD7in5b`
29+
- **Missing models**
30+
- **EPD 2.13" D (monochrome, flexible)**
31+
- **EPD 5.83" (monochrome)**
32+
- **EPD 5.83" B (black/white/red)**
33+
- **EPD 5.83" C (black/white/yellow)**
34+
- **Special drivers**
35+
- **Dummy - no-op driver**
36+
- **Bitmap - output frames as bitmap files (for debugging)**
37+
38+
Should this code mess up your display, disconnecting it from power ought to fix it if nothing else helps.
39+
40+
### Overview
41+
42+
This is a restructuring of Waveshare's code for the purposes of using all the displays with a common interface from the PaperTTY program. Note that these are **SPI** displays - the UART ones are not supported at the moment.
43+
44+
The original code between different models contained **lots** of overlap and this is an attempt to identify the common code and create classes based on that. The original code was analyzed first with a (crude) program that would:
45+
46+
- Collect all the class methods and variables from each source file
47+
- Normalize the source by removing all comments and extra whitespace for each method separately
48+
- Calculate intersections of sets of source code strings (entire methods) to find common code
49+
- Find common class variables with identical values between groups of displays
50+
51+
(I wonder if there exists some nice tool to visually analyze, compare and cluster a codebase based on similarity instead of writing it myself, but I didn't find one...)
52+
53+
Afterwards, the results were used to group the individual display models' code so that subclasses override and build on the base methods and variables. Grouping could have been based on more concrete factors such as chips used in the products but I didn't find such information, so the grouping is based on code similarity and the features of the displays.
54+
55+
Manual adjustments were made to unify methods that only differed very slightly (such as using a single different value somewhere).
56+
57+
Also some bugs were fixed and overall style formatted to be a bit more pythonic (the reference code appears to be translated from C), although most code was left verbatim, including the original comments. This eventually resulted in *hopefully* the same functionality but ~3000 lines shorter.
58+
59+
The actual driver code could be refactored a lot, but only small tweaks have been done for now.
60+
61+
**Since I have no way of actually testing if the code works properly without the hardware, it's very likely I have introduced some new bugs and/or the code needs some fixing.**
62+
63+
### Usage
64+
65+
PaperTTY itself doesn't use much of the drivers' features - also colors are ignored. The only important driver methods it calls are `initialize` and `draw` (and `scrub`).
66+
67+
Functionality has not (intentionally) been removed from the drivers so you should be able to instantiate and use them quite similarly to the Waveshare's demo code:
68+
69+
```python
70+
# This will draw a black rectangle to the corner of the display (2.13" B/W)
71+
72+
# Import the required driver/group
73+
import drivers.drivers_partial as drivers_partial
74+
from PIL import Image, ImageDraw
75+
76+
# Instantiate
77+
epd = drivers_partial.EPD2in13()
78+
# Remember to initialize: by default uses partial refresh if available
79+
epd.init()
80+
# Create an image and draw a black rectangle on it
81+
img = Image.new('1', (epd.width, epd.height), epd.white)
82+
draw = ImageDraw.Draw(img)
83+
draw.rectangle((0,0,50,50), fill=epd.black)
84+
# Set memory twice because of partial refresh
85+
epd.set_frame_memory(img, 0, 0)
86+
epd.display_frame()
87+
epd.set_frame_memory(img, 0, 0)
88+
epd.display_frame()
89+
```
90+
91+
**However**, now each display has a new `draw` method, so you don't need to bother with the specifics of how a particular display is updated, and the above code can be simplified slightly:
92+
93+
```python
94+
# This will draw a black rectangle to the corner of the display (2.13" B/W),
95+
# using the new 'draw' method
96+
97+
# Import the required driver/group
98+
import drivers.drivers_partial as drivers_partial
99+
from PIL import Image, ImageDraw
100+
101+
# Instantiate
102+
epd = drivers_partial.EPD2in13()
103+
# Remember to initialize: by default uses partial refresh if available
104+
epd.init()
105+
# Create an image and draw a black rectangle on it
106+
img = Image.new('1', (epd.width, epd.height), epd.white)
107+
draw = ImageDraw.Draw(img)
108+
draw.rectangle((0,0,50,50), fill=epd.black)
109+
# Just draw it on the screen
110+
epd.draw(0, 0, img)
111+
```
112+
113+
### Class structure
114+
115+
The **B** and **C** variants differ by just their color (EPD 1.54" C is an exception - its resolution is lower) so you *should* be able to use the **C** displays with the **B** driver.
116+
117+
- **`DisplayDriver`** - base class with abstract `init` and `draw` methods
118+
119+
- **`SpecialDriver`** - base class for "dummy" drivers - ie. not actual display hardware
120+
- **`Dummy`** - **dummy, no-op driver**
121+
- **`Bitmap`** - **bitmap driver - renders the content into files**
122+
- **`WaveshareEPD`** - base class for Waveshare EPDs
123+
- **`WavesharePartial`** - base class for variants that (officially) support partial refresh
124+
- **`EPD1in54`** - **EPD 1.54" (monochrome)**
125+
- **`EPD2in13`** - **EPD 2.13" (monochrome)**
126+
- **`EPD2in9`** - **EPD 2.9" (monochrome)**
127+
- **`WaveshareFull`** - base class for variants that **don't** (officially) support partial refresh
128+
- **`EPD2in7`** - **EPD 2.7" (monochrome)**
129+
- **`EPD4in2`** - **EPD 4.2" (monochrome)**
130+
- **`EPD7in5`** - **EPD 7.5" (monochrome)**
131+
- **`WaveshareColor`** - base class for variants that have an extra color (B/C variants)
132+
- **`EPD4in2b`** - **EPD 4.2" B (black/white/red)**
133+
- **`EPD7in5b`** - **EPD 7.5" B (black/white/red)**
134+
- **`WaveshareColorDraw`** - base class for color variants that implement "rotation aware" drawing methods
135+
- **`EPD1in54b`** - **EPD 1.54" B (black/white/red)**
136+
- **`EPD1in54c`** - **EPD 1.54" C (black/white/yellow)**
137+
- **`EPD2in13b`** - **EPD 2.13" B (black/white/red)**
138+
- **`EPD2in7b`** - **EPD 2.7" B (black/white/red)**
139+
- **`EPD2in9b`** - **EPD 2.9" B (black/white/red)**
140+
141+
142+
143+
### Bitmap driver
144+
145+
This is mostly for debugging purposes (and to configure it you'll need to edit the source), but by default it will store the frames in a round-robin fashion as PNG images (`bitmap_frame_[0-4].png`) to the working directory, overwriting the old ones as new frames are drawn. By default just the last 5 frames are stored.

drivers/__init__.py

Whitespace-only changes.

drivers/drivers_base.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Copyright (c) 2018 Jouko Strömmer
2+
# Copyright (c) 2017 Waveshare
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
from abc import ABC, abstractmethod
18+
from PIL import Image
19+
import spidev
20+
import RPi.GPIO as GPIO
21+
import time
22+
23+
24+
class DisplayDriver(ABC):
25+
"""Abstract base class for a display driver - be it Waveshare e-Paper, PaPiRus, OLED..."""
26+
27+
# override these if needed
28+
white = 255
29+
black = 0
30+
31+
def __init__(self):
32+
super().__init__()
33+
self.name = None
34+
self.width = None
35+
self.height = None
36+
self.colors = None
37+
self.type = None
38+
self.supports_partial = None
39+
self.partial_refresh = None
40+
41+
@abstractmethod
42+
def init(self, partial=None):
43+
"""Initialize the display"""
44+
pass
45+
46+
@abstractmethod
47+
def draw(self, x, y, image):
48+
"""Draw an image object on the display at (x,y)"""
49+
pass
50+
51+
def scrub(self, fillsize=16):
52+
"""Scrub display - only works properly with partial refresh"""
53+
self.fill(self.black, fillsize=fillsize)
54+
self.fill(self.white, fillsize=fillsize)
55+
56+
def fill(self, color, fillsize):
57+
"""Slow fill routine"""
58+
image = Image.new('1', (fillsize, self.height), color)
59+
for x in range(0, self.width, fillsize):
60+
self.draw(x, 0, image)
61+
62+
63+
class SpecialDriver(DisplayDriver):
64+
"""Drivers that don't control hardware"""
65+
default_width = 640
66+
default_height = 384
67+
68+
def __init__(self, name, width, height):
69+
super().__init__()
70+
self.name = name
71+
self.width = width
72+
self.height = height
73+
self.type = 'Dummy display driver'
74+
75+
@abstractmethod
76+
def init(self, partial=None):
77+
pass
78+
79+
@abstractmethod
80+
def draw(self, x, y, image):
81+
pass
82+
83+
def scrub(self, fillsize=16):
84+
pass
85+
86+
87+
class Dummy(SpecialDriver):
88+
"""Dummy display driver - does not do anything"""
89+
90+
def __init__(self):
91+
super().__init__(name='No-op driver', width=self.default_width, height=self.default_height)
92+
93+
def init(self, partial=None):
94+
pass
95+
96+
def draw(self, x, y, image):
97+
pass
98+
99+
100+
class Bitmap(SpecialDriver):
101+
"""Output a bitmap for each frame - overwrite old ones"""
102+
103+
def __init__(self, maxfiles=5, file_format="png"):
104+
super().__init__(name="Bitmap output driver", width=self.default_width, height=self.default_height, )
105+
self.maxfiles = maxfiles
106+
self.current_frame = 0
107+
self.frame_buffer = None
108+
self.file_format = file_format
109+
110+
def init(self, partial=None):
111+
self.frame_buffer = Image.new('1', (self.width, self.height), 255)
112+
self.current_frame = 0
113+
114+
def draw(self, x, y, image):
115+
self.frame_buffer.paste(image, box=(x, y))
116+
self.frame_buffer.save("bitmap_frame_{}.{}".format(self.current_frame, self.file_format))
117+
self.current_frame = (self.current_frame + 1) % self.maxfiles
118+
119+
120+
class WaveshareEPD(DisplayDriver):
121+
"""Base class for Waveshare displays with common code for all - the 'epdif.py'
122+
- 1.54" , 1.54" B , 1.54" C
123+
- 2.13" , 2.13" B
124+
- 2.7" , 2.7" B
125+
- 2.9" , 2.9" B
126+
- 4.2" , 4.2" B
127+
- 7.5" , 7.5" B
128+
"""
129+
130+
# SPI methods
131+
132+
# These pins are common across all models
133+
RST_PIN = 17
134+
DC_PIN = 25
135+
CS_PIN = 8
136+
BUSY_PIN = 24
137+
138+
# SPI device, bus = 0, device = 0
139+
SPI = spidev.SpiDev(0, 0)
140+
141+
@staticmethod
142+
def epd_digital_write(pin, value):
143+
GPIO.output(pin, value)
144+
145+
@staticmethod
146+
def epd_digital_read(pin):
147+
return GPIO.input(pin)
148+
149+
@staticmethod
150+
def epd_delay_ms(delaytime):
151+
time.sleep(float(delaytime) / 1000.0)
152+
153+
def spi_transfer(self, data):
154+
self.SPI.writebytes(data)
155+
156+
def epd_init(self):
157+
GPIO.setmode(GPIO.BCM)
158+
GPIO.setwarnings(False)
159+
GPIO.setup(self.RST_PIN, GPIO.OUT)
160+
GPIO.setup(self.DC_PIN, GPIO.OUT)
161+
GPIO.setup(self.CS_PIN, GPIO.OUT)
162+
GPIO.setup(self.BUSY_PIN, GPIO.IN)
163+
self.SPI.max_speed_hz = 2000000
164+
self.SPI.mode = 0b00
165+
return 0
166+
167+
# Basic functionality
168+
169+
def __init__(self, name, width, height):
170+
super().__init__()
171+
self.name = name
172+
self.width = width
173+
self.height = height
174+
self.type = 'Waveshare e-Paper'
175+
176+
def digital_write(self, pin, value):
177+
self.epd_digital_write(pin, value)
178+
179+
def digital_read(self, pin):
180+
return self.epd_digital_read(pin)
181+
182+
def delay_ms(self, delaytime):
183+
self.epd_delay_ms(delaytime)
184+
185+
def send_command(self, command):
186+
self.digital_write(self.DC_PIN, GPIO.LOW)
187+
# the parameter type is list but not int
188+
# so use [command] instead of command
189+
self.spi_transfer([command])
190+
191+
def send_data(self, data):
192+
self.digital_write(self.DC_PIN, GPIO.HIGH)
193+
# the parameter type is list but not int
194+
# so use [data] instead of data
195+
self.spi_transfer([data])
196+
197+
def reset(self):
198+
self.digital_write(self.RST_PIN, GPIO.LOW)
199+
self.delay_ms(200)
200+
self.digital_write(self.RST_PIN, GPIO.HIGH)
201+
self.delay_ms(200)
202+
203+
def init(self, **kwargs):
204+
pass
205+
206+
def draw(self, x, y, image):
207+
pass

0 commit comments

Comments
 (0)