]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_epaperdisplay.py
Add root_group property to display
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _epaperdisplay.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.epaperdisplay`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 import time
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,
33     DISPLAY_COMMAND,
34     DISPLAY_DATA,
35     NO_COMMAND,
36     DELAY,
37 )
38
39 __version__ = "0.0.0+auto.0"
40 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
41
42
43 class EPaperDisplay:
44     # pylint: disable=too-many-instance-attributes, too-many-statements
45     """Manage updating an epaper display over a display bus
46
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
50     the display itself.
51
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.
54     """
55
56     def __init__(
57         self,
58         display_bus: _DisplayBus,
59         start_sequence: ReadableBuffer,
60         stop_sequence: ReadableBuffer,
61         *,
62         width: int,
63         height: int,
64         ram_width: int,
65         ram_height: int,
66         colstart: int = 0,
67         rowstart: int = 0,
68         rotation: int = 0,
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,
89     ) -> None:
90         # pylint: disable=too-many-locals
91         """Create a EPaperDisplay object on the given display bus (`displayio.FourWire` or
92         `paralleldisplay.ParallelBus`).
93
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
101         definition.
102
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
116             to update
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
121             region
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
125             region
126         :param bool color_bits_inverted: True if 0 bits are used to show the color. Otherwise, 1
127             means to show color.
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
139             epaper (ACeP)
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
144         """
145
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)
152         else:
153             raise ValueError("Invalid refresh_display_command")
154
155         if write_color_ram_command is None:
156             write_color_ram_command = NO_COMMAND
157
158         if rotation % 90 != 0:
159             raise ValueError("Display rotation must be in 90 degree increments")
160
161         self._refreshing = False
162         color_depth = 1
163         core_grayscale = True
164
165         if advanced_color_epaper:
166             color_depth = 4
167             grayscale = False
168             core_grayscale = False
169
170         self._core = _DisplayCore(
171             bus=display_bus,
172             width=width,
173             height=height,
174             ram_width=ram_width,
175             ram_height=ram_height,
176             colstart=colstart,
177             rowstart=rowstart,
178             rotation=rotation,
179             color_depth=color_depth,
180             grayscale=core_grayscale,
181             pixels_in_byte_share_row=True,
182             bytes_per_cell=1,
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,
193         )
194
195         if highlight_color != 0x000000:
196             self._core.colorspace.tricolor = True
197             self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
198                 highlight_color
199             )
200             self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
201                 highlight_color
202             )
203         else:
204             self._core.colorspace.tricolor = False
205
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
219         )
220         self._grayscale = grayscale
221
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
226         self._busy = None
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()
231
232         self._ticks_disabled = False
233
234         # Clear the color memory if it isn't in use
235         if highlight_color == 0x00 and write_color_ram_command != NO_COMMAND:
236             """TODO: Clear"""
237
238         self.show(circuitpython_splash)
239
240     def __new__(cls, *args, **kwargs):
241         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
242             allocate_display,
243         )
244
245         display_instance = super().__new__(cls)
246         allocate_display(display_instance)
247         return display_instance
248
249     def show(self, group: Group) -> None:
250         # pylint: disable=unnecessary-pass
251         """
252         .. note:: `show()` is deprecated and will be removed when CircuitPython 9.0.0
253         is released. Use ``.root_group = group`` instead.
254
255         Switches to displaying the given group of layers. When group is None, the default
256         CircuitPython terminal will be shown (eventually).
257         """
258         if group is None:
259             group = circuitpython_splash
260         self._core.set_root_group(group)
261
262     def _set_root_group(self, root_group: Group) -> None:
263         ok = self._core.set_root_group(root_group)
264         if not ok:
265             raise ValueError("Group already used")
266
267     def update_refresh_mode(
268         self, start_sequence: ReadableBuffer, seconds_per_frame: float
269     ) -> None:
270         """Updates the ``start_sequence`` and ``seconds_per_frame`` parameters to enable
271         varying the refresh mode of the display."""
272         self._start_sequence = bytearray(start_sequence)
273         self._milliseconds_per_frame = seconds_per_frame * 1000
274
275     def refresh(self) -> None:
276         """Refreshes the display immediately or raises an exception if too soon. Use
277         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
278         """
279         if not self._refresh():
280             raise RuntimeError("Refresh too soon")
281
282     def _refresh(self) -> bool:
283         if self._refreshing and self._busy is not None:
284             if self._busy.value != self._busy_state:
285                 self._ticks_disabled = True
286                 self._refreshing = False
287                 self._send_command_sequence(False, self._stop_sequence)
288             else:
289                 return False
290         if self._core.current_group is None:
291             return True
292         # Refresh at seconds per frame rate
293         if self.time_to_refresh > 0:
294             return False
295
296         if not self._core.bus_free():
297             # Can't acquire display bus; skip updating this display. Try next display
298             return False
299
300         areas_to_refresh = self._get_refresh_areas()
301         if not areas_to_refresh:
302             return True
303
304         if self._acep:
305             self._start_refresh()
306             self._clean_area()
307             self._finish_refresh()
308             while self._refreshing:
309                 # TODO: Add something here that can change self._refreshing
310                 # or add something in _background()
311                 pass
312
313         self._start_refresh()
314         for area in areas_to_refresh:
315             self._refresh_area(area)
316         self._finish_refresh()
317
318         return True
319
320     def _release(self) -> None:
321         """Release the display and free its resources"""
322         if self._refreshing:
323             self._wait_for_busy()
324             self._ticks_disabled = True
325             self._refreshing = False
326             # Run stop sequence but don't wait for busy because busy is set when sleeping
327             self._send_command_sequence(False, self._stop_sequence)
328         self._core.release_display_core()
329         if self._busy is not None:
330             self._busy.deinit()
331
332     def _background(self) -> None:
333         """Run background refresh tasks."""
334
335         # Wait until initialized
336         if not hasattr(self, "_core"):
337             return
338
339         if self._ticks_disabled:
340             return
341
342         if self._refreshing:
343             refresh_done = False
344             if self._busy is not None:
345                 busy = self._busy.value
346                 refresh_done = busy == self._busy_state
347             else:
348                 refresh_done = (
349                     time.monotonic() * 1000 - self._core.last_refresh
350                     > self._refresh_time
351                 )
352
353             if refresh_done:
354                 self._ticks_disabled = True
355                 self._refreshing = False
356                 # Run stop sequence but don't wait for busy because busy is set when sleeping
357                 self._send_command_sequence(False, self._stop_sequence)
358
359     def _get_refresh_areas(self) -> list[Area]:
360         """Get a list of areas to be refreshed"""
361         areas = []
362         if self._core.full_refresh:
363             areas.append(self._core.area)
364             return areas
365         first_area = None
366         if self._core.current_group is not None:
367             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
368                 areas
369             )
370             first_area = areas[0]
371         if first_area is not None and self._core.row_command == NO_COMMAND:
372             # Do a full refresh if the display doesn't support partial updates
373             areas = [self._core.area]
374         return areas
375
376     def _refresh_area(self, area: Area) -> bool:
377         """Redraw the area."""
378         # pylint: disable=too-many-locals, too-many-branches
379         clipped = Area()
380         # Clip the area to the display by overlapping the areas.
381         # If there is no overlap then we're done.
382         if not self._core.clip_area(area, clipped):
383             return True
384         subrectangles = 1
385         rows_per_buffer = clipped.height()
386         pixels_per_word = 32 // self._core.colorspace.depth
387         pixels_per_buffer = clipped.size()
388
389         # We should have lots of memory
390         buffer_size = clipped.size() // pixels_per_word
391
392         if clipped.size() > buffer_size * pixels_per_word:
393             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
394             if rows_per_buffer == 0:
395                 rows_per_buffer = 1
396             subrectangles = clipped.height() // rows_per_buffer
397             if clipped.height() % rows_per_buffer != 0:
398                 subrectangles += 1
399             pixels_per_buffer = rows_per_buffer * clipped.width()
400             buffer_size = pixels_per_buffer // pixels_per_word
401             if pixels_per_buffer % pixels_per_word:
402                 buffer_size += 1
403
404         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
405
406         passes = 1
407         if self._write_color_ram_command != NO_COMMAND:
408             passes = 2
409         for pass_index in range(passes):
410             remaining_rows = clipped.height()
411             if self._core.row_command != NO_COMMAND:
412                 self._core.set_region_to_update(clipped)
413
414             write_command = self._write_black_ram_command
415             if pass_index == 1:
416                 write_command = self._write_color_ram_command
417
418             self._core.begin_transaction()
419             self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
420             self._core.end_transaction()
421
422             for subrect_index in range(subrectangles):
423                 subrectangle = Area(
424                     x1=clipped.x1,
425                     y1=clipped.y1 + rows_per_buffer * subrect_index,
426                     x2=clipped.x2,
427                     y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
428                 )
429                 if remaining_rows < rows_per_buffer:
430                     subrectangle.y2 = subrectangle.y1 + remaining_rows
431                 remaining_rows -= rows_per_buffer
432
433                 subrectangle_size_bytes = subrectangle.size() // (
434                     8 // self._core.colorspace.depth
435                 )
436
437                 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
438                 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
439
440                 if not self._acep:
441                     self._core.colorspace.grayscale = True
442                     self._core.colorspace.grayscale_bit = 7
443                 if pass_index == 1:
444                     if self._grayscale:  # 4-color grayscale
445                         self._core.colorspace.grayscale_bit = 6
446                         self._core.fill_area(subrectangle, mask, buffer)
447                     elif self._core.colorspace.tricolor:
448                         self._core.colorspace.grayscale = False
449                         self._core.fill_area(subrectangle, mask, buffer)
450                     elif self._core.colorspace.sevencolor:
451                         self._core.fill_area(subrectangle, mask, buffer)
452                 else:
453                     self._core.fill_area(subrectangle, mask, buffer)
454
455                 # Invert it all
456                 if (pass_index == 1 and self._color_bits_inverted) or (
457                     pass_index == 0 and self._black_bits_inverted
458                 ):
459                     for i in range(buffer_size):
460                         buffer[i] = ~buffer[i]
461
462                 if not self._core.begin_transaction():
463                     # Can't acquire display bus; skip the rest of the data. Try next display.
464                     return False
465                 self._core.send(
466                     DISPLAY_DATA,
467                     self._chip_select,
468                     buffer.tobytes()[:subrectangle_size_bytes],
469                 )
470                 self._core.end_transaction()
471         return True
472
473     def _send_command_sequence(
474         self, should_wait_for_busy: bool, sequence: ReadableBuffer
475     ) -> None:
476         i = 0
477         while i < len(sequence):
478             command = sequence[i]
479             data_size = sequence[i + 1]
480             delay = (data_size & DELAY) != 0
481             data_size &= ~DELAY
482             data = sequence[i + 2 : i + 2 + data_size]
483             if self._two_byte_sequence_length:
484                 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
485                 data = sequence[i + 3 : i + 3 + data_size]
486
487             self._core.begin_transaction()
488             self._core.send(
489                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
490             )
491             if data_size > 0:
492                 self._core.send(
493                     DISPLAY_DATA,
494                     CHIP_SELECT_UNTOUCHED,
495                     bytes(data),
496                 )
497             self._core.end_transaction()
498             delay_time_ms = 0
499             if delay:
500                 data_size += 1
501                 delay_time_ms = sequence[i + 1 + data_size]
502                 if delay_time_ms == 255:
503                     delay_time_ms = 500
504             time.sleep(delay_time_ms / 1000)
505             if should_wait_for_busy:
506                 self._wait_for_busy()
507             i += 2 + data_size
508             if self._two_byte_sequence_length:
509                 i += 1
510
511     def _start_refresh(self) -> None:
512         # Run start sequence
513         self._core._bus_reset()  # pylint: disable=protected-access
514         time.sleep(self._start_up_time)
515         self._send_command_sequence(True, self._start_sequence)
516         self._core.start_refresh()
517
518     def _finish_refresh(self) -> None:
519         # Actually refresh the display now that all pixel RAM has been updated
520         self._send_command_sequence(False, self._refresh_sequence)
521         self._ticks_disabled = False
522         self._refreshing = True
523         self._core.finish_refresh()
524
525     def _wait_for_busy(self) -> None:
526         if self._busy is not None:
527             while self._busy.value == self._busy_state:
528                 time.sleep(0.001)
529
530     def _clean_area(self) -> bool:
531         width = self._core.width
532         height = self._core.height
533
534         buffer = bytearray([0x77] * (width // 2))
535         self._core.begin_transaction()
536         self._core.send(
537             DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
538         )
539         self._core.end_transaction()
540         for _ in range(height):
541             if not self._core.begin_transaction():
542                 return False
543             self._core.send(DISPLAY_DATA, self._chip_select, buffer)
544             self._core.end_transaction()
545         return True
546
547     @property
548     def rotation(self) -> int:
549         """The rotation of the display as an int in degrees"""
550         return self._core.rotation
551
552     @rotation.setter
553     def rotation(self, value: int) -> None:
554         if value % 90 != 0:
555             raise ValueError("Display rotation must be in 90 degree increments")
556         transposed = self._core.rotation in (90, 270)
557         will_transposed = value in (90, 270)
558         if transposed != will_transposed:
559             self._core.width, self._core.height = self._core.height, self._core.width
560         self._core.set_rotation(value)
561         if self._core.current_group is not None:
562             self._core.current_group._update_transform(  # pylint: disable=protected-access
563                 self._core.transform
564             )
565
566     @property
567     def time_to_refresh(self) -> float:
568         """Time, in fractional seconds, until the ePaper display can be refreshed."""
569         if self._core.last_refresh == 0:
570             return 0
571
572         # Refresh at seconds per frame rate
573         elapsed_time = time.monotonic() * 1000 - self._core.last_refresh
574         if elapsed_time > self._milliseconds_per_frame:
575             return 0
576         return self._milliseconds_per_frame - elapsed_time
577
578     @property
579     def busy(self) -> bool:
580         """True when the display is refreshing. This uses the ``busy_pin`` when available or the
581         ``refresh_time`` otherwise."""
582         return self._refreshing
583
584     @property
585     def width(self) -> int:
586         """Display Width"""
587         return self._core.width
588
589     @property
590     def height(self) -> int:
591         """Display Height"""
592         return self._core.height
593
594     @property
595     def bus(self) -> _DisplayBus:
596         """Current Display Bus"""
597         return self._core.get_bus()
598
599     @property
600     def root_group(self) -> Group:
601         """The root group on the epaper display.
602         If the root group is set to ``None``, no output will be shown.
603         """
604         return self._core.current_group
605
606     @root_group.setter
607     def root_group(self, new_group: Group) -> None:
608         self._set_root_group(new_group)