]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
Add root_group property to display
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _display.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.display`
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
22 import digitalio
23 import microcontroller
24 from circuitpython_typing import WriteableBuffer, ReadableBuffer
25 from ._displaycore import _DisplayCore
26 from ._displaybus import _DisplayBus
27 from ._colorconverter import ColorConverter
28 from ._group import Group, circuitpython_splash
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     BACKLIGHT_IN_OUT,
36     BACKLIGHT_PWM,
37     NO_COMMAND,
38     DELAY,
39 )
40
41 __version__ = "0.0.0+auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
43
44
45 class Display:
46     # pylint: disable=too-many-instance-attributes, too-many-statements
47     """This initializes a display and connects it into CircuitPython. Unlike other objects
48     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
49     This is done so that CircuitPython can use the display itself.
50
51     Most people should not use this class directly. Use a specific display driver instead
52     that will contain the initialization sequence at minimum.
53     """
54
55     def __init__(
56         self,
57         display_bus: _DisplayBus,
58         init_sequence: ReadableBuffer,
59         *,
60         width: int,
61         height: int,
62         colstart: int = 0,
63         rowstart: int = 0,
64         rotation: int = 0,
65         color_depth: int = 16,
66         grayscale: bool = False,
67         pixels_in_byte_share_row: bool = True,
68         bytes_per_cell: int = 1,
69         reverse_pixels_in_byte: bool = False,
70         reverse_bytes_in_word: bool = True,
71         set_column_command: int = 0x2A,
72         set_row_command: int = 0x2B,
73         write_ram_command: int = 0x2C,
74         backlight_pin: Optional[microcontroller.Pin] = None,
75         brightness_command: Optional[int] = None,
76         brightness: float = 1.0,
77         single_byte_bounds: bool = False,
78         data_as_commands: bool = False,
79         auto_refresh: bool = True,
80         native_frames_per_second: int = 60,
81         backlight_on_high: bool = True,
82         SH1107_addressing: bool = False,
83     ):
84         # pylint: disable=too-many-locals,invalid-name, too-many-branches
85         """Create a Display object on the given display bus (`displayio.FourWire` or
86         `paralleldisplay.ParallelBus`).
87
88         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
89         with a command byte followed by a byte to determine the parameter count and if a
90         delay is need after. When the top bit of the second byte is 1, the next byte will be
91         the delay time in milliseconds. The remaining 7 bits are the parameter count
92         excluding any delay byte. The third through final bytes are the remaining command
93         parameters. The next byte will begin a new command definition. Here is a portion of
94         ILI9341 init code:
95
96         .. code-block:: python
97
98             init_sequence = (
99                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
100 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
101                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
102                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
103             )
104             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
105
106         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
107         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
108         Multiple byte literals (b”“) are merged together on load. The parens are needed to
109         allow byte literals on subsequent lines.
110
111         The initialization sequence should always leave the display memory access inline with
112         the scan of the display to minimize tearing artifacts.
113         """
114
115         if rotation % 90 != 0:
116             raise ValueError("Display rotation must be in 90 degree increments")
117
118         if SH1107_addressing and color_depth != 1:
119             raise ValueError("color_depth must be 1 when SH1107_addressing is True")
120
121         # Turn off auto-refresh as we init
122         self._auto_refresh = False
123         ram_width = 0x100
124         ram_height = 0x100
125         if single_byte_bounds:
126             ram_width = 0xFF
127             ram_height = 0xFF
128
129         self._core = _DisplayCore(
130             bus=display_bus,
131             width=width,
132             height=height,
133             ram_width=ram_width,
134             ram_height=ram_height,
135             colstart=colstart,
136             rowstart=rowstart,
137             rotation=rotation,
138             color_depth=color_depth,
139             grayscale=grayscale,
140             pixels_in_byte_share_row=pixels_in_byte_share_row,
141             bytes_per_cell=bytes_per_cell,
142             reverse_pixels_in_byte=reverse_pixels_in_byte,
143             reverse_bytes_in_word=reverse_bytes_in_word,
144             column_command=set_column_command,
145             row_command=set_row_command,
146             set_current_column_command=NO_COMMAND,
147             set_current_row_command=NO_COMMAND,
148             data_as_commands=data_as_commands,
149             always_toggle_chip_select=False,
150             sh1107_addressing=(SH1107_addressing and color_depth == 1),
151             address_little_endian=False,
152         )
153
154         self._write_ram_command = write_ram_command
155         self._brightness_command = brightness_command
156         self._first_manual_refresh = not auto_refresh
157         self._backlight_on_high = backlight_on_high
158
159         self._native_frames_per_second = native_frames_per_second
160         self._native_ms_per_frame = 1000 // native_frames_per_second
161
162         self._brightness = brightness
163         self._auto_refresh = auto_refresh
164
165         i = 0
166         while i < len(init_sequence):
167             command = init_sequence[i]
168             data_size = init_sequence[i + 1]
169             delay = (data_size & DELAY) != 0
170             data_size &= ~DELAY
171             while self._core.begin_transaction():
172                 pass
173
174             if self._core.data_as_commands:
175                 full_command = bytearray(data_size + 1)
176                 full_command[0] = command
177                 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
178                 self._core.send(
179                     DISPLAY_COMMAND,
180                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
181                     full_command,
182                 )
183             else:
184                 self._core.send(
185                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
186                 )
187                 self._core.send(
188                     DISPLAY_DATA,
189                     CHIP_SELECT_UNTOUCHED,
190                     init_sequence[i + 2 : i + 2 + data_size],
191                 )
192             self._core.end_transaction()
193             delay_time_ms = 10
194             if delay:
195                 data_size += 1
196                 delay_time_ms = init_sequence[i + 1 + data_size]
197                 if delay_time_ms == 255:
198                     delay_time_ms = 500
199             time.sleep(delay_time_ms / 1000)
200             i += 2 + data_size
201
202         self._current_group = None
203         self._last_refresh_call = 0
204         self._refresh_thread = None
205         self._colorconverter = ColorConverter()
206
207         self._backlight_type = None
208         if backlight_pin is not None:
209             try:
210                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
211
212                 # 100Hz looks decent and doesn't keep the CPU too busy
213                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
214                 self._backlight_type = BACKLIGHT_PWM
215             except ImportError:
216                 # PWMOut not implemented on this platform
217                 pass
218             if self._backlight_type is None:
219                 self._backlight_type = BACKLIGHT_IN_OUT
220                 self._backlight = digitalio.DigitalInOut(backlight_pin)
221                 self._backlight.switch_to_output()
222         self.brightness = brightness
223         if not circuitpython_splash._in_group:
224             self._set_root_group(circuitpython_splash)
225         self.auto_refresh = auto_refresh
226
227     def __new__(cls, *args, **kwargs):
228         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
229             allocate_display,
230         )
231
232         display_instance = super().__new__(cls)
233         allocate_display(display_instance)
234         return display_instance
235
236     def _send_pixels(self, pixels):
237         if not self._core.data_as_commands:
238             self._core.send(
239                 DISPLAY_COMMAND,
240                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
241                 bytes([self._write_ram_command]),
242             )
243         self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
244
245     def show(self, group: Group) -> None:
246         """
247         .. note:: `show()` is deprecated and will be removed when CircuitPython 9.0.0
248         is released. Use ``.root_group = group`` instead.
249
250         Switches to displaying the given group of layers. When group is None, the
251         default CircuitPython terminal will be shown.
252
253         :param Group group: The group to show.
254         """
255         if group is None:
256             group = circuitpython_splash
257         self._core.set_root_group(group)
258
259     def _set_root_group(self, root_group: Group) -> None:
260         ok = self._core.set_root_group(root_group)
261         if not ok:
262             raise ValueError("Group already used")
263
264     def refresh(
265         self,
266         *,
267         target_frames_per_second: Optional[int] = None,
268         minimum_frames_per_second: int = 0,
269     ) -> bool:
270         """When auto refresh is off, waits for the target frame rate and then refreshes the
271         display, returning True. If the call has taken too long since the last refresh call
272         for the given target frame rate, then the refresh returns False immediately without
273         updating the screen to hopefully help getting caught up.
274
275         If the time since the last successful refresh is below the minimum frame rate, then
276         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
277
278         When auto refresh is on, updates the display immediately. (The display will also
279         update without calls to this.)
280         """
281         maximum_ms_per_real_frame = 0xFFFFFFFF
282         if minimum_frames_per_second > 0:
283             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
284
285         if target_frames_per_second is None:
286             target_ms_per_frame = 0xFFFFFFFF
287         else:
288             target_ms_per_frame = 1000 // target_frames_per_second
289
290         if (
291             not self._auto_refresh
292             and not self._first_manual_refresh
293             and target_ms_per_frame != 0xFFFFFFFF
294         ):
295             current_time = time.monotonic() * 1000
296             current_ms_since_real_refresh = current_time - self._core.last_refresh
297             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
298                 raise RuntimeError("Below minimum frame rate")
299             current_ms_since_last_call = current_time - self._last_refresh_call
300             self._last_refresh_call = current_time
301             if current_ms_since_last_call > target_ms_per_frame:
302                 return False
303
304             remaining_time = target_ms_per_frame - (
305                 current_ms_since_real_refresh % target_ms_per_frame
306             )
307             time.sleep(remaining_time / 1000)
308         self._first_manual_refresh = False
309         self._refresh_display()
310         return True
311
312     def _refresh_display(self):
313         if not self._core.start_refresh():
314             return False
315
316         areas_to_refresh = self._get_refresh_areas()
317         for area in areas_to_refresh:
318             self._refresh_area(area)
319
320         self._core.finish_refresh()
321
322         return True
323
324     def _get_refresh_areas(self) -> list[Area]:
325         """Get a list of areas to be refreshed"""
326         areas = []
327         if self._core.full_refresh:
328             areas.append(self._core.area)
329         elif self._core.current_group is not None:
330             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
331                 areas
332             )
333         return areas
334
335     def _background(self):
336         """Run background refresh tasks. Do not call directly"""
337         if (
338             self._auto_refresh
339             and (time.monotonic() * 1000 - self._core.last_refresh)
340             > self._native_ms_per_frame
341         ):
342             self.refresh()
343
344     def _refresh_area(self, area) -> bool:
345         """Loop through dirty areas and redraw that area."""
346         # pylint: disable=too-many-locals, too-many-branches
347
348         clipped = Area()
349         # Clip the area to the display by overlapping the areas.
350         # If there is no overlap then we're done.
351         if not self._core.clip_area(area, clipped):
352             return True
353
354         rows_per_buffer = clipped.height()
355         pixels_per_word = 32 // self._core.colorspace.depth
356         pixels_per_buffer = clipped.size()
357
358         # We should have lots of memory
359         buffer_size = clipped.size() // pixels_per_word
360
361         subrectangles = 1
362         # for SH1107 and other boundary constrained controllers
363         #      write one single row at a time
364         if self._core.sh1107_addressing:
365             subrectangles = rows_per_buffer // 8
366             rows_per_buffer = 8
367         elif clipped.size() > buffer_size * pixels_per_word:
368             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
369             if rows_per_buffer == 0:
370                 rows_per_buffer = 1
371             # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
372             if (
373                 self._core.colorspace.depth < 8
374                 and self._core.colorspace.pixels_in_byte_share_row
375             ):
376                 pixels_per_byte = 8 // self._core.colorspace.depth
377                 if rows_per_buffer % pixels_per_byte != 0:
378                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
379             subrectangles = clipped.height() // rows_per_buffer
380             if clipped.height() % rows_per_buffer != 0:
381                 subrectangles += 1
382             pixels_per_buffer = rows_per_buffer * clipped.width()
383             buffer_size = pixels_per_buffer // pixels_per_word
384             if pixels_per_buffer % pixels_per_word:
385                 buffer_size += 1
386         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
387         remaining_rows = clipped.height()
388
389         for subrect_index in range(subrectangles):
390             subrectangle = Area(
391                 x1=clipped.x1,
392                 y1=clipped.y1 + rows_per_buffer * subrect_index,
393                 x2=clipped.x2,
394                 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
395             )
396             if remaining_rows < rows_per_buffer:
397                 subrectangle.y2 = subrectangle.y1 + remaining_rows
398             remaining_rows -= rows_per_buffer
399             self._core.set_region_to_update(subrectangle)
400             if self._core.colorspace.depth >= 8:
401                 subrectangle_size_bytes = subrectangle.size() * (
402                     self._core.colorspace.depth // 8
403                 )
404             else:
405                 subrectangle_size_bytes = subrectangle.size() // (
406                     8 // self._core.colorspace.depth
407                 )
408
409             buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
410             mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
411             self._core.fill_area(subrectangle, mask, buffer)
412
413             # Can't acquire display bus; skip the rest of the data.
414             if not self._core.bus_free():
415                 return False
416
417             self._core.begin_transaction()
418             self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
419             self._core.end_transaction()
420         return True
421
422     def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
423         """Extract the pixels from a single row"""
424         if self._core.colorspace.depth != 16:
425             raise ValueError("Display must have a 16 bit colorspace.")
426
427         area = Area(0, y, self._core.width, y + 1)
428         pixels_per_word = 32 // self._core.colorspace.depth
429         buffer_size = self._core.width // pixels_per_word
430         pixels_per_buffer = area.size()
431         if pixels_per_buffer % pixels_per_word:
432             buffer_size += 1
433
434         buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
435         mask_length = (pixels_per_buffer // 32) + 1
436         mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
437         self._core.fill_area(area, mask, buffer)
438         return buffer
439
440     def _release(self) -> None:
441         """Release the display and free its resources"""
442         self.auto_refresh = False
443         self._core.release_display_core()
444
445     def _reset(self) -> None:
446         """Reset the display"""
447         self.auto_refresh = True
448         circuitpython_splash.x = 0
449         circuitpython_splash.y = 0
450         if not circuitpython_splash._in_group:  # pylint: disable=protected-access
451             self._set_root_group(circuitpython_splash)
452
453     @property
454     def auto_refresh(self) -> bool:
455         """True when the display is refreshed automatically."""
456         return self._auto_refresh
457
458     @auto_refresh.setter
459     def auto_refresh(self, value: bool):
460         self._first_manual_refresh = not value
461         self._auto_refresh = value
462
463     @property
464     def brightness(self) -> float:
465         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
466         return self._brightness
467
468     @brightness.setter
469     def brightness(self, value: float):
470         if 0 <= float(value) <= 1.0:
471             if not self._backlight_on_high:
472                 value = 1.0 - value
473
474             if self._backlight_type == BACKLIGHT_PWM:
475                 self._backlight.duty_cycle = value * 0xFFFF
476             elif self._backlight_type == BACKLIGHT_IN_OUT:
477                 self._backlight.value = value > 0.99
478             elif self._brightness_command is not None:
479                 okay = self._core.begin_transaction()
480                 if okay:
481                     if self._core.data_as_commands:
482                         self._core.send(
483                             DISPLAY_COMMAND,
484                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
485                             bytes([self._brightness_command, round(0xFF * value)]),
486                         )
487                     else:
488                         self._core.send(
489                             DISPLAY_COMMAND,
490                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
491                             bytes([self._brightness_command]),
492                         )
493                         self._core.send(
494                             DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
495                         )
496                     self._core.end_transaction()
497             self._brightness = value
498         else:
499             raise ValueError("Brightness must be between 0.0 and 1.0")
500
501     @property
502     def width(self) -> int:
503         """Display Width"""
504         return self._core.get_width()
505
506     @property
507     def height(self) -> int:
508         """Display Height"""
509         return self._core.get_height()
510
511     @property
512     def rotation(self) -> int:
513         """The rotation of the display as an int in degrees."""
514         return self._core.get_rotation()
515
516     @rotation.setter
517     def rotation(self, value: int):
518         if value % 90 != 0:
519             raise ValueError("Display rotation must be in 90 degree increments")
520         transposed = self._core.rotation in (90, 270)
521         will_transposed = value in (90, 270)
522         if transposed != will_transposed:
523             self._core.width, self._core.height = self._core.height, self._core.width
524         self._core.set_rotation(value)
525         if self._core.current_group is not None:
526             self._core.current_group._update_transform(  # pylint: disable=protected-access
527                 self._core.transform
528             )
529
530     @property
531     def bus(self) -> _DisplayBus:
532         """Current Display Bus"""
533         return self._core.get_bus()
534
535     @property
536     def root_group(self) -> Group:
537         """The root group on the display.
538         If the root group is set to `displayio.CIRCUITPYTHON_TERMINAL`, the default
539         CircuitPython terminal will be shown.
540         If the root group is set to ``None``, no output will be shown."""
541         return self._core.current_group
542
543     @root_group.setter
544     def root_group(self, new_group: Group) -> None:
545         self._set_root_group(new_group)