1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   6 `displayio.epaperdisplay`
 
   7 ================================================================================
 
  11 **Software and Dependencies:**
 
  14   https://github.com/adafruit/Adafruit_Blinka/releases
 
  16 * Author(s): Melissa LeBlanc-Williams
 
  21 from typing import Optional, Union
 
  22 import microcontroller
 
  23 from digitalio import DigitalInOut
 
  24 from circuitpython_typing import ReadableBuffer
 
  25 from ._displaycore import _DisplayCore
 
  26 from ._group import Group, circuitpython_splash
 
  27 from ._colorconverter import ColorConverter
 
  28 from ._displaybus import _DisplayBus
 
  29 from ._area import Area
 
  30 from ._constants import (
 
  31     CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
  32     CHIP_SELECT_UNTOUCHED,
 
  39 __version__ = "0.0.0+auto.0"
 
  40 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  44     # pylint: disable=too-many-instance-attributes, too-many-statements
 
  45     """Manage updating an epaper display over a display bus
 
  47     This initializes an epaper display and connects it into CircuitPython. Unlike other
 
  48     objects in CircuitPython, EPaperDisplay objects live until
 
  49     displayio.release_displays() is called. This is done so that CircuitPython can use
 
  52     Most people should not use this class directly. Use a specific display driver instead
 
  53     that will contain the startup and shutdown sequences at minimum.
 
  58         display_bus: _DisplayBus,
 
  59         start_sequence: ReadableBuffer,
 
  60         stop_sequence: ReadableBuffer,
 
  69         set_column_window_command: Optional[int] = None,
 
  70         set_row_window_command: Optional[int] = None,
 
  71         set_current_column_command: Optional[int] = None,
 
  72         set_current_row_command: Optional[int] = None,
 
  73         write_black_ram_command: int,
 
  74         black_bits_inverted: bool = False,
 
  75         write_color_ram_command: Optional[int] = None,
 
  76         color_bits_inverted: bool = False,
 
  77         highlight_color: int = 0x000000,
 
  78         refresh_display_command: Union[int, ReadableBuffer],
 
  79         refresh_time: float = 40.0,
 
  80         busy_pin: Optional[microcontroller.Pin] = None,
 
  81         busy_state: bool = True,
 
  82         seconds_per_frame: float = 180,
 
  83         always_toggle_chip_select: bool = False,
 
  84         grayscale: bool = False,
 
  85         advanced_color_epaper: bool = False,
 
  86         two_byte_sequence_length: bool = False,
 
  87         start_up_time: float = 0,
 
  88         address_little_endian: bool = False,
 
  90         # pylint: disable=too-many-locals
 
  91         """Create a EPaperDisplay object on the given display bus (`displayio.FourWire` or
 
  92         `paralleldisplay.ParallelBus`).
 
  94         The ``start_sequence`` and ``stop_sequence`` are bitpacked to minimize the ram impact. Every
 
  95         command begins with a command byte followed by a byte to determine the parameter count and
 
  96         delay. When the top bit of the second byte is 1 (0x80), a delay will occur after the command
 
  97         parameters are sent. The remaining 7 bits are the parameter count excluding any delay
 
  98         byte. The bytes following are the parameters. When the delay bit is set, a single byte after
 
  99         the parameters specifies the delay duration in milliseconds. The value 0xff will lead to an
 
 100         extra long 500 ms delay instead of 255 ms. The next byte will begin a new command
 
 103         :param display_bus: The bus that the display is connected to
 
 104         :type _DisplayBus: displayio.FourWire or paralleldisplay.ParallelBus
 
 105         :param ~circuitpython_typing.ReadableBuffer start_sequence: Byte-packed command sequence.
 
 106         :param ~circuitpython_typing.ReadableBuffer stop_sequence: Byte-packed command sequence.
 
 107         :param int width: Width in pixels
 
 108         :param int height: Height in pixels
 
 109         :param int ram_width: RAM width in pixels
 
 110         :param int ram_height: RAM height in pixels
 
 111         :param int colstart: The index if the first visible column
 
 112         :param int rowstart: The index if the first visible row
 
 113         :param int rotation: The rotation of the display in degrees clockwise. Must be in
 
 114             90 degree increments (0, 90, 180, 270)
 
 115         :param int set_column_window_command: Command used to set the start and end columns
 
 117         :param int set_row_window_command: Command used so set the start and end rows to update
 
 118         :param int set_current_column_command: Command used to set the current column location
 
 119         :param int set_current_row_command: Command used to set the current row location
 
 120         :param int write_black_ram_command: Command used to write pixels values into the update
 
 122         :param bool black_bits_inverted: True if 0 bits are used to show black pixels. Otherwise,
 
 123             1 means to show black.
 
 124         :param int write_color_ram_command: Command used to write pixels values into the update
 
 126         :param bool color_bits_inverted: True if 0 bits are used to show the color. Otherwise, 1
 
 128         :param int highlight_color: RGB888 of source color to highlight with third ePaper color.
 
 129         :param int refresh_display_command: Command used to start a display refresh. Single int
 
 130             or byte-packed command sequence
 
 131         :param float refresh_time: Time it takes to refresh the display before the stop_sequence
 
 132             should be sent. Ignored when busy_pin is provided.
 
 133         :param microcontroller.Pin busy_pin: Pin used to signify the display is busy
 
 134         :param bool busy_state: State of the busy pin when the display is busy
 
 135         :param float seconds_per_frame: Minimum number of seconds between screen refreshes
 
 136         :param bool always_toggle_chip_select: When True, chip select is toggled every byte
 
 137         :param bool grayscale: When true, the color ram is the low bit of 2-bit grayscale
 
 138         :param bool advanced_color_epaper: When true, the display is a 7-color advanced color
 
 140         :param bool two_byte_sequence_length: When true, use two bytes to define sequence length
 
 141         :param float start_up_time: Time to wait after reset before sending commands
 
 142         :param bool address_little_endian: Send the least significant byte (not bit) of
 
 143             multi-byte addresses first. Ignored when ram is addressed with one byte
 
 146         if isinstance(refresh_display_command, int):
 
 147             refresh_sequence = bytearray([refresh_display_command, 0])
 
 148             if two_byte_sequence_length:
 
 149                 refresh_sequence += bytes([0])
 
 150         elif isinstance(refresh_display_command, ReadableBuffer):
 
 151             refresh_sequence = bytearray(refresh_display_command)
 
 153             raise ValueError("Invalid refresh_display_command")
 
 155         if write_color_ram_command is None:
 
 156             write_color_ram_command = NO_COMMAND
 
 158         if rotation % 90 != 0:
 
 159             raise ValueError("Display rotation must be in 90 degree increments")
 
 161         self._refreshing = False
 
 163         core_grayscale = True
 
 165         if advanced_color_epaper:
 
 168             core_grayscale = False
 
 170         self._core = _DisplayCore(
 
 175             ram_height=ram_height,
 
 179             color_depth=color_depth,
 
 180             grayscale=core_grayscale,
 
 181             pixels_in_byte_share_row=True,
 
 183             reverse_pixels_in_byte=True,
 
 184             reverse_bytes_in_word=True,
 
 185             column_command=set_column_window_command,
 
 186             row_command=set_row_window_command,
 
 187             set_current_column_command=set_current_column_command,
 
 188             set_current_row_command=set_current_row_command,
 
 189             data_as_commands=False,
 
 190             always_toggle_chip_select=always_toggle_chip_select,
 
 191             sh1107_addressing=False,
 
 192             address_little_endian=address_little_endian,
 
 195         if highlight_color != 0x000000:
 
 196             self._core.colorspace.tricolor = True
 
 197             self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
 
 200             self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
 
 204             self._core.colorspace.tricolor = False
 
 206         self._acep = advanced_color_epaper
 
 207         self._core.colorspace.sevencolor = advanced_color_epaper
 
 208         self._write_black_ram_command = write_black_ram_command
 
 209         self._black_bits_inverted = black_bits_inverted
 
 210         self._write_color_ram_command = write_color_ram_command
 
 211         self._color_bits_inverted = color_bits_inverted
 
 212         self._refresh_time_ms = refresh_time * 1000
 
 213         self._busy_state = busy_state
 
 214         self._milliseconds_per_frame = seconds_per_frame * 1000
 
 215         self._chip_select = (
 
 216             CHIP_SELECT_TOGGLE_EVERY_BYTE
 
 217             if always_toggle_chip_select
 
 218             else CHIP_SELECT_UNTOUCHED
 
 220         self._grayscale = grayscale
 
 222         self._start_sequence = start_sequence
 
 223         self._start_up_time = start_up_time
 
 224         self._stop_sequence = stop_sequence
 
 225         self._refesh_sequence = refresh_sequence
 
 227         self._two_byte_sequence_length = two_byte_sequence_length
 
 228         if busy_pin is not None:
 
 229             self._busy = DigitalInOut(busy_pin)
 
 230             self._busy.switch_to_input()
 
 232         # Clear the color memory if it isn't in use
 
 233         if highlight_color == 0x00 and write_color_ram_command != NO_COMMAND:
 
 236         self.show(circuitpython_splash)
 
 238     def __new__(cls, *args, **kwargs):
 
 239         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
 
 243         display_instance = super().__new__(cls)
 
 244         display_instance._refreshing = False
 
 245         allocate_display(display_instance)
 
 246         return display_instance
 
 248     def show(self, group: Group) -> None:
 
 249         # pylint: disable=unnecessary-pass
 
 250         """Switches to displaying the given group of layers. When group is None, the default
 
 251         CircuitPython terminal will be shown (eventually).
 
 254             group = circuitpython_splash
 
 255         self._core.set_root_group(group)
 
 257     def update_refresh_mode(
 
 258         self, start_sequence: ReadableBuffer, seconds_per_frame: float
 
 260         """Updates the ``start_sequence`` and ``seconds_per_frame`` parameters to enable
 
 261         varying the refresh mode of the display."""
 
 262         self._start_sequence = bytearray(start_sequence)
 
 263         self._milliseconds_per_frame = seconds_per_frame * 1000
 
 265     def refresh(self) -> None:
 
 266         """Refreshes the display immediately or raises an exception if too soon. Use
 
 267         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
 
 269         if not self._refresh():
 
 270             raise RuntimeError("Refresh too soon")
 
 272     def _refresh(self) -> bool:
 
 273         if self._refreshing and self._busy is not None:
 
 274             if self._busy.value != self._busy_state:
 
 275                 self._refreshing = False
 
 276                 self._send_command_sequence(False, self._stop_sequence)
 
 279         if self._core.current_group is None:
 
 281         # Refresh at seconds per frame rate
 
 282         if self.time_to_refresh > 0:
 
 285         if not self._core.bus_free():
 
 286             # Can't acquire display bus; skip updating this display. Try next display
 
 289         areas_to_refresh = self._get_refresh_areas()
 
 290         if not areas_to_refresh:
 
 294             self._start_refresh()
 
 296             self._finish_refresh()
 
 297             while self._refreshing:
 
 298                 # TODO: Add something here that can change self._refreshing
 
 301         self._start_refresh()
 
 302         for area in areas_to_refresh:
 
 303             self._refresh_area(area)
 
 304         self._core.finish_refresh()
 
 308     def _release(self) -> None:
 
 309         """Release the display and free its resources"""
 
 311             self._wait_for_busy()
 
 312             self._refreshing = False
 
 313             # Run stop sequence but don't wait for busy because busy is set when sleeping
 
 314             self._send_command_sequence(False, self._stop_sequence)
 
 315         self._core.release_display_core()
 
 316         if self._busy is not None:
 
 319     def _background(self) -> None:
 
 320         """Run background refresh tasks."""
 
 323             if self._busy is not None:
 
 324                 busy = self._busy.value
 
 325                 refresh_done = busy == self._busy_state
 
 328                     time.monotonic() * 1000 - self._core.last_refresh
 
 332                 self._refreshing = False
 
 333                 self._finish_refresh()
 
 334                 # Run stop sequence but don't wait for busy because busy is set when sleeping
 
 335                 self._send_command_sequence(False, self._stop_sequence)
 
 337     def _get_refresh_areas(self) -> list[Area]:
 
 338         """Get a list of areas to be refreshed"""
 
 340         if self._core.full_refresh:
 
 341             areas.append(self._core.area)
 
 344         if self._core.current_group is not None:
 
 345             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
 
 348             first_area = areas[0]
 
 349         if first_area is not None and self._core.row_command == NO_COMMAND:
 
 350             # Do a full refresh if the display doesn't support partial updates
 
 351             areas = [self._core.area]
 
 354     def _refresh_area(self, area: Area) -> bool:
 
 355         """Redraw the area."""
 
 356         # pylint: disable=too-many-locals, too-many-branches
 
 359         # Clip the area to the display by overlapping the areas.
 
 360         # If there is no overlap then we're done.
 
 361         if not self._core.clip_area(area, clipped):
 
 365         rows_per_buffer = clipped.height()
 
 366         pixels_per_word = 32 // self._core.colorspace.depth
 
 367         pixels_per_buffer = clipped.size()
 
 369         # We should have lots of memory
 
 370         buffer_size = clipped.size() // pixels_per_word
 
 372         if clipped.size() > buffer_size * pixels_per_word:
 
 373             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
 
 374             if rows_per_buffer == 0:
 
 376             subrectangles = clipped.height() // rows_per_buffer
 
 377             if clipped.height() % rows_per_buffer != 0:
 
 379             pixels_per_buffer = rows_per_buffer * clipped.width()
 
 380             buffer_size = pixels_per_buffer // pixels_per_word
 
 381             if pixels_per_buffer % pixels_per_word:
 
 384         mask_length = (pixels_per_buffer // 8) + 1  # 1 bit per pixel + 1
 
 387         if self._write_color_ram_command != NO_COMMAND:
 
 389         for pass_index in range(passes):
 
 390             remaining_rows = clipped.height()
 
 391             if self._core.row_command != NO_COMMAND:
 
 392                 self._core.set_region_to_update(clipped)
 
 394             write_command = self._write_black_ram_command
 
 396                 write_command = self._write_color_ram_command
 
 398             self._core.begin_transaction()
 
 399             self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
 
 400             self._core.end_transaction()
 
 402             for subrect_index in range(subrectangles):
 
 405                     clipped.y1 + rows_per_buffer * subrect_index,
 
 407                     clipped.y1 + rows_per_buffer * (subrect_index + 1),
 
 409                 if remaining_rows < rows_per_buffer:
 
 410                     subrectangle.y2 = subrectangle.y1 + remaining_rows
 
 411                 remaining_rows -= rows_per_buffer
 
 413                 subrectangle_size_bytes = subrectangle.size() // (
 
 414                     8 // self._core.colorspace.depth
 
 417                 buffer = memoryview(bytearray([0] * (buffer_size * 4)))
 
 418                 mask = memoryview(bytearray([0] * mask_length))
 
 421                     self._core.colorspace.grayscale = True
 
 422                     self._core.colorspace.grayscale_bit = 7
 
 424                     if self._grayscale:  # 4-color grayscale
 
 425                         self._core.colorspace.grayscale_bit = 6
 
 426                         self._core.fill_area(subrectangle, mask, buffer)
 
 427                     elif self._core.colorspace.tricolor:
 
 428                         self._core.colorspace.grayscale = False
 
 429                         self._core.fill_area(subrectangle, mask, buffer)
 
 430                     elif self._core.colorspace.sevencolor:
 
 431                         self._core.fill_area(subrectangle, mask, buffer)
 
 433                     self._core.fill_area(subrectangle, mask, buffer)
 
 436                 if (pass_index == 1 and self._color_bits_inverted) or (
 
 437                     pass_index == 0 and self._black_bits_inverted
 
 439                     for i, _ in enumerate(buffer):
 
 440                         buffer[i] = ~buffer[i]
 
 442                 if not self._core.begin_transaction():
 
 443                     # Can't acquire display bus; skip the rest of the data. Try next display.
 
 446                     DISPLAY_DATA, self._chip_select, buffer[:subrectangle_size_bytes]
 
 448                 self._core.end_transaction()
 
 451     def _send_command_sequence(
 
 452         self, should_wait_for_busy: bool, sequence: ReadableBuffer
 
 455         while i < len(sequence):
 
 456             command = sequence[i]
 
 457             data_size = sequence[i + 1]
 
 458             delay = (data_size & DELAY) != 0
 
 460             data = sequence[i + 2 : i + 2 + data_size]
 
 461             if self._two_byte_sequence_length:
 
 462                 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
 
 463                 data = sequence[i + 3 : i + 3 + data_size]
 
 465             self._core.begin_transaction()
 
 467                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
 
 471                 CHIP_SELECT_UNTOUCHED,
 
 474             self._core.end_transaction()
 
 478                 delay_time_ms = sequence[i + 1 + data_size]
 
 479                 if delay_time_ms == 255:
 
 481             time.sleep(delay_time_ms / 1000)
 
 482             if should_wait_for_busy:
 
 483                 self._wait_for_busy()
 
 485             if self._two_byte_sequence_length:
 
 488     def _start_refresh(self) -> None:
 
 490         self._core._bus_reset()  # pylint: disable=protected-access
 
 491         time.sleep(self._start_up_time)
 
 492         self._send_command_sequence(True, self._start_sequence)
 
 493         self._core.start_refresh()
 
 495     def _finish_refresh(self) -> None:
 
 496         # Actually refresh the display now that all pixel RAM has been updated
 
 497         self._send_command_sequence(False, self._refesh_sequence)
 
 498         self._refreshing = True
 
 499         self._core.finish_refresh()
 
 501     def _wait_for_busy(self) -> None:
 
 502         if self._busy is not None:
 
 503             while self._busy.value != self._busy_state:
 
 506     def _clean_area(self) -> bool:
 
 507         width = self._core.width
 
 508         height = self._core.height
 
 510         buffer = bytearray([0x77] * (width // 2))
 
 511         self._core.begin_transaction()
 
 513             DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
 
 515         self._core.end_transaction()
 
 516         for _ in range(height):
 
 517             if not self._core.begin_transaction():
 
 519             self._core.send(DISPLAY_DATA, self._chip_select, buffer)
 
 520             self._core.end_transaction()
 
 524     def rotation(self) -> int:
 
 525         """The rotation of the display as an int in degrees"""
 
 526         return self._core.rotation
 
 529     def rotation(self, value: int) -> None:
 
 531             raise ValueError("Display rotation must be in 90 degree increments")
 
 532         transposed = self._core.rotation in (90, 270)
 
 533         will_transposed = value in (90, 270)
 
 534         if transposed != will_transposed:
 
 535             self._core.width, self._core.height = self._core.height, self._core.width
 
 536         self._core.set_rotation(value)
 
 539     def time_to_refresh(self) -> float:
 
 540         """Time, in fractional seconds, until the ePaper display can be refreshed."""
 
 541         if self._core.last_refresh == 0:
 
 544         # Refresh at seconds per frame rate
 
 545         elapsed_time = time.monotonic() * 1000 - self._core.last_refresh
 
 546         if elapsed_time > self._milliseconds_per_frame:
 
 548         return self._milliseconds_per_frame - elapsed_time
 
 551     def busy(self) -> bool:
 
 552         """True when the display is refreshing. This uses the ``busy_pin`` when available or the
 
 553         ``refresh_time`` otherwise."""
 
 554         return self._refreshing
 
 557     def width(self) -> int:
 
 559         return self._core.width
 
 562     def height(self) -> int:
 
 564         return self._core.height
 
 567     def bus(self) -> _DisplayBus:
 
 568         """Current Display Bus"""
 
 569         return self._core.get_bus()
 
 572     def root_group(self) -> Group:
 
 573         """The root group on the epaper display.
 
 574         If the root group is set to ``None``, no output will be shown.
 
 576         return self._core.root_group
 
 579     def root_group(self, new_group: Group) -> None:
 
 580         self._core.root_group = new_group