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
25 ================================================================================
29 **Software and Dependencies:**
32 https://github.com/adafruit/Adafruit_Blinka/releases
34 * Author(s): Melissa LeBlanc-Williams
41 from displayio.bitmap import Bitmap
42 from displayio.colorconverter import ColorConverter
43 from displayio import Rectangle
44 from displayio import Transform
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49 # pylint: disable=unnecessary-pass, unused-argument
53 """Manage updating an epaper display over a display bus
55 This initializes an epaper display and connects it into CircuitPython. Unlike other
56 objects in CircuitPython, EPaperDisplay objects live until
57 displayio.release_displays() is called. This is done so that CircuitPython can use
60 Most people should not use this class directly. Use a specific display driver instead
61 that will contain the startup and shutdown sequences at minimum.
64 # pylint: disable=too-many-locals
78 set_column_window_command=None,
79 set_row_window_command=None,
80 single_byte_bounds=False,
81 write_black_ram_command,
82 black_bits_inverted=False,
83 write_color_ram_command=None,
84 color_bits_inverted=False,
85 highlight_color=0x000000,
86 refresh_display_command,
90 seconds_per_frame=180,
91 always_toggle_chip_select=False
94 Create a EPaperDisplay object on the given display bus (displayio.FourWire or
95 displayio.ParallelBus).
97 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
98 command begins with a command byte followed by a byte to determine the parameter
99 count and if a delay is need after. When the top bit of the second byte is 1, the
100 next byte will be the delay time in milliseconds. The remaining 7 bits are the
101 parameter count excluding any delay byte. The third through final bytes are the
102 remaining command parameters. The next byte will begin a new command definition.
106 # pylint: enable=too-many-locals
108 def show(self, group):
109 """Switches to displaying the given group of layers. When group is None, the default
110 CircuitPython terminal will be shown (eventually).
115 """Refreshes the display immediately or raises an exception if too soon. Use
116 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
121 def time_to_refresh(self):
122 """Time, in fractional seconds, until the ePaper display can be refreshed."""
137 """Current Display Bus"""
142 """Manage updating a display over SPI four wire protocol in the background while
143 Python code runs. It doesn’t handle display initialization.
157 """Create a FourWire object associated with the given pins.
159 The SPI bus and pins are then in use by the display until
160 displayio.release_displays() is called even after a reload. (It does this so
161 CircuitPython can use the display after your code is done.)
162 So, the first time you initialize a display bus in code.py you should call
163 :py:func`displayio.release_displays` first, otherwise it will error after the
166 self._dc = digitalio.DigitalInOut(command)
167 self._dc.switch_to_output()
168 self._chip_select = digitalio.DigitalInOut(chip_select)
169 self._chip_select.switch_to_output(value=True)
171 if reset is not None:
172 self._reset = digitalio.DigitalInOut(reset)
173 self._reset.switch_to_output(value=True)
177 while self._spi.try_lock():
179 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
186 self._chip_select.deinit()
187 if self._reset is not None:
191 """Performs a hardware reset via the reset pin.
192 Raises an exception if called when no reset pin is available.
194 if self._reset is not None:
195 self._reset.value = False
197 self._reset.value = True
200 raise RuntimeError("No reset pin defined")
202 def send(self, is_command, data, *, toggle_every_byte=False):
203 """Sends the given command value followed by the full set of data. Display state,
204 such as vertical scroll, set via ``send`` may or may not be reset once the code is
207 while self._spi.try_lock():
209 self._dc.value = not is_command
210 if toggle_every_byte:
212 self._spi.write(bytes([byte]))
213 self._chip_select.value = True
215 self._chip_select.value = False
217 self._spi.write(data)
222 """Manage a group of sprites and groups and how they are inter-related."""
224 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
225 """Create a Group of a given size and scale. Scale is in
226 one dimension. For example, scale=2 leads to a layer’s
227 pixel being 2x2 pixels when in the group.
229 if not isinstance(max_size, int) or max_size < 1:
230 raise ValueError("Max Size must be >= 1")
231 self._max_size = max_size
232 if not isinstance(scale, int) or scale < 1:
233 raise ValueError("Scale must be >= 1")
239 self._supported_types = (TileGrid, Group)
240 self._absolute_transform = None
241 self.in_group = False
242 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
244 def update_transform(self, parent_transform):
245 """Update the parent transform and child transforms"""
246 self.in_group = parent_transform is not None
250 if parent_transform.transpose_xy:
252 self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
253 self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
254 self._absolute_transform.dx = parent_transform.dx * self._scale
255 self._absolute_transform.dy = parent_transform.dy * self._scale
256 self._absolute_transform.transpose_xy = parent_transform.transpose_xy
257 self._absolute_transform.mirror_x = parent_transform.mirror_x
258 self._absolute_transform.mirror_y = parent_transform.mirror_y
259 self._absolute_transform.scale = parent_transform.scale * self._scale
260 self._update_child_transforms()
262 def _update_child_transforms(self):
264 for layer in self._layers:
265 layer.update_transform(self._absolute_transform)
267 def _removal_cleanup(self, index):
268 layer = self._layers[index]
269 layer.update_transform(None)
271 def _layer_update(self, index):
272 layer = self._layers[index]
273 layer.update_transform(self._absolute_transform)
275 def append(self, layer):
276 """Append a layer to the group. It will be drawn
279 self.insert(len(self._layers), layer)
281 def insert(self, index, layer):
282 """Insert a layer into the group."""
283 if not isinstance(layer, self._supported_types):
284 raise ValueError("Invalid Group Member")
286 raise ValueError("Layer already in a group.")
287 if len(self._layers) == self._max_size:
288 raise RuntimeError("Group full")
289 self._layers.insert(index, layer)
290 self._layer_update(index)
292 def index(self, layer):
293 """Returns the index of the first copy of layer.
294 Raises ValueError if not found.
296 return self._layers.index(layer)
298 def pop(self, index=-1):
299 """Remove the ith item and return it."""
300 self._removal_cleanup(index)
301 return self._layers.pop(index)
303 def remove(self, layer):
304 """Remove the first copy of layer. Raises ValueError
305 if it is not present."""
306 index = self.index(layer)
307 self._layers.pop(index)
310 """Returns the number of layers in a Group"""
311 return len(self._layers)
313 def __getitem__(self, index):
314 """Returns the value at the given index."""
315 return self._layers[index]
317 def __setitem__(self, index, value):
318 """Sets the value at the given index."""
319 self._removal_cleanup(index)
320 self._layers[index] = value
321 self._layer_update(index)
323 def __delitem__(self, index):
324 """Deletes the value at the given index."""
325 del self._layers[index]
327 def _fill_area(self, buffer):
331 for layer in self._layers:
332 if isinstance(layer, (Group, TileGrid)):
333 layer._fill_area(buffer) # pylint: disable=protected-access
337 """True when the Group and all of it’s layers are not visible. When False, the
338 Group’s layers are visible if they haven’t been hidden.
343 def hidden(self, value):
344 if not isinstance(value, (bool, int)):
345 raise ValueError("Expecting a boolean or integer value")
346 self._hidden = bool(value)
350 """Scales each pixel within the Group in both directions. For example, when
351 scale=2 each pixel will be represented by 2x2 pixels.
356 def scale(self, value):
357 if not isinstance(value, int) or value < 1:
358 raise ValueError("Scale must be >= 1")
359 if self._scale != value:
360 parent_scale = self._absolute_transform.scale / self._scale
361 self._absolute_transform.dx = (
362 self._absolute_transform.dx / self._scale * value
364 self._absolute_transform.dy = (
365 self._absolute_transform.dy / self._scale * value
367 self._absolute_transform.scale = parent_scale * value
370 self._update_child_transforms()
374 """X position of the Group in the parent."""
379 if not isinstance(value, int):
380 raise ValueError("x must be an integer")
382 if self._absolute_transform.transpose_xy:
383 dy_value = self._absolute_transform.dy / self._scale
384 self._absolute_transform.y += dy_value * (value - self._x)
386 dx_value = self._absolute_transform.dx / self._scale
387 self._absolute_transform.x += dx_value * (value - self._x)
389 self._update_child_transforms()
393 """Y position of the Group in the parent."""
398 if not isinstance(value, int):
399 raise ValueError("y must be an integer")
401 if self._absolute_transform.transpose_xy:
402 dx_value = self._absolute_transform.dx / self._scale
403 self._absolute_transform.x += dx_value * (value - self._y)
405 dy_value = self._absolute_transform.dy / self._scale
406 self._absolute_transform.y += dy_value * (value - self._y)
408 self._update_child_transforms()
412 """Manage updating a display over I2C in the background while Python code runs.
413 It doesn’t handle display initialization.
416 def __init__(self, i2c_bus, *, device_address, reset=None):
417 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
419 The I2C bus and pins are then in use by the display until displayio.release_displays() is
420 called even after a reload. (It does this so CircuitPython can use the display after your
421 code is done.) So, the first time you initialize a display bus in code.py you should call
422 :py:func`displayio.release_displays` first, otherwise it will error after the first
428 """Performs a hardware reset via the reset pin. Raises an exception if called
429 when no reset pin is available.
433 def send(self, command, data):
434 """Sends the given command value followed by the full set of data. Display state,
435 such as vertical scroll, set via send may or may not be reset once the code is
443 Loads values straight from disk. This minimizes memory use but can lead to much slower
444 pixel load times. These load times may result in frame tearing where only part of the
447 def __init__(self, file):
448 self._image = Image.open(file)
452 """Width of the bitmap. (read only)"""
453 return self._image.width
457 """Height of the bitmap. (read only)"""
458 return self._image.height
462 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
463 format internally to save memory.
466 def __init__(self, color_count):
467 """Create a Palette object to store a set number of colors."""
468 self._needs_refresh = False
471 for _ in range(color_count):
472 self._colors.append(self._make_color(0))
474 def _make_color(self, value, transparent=False):
476 "transparent": transparent,
479 if isinstance(value, (tuple, list, bytes, bytearray)):
480 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
481 elif isinstance(value, int):
482 if not 0 <= value <= 0xFFFFFF:
483 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
485 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
486 color["rgb888"] = value
487 self._needs_refresh = True
492 """Returns the number of colors in a Palette"""
493 return len(self._colors)
495 def __setitem__(self, index, value):
496 """Sets the pixel color at the given index. The index should be
497 an integer in the range 0 to color_count-1.
499 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
500 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
501 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
503 if self._colors[index]["rgb888"] != value:
504 self._colors[index] = self._make_color(value)
506 def __getitem__(self, index):
507 if not 0 <= index < len(self._colors):
508 raise ValueError("Palette index out of range")
509 return self._colors[index]
511 def make_transparent(self, palette_index):
512 """Set the palette index to be a transparent color"""
513 self._colors[palette_index]["transparent"] = True
515 def make_opaque(self, palette_index):
516 """Set the palette index to be an opaque color"""
517 self._colors[palette_index]["transparent"] = False
521 """Manage updating a display over 8-bit parallel bus in the background while Python code
522 runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
523 It doesn’t handle display initialization.
526 def __init__(self, i2c_bus, *, device_address, reset=None):
527 """Create a ParallelBus object associated with the given pins. The
528 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
531 The parallel bus and pins are then in use by the display until
532 displayio.release_displays() is called even after a reload. (It does this so
533 CircuitPython can use the display after your code is done.) So, the first time you
534 initialize a display bus in code.py you should call
535 :py:func`displayio.release_displays` first, otherwise it will error after the first
541 """Performs a hardware reset via the reset pin. Raises an exception if called when
542 no reset pin is available.
546 def send(self, command, data):
547 """Sends the given command value followed by the full set of data. Display state,
548 such as vertical scroll, set via ``send`` may or may not be reset once the code is
555 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
556 by the column boundaries of the shape on each row. Each row’s boundary defaults to the
560 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
561 """Create a Shape object with the given fixed size. Each pixel is one bit and is
562 stored by the column boundaries of the shape on each row. Each row’s boundary
563 defaults to the full row.
565 super().__init__(width, height, 2)
567 def set_boundary(self, y, start_x, end_x):
568 """Loads pre-packed data into the given row."""
572 # pylint: disable=too-many-instance-attributes
574 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
575 grids can share bitmaps and pixel shaders.
577 A single tile grid is also known as a Sprite.
593 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
594 used to convert the value and its location to a display native pixel color. This may
595 be a simple color palette lookup, a gradient, a pattern or a color transformer.
597 tile_width and tile_height match the height of the bitmap by default.
599 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
600 raise ValueError("Unsupported Bitmap type")
601 self._bitmap = bitmap
602 bitmap_width = bitmap.width
603 bitmap_height = bitmap.height
605 if not isinstance(pixel_shader, (ColorConverter, Palette)):
606 raise ValueError("Unsupported Pixel Shader type")
607 self._pixel_shader = pixel_shader
611 self._width = width # Number of Tiles Wide
612 self._height = height # Number of Tiles High
613 self._transpose_xy = False
616 if tile_width is None:
617 tile_width = bitmap_width
618 if tile_height is None:
619 tile_height = bitmap_height
620 if bitmap_width % tile_width != 0:
621 raise ValueError("Tile width must exactly divide bitmap width")
622 self._tile_width = tile_width
623 if bitmap_height % tile_height != 0:
624 raise ValueError("Tile height must exactly divide bitmap height")
625 self._tile_height = tile_height
626 if not 0 <= default_tile <= 255:
627 raise ValueError("Default Tile is out of range")
628 self._pixel_width = width * tile_width
629 self._pixel_height = height * tile_height
630 self._tiles = (self._width * self._height) * [default_tile]
631 self.in_group = False
632 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
633 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
636 def update_transform(self, absolute_transform):
637 """Update the parent transform and child transforms"""
638 self._absolute_transform = absolute_transform
639 if self._absolute_transform is not None:
640 self._update_current_x()
641 self._update_current_y()
643 def _update_current_x(self):
644 if self._transpose_xy:
645 width = self._pixel_height
647 width = self._pixel_width
648 if self._absolute_transform.transpose_xy:
649 self._current_area.y1 = (
650 self._absolute_transform.y + self._absolute_transform.dy * self._x
652 self._current_area.y2 = (
653 self._absolute_transform.y
654 + self._absolute_transform.dy * (self._x + width)
656 if self._current_area.y2 < self._current_area.y1:
657 self._current_area.y1, self._current_area.y2 = (
658 self._current_area.y2,
659 self._current_area.y1,
662 self._current_area.x1 = (
663 self._absolute_transform.x + self._absolute_transform.dx * self._x
665 self._current_area.x2 = (
666 self._absolute_transform.x
667 + self._absolute_transform.dx * (self._x + width)
669 if self._current_area.x2 < self._current_area.x1:
670 self._current_area.x1, self._current_area.x2 = (
671 self._current_area.x2,
672 self._current_area.x1,
675 def _update_current_y(self):
676 if self._transpose_xy:
677 height = self._pixel_width
679 height = self._pixel_height
680 if self._absolute_transform.transpose_xy:
681 self._current_area.x1 = (
682 self._absolute_transform.x + self._absolute_transform.dx * self._y
684 self._current_area.x2 = (
685 self._absolute_transform.x
686 + self._absolute_transform.dx * (self._y + height)
688 if self._current_area.x2 < self._current_area.x1:
689 self._current_area.x1, self._current_area.x2 = (
690 self._current_area.x2,
691 self._current_area.x1,
694 self._current_area.y1 = (
695 self._absolute_transform.y + self._absolute_transform.dy * self._y
697 self._current_area.y2 = (
698 self._absolute_transform.y
699 + self._absolute_transform.dy * (self._y + height)
701 if self._current_area.y2 < self._current_area.y1:
702 self._current_area.y1, self._current_area.y2 = (
703 self._current_area.y2,
704 self._current_area.y1,
707 # pylint: disable=too-many-locals
708 def _fill_area(self, buffer):
709 """Draw onto the image"""
715 (self._width * self._tile_width, self._height * self._tile_height),
719 tile_count_x = self._bitmap.width // self._tile_width
723 for tile_x in range(0, self._width):
724 for tile_y in range(0, self._height):
725 tile_index = self._tiles[tile_y * self._width + tile_x]
726 tile_index_x = tile_index % tile_count_x
727 tile_index_y = tile_index // tile_count_x
728 for pixel_x in range(self._tile_width):
729 for pixel_y in range(self._tile_height):
730 image_x = tile_x * self._tile_width + pixel_x
731 image_y = tile_y * self._tile_height + pixel_y
732 bitmap_x = tile_index_x * self._tile_width + pixel_x
733 bitmap_y = tile_index_y * self._tile_height + pixel_y
734 pixel_color = self._pixel_shader[
735 self._bitmap[bitmap_x, bitmap_y]
737 if not pixel_color["transparent"]:
738 image.putpixel((image_x, image_y), pixel_color["rgb888"])
739 if self._absolute_transform is not None:
740 if self._absolute_transform.scale > 1:
741 image = image.resize(
743 self._pixel_width * self._absolute_transform.scale,
744 self._pixel_height * self._absolute_transform.scale,
746 resample=Image.NEAREST,
748 if self._absolute_transform.mirror_x:
749 image = image.transpose(Image.FLIP_LEFT_RIGHT)
750 if self._absolute_transform.mirror_y:
751 image = image.transpose(Image.FLIP_TOP_BOTTOM)
752 if self._absolute_transform.transpose_xy:
753 image = image.transpose(Image.TRANSPOSE)
754 x *= self._absolute_transform.dx
755 y *= self._absolute_transform.dy
756 x += self._absolute_transform.x
757 y += self._absolute_transform.y
758 buffer.alpha_composite(image, (x, y))
760 # pylint: enable=too-many-locals
764 """True when the TileGrid is hidden. This may be False even
765 when a part of a hidden Group."""
769 def hidden(self, value):
770 if not isinstance(value, (bool, int)):
771 raise ValueError("Expecting a boolean or integer value")
772 self._hidden = bool(value)
776 """X position of the left edge in the parent."""
781 if not isinstance(value, int):
782 raise TypeError("X should be a integer type")
785 self._update_current_x()
789 """Y position of the top edge in the parent."""
794 if not isinstance(value, int):
795 raise TypeError("Y should be a integer type")
798 self._update_current_y()
802 """If true, the left edge rendered will be the right edge of the right-most tile."""
806 def flip_x(self, value):
807 if not isinstance(value, bool):
808 raise TypeError("Flip X should be a boolean type")
809 if self._flip_x != value:
814 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
818 def flip_y(self, value):
819 if not isinstance(value, bool):
820 raise TypeError("Flip Y should be a boolean type")
821 if self._flip_y != value:
825 def transpose_xy(self):
826 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
827 degree rotation can be achieved along with the corresponding mirrored version.
829 return self._transpose_xy
832 def transpose_xy(self, value):
833 if not isinstance(value, bool):
834 raise TypeError("Transpose XY should be a boolean type")
835 if self._transpose_xy != value:
836 self._transpose_xy = value
837 self._update_current_x()
838 self._update_current_y()
841 def pixel_shader(self):
842 """The pixel shader of the tilegrid."""
845 def __getitem__(self, index):
846 """Returns the tile index at the given index. The index can either be
847 an x,y tuple or an int equal to ``y * width + x``'.
849 if isinstance(index, (tuple, list)):
852 index = y * self._width + x
853 elif isinstance(index, int):
854 x = index % self._width
855 y = index // self._width
856 if x > self._width or y > self._height or index >= len(self._tiles):
857 raise ValueError("Tile index out of bounds")
858 return self._tiles[index]
860 def __setitem__(self, index, value):
861 """Sets the tile index at the given index. The index can either be
862 an x,y tuple or an int equal to ``y * width + x``.
864 if isinstance(index, (tuple, list)):
867 index = y * self._width + x
868 elif isinstance(index, int):
869 x = index % self._width
870 y = index // self._width
871 if x > self._width or y > self._height or index >= len(self._tiles):
872 raise ValueError("Tile index out of bounds")
873 if not 0 <= value <= 255:
874 raise ValueError("Tile value out of bounds")
875 self._tiles[index] = value
878 # pylint: enable=too-many-instance-attributes