]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_epaperdisplay.py
Initial eink implementation - not complete
[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
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.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,
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         color_depth = 1
162         core_grayscale = True
163
164         if advanced_color_epaper:
165             color_depth = 4
166             grayscale = False
167             core_grayscale = False
168
169         self._core = _DisplayCore(
170             bus=display_bus,
171             width=width,
172             height=height,
173             ram_width=ram_width,
174             ram_height=ram_height,
175             colstart=colstart,
176             rowstart=rowstart,
177             rotation=rotation,
178             color_depth=color_depth,
179             grayscale=core_grayscale,
180             pixels_in_byte_share_row=True,
181             bytes_per_cell=1,
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,
192         )
193
194         if highlight_color != 0x000000:
195             self._core.colorspace.tricolor = True
196             self._core.colorspace.tricolor_hue = ColorConverter._compute_hue(
197                 highlight_color
198             )
199             self._core.colorspace.tricolor_luma = ColorConverter._compute_luma(
200                 highlight_color
201             )
202         else:
203             self._core.colorspace.tricolor = False
204
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
213         )
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
221         )
222         self._grayscale = grayscale
223
224         self._start_sequence = start_sequence
225         self._start_up_time = (
226             start_up_time  # TODO: Verify we are using seconds instead of ms
227         )
228         self._stop_sequence = stop_sequence
229         self._refesh_sequence = refresh_sequence
230         self._busy = None
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()
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.show(circuitpython_splash)
241
242     def __new__(cls, *args, **kwargs):
243         from . 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     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).
255         """
256         if group is None:
257             group = circuitpython_splash
258         self._core.set_root_group(group)
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         # TODO: Implement
266
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.
271         """
272         # TODO: Implement
273
274     def _get_refresh_areas(self) -> list[Area]:
275         """Get a list of areas to be refreshed"""
276         areas = []
277         if self._core.full_refresh:
278             areas.append(self._core.area)
279             return areas
280         first_area = None
281         if self._core.current_group is not None:
282             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
283                 areas
284             )
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]
289         return areas
290
291     def _send_command_sequence(
292         self, should_wait_for_busy: bool, sequence: ReadableBuffer
293     ) -> None:
294         i = 0
295         while i < len(sequence):
296             command = sequence[i]
297             data_size = sequence[i + 1]
298             delay = (data_size & DELAY) != 0
299             data_size &= ~DELAY
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]
304
305             self._core.begin_transaction()
306             self._core.send(
307                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
308             )
309             self._core.send(
310                 DISPLAY_DATA,
311                 CHIP_SELECT_UNTOUCHED,
312                 data,
313             )
314             self._core.end_transaction()
315             delay_time_ms = 0
316             if delay:
317                 data_size += 1
318                 delay_time_ms = sequence[i + 1 + data_size]
319                 if delay_time_ms == 255:
320                     delay_time_ms = 500
321             time.sleep(delay_time_ms / 1000)
322             if should_wait_for_busy:
323                 self._wait_for_busy()
324             i += 2 + data_size
325             if self._two_byte_sequence_length:
326                 i += 1
327
328     def _finish_refresh(self) -> None:
329         self._send_command_sequence(False, self._refesh_sequence)
330         self._refreshing = True
331         self._core.finish_refresh()
332
333     def _wait_for_busy(self) -> None:
334         if self._busy is not None:
335             while self._busy.value != self._busy_state:
336                 time.sleep(0.001)
337
338     @property
339     def rotation(self) -> int:
340         """The rotation of the display as an int in degrees"""
341         return self._core.rotation
342
343     @rotation.setter
344     def rotation(self, value: int) -> None:
345         if value % 90 != 0:
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)
352
353     @property
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
357
358     @property
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
363
364     @property
365     def width(self) -> int:
366         """Display Width"""
367         return self._core.width
368
369     @property
370     def height(self) -> int:
371         """Display Height"""
372         return self._core.height
373
374     @property
375     def bus(self) -> _DisplayBus:
376         """Current Display Bus"""
377         return self._core._bus
378
379     @property
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.
383         """
384         return self._core.root_group
385
386     @root_group.setter
387     def root_group(self, new_group: Group) -> None:
388         self._core.root_group = new_group