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