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