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 displayio._displaycore import _DisplayCore
26 from displayio._group import Group, circuitpython_splash
27 from displayio._colorconverter import ColorConverter
28 from busdisplay._displaybus import _DisplayBus
29 from displayio._area import Area
30 from displayio._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,
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._refresh_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 self._ticks_disabled = False
234 # Clear the color memory if it isn't in use
235 if highlight_color == 0x00 and write_color_ram_command != NO_COMMAND:
238 self._set_root_group(circuitpython_splash)
240 def __new__(cls, *args, **kwargs):
241 from ..displayio import ( # pylint: disable=import-outside-toplevel, cyclic-import
245 display_instance = super().__new__(cls)
246 allocate_display(display_instance)
247 return display_instance
249 def show(self, _group: Group) -> None: # pylint: disable=missing-function-docstring
250 raise AttributeError(".show(x) removed. Use .root_group = x")
252 def _set_root_group(self, root_group: Group) -> None:
253 ok = self._core.set_root_group(root_group)
255 raise ValueError("Group already used")
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._ticks_disabled = True
276 self._refreshing = False
277 self._send_command_sequence(False, self._stop_sequence)
280 if self._core.current_group is None:
282 # Refresh at seconds per frame rate
283 if self.time_to_refresh > 0:
286 if not self._core.bus_free():
287 # Can't acquire display bus; skip updating this display. Try next display
290 areas_to_refresh = self._get_refresh_areas()
291 if not areas_to_refresh:
295 self._start_refresh()
297 self._finish_refresh()
298 while self._refreshing:
299 # TODO: Add something here that can change self._refreshing
300 # or add something in _background()
303 self._start_refresh()
304 for area in areas_to_refresh:
305 self._refresh_area(area)
306 self._finish_refresh()
310 def _release(self) -> None:
311 """Release the display and free its resources"""
313 self._wait_for_busy()
314 self._ticks_disabled = True
315 self._refreshing = False
316 # Run stop sequence but don't wait for busy because busy is set when sleeping
317 self._send_command_sequence(False, self._stop_sequence)
318 self._core.release_display_core()
319 if self._busy is not None:
322 def _background(self) -> None:
323 """Run background refresh tasks."""
325 # Wait until initialized
326 if not hasattr(self, "_core"):
329 if self._ticks_disabled:
334 if self._busy is not None:
335 busy = self._busy.value
336 refresh_done = busy == self._busy_state
339 time.monotonic() * 1000 - self._core.last_refresh
344 self._ticks_disabled = True
345 self._refreshing = False
346 # Run stop sequence but don't wait for busy because busy is set when sleeping
347 self._send_command_sequence(False, self._stop_sequence)
349 def _get_refresh_areas(self) -> list[Area]:
350 """Get a list of areas to be refreshed"""
352 if self._core.full_refresh:
353 areas.append(self._core.area)
356 if self._core.current_group is not None:
357 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
360 first_area = areas[0]
361 if first_area is not None and self._core.row_command == NO_COMMAND:
362 # Do a full refresh if the display doesn't support partial updates
363 areas = [self._core.area]
366 def _refresh_area(self, area: Area) -> bool:
367 """Redraw the area."""
368 # pylint: disable=too-many-locals, too-many-branches
370 # Clip the area to the display by overlapping the areas.
371 # If there is no overlap then we're done.
372 if not self._core.clip_area(area, clipped):
375 rows_per_buffer = clipped.height()
376 pixels_per_word = 32 // self._core.colorspace.depth
377 pixels_per_buffer = clipped.size()
379 # We should have lots of memory
380 buffer_size = clipped.size() // pixels_per_word
382 if clipped.size() > buffer_size * pixels_per_word:
383 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
384 if rows_per_buffer == 0:
386 subrectangles = clipped.height() // rows_per_buffer
387 if clipped.height() % rows_per_buffer != 0:
389 pixels_per_buffer = rows_per_buffer * clipped.width()
390 buffer_size = pixels_per_buffer // pixels_per_word
391 if pixels_per_buffer % pixels_per_word:
394 mask_length = (pixels_per_buffer // 32) + 1 # 1 bit per pixel + 1
397 if self._write_color_ram_command != NO_COMMAND:
399 for pass_index in range(passes):
400 remaining_rows = clipped.height()
401 if self._core.row_command != NO_COMMAND:
402 self._core.set_region_to_update(clipped)
404 write_command = self._write_black_ram_command
406 write_command = self._write_color_ram_command
408 self._core.begin_transaction()
409 self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
410 self._core.end_transaction()
412 for subrect_index in range(subrectangles):
415 y1=clipped.y1 + rows_per_buffer * subrect_index,
417 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
419 if remaining_rows < rows_per_buffer:
420 subrectangle.y2 = subrectangle.y1 + remaining_rows
421 remaining_rows -= rows_per_buffer
423 subrectangle_size_bytes = subrectangle.size() // (
424 8 // self._core.colorspace.depth
427 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
428 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
431 self._core.colorspace.grayscale = True
432 self._core.colorspace.grayscale_bit = 7
434 if self._grayscale: # 4-color grayscale
435 self._core.colorspace.grayscale_bit = 6
436 self._core.fill_area(subrectangle, mask, buffer)
437 elif self._core.colorspace.tricolor:
438 self._core.colorspace.grayscale = False
439 self._core.fill_area(subrectangle, mask, buffer)
440 elif self._core.colorspace.sevencolor:
441 self._core.fill_area(subrectangle, mask, buffer)
443 self._core.fill_area(subrectangle, mask, buffer)
446 if (pass_index == 1 and self._color_bits_inverted) or (
447 pass_index == 0 and self._black_bits_inverted
449 for i in range(buffer_size):
450 buffer[i] = ~buffer[i]
452 if not self._core.begin_transaction():
453 # Can't acquire display bus; skip the rest of the data. Try next display.
458 buffer.tobytes()[:subrectangle_size_bytes],
460 self._core.end_transaction()
463 def _send_command_sequence(
464 self, should_wait_for_busy: bool, sequence: ReadableBuffer
467 while i < len(sequence):
468 command = sequence[i]
469 data_size = sequence[i + 1]
470 delay = (data_size & DELAY) != 0
472 data = sequence[i + 2 : i + 2 + data_size]
473 if self._two_byte_sequence_length:
474 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
475 data = sequence[i + 3 : i + 3 + data_size]
477 self._core.begin_transaction()
479 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
484 CHIP_SELECT_UNTOUCHED,
487 self._core.end_transaction()
491 delay_time_ms = sequence[i + 1 + data_size]
492 if delay_time_ms == 255:
494 time.sleep(delay_time_ms / 1000)
495 if should_wait_for_busy:
496 self._wait_for_busy()
498 if self._two_byte_sequence_length:
501 def _start_refresh(self) -> None:
503 self._core._bus_reset() # pylint: disable=protected-access
504 time.sleep(self._start_up_time)
505 self._send_command_sequence(True, self._start_sequence)
506 self._core.start_refresh()
508 def _finish_refresh(self) -> None:
509 # Actually refresh the display now that all pixel RAM has been updated
510 self._send_command_sequence(False, self._refresh_sequence)
511 self._ticks_disabled = False
512 self._refreshing = True
513 self._core.finish_refresh()
515 def _wait_for_busy(self) -> None:
516 if self._busy is not None:
517 while self._busy.value == self._busy_state:
520 def _clean_area(self) -> bool:
521 width = self._core.width
522 height = self._core.height
524 buffer = bytearray([0x77] * (width // 2))
525 self._core.begin_transaction()
527 DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
529 self._core.end_transaction()
530 for _ in range(height):
531 if not self._core.begin_transaction():
533 self._core.send(DISPLAY_DATA, self._chip_select, buffer)
534 self._core.end_transaction()
538 def rotation(self) -> int:
539 """The rotation of the display as an int in degrees"""
540 return self._core.rotation
543 def rotation(self, value: int) -> None:
545 raise ValueError("Display rotation must be in 90 degree increments")
546 transposed = self._core.rotation in (90, 270)
547 will_transposed = value in (90, 270)
548 if transposed != will_transposed:
549 self._core.width, self._core.height = self._core.height, self._core.width
550 self._core.set_rotation(value)
551 if self._core.current_group is not None:
552 self._core.current_group._update_transform( # pylint: disable=protected-access
557 def time_to_refresh(self) -> float:
558 """Time, in fractional seconds, until the ePaper display can be refreshed."""
559 if self._core.last_refresh == 0:
562 # Refresh at seconds per frame rate
563 elapsed_time = time.monotonic() * 1000 - self._core.last_refresh
564 if elapsed_time > self._milliseconds_per_frame:
566 return self._milliseconds_per_frame - elapsed_time
569 def busy(self) -> bool:
570 """True when the display is refreshing. This uses the ``busy_pin`` when available or the
571 ``refresh_time`` otherwise."""
572 return self._refreshing
575 def width(self) -> int:
577 return self._core.width
580 def height(self) -> int:
582 return self._core.height
585 def bus(self) -> _DisplayBus:
586 """Current Display Bus"""
587 return self._core.get_bus()
590 def root_group(self) -> Group:
591 """The root group on the epaper display.
592 If the root group is set to ``None``, no output will be shown.
594 return self._core.current_group
597 def root_group(self, new_group: Group) -> None:
598 self._set_root_group(new_group)