/ 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.")