]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
0a82e6e90b7ff842e3c4644be985e8150ef15754
[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         """
256         if group is None:
257             group = circuitpython_splash
258         self._core.set_root_group(group)
259
260     def _set_root_group(self, root_group: Group) -> None:
261         ok = self._core.set_root_group(root_group)
262         if not ok:
263             raise ValueError("Group already used")
264
265     def refresh(
266         self,
267         *,
268         target_frames_per_second: Optional[int] = None,
269         minimum_frames_per_second: int = 0,
270     ) -> bool:
271         """When auto refresh is off, waits for the target frame rate and then refreshes the
272         display, returning True. If the call has taken too long since the last refresh call
273         for the given target frame rate, then the refresh returns False immediately without
274         updating the screen to hopefully help getting caught up.
275
276         If the time since the last successful refresh is below the minimum frame rate, then
277         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
278
279         When auto refresh is on, updates the display immediately. (The display will also
280         update without calls to this.)
281         """
282         maximum_ms_per_real_frame = 0xFFFFFFFF
283         if minimum_frames_per_second > 0:
284             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
285
286         if target_frames_per_second is None:
287             target_ms_per_frame = 0xFFFFFFFF
288         else:
289             target_ms_per_frame = 1000 // target_frames_per_second
290
291         if (
292             not self._auto_refresh
293             and not self._first_manual_refresh
294             and target_ms_per_frame != 0xFFFFFFFF
295         ):
296             current_time = time.monotonic() * 1000
297             current_ms_since_real_refresh = current_time - self._core.last_refresh
298             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
299                 raise RuntimeError("Below minimum frame rate")
300             current_ms_since_last_call = current_time - self._last_refresh_call
301             self._last_refresh_call = current_time
302             if current_ms_since_last_call > target_ms_per_frame:
303                 return False
304
305             remaining_time = target_ms_per_frame - (
306                 current_ms_since_real_refresh % target_ms_per_frame
307             )
308             time.sleep(remaining_time / 1000)
309         self._first_manual_refresh = False
310         self._refresh_display()
311         return True
312
313     def _refresh_display(self):
314         if not self._core.start_refresh():
315             return False
316
317         areas_to_refresh = self._get_refresh_areas()
318         for area in areas_to_refresh:
319             self._refresh_area(area)
320
321         self._core.finish_refresh()
322
323         return True
324
325     def _get_refresh_areas(self) -> list[Area]:
326         """Get a list of areas to be refreshed"""
327         areas = []
328         if self._core.full_refresh:
329             areas.append(self._core.area)
330         elif self._core.current_group is not None:
331             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
332                 areas
333             )
334         return areas
335
336     def _background(self):
337         """Run background refresh tasks. Do not call directly"""
338         if (
339             self._auto_refresh
340             and (time.monotonic() * 1000 - self._core.last_refresh)
341             > self._native_ms_per_frame
342         ):
343             self.refresh()
344
345     def _refresh_area(self, area) -> bool:
346         """Loop through dirty areas and redraw that area."""
347         # pylint: disable=too-many-locals, too-many-branches
348
349         clipped = Area()
350         # Clip the area to the display by overlapping the areas.
351         # If there is no overlap then we're done.
352         if not self._core.clip_area(area, clipped):
353             return True
354
355         rows_per_buffer = clipped.height()
356         pixels_per_word = 32 // self._core.colorspace.depth
357         pixels_per_buffer = clipped.size()
358
359         # We should have lots of memory
360         buffer_size = clipped.size() // pixels_per_word
361
362         subrectangles = 1
363         # for SH1107 and other boundary constrained controllers
364         #      write one single row at a time
365         if self._core.sh1107_addressing:
366             subrectangles = rows_per_buffer // 8
367             rows_per_buffer = 8
368         elif clipped.size() > buffer_size * pixels_per_word:
369             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
370             if rows_per_buffer == 0:
371                 rows_per_buffer = 1
372             # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
373             if (
374                 self._core.colorspace.depth < 8
375                 and self._core.colorspace.pixels_in_byte_share_row
376             ):
377                 pixels_per_byte = 8 // self._core.colorspace.depth
378                 if rows_per_buffer % pixels_per_byte != 0:
379                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
380             subrectangles = clipped.height() // rows_per_buffer
381             if clipped.height() % rows_per_buffer != 0:
382                 subrectangles += 1
383             pixels_per_buffer = rows_per_buffer * clipped.width()
384             buffer_size = pixels_per_buffer // pixels_per_word
385             if pixels_per_buffer % pixels_per_word:
386                 buffer_size += 1
387         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
388         remaining_rows = clipped.height()
389
390         for subrect_index in range(subrectangles):
391             subrectangle = Area(
392                 x1=clipped.x1,
393                 y1=clipped.y1 + rows_per_buffer * subrect_index,
394                 x2=clipped.x2,
395                 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
396             )
397             if remaining_rows < rows_per_buffer:
398                 subrectangle.y2 = subrectangle.y1 + remaining_rows
399             remaining_rows -= rows_per_buffer
400             self._core.set_region_to_update(subrectangle)
401             if self._core.colorspace.depth >= 8:
402                 subrectangle_size_bytes = subrectangle.size() * (
403                     self._core.colorspace.depth // 8
404                 )
405             else:
406                 subrectangle_size_bytes = subrectangle.size() // (
407                     8 // self._core.colorspace.depth
408                 )
409
410             buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
411             mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
412             self._core.fill_area(subrectangle, mask, buffer)
413
414             # Can't acquire display bus; skip the rest of the data.
415             if not self._core.bus_free():
416                 return False
417
418             self._core.begin_transaction()
419             self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
420             self._core.end_transaction()
421         return True
422
423     def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
424         """Extract the pixels from a single row"""
425         if self._core.colorspace.depth != 16:
426             raise ValueError("Display must have a 16 bit colorspace.")
427
428         area = Area(0, y, self._core.width, y + 1)
429         pixels_per_word = 32 // self._core.colorspace.depth
430         buffer_size = self._core.width // pixels_per_word
431         pixels_per_buffer = area.size()
432         if pixels_per_buffer % pixels_per_word:
433             buffer_size += 1
434
435         buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
436         mask_length = (pixels_per_buffer // 32) + 1
437         mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
438         self._core.fill_area(area, mask, buffer)
439         return buffer
440
441     def _release(self) -> None:
442         """Release the display and free its resources"""
443         self.auto_refresh = False
444         self._core.release_display_core()
445
446     def _reset(self) -> None:
447         """Reset the display"""
448         self.auto_refresh = True
449         circuitpython_splash.x = 0
450         circuitpython_splash.y = 0
451         if not circuitpython_splash._in_group:  # pylint: disable=protected-access
452             self._set_root_group(circuitpython_splash)
453
454     @property
455     def auto_refresh(self) -> bool:
456         """True when the display is refreshed automatically."""
457         return self._auto_refresh
458
459     @auto_refresh.setter
460     def auto_refresh(self, value: bool):
461         self._first_manual_refresh = not value
462         self._auto_refresh = value
463
464     @property
465     def brightness(self) -> float:
466         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
467         return self._brightness
468
469     @brightness.setter
470     def brightness(self, value: float):
471         if 0 <= float(value) <= 1.0:
472             if not self._backlight_on_high:
473                 value = 1.0 - value
474
475             if self._backlight_type == BACKLIGHT_PWM:
476                 self._backlight.duty_cycle = value * 0xFFFF
477             elif self._backlight_type == BACKLIGHT_IN_OUT:
478                 self._backlight.value = value > 0.99
479             elif self._brightness_command is not None:
480                 okay = self._core.begin_transaction()
481                 if okay:
482                     if self._core.data_as_commands:
483                         self._core.send(
484                             DISPLAY_COMMAND,
485                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
486                             bytes([self._brightness_command, round(0xFF * value)]),
487                         )
488                     else:
489                         self._core.send(
490                             DISPLAY_COMMAND,
491                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
492                             bytes([self._brightness_command]),
493                         )
494                         self._core.send(
495                             DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
496                         )
497                     self._core.end_transaction()
498             self._brightness = value
499         else:
500             raise ValueError("Brightness must be between 0.0 and 1.0")
501
502     @property
503     def width(self) -> int:
504         """Display Width"""
505         return self._core.get_width()
506
507     @property
508     def height(self) -> int:
509         """Display Height"""
510         return self._core.get_height()
511
512     @property
513     def rotation(self) -> int:
514         """The rotation of the display as an int in degrees."""
515         return self._core.get_rotation()
516
517     @rotation.setter
518     def rotation(self, value: int):
519         if value % 90 != 0:
520             raise ValueError("Display rotation must be in 90 degree increments")
521         transposed = self._core.rotation in (90, 270)
522         will_transposed = value in (90, 270)
523         if transposed != will_transposed:
524             self._core.width, self._core.height = self._core.height, self._core.width
525         self._core.set_rotation(value)
526         if self._core.current_group is not None:
527             self._core.current_group._update_transform(  # pylint: disable=protected-access
528                 self._core.transform
529             )
530
531     @property
532     def bus(self) -> _DisplayBus:
533         """Current Display Bus"""
534         return self._core.get_bus()
535
536     @property
537     def root_group(self) -> Group:
538         """
539         The root group on the display.
540         If the root group is set to `displayio.CIRCUITPYTHON_TERMINAL`, the default
541         CircuitPython terminal will be shown.
542         If the root group is set to ``None``, no output will be shown.
543         """
544         return self._core.current_group
545
546     @root_group.setter
547     def root_group(self, new_group: Group) -> None:
548         self._set_root_group(new_group)