/ adafruit_epd / epd.py
epd.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2018 Dean Miller for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_epd.epd` - Adafruit EPD - ePaper display driver 24 ==================================================================================== 25 CircuitPython driver for Adafruit ePaper display breakouts 26 * Author(s): Dean Miller 27 """ 28 29 import time 30 from micropython import const 31 from digitalio import Direction 32 from adafruit_epd import mcp_sram 33 34 __version__ = "0.0.0-auto.0" 35 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EPD.git" 36 37 38 class Adafruit_EPD: # pylint: disable=too-many-instance-attributes, too-many-public-methods 39 """Base class for EPD displays 40 """ 41 42 BLACK = const(0) 43 WHITE = const(1) 44 INVERSE = const(2) 45 RED = const(3) 46 DARK = const(4) 47 LIGHT = const(5) 48 49 def __init__( 50 self, width, height, spi, cs_pin, dc_pin, sramcs_pin, rst_pin, busy_pin 51 ): # pylint: disable=too-many-arguments 52 self._width = width 53 self._height = height 54 55 # Setup reset pin, if we have one 56 self._rst = rst_pin 57 if rst_pin: 58 self._rst.direction = Direction.OUTPUT 59 60 # Setup busy pin, if we have one 61 self._busy = busy_pin 62 if busy_pin: 63 self._busy.direction = Direction.INPUT 64 65 # Setup dc pin (required) 66 self._dc = dc_pin 67 self._dc.direction = Direction.OUTPUT 68 self._dc.value = False 69 70 # Setup cs pin (required) 71 self._cs = cs_pin 72 self._cs.direction = Direction.OUTPUT 73 self._cs.value = True 74 75 # SPI interface (required) 76 self.spi_device = spi 77 while not self.spi_device.try_lock(): 78 time.sleep(0.01) 79 self.spi_device.configure(baudrate=1000000) # 1 Mhz 80 self.spi_device.unlock() 81 82 self._spibuf = bytearray(1) 83 self._single_byte_tx = False 84 85 self.sram = None 86 if sramcs_pin: 87 self.sram = mcp_sram.Adafruit_MCP_SRAM(sramcs_pin, spi) 88 89 self._buf = bytearray(3) 90 self._buffer1_size = self._buffer2_size = 0 91 self._buffer1 = self._buffer2 = None 92 self._framebuf1 = self._framebuf2 = None 93 self._colorframebuf = self._blackframebuf = None 94 self._black_inverted = self._color_inverted = True 95 self.hardware_reset() 96 97 def display(self): # pylint: disable=too-many-branches 98 """show the contents of the display buffer""" 99 self.power_up() 100 101 self.set_ram_address(0, 0) 102 103 if self.sram: 104 while not self.spi_device.try_lock(): 105 time.sleep(0.01) 106 self.sram.cs_pin.value = False 107 # send read command 108 self._buf[0] = mcp_sram.Adafruit_MCP_SRAM.SRAM_READ 109 # send start address 110 self._buf[1] = 0 111 self._buf[2] = 0 112 self.spi_device.write(self._buf, end=3) 113 self.spi_device.unlock() 114 115 # first data byte from SRAM will be transfered in at the 116 # same time as the EPD command is transferred out 117 databyte = self.write_ram(0) 118 119 while not self.spi_device.try_lock(): 120 time.sleep(0.01) 121 self._dc.value = True 122 123 if self.sram: 124 for _ in range(self._buffer1_size): 125 databyte = self._spi_transfer(databyte) 126 self.sram.cs_pin.value = True 127 else: 128 for databyte in self._buffer1: 129 self._spi_transfer(databyte) 130 131 self._cs.value = True 132 self.spi_device.unlock() 133 time.sleep(0.002) 134 135 if self.sram: 136 while not self.spi_device.try_lock(): 137 time.sleep(0.01) 138 self.sram.cs_pin.value = False 139 # send read command 140 self._buf[0] = mcp_sram.Adafruit_MCP_SRAM.SRAM_READ 141 # send start address 142 self._buf[1] = (self._buffer1_size >> 8) & 0xFF 143 self._buf[2] = self._buffer1_size & 0xFF 144 self.spi_device.write(self._buf, end=3) 145 self.spi_device.unlock() 146 147 if self._buffer2_size != 0: 148 # first data byte from SRAM will be transfered in at the 149 # same time as the EPD command is transferred out 150 databyte = self.write_ram(1) 151 152 while not self.spi_device.try_lock(): 153 time.sleep(0.01) 154 self._dc.value = True 155 156 if self.sram: 157 for _ in range(self._buffer2_size): 158 databyte = self._spi_transfer(databyte) 159 self.sram.cs_pin.value = True 160 else: 161 for databyte in self._buffer2: 162 self._spi_transfer(databyte) 163 164 self._cs.value = True 165 self.spi_device.unlock() 166 else: 167 if self.sram: 168 self.sram.cs_pin.value = True 169 170 self.update() 171 172 def hardware_reset(self): 173 """If we have a reset pin, do a hardware reset by toggling it""" 174 if self._rst: 175 self._rst.value = False 176 time.sleep(0.1) 177 self._rst.value = True 178 time.sleep(0.1) 179 180 def command(self, cmd, data=None, end=True): 181 """Send command byte to display.""" 182 self._cs.value = True 183 self._dc.value = False 184 self._cs.value = False 185 186 while not self.spi_device.try_lock(): 187 time.sleep(0.01) 188 ret = self._spi_transfer(cmd) 189 190 if data is not None: 191 self._dc.value = True 192 for b in data: 193 self._spi_transfer(b) 194 if end: 195 self._cs.value = True 196 self.spi_device.unlock() 197 198 return ret 199 200 def _spi_transfer(self, databyte): 201 """Transfer one byte, toggling the cs pin if required by the EPD chipset""" 202 self._spibuf[0] = databyte 203 if self._single_byte_tx: 204 self._cs.value = False 205 self.spi_device.write_readinto(self._spibuf, self._spibuf) 206 if self._single_byte_tx: 207 self._cs.value = True 208 return self._spibuf[0] 209 210 def power_up(self): 211 """Power up the display in preparation for writing RAM and updating. 212 must be implemented in subclass""" 213 raise NotImplementedError() 214 215 def power_down(self): 216 """Power down the display, must be implemented in subclass""" 217 raise NotImplementedError() 218 219 def update(self): 220 """Update the display from internal memory, must be implemented in subclass""" 221 raise NotImplementedError() 222 223 def write_ram(self, index): 224 """Send the one byte command for starting the RAM write process. Returns 225 the byte read at the same time over SPI. index is the RAM buffer, can be 226 0 or 1 for tri-color displays. must be implemented in subclass""" 227 raise NotImplementedError() 228 229 def set_ram_address(self, x, y): 230 """Set the RAM address location, must be implemented in subclass""" 231 raise NotImplementedError() 232 233 def set_black_buffer(self, index, inverted): 234 """Set the index for the black buffer data (0 or 1) and whether its inverted""" 235 if index == 0: 236 self._blackframebuf = self._framebuf1 237 elif index == 1: 238 self._blackframebuf = self._framebuf2 239 else: 240 raise RuntimeError("Buffer index must be 0 or 1") 241 self._black_inverted = inverted 242 243 def set_color_buffer(self, index, inverted): 244 """Set the index for the color buffer data (0 or 1) and whether its inverted""" 245 if index == 0: 246 self._colorframebuf = self._framebuf1 247 elif index == 1: 248 self._colorframebuf = self._framebuf2 249 else: 250 raise RuntimeError("Buffer index must be 0 or 1") 251 self._color_inverted = inverted 252 253 def _color_dup(self, func, args, color): 254 black = getattr(self._blackframebuf, func) 255 red = getattr(self._colorframebuf, func) 256 if self._blackframebuf is self._colorframebuf: # monochrome 257 black(*args, color=(color != Adafruit_EPD.WHITE) != self._black_inverted) 258 else: 259 black(*args, color=(color == Adafruit_EPD.BLACK) != self._black_inverted) 260 red(*args, color=(color == Adafruit_EPD.RED) != self._color_inverted) 261 262 def pixel(self, x, y, color): 263 """draw a single pixel in the display buffer""" 264 self._color_dup("pixel", (x, y), color) 265 266 def fill(self, color): 267 """fill the screen with the passed color""" 268 red_fill = ((color == Adafruit_EPD.RED) != self._color_inverted) * 0xFF 269 black_fill = ((color == Adafruit_EPD.BLACK) != self._black_inverted) * 0xFF 270 271 if self.sram: 272 self.sram.erase(0x00, self._buffer1_size, black_fill) 273 self.sram.erase(self._buffer1_size, self._buffer2_size, red_fill) 274 else: 275 self._blackframebuf.fill(black_fill) 276 self._colorframebuf.fill(red_fill) 277 278 def rect(self, x, y, width, height, color): # pylint: disable=too-many-arguments 279 """draw a rectangle""" 280 self._color_dup("rect", (x, y, width, height), color) 281 282 def fill_rect( 283 self, x, y, width, height, color 284 ): # pylint: disable=too-many-arguments 285 """fill a rectangle with the passed color""" 286 self._color_dup("fill_rect", (x, y, width, height), color) 287 288 def line(self, x_0, y_0, x_1, y_1, color): # pylint: disable=too-many-arguments 289 """Draw a line from (x_0, y_0) to (x_1, y_1) in passed color""" 290 self._color_dup("line", (x_0, y_0, x_1, y_1), color) 291 292 def text(self, string, x, y, color, *, font_name="font5x8.bin"): 293 """Write text string at location (x, y) in given color, using font file""" 294 if self._blackframebuf is self._colorframebuf: # monochrome 295 self._blackframebuf.text( 296 string, 297 x, 298 y, 299 font_name=font_name, 300 color=(color != Adafruit_EPD.WHITE) != self._black_inverted, 301 ) 302 else: 303 self._blackframebuf.text( 304 string, 305 x, 306 y, 307 font_name=font_name, 308 color=(color == Adafruit_EPD.BLACK) != self._black_inverted, 309 ) 310 self._colorframebuf.text( 311 string, 312 x, 313 y, 314 font_name=font_name, 315 color=(color == Adafruit_EPD.RED) != self._color_inverted, 316 ) 317 318 @property 319 def width(self): 320 """The width of the display, accounting for rotation""" 321 if self.rotation in (0, 2): 322 return self._width 323 return self._height 324 325 @property 326 def height(self): 327 """The height of the display, accounting for rotation""" 328 if self.rotation in (0, 2): 329 return self._height 330 return self._width 331 332 @property 333 def rotation(self): 334 """The rotation of the display, can be one of (0, 1, 2, 3)""" 335 return self._framebuf1.rotation 336 337 @rotation.setter 338 def rotation(self, val): 339 self._framebuf1.rotation = val 340 if self._framebuf2: 341 self._framebuf2.rotation = val 342 343 def hline(self, x, y, width, color): 344 """draw a horizontal line""" 345 self.fill_rect(x, y, width, 1, color) 346 347 def vline(self, x, y, height, color): 348 """draw a vertical line""" 349 self.fill_rect(x, y, 1, height, color) 350 351 def image(self, image): 352 """Set buffer to value of Python Imaging Library image. The image should 353 be in RGB mode and a size equal to the display size. 354 """ 355 imwidth, imheight = image.size 356 if imwidth != self.width or imheight != self.height: 357 raise ValueError( 358 "Image must be same dimensions as display ({0}x{1}).".format( 359 self.width, self.height 360 ) 361 ) 362 if self.sram: 363 raise RuntimeError("PIL image is not for use with SRAM assist") 364 # Grab all the pixels from the image, faster than getpixel. 365 pix = image.load() 366 # clear out any display buffers 367 self.fill(Adafruit_EPD.WHITE) 368 369 if image.mode == "RGB": # RGB Mode 370 for y in range(image.size[1]): 371 for x in range(image.size[0]): 372 pixel = pix[x, y] 373 if (pixel[1] < 0x80 <= pixel[0]) and (pixel[2] < 0x80): 374 # reddish 375 self.pixel(x, y, Adafruit_EPD.RED) 376 elif (pixel[0] < 0x80) and (pixel[1] < 0x80) and (pixel[2] < 0x80): 377 # dark 378 self.pixel(x, y, Adafruit_EPD.BLACK) 379 elif image.mode == "L": # Grayscale 380 for y in range(image.size[1]): 381 for x in range(image.size[0]): 382 pixel = pix[x, y] 383 if pixel < 0x80: 384 self.pixel(x, y, Adafruit_EPD.BLACK) 385 else: 386 raise ValueError("Image must be in mode RGB or mode L.")