1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
9 epaperdisplay for Blinka
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 busdisplay._displaybus import _DisplayBus
26 from displayio._displaycore import _DisplayCore
27 from displayio._group import Group, circuitpython_splash
28 from displayio._colorconverter import ColorConverter
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 (`fourwire.FourWire` or
92 `paralleldisplaybus.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: fourwire.FourWire or paralleldisplaybus.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
164 # Disable while initializing
165 self._ticks_disabled = True
167 if advanced_color_epaper:
170 core_grayscale = False
172 self._core = _DisplayCore(
177 ram_height=ram_height,
181 color_depth=color_depth,
182 grayscale=core_grayscale,
183 pixels_in_byte_share_row=True,
185 reverse_pixels_in_byte=True,
186 reverse_bytes_in_word=True,
187 column_command=set_column_window_command,
188 row_command=set_row_window_command,
189 set_current_column_command=set_current_column_command,
190 set_current_row_command=set_current_row_command,
191 data_as_commands=False,
192 always_toggle_chip_select=always_toggle_chip_select,
193 sh1107_addressing=False,
194 address_little_endian=address_little_endian,
197 if highlight_color != 0x000000:
198 self._core.colorspace.tricolor = True
199 self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
202 self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
206 self._core.colorspace.tricolor = False
208 self._acep = advanced_color_epaper
209 self._core.colorspace.sevencolor = advanced_color_epaper
210 self._write_black_ram_command = write_black_ram_command
211 self._black_bits_inverted = black_bits_inverted
212 self._write_color_ram_command = write_color_ram_command
213 self._color_bits_inverted = color_bits_inverted
214 self._refresh_time_ms = refresh_time * 1000
215 self._busy_state = busy_state
216 self._milliseconds_per_frame = seconds_per_frame * 1000
217 self._chip_select = (
218 CHIP_SELECT_TOGGLE_EVERY_BYTE
219 if always_toggle_chip_select
220 else CHIP_SELECT_UNTOUCHED
222 self._grayscale = grayscale
224 self._start_sequence = start_sequence
225 self._start_up_time = start_up_time
226 self._stop_sequence = stop_sequence
227 self._refresh_sequence = refresh_sequence
229 self._two_byte_sequence_length = two_byte_sequence_length
230 if busy_pin is not None:
231 self._busy = DigitalInOut(busy_pin)
232 self._busy.switch_to_input()
234 self._ticks_disabled = False
236 # Clear the color memory if it isn't in use
237 if highlight_color == 0x00 and write_color_ram_command != NO_COMMAND:
240 self._set_root_group(circuitpython_splash)
242 def __new__(cls, *args, **kwargs):
243 from displayio import ( # pylint: disable=import-outside-toplevel, cyclic-import
247 display_instance = super().__new__(cls)
248 allocate_display(display_instance)
249 return display_instance
252 def show(_group: Group) -> None: # pylint: disable=missing-function-docstring
253 raise AttributeError(".show(x) removed. Use .root_group = x")
255 def _set_root_group(self, root_group: Group) -> None:
256 ok = self._core.set_root_group(root_group)
258 raise ValueError("Group already used")
260 def update_refresh_mode(
261 self, start_sequence: ReadableBuffer, seconds_per_frame: float
263 """Updates the ``start_sequence`` and ``seconds_per_frame`` parameters to enable
264 varying the refresh mode of the display."""
265 self._start_sequence = bytearray(start_sequence)
266 self._milliseconds_per_frame = seconds_per_frame * 1000
268 def refresh(self) -> None:
269 """Refreshes the display immediately or raises an exception if too soon. Use
270 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
272 if not self._refresh():
273 raise RuntimeError("Refresh too soon")
275 def _refresh(self) -> bool:
276 if self._refreshing and self._busy is not None:
277 if self._busy.value != self._busy_state:
278 self._ticks_disabled = True
279 self._refreshing = False
280 self._send_command_sequence(False, self._stop_sequence)
283 if self._core.current_group is None:
285 # Refresh at seconds per frame rate
286 if self.time_to_refresh > 0:
289 if not self._core.bus_free():
290 # Can't acquire display bus; skip updating this display. Try next display
293 areas_to_refresh = self._get_refresh_areas()
294 if not areas_to_refresh:
298 self._start_refresh()
300 self._finish_refresh()
301 while self._refreshing:
302 # TODO: Add something here that can change self._refreshing
303 # or add something in _background()
306 self._start_refresh()
307 for area in areas_to_refresh:
308 self._refresh_area(area)
309 self._finish_refresh()
313 def _release(self) -> None:
314 """Release the display and free its resources"""
316 self._wait_for_busy()
317 self._ticks_disabled = True
318 self._refreshing = False
319 # Run stop sequence but don't wait for busy because busy is set when sleeping
320 self._send_command_sequence(False, self._stop_sequence)
321 self._core.release_display_core()
322 if self._busy is not None:
325 def _background(self) -> None:
326 """Run background refresh tasks."""
328 # Wait until initialized
329 if not hasattr(self, "_core"):
332 if self._ticks_disabled:
337 if self._busy is not None:
338 busy = self._busy.value
339 refresh_done = busy == self._busy_state
342 time.monotonic() * 1000 - self._core.last_refresh
347 self._ticks_disabled = True
348 self._refreshing = False
349 # Run stop sequence but don't wait for busy because busy is set when sleeping
350 self._send_command_sequence(False, self._stop_sequence)
352 def _get_refresh_areas(self) -> list[Area]:
353 """Get a list of areas to be refreshed"""
355 if self._core.full_refresh:
356 areas.append(self._core.area)
359 if self._core.current_group is not None:
360 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
363 first_area = areas[0]
364 if first_area is not None and self._core.row_command == NO_COMMAND:
365 # Do a full refresh if the display doesn't support partial updates
366 areas = [self._core.area]
369 def _refresh_area(self, area: Area) -> bool:
370 """Redraw the area."""
371 # pylint: disable=too-many-locals, too-many-branches
373 # Clip the area to the display by overlapping the areas.
374 # If there is no overlap then we're done.
375 if not self._core.clip_area(area, clipped):
378 rows_per_buffer = clipped.height()
379 pixels_per_word = 32 // self._core.colorspace.depth
380 pixels_per_buffer = clipped.size()
382 # We should have lots of memory
383 buffer_size = clipped.size() // pixels_per_word
385 if clipped.size() > buffer_size * pixels_per_word:
386 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
387 if rows_per_buffer == 0:
389 subrectangles = clipped.height() // rows_per_buffer
390 if clipped.height() % rows_per_buffer != 0:
392 pixels_per_buffer = rows_per_buffer * clipped.width()
393 buffer_size = pixels_per_buffer // pixels_per_word
394 if pixels_per_buffer % pixels_per_word:
397 mask_length = (pixels_per_buffer // 32) + 1 # 1 bit per pixel + 1
400 if self._write_color_ram_command != NO_COMMAND:
402 for pass_index in range(passes):
403 remaining_rows = clipped.height()
404 if self._core.row_command != NO_COMMAND:
405 self._core.set_region_to_update(clipped)
407 write_command = self._write_black_ram_command
409 write_command = self._write_color_ram_command
411 self._core.begin_transaction()
412 self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
413 self._core.end_transaction()
415 for subrect_index in range(subrectangles):
418 y1=clipped.y1 + rows_per_buffer * subrect_index,
420 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
422 if remaining_rows < rows_per_buffer:
423 subrectangle.y2 = subrectangle.y1 + remaining_rows
424 remaining_rows -= rows_per_buffer
426 subrectangle_size_bytes = subrectangle.size() // (
427 8 // self._core.colorspace.depth
430 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
431 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
434 self._core.colorspace.grayscale = True
435 self._core.colorspace.grayscale_bit = 7
437 if self._grayscale: # 4-color grayscale
438 self._core.colorspace.grayscale_bit = 6
439 self._core.fill_area(subrectangle, mask, buffer)
440 elif self._core.colorspace.tricolor:
441 self._core.colorspace.grayscale = False
442 self._core.fill_area(subrectangle, mask, buffer)
443 elif self._core.colorspace.sevencolor:
444 self._core.fill_area(subrectangle, mask, buffer)
446 self._core.fill_area(subrectangle, mask, buffer)
449 if (pass_index == 1 and self._color_bits_inverted) or (
450 pass_index == 0 and self._black_bits_inverted
452 for i in range(buffer_size):
453 buffer[i] = ~buffer[i]
455 if not self._core.begin_transaction():
456 # Can't acquire display bus; skip the rest of the data. Try next display.
461 buffer.tobytes()[:subrectangle_size_bytes],
463 self._core.end_transaction()
466 def _send_command_sequence(
467 self, should_wait_for_busy: bool, sequence: ReadableBuffer
470 while i < len(sequence):
471 command = sequence[i]
472 data_size = sequence[i + 1]
473 delay = (data_size & DELAY) != 0
475 data = sequence[i + 2 : i + 2 + data_size]
476 if self._two_byte_sequence_length:
477 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
478 data = sequence[i + 3 : i + 3 + data_size]
480 self._core.begin_transaction()
482 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
487 CHIP_SELECT_UNTOUCHED,
490 self._core.end_transaction()
494 delay_time_ms = sequence[i + 1 + data_size]
495 if delay_time_ms == 255:
497 time.sleep(delay_time_ms / 1000)
498 if should_wait_for_busy:
499 self._wait_for_busy()
501 if self._two_byte_sequence_length:
504 def _start_refresh(self) -> None:
506 self._core._bus_reset() # pylint: disable=protected-access
507 time.sleep(self._start_up_time)
508 self._send_command_sequence(True, self._start_sequence)
509 self._core.start_refresh()
511 def _finish_refresh(self) -> None:
512 # Actually refresh the display now that all pixel RAM has been updated
513 self._send_command_sequence(False, self._refresh_sequence)
514 self._ticks_disabled = False
515 self._refreshing = True
516 self._core.finish_refresh()
518 def _wait_for_busy(self) -> None:
519 if self._busy is not None:
520 while self._busy.value == self._busy_state:
523 def _clean_area(self) -> bool:
524 width = self._core.width
525 height = self._core.height
527 buffer = bytearray([0x77] * (width // 2))
528 self._core.begin_transaction()
530 DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
532 self._core.end_transaction()
533 for _ in range(height):
534 if not self._core.begin_transaction():
536 self._core.send(DISPLAY_DATA, self._chip_select, buffer)
537 self._core.end_transaction()
541 def rotation(self) -> int:
542 """The rotation of the display as an int in degrees"""
543 return self._core.rotation
546 def rotation(self, value: int) -> None:
548 raise ValueError("Display rotation must be in 90 degree increments")
549 transposed = self._core.rotation in (90, 270)
550 will_transposed = value in (90, 270)
551 if transposed != will_transposed:
552 self._core.width, self._core.height = self._core.height, self._core.width
553 self._core.set_rotation(value)
554 if self._core.current_group is not None:
555 self._core.current_group._update_transform( # pylint: disable=protected-access
560 def time_to_refresh(self) -> float:
561 """Time, in fractional seconds, until the ePaper display can be refreshed."""
562 if self._core.last_refresh == 0:
565 # Refresh at seconds per frame rate
566 elapsed_time = time.monotonic() * 1000 - self._core.last_refresh
567 if elapsed_time > self._milliseconds_per_frame:
569 return self._milliseconds_per_frame - elapsed_time
572 def busy(self) -> bool:
573 """True when the display is refreshing. This uses the ``busy_pin`` when available or the
574 ``refresh_time`` otherwise."""
575 return self._refreshing
578 def width(self) -> int:
580 return self._core.width
583 def height(self) -> int:
585 return self._core.height
588 def bus(self) -> _DisplayBus:
589 """Current Display Bus"""
590 return self._core.get_bus()
593 def root_group(self) -> Group:
594 """The root group on the epaper display.
595 If the root group is set to ``None``, no output will be shown.
597 return self._core.current_group
600 def root_group(self, new_group: Group) -> None:
601 self._set_root_group(new_group)