1 # The MIT License (MIT)
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
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:
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
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
24 `displayio.epaperdisplay`
25 ================================================================================
29 **Software and Dependencies:**
32 https://github.com/adafruit/Adafruit_Blinka/releases
34 * Author(s): Melissa LeBlanc-Williams
40 from recordclass import recordclass
42 from displayio.bitmap import Bitmap
43 from displayio.colorconverter import ColorConverter
45 __version__ = "0.0.0-auto.0"
46 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48 # pylint: disable=unnecessary-pass, unused-argument
50 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
51 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
55 """Manage updating an epaper display over a display bus
57 This initializes an epaper display and connects it into CircuitPython. Unlike other
58 objects in CircuitPython, EPaperDisplay objects live until
59 displayio.release_displays() is called. This is done so that CircuitPython can use
62 Most people should not use this class directly. Use a specific display driver instead
63 that will contain the startup and shutdown sequences at minimum.
66 # pylint: disable=too-many-locals
80 set_column_window_command=None,
81 set_row_window_command=None,
82 single_byte_bounds=False,
83 write_black_ram_command,
84 black_bits_inverted=False,
85 write_color_ram_command=None,
86 color_bits_inverted=False,
87 highlight_color=0x000000,
88 refresh_display_command,
92 seconds_per_frame=180,
93 always_toggle_chip_select=False
96 Create a EPaperDisplay object on the given display bus (displayio.FourWire or
97 displayio.ParallelBus).
99 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
100 command begins with a command byte followed by a byte to determine the parameter
101 count and if a delay is need after. When the top bit of the second byte is 1, the
102 next byte will be the delay time in milliseconds. The remaining 7 bits are the
103 parameter count excluding any delay byte. The third through final bytes are the
104 remaining command parameters. The next byte will begin a new command definition.
108 # pylint: enable=too-many-locals
110 def show(self, group):
111 """Switches to displaying the given group of layers. When group is None, the default
112 CircuitPython terminal will be shown (eventually).
117 """Refreshes the display immediately or raises an exception if too soon. Use
118 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
123 def time_to_refresh(self):
124 """Time, in fractional seconds, until the ePaper display can be refreshed."""
139 """Current Display Bus"""
144 """Manage updating a display over SPI four wire protocol in the background while
145 Python code runs. It doesn’t handle display initialization.
159 """Create a FourWire object associated with the given pins.
161 The SPI bus and pins are then in use by the display until
162 displayio.release_displays() is called even after a reload. (It does this so
163 CircuitPython can use the display after your code is done.)
164 So, the first time you initialize a display bus in code.py you should call
165 :py:func`displayio.release_displays` first, otherwise it will error after the
168 self._dc = digitalio.DigitalInOut(command)
169 self._dc.switch_to_output()
170 self._chip_select = digitalio.DigitalInOut(chip_select)
171 self._chip_select.switch_to_output(value=True)
173 if reset is not None:
174 self._reset = digitalio.DigitalInOut(reset)
175 self._reset.switch_to_output(value=True)
179 while self._spi.try_lock():
181 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
188 self._chip_select.deinit()
189 if self._reset is not None:
193 """Performs a hardware reset via the reset pin.
194 Raises an exception if called when no reset pin is available.
196 if self._reset is not None:
197 self._reset.value = False
199 self._reset.value = True
202 raise RuntimeError("No reset pin defined")
204 def send(self, is_command, data, *, toggle_every_byte=False):
205 """Sends the given command value followed by the full set of data. Display state,
206 such as vertical scroll, set via ``send`` may or may not be reset once the code is
209 while self._spi.try_lock():
211 self._dc.value = not is_command
212 if toggle_every_byte:
214 self._spi.write(bytes([byte]))
215 self._chip_select.value = True
217 self._chip_select.value = False
219 self._spi.write(data)
224 """Manage a group of sprites and groups and how they are inter-related."""
226 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
227 """Create a Group of a given size and scale. Scale is in
228 one dimension. For example, scale=2 leads to a layer’s
229 pixel being 2x2 pixels when in the group.
231 if not isinstance(max_size, int) or max_size < 1:
232 raise ValueError("Max Size must be >= 1")
233 self._max_size = max_size
234 if not isinstance(scale, int) or scale < 1:
235 raise ValueError("Scale must be >= 1")
241 self._supported_types = (TileGrid, Group)
242 self._absolute_transform = None
243 self.in_group = False
244 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
246 def update_transform(self, parent_transform):
247 """Update the parent transform and child transforms"""
248 self.in_group = parent_transform is not None
252 if parent_transform.transpose_xy:
254 self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
255 self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
256 self._absolute_transform.dx = parent_transform.dx * self._scale
257 self._absolute_transform.dy = parent_transform.dy * self._scale
258 self._absolute_transform.transpose_xy = parent_transform.transpose_xy
259 self._absolute_transform.mirror_x = parent_transform.mirror_x
260 self._absolute_transform.mirror_y = parent_transform.mirror_y
261 self._absolute_transform.scale = parent_transform.scale * self._scale
262 self._update_child_transforms()
264 def _update_child_transforms(self):
266 for layer in self._layers:
267 layer.update_transform(self._absolute_transform)
269 def _removal_cleanup(self, index):
270 layer = self._layers[index]
271 layer.update_transform(None)
273 def _layer_update(self, index):
274 layer = self._layers[index]
275 layer.update_transform(self._absolute_transform)
277 def append(self, layer):
278 """Append a layer to the group. It will be drawn
281 self.insert(len(self._layers), layer)
283 def insert(self, index, layer):
284 """Insert a layer into the group."""
285 if not isinstance(layer, self._supported_types):
286 raise ValueError("Invalid Group Member")
288 raise ValueError("Layer already in a group.")
289 if len(self._layers) == self._max_size:
290 raise RuntimeError("Group full")
291 self._layers.insert(index, layer)
292 self._layer_update(index)
294 def index(self, layer):
295 """Returns the index of the first copy of layer.
296 Raises ValueError if not found.
298 return self._layers.index(layer)
300 def pop(self, index=-1):
301 """Remove the ith item and return it."""
302 self._removal_cleanup(index)
303 return self._layers.pop(index)
305 def remove(self, layer):
306 """Remove the first copy of layer. Raises ValueError
307 if it is not present."""
308 index = self.index(layer)
309 self._layers.pop(index)
312 """Returns the number of layers in a Group"""
313 return len(self._layers)
315 def __getitem__(self, index):
316 """Returns the value at the given index."""
317 return self._layers[index]
319 def __setitem__(self, index, value):
320 """Sets the value at the given index."""
321 self._removal_cleanup(index)
322 self._layers[index] = value
323 self._layer_update(index)
325 def __delitem__(self, index):
326 """Deletes the value at the given index."""
327 del self._layers[index]
329 def _fill_area(self, buffer):
333 for layer in self._layers:
334 if isinstance(layer, (Group, TileGrid)):
335 layer._fill_area(buffer) # pylint: disable=protected-access
339 """True when the Group and all of it’s layers are not visible. When False, the
340 Group’s layers are visible if they haven’t been hidden.
345 def hidden(self, value):
346 if not isinstance(value, (bool, int)):
347 raise ValueError("Expecting a boolean or integer value")
348 self._hidden = bool(value)
352 """Scales each pixel within the Group in both directions. For example, when
353 scale=2 each pixel will be represented by 2x2 pixels.
358 def scale(self, value):
359 if not isinstance(value, int) or value < 1:
360 raise ValueError("Scale must be >= 1")
361 if self._scale != value:
362 parent_scale = self._absolute_transform.scale / self._scale
363 self._absolute_transform.dx = (
364 self._absolute_transform.dx / self._scale * value
366 self._absolute_transform.dy = (
367 self._absolute_transform.dy / self._scale * value
369 self._absolute_transform.scale = parent_scale * value
372 self._update_child_transforms()
376 """X position of the Group in the parent."""
381 if not isinstance(value, int):
382 raise ValueError("x must be an integer")
384 if self._absolute_transform.transpose_xy:
385 dy_value = self._absolute_transform.dy / self._scale
386 self._absolute_transform.y += dy_value * (value - self._x)
388 dx_value = self._absolute_transform.dx / self._scale
389 self._absolute_transform.x += dx_value * (value - self._x)
391 self._update_child_transforms()
395 """Y position of the Group in the parent."""
400 if not isinstance(value, int):
401 raise ValueError("y must be an integer")
403 if self._absolute_transform.transpose_xy:
404 dx_value = self._absolute_transform.dx / self._scale
405 self._absolute_transform.x += dx_value * (value - self._y)
407 dy_value = self._absolute_transform.dy / self._scale
408 self._absolute_transform.y += dy_value * (value - self._y)
410 self._update_child_transforms()
414 """Manage updating a display over I2C in the background while Python code runs.
415 It doesn’t handle display initialization.
418 def __init__(self, i2c_bus, *, device_address, reset=None):
419 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
421 The I2C bus and pins are then in use by the display until displayio.release_displays() is
422 called even after a reload. (It does this so CircuitPython can use the display after your
423 code is done.) So, the first time you initialize a display bus in code.py you should call
424 :py:func`displayio.release_displays` first, otherwise it will error after the first
430 """Performs a hardware reset via the reset pin. Raises an exception if called
431 when no reset pin is available.
435 def send(self, command, data):
436 """Sends the given command value followed by the full set of data. Display state,
437 such as vertical scroll, set via send may or may not be reset once the code is
445 Loads values straight from disk. This minimizes memory use but can lead to much slower
446 pixel load times. These load times may result in frame tearing where only part of the
449 def __init__(self, file):
450 self._image = Image.open(file)
454 """Width of the bitmap. (read only)"""
455 return self._image.width
459 """Height of the bitmap. (read only)"""
460 return self._image.height
464 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
465 format internally to save memory.
468 def __init__(self, color_count):
469 """Create a Palette object to store a set number of colors."""
470 self._needs_refresh = False
473 for _ in range(color_count):
474 self._colors.append(self._make_color(0))
476 def _make_color(self, value, transparent=False):
478 "transparent": transparent,
481 if isinstance(value, (tuple, list, bytes, bytearray)):
482 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
483 elif isinstance(value, int):
484 if not 0 <= value <= 0xFFFFFF:
485 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
487 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
488 color["rgb888"] = value
489 self._needs_refresh = True
494 """Returns the number of colors in a Palette"""
495 return len(self._colors)
497 def __setitem__(self, index, value):
498 """Sets the pixel color at the given index. The index should be
499 an integer in the range 0 to color_count-1.
501 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
502 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
503 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
505 if self._colors[index]["rgb888"] != value:
506 self._colors[index] = self._make_color(value)
508 def __getitem__(self, index):
509 if not 0 <= index < len(self._colors):
510 raise ValueError("Palette index out of range")
511 return self._colors[index]
513 def make_transparent(self, palette_index):
514 """Set the palette index to be a transparent color"""
515 self._colors[palette_index]["transparent"] = True
517 def make_opaque(self, palette_index):
518 """Set the palette index to be an opaque color"""
519 self._colors[palette_index]["transparent"] = False
523 """Manage updating a display over 8-bit parallel bus in the background while Python code
524 runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
525 It doesn’t handle display initialization.
528 def __init__(self, i2c_bus, *, device_address, reset=None):
529 """Create a ParallelBus object associated with the given pins. The
530 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
533 The parallel bus and pins are then in use by the display until
534 displayio.release_displays() is called even after a reload. (It does this so
535 CircuitPython can use the display after your code is done.) So, the first time you
536 initialize a display bus in code.py you should call
537 :py:func`displayio.release_displays` first, otherwise it will error after the first
543 """Performs a hardware reset via the reset pin. Raises an exception if called when
544 no reset pin is available.
548 def send(self, command, data):
549 """Sends the given command value followed by the full set of data. Display state,
550 such as vertical scroll, set via ``send`` may or may not be reset once the code is
557 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
558 by the column boundaries of the shape on each row. Each row’s boundary defaults to the
562 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
563 """Create a Shape object with the given fixed size. Each pixel is one bit and is
564 stored by the column boundaries of the shape on each row. Each row’s boundary
565 defaults to the full row.
567 super().__init__(width, height, 2)
569 def set_boundary(self, y, start_x, end_x):
570 """Loads pre-packed data into the given row."""
574 # pylint: disable=too-many-instance-attributes
576 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
577 grids can share bitmaps and pixel shaders.
579 A single tile grid is also known as a Sprite.
595 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
596 used to convert the value and its location to a display native pixel color. This may
597 be a simple color palette lookup, a gradient, a pattern or a color transformer.
599 tile_width and tile_height match the height of the bitmap by default.
601 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
602 raise ValueError("Unsupported Bitmap type")
603 self._bitmap = bitmap
604 bitmap_width = bitmap.width
605 bitmap_height = bitmap.height
607 if not isinstance(pixel_shader, (ColorConverter, Palette)):
608 raise ValueError("Unsupported Pixel Shader type")
609 self._pixel_shader = pixel_shader
613 self._width = width # Number of Tiles Wide
614 self._height = height # Number of Tiles High
615 self._transpose_xy = False
618 if tile_width is None:
619 tile_width = bitmap_width
620 if tile_height is None:
621 tile_height = bitmap_height
622 if bitmap_width % tile_width != 0:
623 raise ValueError("Tile width must exactly divide bitmap width")
624 self._tile_width = tile_width
625 if bitmap_height % tile_height != 0:
626 raise ValueError("Tile height must exactly divide bitmap height")
627 self._tile_height = tile_height
628 if not 0 <= default_tile <= 255:
629 raise ValueError("Default Tile is out of range")
630 self._pixel_width = width * tile_width
631 self._pixel_height = height * tile_height
632 self._tiles = (self._width * self._height) * [default_tile]
633 self.in_group = False
634 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
635 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
638 def update_transform(self, absolute_transform):
639 """Update the parent transform and child transforms"""
640 self._absolute_transform = absolute_transform
641 if self._absolute_transform is not None:
642 self._update_current_x()
643 self._update_current_y()
645 def _update_current_x(self):
646 if self._transpose_xy:
647 width = self._pixel_height
649 width = self._pixel_width
650 if self._absolute_transform.transpose_xy:
651 self._current_area.y1 = (
652 self._absolute_transform.y + self._absolute_transform.dy * self._x
654 self._current_area.y2 = (
655 self._absolute_transform.y
656 + self._absolute_transform.dy * (self._x + width)
658 if self._current_area.y2 < self._current_area.y1:
659 self._current_area.y1, self._current_area.y2 = (
660 self._current_area.y2,
661 self._current_area.y1,
664 self._current_area.x1 = (
665 self._absolute_transform.x + self._absolute_transform.dx * self._x
667 self._current_area.x2 = (
668 self._absolute_transform.x
669 + self._absolute_transform.dx * (self._x + width)
671 if self._current_area.x2 < self._current_area.x1:
672 self._current_area.x1, self._current_area.x2 = (
673 self._current_area.x2,
674 self._current_area.x1,
677 def _update_current_y(self):
678 if self._transpose_xy:
679 height = self._pixel_width
681 height = self._pixel_height
682 if self._absolute_transform.transpose_xy:
683 self._current_area.x1 = (
684 self._absolute_transform.x + self._absolute_transform.dx * self._y
686 self._current_area.x2 = (
687 self._absolute_transform.x
688 + self._absolute_transform.dx * (self._y + height)
690 if self._current_area.x2 < self._current_area.x1:
691 self._current_area.x1, self._current_area.x2 = (
692 self._current_area.x2,
693 self._current_area.x1,
696 self._current_area.y1 = (
697 self._absolute_transform.y + self._absolute_transform.dy * self._y
699 self._current_area.y2 = (
700 self._absolute_transform.y
701 + self._absolute_transform.dy * (self._y + height)
703 if self._current_area.y2 < self._current_area.y1:
704 self._current_area.y1, self._current_area.y2 = (
705 self._current_area.y2,
706 self._current_area.y1,
709 # pylint: disable=too-many-locals
710 def _fill_area(self, buffer):
711 """Draw onto the image"""
717 (self._width * self._tile_width, self._height * self._tile_height),
721 tile_count_x = self._bitmap.width // self._tile_width
725 for tile_x in range(0, self._width):
726 for tile_y in range(0, self._height):
727 tile_index = self._tiles[tile_y * self._width + tile_x]
728 tile_index_x = tile_index % tile_count_x
729 tile_index_y = tile_index // tile_count_x
730 for pixel_x in range(self._tile_width):
731 for pixel_y in range(self._tile_height):
732 image_x = tile_x * self._tile_width + pixel_x
733 image_y = tile_y * self._tile_height + pixel_y
734 bitmap_x = tile_index_x * self._tile_width + pixel_x
735 bitmap_y = tile_index_y * self._tile_height + pixel_y
736 pixel_color = self._pixel_shader[
737 self._bitmap[bitmap_x, bitmap_y]
739 if not pixel_color["transparent"]:
740 image.putpixel((image_x, image_y), pixel_color["rgb888"])
741 if self._absolute_transform is not None:
742 if self._absolute_transform.scale > 1:
743 image = image.resize(
745 self._pixel_width * self._absolute_transform.scale,
746 self._pixel_height * self._absolute_transform.scale,
748 resample=Image.NEAREST,
750 if self._absolute_transform.mirror_x:
751 image = image.transpose(Image.FLIP_LEFT_RIGHT)
752 if self._absolute_transform.mirror_y:
753 image = image.transpose(Image.FLIP_TOP_BOTTOM)
754 if self._absolute_transform.transpose_xy:
755 image = image.transpose(Image.TRANSPOSE)
756 x *= self._absolute_transform.dx
757 y *= self._absolute_transform.dy
758 x += self._absolute_transform.x
759 y += self._absolute_transform.y
760 buffer.alpha_composite(image, (x, y))
762 # pylint: enable=too-many-locals
766 """True when the TileGrid is hidden. This may be False even
767 when a part of a hidden Group."""
771 def hidden(self, value):
772 if not isinstance(value, (bool, int)):
773 raise ValueError("Expecting a boolean or integer value")
774 self._hidden = bool(value)
778 """X position of the left edge in the parent."""
783 if not isinstance(value, int):
784 raise TypeError("X should be a integer type")
787 self._update_current_x()
791 """Y position of the top edge in the parent."""
796 if not isinstance(value, int):
797 raise TypeError("Y should be a integer type")
800 self._update_current_y()
804 """If true, the left edge rendered will be the right edge of the right-most tile."""
808 def flip_x(self, value):
809 if not isinstance(value, bool):
810 raise TypeError("Flip X should be a boolean type")
811 if self._flip_x != value:
816 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
820 def flip_y(self, value):
821 if not isinstance(value, bool):
822 raise TypeError("Flip Y should be a boolean type")
823 if self._flip_y != value:
827 def transpose_xy(self):
828 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
829 degree rotation can be achieved along with the corresponding mirrored version.
831 return self._transpose_xy
834 def transpose_xy(self, value):
835 if not isinstance(value, bool):
836 raise TypeError("Transpose XY should be a boolean type")
837 if self._transpose_xy != value:
838 self._transpose_xy = value
839 self._update_current_x()
840 self._update_current_y()
843 def pixel_shader(self):
844 """The pixel shader of the tilegrid."""
847 def __getitem__(self, index):
848 """Returns the tile index at the given index. The index can either be
849 an x,y tuple or an int equal to ``y * width + x``'.
851 if isinstance(index, (tuple, list)):
854 index = y * self._width + x
855 elif isinstance(index, int):
856 x = index % self._width
857 y = index // self._width
858 if x > self._width or y > self._height or index >= len(self._tiles):
859 raise ValueError("Tile index out of bounds")
860 return self._tiles[index]
862 def __setitem__(self, index, value):
863 """Sets the tile index at the given index. The index can either be
864 an x,y tuple or an int equal to ``y * width + x``.
866 if isinstance(index, (tuple, list)):
869 index = y * self._width + x
870 elif isinstance(index, int):
871 x = index % self._width
872 y = index // self._width
873 if x > self._width or y > self._height or index >= len(self._tiles):
874 raise ValueError("Tile index out of bounds")
875 if not 0 <= value <= 255:
876 raise ValueError("Tile value out of bounds")
877 self._tiles[index] = value
880 # pylint: enable=too-many-instance-attributes