]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - epaperdisplay/__init__.py
Update some doc links
[hackapet/Adafruit_Blinka_Displayio.git] / epaperdisplay / __init__.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `epaperdisplay`
7 ================================================================================
8
9 epaperdisplay 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 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,
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 (`fourwire.FourWire` or
92         `paralleldisplaybus.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: fourwire.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._set_root_group(circuitpython_splash)
239
240     def __new__(cls, *args, **kwargs):
241         from displayio 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     @staticmethod
250     def show(_group: Group) -> None:  # pylint: disable=missing-function-docstring
251         raise AttributeError(".show(x) removed. Use .root_group = x")
252
253     def _set_root_group(self, root_group: Group) -> None:
254         ok = self._core.set_root_group(root_group)
255         if not ok:
256             raise ValueError("Group already used")
257
258     def update_refresh_mode(
259         self, start_sequence: ReadableBuffer, seconds_per_frame: float
260     ) -> None:
261         """Updates the ``start_sequence`` and ``seconds_per_frame`` parameters to enable
262         varying the refresh mode of the display."""
263         self._start_sequence = bytearray(start_sequence)
264         self._milliseconds_per_frame = seconds_per_frame * 1000
265
266     def refresh(self) -> None:
267         """Refreshes the display immediately or raises an exception if too soon. Use
268         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
269         """
270         if not self._refresh():
271             raise RuntimeError("Refresh too soon")
272
273     def _refresh(self) -> bool:
274         if self._refreshing and self._busy is not None:
275             if self._busy.value != self._busy_state:
276                 self._ticks_disabled = True
277                 self._refreshing = False
278                 self._send_command_sequence(False, self._stop_sequence)
279             else:
280                 return False
281         if self._core.current_group is None:
282             return True
283         # Refresh at seconds per frame rate
284         if self.time_to_refresh > 0:
285             return False
286
287         if not self._core.bus_free():
288             # Can't acquire display bus; skip updating this display. Try next display
289             return False
290
291         areas_to_refresh = self._get_refresh_areas()
292         if not areas_to_refresh:
293             return True
294
295         if self._acep:
296             self._start_refresh()
297             self._clean_area()
298             self._finish_refresh()
299             while self._refreshing:
300                 # TODO: Add something here that can change self._refreshing
301                 # or add something in _background()
302                 pass
303
304         self._start_refresh()
305         for area in areas_to_refresh:
306             self._refresh_area(area)
307         self._finish_refresh()
308
309         return True
310
311     def _release(self) -> None:
312         """Release the display and free its resources"""
313         if self._refreshing:
314             self._wait_for_busy()
315             self._ticks_disabled = True
316             self._refreshing = False
317             # Run stop sequence but don't wait for busy because busy is set when sleeping
318             self._send_command_sequence(False, self._stop_sequence)
319         self._core.release_display_core()
320         if self._busy is not None:
321             self._busy.deinit()
322
323     def _background(self) -> None:
324         """Run background refresh tasks."""
325
326         # Wait until initialized
327         if not hasattr(self, "_core"):
328             return
329
330         if self._ticks_disabled:
331             return
332
333         if self._refreshing:
334             refresh_done = False
335             if self._busy is not None:
336                 busy = self._busy.value
337                 refresh_done = busy == self._busy_state
338             else:
339                 refresh_done = (
340                     time.monotonic() * 1000 - self._core.last_refresh
341                     > self._refresh_time
342                 )
343
344             if refresh_done:
345                 self._ticks_disabled = True
346                 self._refreshing = False
347                 # Run stop sequence but don't wait for busy because busy is set when sleeping
348                 self._send_command_sequence(False, self._stop_sequence)
349
350     def _get_refresh_areas(self) -> list[Area]:
351         """Get a list of areas to be refreshed"""
352         areas = []
353         if self._core.full_refresh:
354             areas.append(self._core.area)
355             return areas
356         first_area = None
357         if self._core.current_group is not None:
358             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
359                 areas
360             )
361             first_area = areas[0]
362         if first_area is not None and self._core.row_command == NO_COMMAND:
363             # Do a full refresh if the display doesn't support partial updates
364             areas = [self._core.area]
365         return areas
366
367     def _refresh_area(self, area: Area) -> bool:
368         """Redraw the area."""
369         # pylint: disable=too-many-locals, too-many-branches
370         clipped = Area()
371         # Clip the area to the display by overlapping the areas.
372         # If there is no overlap then we're done.
373         if not self._core.clip_area(area, clipped):
374             return True
375         subrectangles = 1
376         rows_per_buffer = clipped.height()
377         pixels_per_word = 32 // self._core.colorspace.depth
378         pixels_per_buffer = clipped.size()
379
380         # We should have lots of memory
381         buffer_size = clipped.size() // pixels_per_word
382
383         if clipped.size() > buffer_size * pixels_per_word:
384             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
385             if rows_per_buffer == 0:
386                 rows_per_buffer = 1
387             subrectangles = clipped.height() // rows_per_buffer
388             if clipped.height() % rows_per_buffer != 0:
389                 subrectangles += 1
390             pixels_per_buffer = rows_per_buffer * clipped.width()
391             buffer_size = pixels_per_buffer // pixels_per_word
392             if pixels_per_buffer % pixels_per_word:
393                 buffer_size += 1
394
395         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
396
397         passes = 1
398         if self._write_color_ram_command != NO_COMMAND:
399             passes = 2
400         for pass_index in range(passes):
401             remaining_rows = clipped.height()
402             if self._core.row_command != NO_COMMAND:
403                 self._core.set_region_to_update(clipped)
404
405             write_command = self._write_black_ram_command
406             if pass_index == 1:
407                 write_command = self._write_color_ram_command
408
409             self._core.begin_transaction()
410             self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
411             self._core.end_transaction()
412
413             for subrect_index in range(subrectangles):
414                 subrectangle = Area(
415                     x1=clipped.x1,
416                     y1=clipped.y1 + rows_per_buffer * subrect_index,
417                     x2=clipped.x2,
418                     y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
419                 )
420                 if remaining_rows < rows_per_buffer:
421                     subrectangle.y2 = subrectangle.y1 + remaining_rows
422                 remaining_rows -= rows_per_buffer
423
424                 subrectangle_size_bytes = subrectangle.size() // (
425                     8 // self._core.colorspace.depth
426                 )
427
428                 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
429                 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
430
431                 if not self._acep:
432                     self._core.colorspace.grayscale = True
433                     self._core.colorspace.grayscale_bit = 7
434                 if pass_index == 1:
435                     if self._grayscale:  # 4-color grayscale
436                         self._core.colorspace.grayscale_bit = 6
437                         self._core.fill_area(subrectangle, mask, buffer)
438                     elif self._core.colorspace.tricolor:
439                         self._core.colorspace.grayscale = False
440                         self._core.fill_area(subrectangle, mask, buffer)
441                     elif self._core.colorspace.sevencolor:
442                         self._core.fill_area(subrectangle, mask, buffer)
443                 else:
444                     self._core.fill_area(subrectangle, mask, buffer)
445
446                 # Invert it all
447                 if (pass_index == 1 and self._color_bits_inverted) or (
448                     pass_index == 0 and self._black_bits_inverted
449                 ):
450                     for i in range(buffer_size):
451                         buffer[i] = ~buffer[i]
452
453                 if not self._core.begin_transaction():
454                     # Can't acquire display bus; skip the rest of the data. Try next display.
455                     return False
456                 self._core.send(
457                     DISPLAY_DATA,
458                     self._chip_select,
459                     buffer.tobytes()[:subrectangle_size_bytes],
460                 )
461                 self._core.end_transaction()
462         return True
463
464     def _send_command_sequence(
465         self, should_wait_for_busy: bool, sequence: ReadableBuffer
466     ) -> None:
467         i = 0
468         while i < len(sequence):
469             command = sequence[i]
470             data_size = sequence[i + 1]
471             delay = (data_size & DELAY) != 0
472             data_size &= ~DELAY
473             data = sequence[i + 2 : i + 2 + data_size]
474             if self._two_byte_sequence_length:
475                 data_size = ((data_size & ~DELAY) << 8) + sequence[i + 2]
476                 data = sequence[i + 3 : i + 3 + data_size]
477
478             self._core.begin_transaction()
479             self._core.send(
480                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
481             )
482             if data_size > 0:
483                 self._core.send(
484                     DISPLAY_DATA,
485                     CHIP_SELECT_UNTOUCHED,
486                     bytes(data),
487                 )
488             self._core.end_transaction()
489             delay_time_ms = 0
490             if delay:
491                 data_size += 1
492                 delay_time_ms = sequence[i + 1 + data_size]
493                 if delay_time_ms == 255:
494                     delay_time_ms = 500
495             time.sleep(delay_time_ms / 1000)
496             if should_wait_for_busy:
497                 self._wait_for_busy()
498             i += 2 + data_size
499             if self._two_byte_sequence_length:
500                 i += 1
501
502     def _start_refresh(self) -> None:
503         # Run start sequence
504         self._core._bus_reset()  # pylint: disable=protected-access
505         time.sleep(self._start_up_time)
506         self._send_command_sequence(True, self._start_sequence)
507         self._core.start_refresh()
508
509     def _finish_refresh(self) -> None:
510         # Actually refresh the display now that all pixel RAM has been updated
511         self._send_command_sequence(False, self._refresh_sequence)
512         self._ticks_disabled = False
513         self._refreshing = True
514         self._core.finish_refresh()
515
516     def _wait_for_busy(self) -> None:
517         if self._busy is not None:
518             while self._busy.value == self._busy_state:
519                 time.sleep(0.001)
520
521     def _clean_area(self) -> bool:
522         width = self._core.width
523         height = self._core.height
524
525         buffer = bytearray([0x77] * (width // 2))
526         self._core.begin_transaction()
527         self._core.send(
528             DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
529         )
530         self._core.end_transaction()
531         for _ in range(height):
532             if not self._core.begin_transaction():
533                 return False
534             self._core.send(DISPLAY_DATA, self._chip_select, buffer)
535             self._core.end_transaction()
536         return True
537
538     @property
539     def rotation(self) -> int:
540         """The rotation of the display as an int in degrees"""
541         return self._core.rotation
542
543     @rotation.setter
544     def rotation(self, value: int) -> None:
545         if value % 90 != 0:
546             raise ValueError("Display rotation must be in 90 degree increments")
547         transposed = self._core.rotation in (90, 270)
548         will_transposed = value in (90, 270)
549         if transposed != will_transposed:
550             self._core.width, self._core.height = self._core.height, self._core.width
551         self._core.set_rotation(value)
552         if self._core.current_group is not None:
553             self._core.current_group._update_transform(  # pylint: disable=protected-access
554                 self._core.transform
555             )
556
557     @property
558     def time_to_refresh(self) -> float:
559         """Time, in fractional seconds, until the ePaper display can be refreshed."""
560         if self._core.last_refresh == 0:
561             return 0
562
563         # Refresh at seconds per frame rate
564         elapsed_time = time.monotonic() * 1000 - self._core.last_refresh
565         if elapsed_time > self._milliseconds_per_frame:
566             return 0
567         return self._milliseconds_per_frame - elapsed_time
568
569     @property
570     def busy(self) -> bool:
571         """True when the display is refreshing. This uses the ``busy_pin`` when available or the
572         ``refresh_time`` otherwise."""
573         return self._refreshing
574
575     @property
576     def width(self) -> int:
577         """Display Width"""
578         return self._core.width
579
580     @property
581     def height(self) -> int:
582         """Display Height"""
583         return self._core.height
584
585     @property
586     def bus(self) -> _DisplayBus:
587         """Current Display Bus"""
588         return self._core.get_bus()
589
590     @property
591     def root_group(self) -> Group:
592         """The root group on the epaper display.
593         If the root group is set to ``None``, no output will be shown.
594         """
595         return self._core.current_group
596
597     @root_group.setter
598     def root_group(self, new_group: Group) -> None:
599         self._set_root_group(new_group)