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
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")
162 core_grayscale = True
164 if advanced_color_epaper:
167 core_grayscale = False
169 self._core = _DisplayCore(
174 ram_height=ram_height,
178 color_depth=color_depth,
179 grayscale=core_grayscale,
180 pixels_in_byte_share_row=True,
182 reverse_pixels_in_byte=True,
183 reverse_bytes_in_word=True,
184 column_command=set_column_window_command,
185 row_command=set_row_window_command,
186 set_current_column_command=set_current_column_command,
187 set_current_row_command=set_current_row_command,
188 data_as_commands=False,
189 always_toggle_chip_select=always_toggle_chip_select,
190 sh1107_addressing=False,
191 address_little_endian=address_little_endian,
194 if highlight_color != 0x000000:
195 self._core.colorspace.tricolor = True
196 self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
199 self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
203 self._core.colorspace.tricolor = False
205 self._acep = advanced_color_epaper
206 self._core.colorspace.sevencolor = advanced_color_epaper
207 self._write_black_ram_command = write_black_ram_command
208 self._black_bits_inverted = black_bits_inverted
209 self._write_color_ram_command = write_color_ram_command
210 self._color_bits_inverted = color_bits_inverted
211 self._refresh_time = (
212 refresh_time # TODO: Verify we are using seconds instead of ms
214 self._busy_state = busy_state
215 self._refreshing = False
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 = (
226 start_up_time # TODO: Verify we are using seconds instead of ms
228 self._stop_sequence = stop_sequence
229 self._refesh_sequence = refresh_sequence
231 self._two_byte_sequence_length = two_byte_sequence_length
232 if busy_pin is not None:
233 self._busy = DigitalInOut(busy_pin)
234 self._busy.switch_to_input()
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.show(circuitpython_splash)
242 def __new__(cls, *args, **kwargs):
243 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
247 display_instance = super().__new__(cls)
248 allocate_display(display_instance)
249 return display_instance
251 def show(self, group: Group) -> None:
252 # pylint: disable=unnecessary-pass
253 """Switches to displaying the given group of layers. When group is None, the default
254 CircuitPython terminal will be shown (eventually).
257 group = circuitpython_splash
258 self._core.set_root_group(group)
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."""
267 def refresh(self) -> None:
268 # pylint: disable=unnecessary-pass
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.
274 def _get_refresh_areas(self) -> list[Area]:
275 """Get a list of areas to be refreshed"""
277 if self._core.full_refresh:
278 areas.append(self._core.area)
281 if self._core.current_group is not None:
282 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
285 first_area = areas[0]
286 if first_area is not None and self._core.row_command == NO_COMMAND:
287 # Do a full refresh if the display doesn't support partial updates
288 areas = [self._core.area]
291 def _send_command_sequence(
292 self, should_wait_for_busy: bool, sequence: ReadableBuffer
295 while i < len(sequence):
296 command = sequence[i]
297 data_size = sequence[i + 1]
298 delay = (data_size & DELAY) != 0
300 data = sequence[i + 2 : i + 2 + data_size]
301 if self._two_byte_sequence_length:
302 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
303 data = sequence[i + 3 : i + 3 + data_size]
305 self._core.begin_transaction()
307 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
311 CHIP_SELECT_UNTOUCHED,
314 self._core.end_transaction()
318 delay_time_ms = sequence[i + 1 + data_size]
319 if delay_time_ms == 255:
321 time.sleep(delay_time_ms / 1000)
322 if should_wait_for_busy:
323 self._wait_for_busy()
325 if self._two_byte_sequence_length:
328 def _finish_refresh(self) -> None:
329 self._send_command_sequence(False, self._refesh_sequence)
330 self._refreshing = True
331 self._core.finish_refresh()
333 def _wait_for_busy(self) -> None:
334 if self._busy is not None:
335 while self._busy.value != self._busy_state:
339 def rotation(self) -> int:
340 """The rotation of the display as an int in degrees"""
341 return self._core.rotation
344 def rotation(self, value: int) -> None:
346 raise ValueError("Display rotation must be in 90 degree increments")
347 transposed = self._core.rotation in (90, 270)
348 will_transposed = value in (90, 270)
349 if transposed != will_transposed:
350 self._core.width, self._core.height = self._core.height, self._core.width
351 self._core.set_rotation(value)
354 def time_to_refresh(self) -> float:
355 """Time, in fractional seconds, until the ePaper display can be refreshed."""
356 return 0.0 # TODO: Implement
359 def busy(self) -> bool:
360 """True when the display is refreshing. This uses the ``busy_pin`` when available or the
361 ``refresh_time`` otherwise."""
362 return self._refreshing
365 def width(self) -> int:
367 return self._core.width
370 def height(self) -> int:
372 return self._core.height
375 def bus(self) -> _DisplayBus:
376 """Current Display Bus"""
377 return self._core._bus
380 def root_group(self) -> Group:
381 """The root group on the epaper display.
382 If the root group is set to ``None``, no output will be shown.
384 return self._core.root_group
387 def root_group(self, new_group: Group) -> None:
388 self._core.root_group = new_group