]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - epaperdisplay/__init__.py
Fix indexed bitmaps by correcting argument order at call
[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 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
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         # Disable while initializing
165         self._ticks_disabled = True
166
167         if advanced_color_epaper:
168             color_depth = 4
169             grayscale = False
170             core_grayscale = False
171
172         self._core = _DisplayCore(
173             bus=display_bus,
174             width=width,
175             height=height,
176             ram_width=ram_width,
177             ram_height=ram_height,
178             colstart=colstart,
179             rowstart=rowstart,
180             rotation=rotation,
181             color_depth=color_depth,
182             grayscale=core_grayscale,
183             pixels_in_byte_share_row=True,
184             bytes_per_cell=1,
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,
195         )
196
197         if highlight_color != 0x000000:
198             self._core.colorspace.tricolor = True
199             self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
200                 highlight_color
201             )
202             self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
203                 highlight_color
204             )
205         else:
206             self._core.colorspace.tricolor = False
207
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
221         )
222         self._grayscale = grayscale
223
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
228         self._busy = None
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()
233
234         self._ticks_disabled = False
235
236         # Clear the color memory if it isn't in use
237         if highlight_color == 0x00 and write_color_ram_command != NO_COMMAND:
238             """TODO: Clear"""
239
240         self._set_root_group(circuitpython_splash)
241
242     def __new__(cls, *args, **kwargs):
243         from displayio import (  # pylint: disable=import-outside-toplevel, cyclic-import
244             allocate_display,
245         )
246
247         display_instance = super().__new__(cls)
248         allocate_display(display_instance)
249         return display_instance
250
251     @staticmethod
252     def show(_group: Group) -> None:  # pylint: disable=missing-function-docstring
253         raise AttributeError(".show(x) removed. Use .root_group = x")
254
255     def _set_root_group(self, root_group: Group) -> None:
256         ok = self._core.set_root_group(root_group)
257         if not ok:
258             raise ValueError("Group already used")
259
260     def update_refresh_mode(
261         self, start_sequence: ReadableBuffer, seconds_per_frame: float
262     ) -> None:
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
267
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.
271         """
272         if not self._refresh():
273             raise RuntimeError("Refresh too soon")
274
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)
281             else:
282                 return False
283         if self._core.current_group is None:
284             return True
285         # Refresh at seconds per frame rate
286         if self.time_to_refresh > 0:
287             return False
288
289         if not self._core.bus_free():
290             # Can't acquire display bus; skip updating this display. Try next display
291             return False
292
293         areas_to_refresh = self._get_refresh_areas()
294         if not areas_to_refresh:
295             return True
296
297         if self._acep:
298             self._start_refresh()
299             self._clean_area()
300             self._finish_refresh()
301             while self._refreshing:
302                 # TODO: Add something here that can change self._refreshing
303                 # or add something in _background()
304                 pass
305
306         self._start_refresh()
307         for area in areas_to_refresh:
308             self._refresh_area(area)
309         self._finish_refresh()
310
311         return True
312
313     def _release(self) -> None:
314         """Release the display and free its resources"""
315         if self._refreshing:
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:
323             self._busy.deinit()
324
325     def _background(self) -> None:
326         """Run background refresh tasks."""
327
328         # Wait until initialized
329         if not hasattr(self, "_core"):
330             return
331
332         if self._ticks_disabled:
333             return
334
335         if self._refreshing:
336             refresh_done = False
337             if self._busy is not None:
338                 busy = self._busy.value
339                 refresh_done = busy == self._busy_state
340             else:
341                 refresh_done = (
342                     time.monotonic() * 1000 - self._core.last_refresh
343                     > self._refresh_time
344                 )
345
346             if refresh_done:
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)
351
352     def _get_refresh_areas(self) -> list[Area]:
353         """Get a list of areas to be refreshed"""
354         areas = []
355         if self._core.full_refresh:
356             areas.append(self._core.area)
357             return areas
358         first_area = None
359         if self._core.current_group is not None:
360             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
361                 areas
362             )
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]
367         return areas
368
369     def _refresh_area(self, area: Area) -> bool:
370         """Redraw the area."""
371         # pylint: disable=too-many-locals, too-many-branches
372         clipped = Area()
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):
376             return True
377         subrectangles = 1
378         rows_per_buffer = clipped.height()
379         pixels_per_word = 32 // self._core.colorspace.depth
380         pixels_per_buffer = clipped.size()
381
382         # We should have lots of memory
383         buffer_size = clipped.size() // pixels_per_word
384
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:
388                 rows_per_buffer = 1
389             subrectangles = clipped.height() // rows_per_buffer
390             if clipped.height() % rows_per_buffer != 0:
391                 subrectangles += 1
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:
395                 buffer_size += 1
396
397         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
398
399         passes = 1
400         if self._write_color_ram_command != NO_COMMAND:
401             passes = 2
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)
406
407             write_command = self._write_black_ram_command
408             if pass_index == 1:
409                 write_command = self._write_color_ram_command
410
411             self._core.begin_transaction()
412             self._core.send(DISPLAY_COMMAND, self._chip_select, bytes([write_command]))
413             self._core.end_transaction()
414
415             for subrect_index in range(subrectangles):
416                 subrectangle = Area(
417                     x1=clipped.x1,
418                     y1=clipped.y1 + rows_per_buffer * subrect_index,
419                     x2=clipped.x2,
420                     y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
421                 )
422                 if remaining_rows < rows_per_buffer:
423                     subrectangle.y2 = subrectangle.y1 + remaining_rows
424                 remaining_rows -= rows_per_buffer
425
426                 subrectangle_size_bytes = subrectangle.size() // (
427                     8 // self._core.colorspace.depth
428                 )
429
430                 buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
431                 mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
432
433                 if not self._acep:
434                     self._core.colorspace.grayscale = True
435                     self._core.colorspace.grayscale_bit = 7
436                 if pass_index == 1:
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)
445                 else:
446                     self._core.fill_area(subrectangle, mask, buffer)
447
448                 # Invert it all
449                 if (pass_index == 1 and self._color_bits_inverted) or (
450                     pass_index == 0 and self._black_bits_inverted
451                 ):
452                     for i in range(buffer_size):
453                         buffer[i] = ~buffer[i]
454
455                 if not self._core.begin_transaction():
456                     # Can't acquire display bus; skip the rest of the data. Try next display.
457                     return False
458                 self._core.send(
459                     DISPLAY_DATA,
460                     self._chip_select,
461                     buffer.tobytes()[:subrectangle_size_bytes],
462                 )
463                 self._core.end_transaction()
464         return True
465
466     def _send_command_sequence(
467         self, should_wait_for_busy: bool, sequence: ReadableBuffer
468     ) -> None:
469         i = 0
470         while i < len(sequence):
471             command = sequence[i]
472             data_size = sequence[i + 1]
473             delay = (data_size & DELAY) != 0
474             data_size &= ~DELAY
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]
479
480             self._core.begin_transaction()
481             self._core.send(
482                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
483             )
484             if data_size > 0:
485                 self._core.send(
486                     DISPLAY_DATA,
487                     CHIP_SELECT_UNTOUCHED,
488                     bytes(data),
489                 )
490             self._core.end_transaction()
491             delay_time_ms = 0
492             if delay:
493                 data_size += 1
494                 delay_time_ms = sequence[i + 1 + data_size]
495                 if delay_time_ms == 255:
496                     delay_time_ms = 500
497             time.sleep(delay_time_ms / 1000)
498             if should_wait_for_busy:
499                 self._wait_for_busy()
500             i += 2 + data_size
501             if self._two_byte_sequence_length:
502                 i += 1
503
504     def _start_refresh(self) -> None:
505         # Run start sequence
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()
510
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()
517
518     def _wait_for_busy(self) -> None:
519         if self._busy is not None:
520             while self._busy.value == self._busy_state:
521                 time.sleep(0.001)
522
523     def _clean_area(self) -> bool:
524         width = self._core.width
525         height = self._core.height
526
527         buffer = bytearray([0x77] * (width // 2))
528         self._core.begin_transaction()
529         self._core.send(
530             DISPLAY_COMMAND, self._chip_select, bytes([self._write_black_ram_command])
531         )
532         self._core.end_transaction()
533         for _ in range(height):
534             if not self._core.begin_transaction():
535                 return False
536             self._core.send(DISPLAY_DATA, self._chip_select, buffer)
537             self._core.end_transaction()
538         return True
539
540     @property
541     def rotation(self) -> int:
542         """The rotation of the display as an int in degrees"""
543         return self._core.rotation
544
545     @rotation.setter
546     def rotation(self, value: int) -> None:
547         if value % 90 != 0:
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
556                 self._core.transform
557             )
558
559     @property
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:
563             return 0
564
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:
568             return 0
569         return self._milliseconds_per_frame - elapsed_time
570
571     @property
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
576
577     @property
578     def width(self) -> int:
579         """Display Width"""
580         return self._core.width
581
582     @property
583     def height(self) -> int:
584         """Display Height"""
585         return self._core.height
586
587     @property
588     def bus(self) -> _DisplayBus:
589         """Current Display Bus"""
590         return self._core.get_bus()
591
592     @property
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.
596         """
597         return self._core.current_group
598
599     @root_group.setter
600     def root_group(self, new_group: Group) -> None:
601         self._set_root_group(new_group)