]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
root_group properties/functions on Display object and displaycore.
[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 import threading
23 from typing import Optional
24 from dataclasses import astuple
25 import digitalio
26 from PIL import Image, ImageDraw
27 import numpy
28 import microcontroller
29 import circuitpython_typing
30 from ._displaycore import _DisplayCore
31 from ._displaybus import _DisplayBus
32 from ._colorconverter import ColorConverter
33 from ._group import Group
34 from ._structs import RectangleStruct
35 from ._constants import (
36     CHIP_SELECT_TOGGLE_EVERY_BYTE,
37     CHIP_SELECT_UNTOUCHED,
38     DISPLAY_COMMAND,
39     DISPLAY_DATA,
40     BACKLIGHT_IN_OUT,
41     BACKLIGHT_PWM,
42 )
43
44 __version__ = "0.0.0+auto.0"
45 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
46
47 displays = []
48
49
50 class Display:
51     # pylint: disable=too-many-instance-attributes
52     """This initializes a display and connects it into CircuitPython. Unlike other objects
53     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
54     This is done so that CircuitPython can use the display itself.
55
56     Most people should not use this class directly. Use a specific display driver instead
57     that will contain the initialization sequence at minimum.
58     """
59
60     def __init__(
61         self,
62         display_bus: _DisplayBus,
63         init_sequence: circuitpython_typing.ReadableBuffer,
64         *,
65         width: int,
66         height: int,
67         colstart: int = 0,
68         rowstart: int = 0,
69         rotation: int = 0,
70         color_depth: int = 16,
71         grayscale: bool = False,
72         pixels_in_byte_share_row: bool = True,
73         bytes_per_cell: int = 1,
74         reverse_pixels_in_byte: bool = False,
75         reverse_bytes_in_word: bool = True,
76         set_column_command: int = 0x2A,
77         set_row_command: int = 0x2B,
78         write_ram_command: int = 0x2C,
79         backlight_pin: Optional[microcontroller.Pin] = None,
80         brightness_command: Optional[int] = None,
81         brightness: float = 1.0,
82         auto_brightness: bool = False,
83         single_byte_bounds: bool = False,
84         data_as_commands: bool = False,
85         auto_refresh: bool = True,
86         native_frames_per_second: int = 60,
87         backlight_on_high: bool = True,
88         SH1107_addressing: bool = False,
89         set_vertical_scroll: int = 0,
90     ):
91         # pylint: disable=unused-argument,too-many-locals,invalid-name
92         """Create a Display object on the given display bus (`displayio.FourWire` or
93         `paralleldisplay.ParallelBus`).
94
95         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
96         with a command byte followed by a byte to determine the parameter count and if a
97         delay is need after. When the top bit of the second byte is 1, the next byte will be
98         the delay time in milliseconds. The remaining 7 bits are the parameter count
99         excluding any delay byte. The third through final bytes are the remaining command
100         parameters. The next byte will begin a new command definition. Here is a portion of
101         ILI9341 init code:
102
103         .. code-block:: python
104
105             init_sequence = (
106                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
107 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
108                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
109                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
110             )
111             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
112
113         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
114         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
115         Multiple byte literals (b”“) are merged together on load. The parens are needed to
116         allow byte literals on subsequent lines.
117
118         The initialization sequence should always leave the display memory access inline with
119         the scan of the display to minimize tearing artifacts.
120         """
121         ram_width = 0x100
122         ram_height = 0x100
123         if single_byte_bounds:
124             ram_width = 0xFF
125             ram_height = 0xFF
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         )
142
143         self._set_column_command = set_column_command
144         self._set_row_command = set_row_command
145         self._write_ram_command = write_ram_command
146         self._brightness_command = brightness_command
147         self._data_as_commands = data_as_commands
148         self._single_byte_bounds = single_byte_bounds
149         self._width = width
150         self._height = height
151         self._colstart = colstart
152         self._rowstart = rowstart
153         self._rotation = rotation
154         self._auto_brightness = auto_brightness
155         self._brightness = 1.0
156         self._auto_refresh = auto_refresh
157         self._initialize(init_sequence)
158         self._buffer = Image.new("RGB", (width, height))
159         self._subrectangles = []
160         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
161
162         displays.append(self)
163         self._refresh_thread = None
164         if self._auto_refresh:
165             self.auto_refresh = True
166         self._colorconverter = ColorConverter()
167
168         self._backlight_type = None
169         if backlight_pin is not None:
170             try:
171                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
172
173                 # 100Hz looks decent and doesn't keep the CPU too busy
174                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
175                 self._backlight_type = BACKLIGHT_PWM
176             except ImportError:
177                 # PWMOut not implemented on this platform
178                 pass
179             if self._backlight_type is None:
180                 self._backlight_type = BACKLIGHT_IN_OUT
181                 self._backlight = digitalio.DigitalInOut(backlight_pin)
182                 self._backlight.switch_to_output()
183             self.brightness = brightness
184
185     def _initialize(self, init_sequence):
186         i = 0
187         while i < len(init_sequence):
188             command = init_sequence[i]
189             data_size = init_sequence[i + 1]
190             delay = (data_size & 0x80) > 0
191             data_size &= ~0x80
192
193             if self._data_as_commands:
194                 self._core.send(
195                     DISPLAY_COMMAND,
196                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
197                     bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
198                 )
199             else:
200                 self._core.send(
201                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
202                 )
203                 self._core.send(
204                     DISPLAY_DATA,
205                     CHIP_SELECT_UNTOUCHED,
206                     init_sequence[i + 2 : i + 2 + data_size],
207                 )
208             delay_time_ms = 10
209             if delay:
210                 data_size += 1
211                 delay_time_ms = init_sequence[i + 1 + data_size]
212                 if delay_time_ms == 255:
213                     delay_time_ms = 500
214             time.sleep(delay_time_ms / 1000)
215             i += 2 + data_size
216
217     def _send(self, command, data):
218         self._core.begin_transaction()
219         if self._data_as_commands:
220             self._core.send(
221                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
222             )
223         else:
224             self._core.send(
225                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
226             )
227             self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
228         self._core.end_transaction()
229
230     def _send_pixels(self, data):
231         if not self._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, data)
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.set_root_group(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         # pylint: disable=unused-argument, protected-access
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         if not self._core.start_refresh():
264             return False
265
266         force_full_refresh = False
267         # Go through groups and and add each to buffer
268         if self._core._current_group is not None:
269             buffer = Image.new("RGBA", (self._core._width, self._core._height))
270             # Recursively have everything draw to the image
271             self._core._current_group._fill_area(
272                 buffer
273             )  # pylint: disable=protected-access
274             # save image to buffer (or probably refresh buffer so we can compare)
275             self._buffer.paste(buffer)
276         else:
277             # show nothing
278             buffer = Image.new("RGBA", (self._core._width, self._core._height))
279             draw = ImageDraw.Draw(buffer)
280             draw.rectangle([(0, 0), buffer.size], fill=(0, 0, 0))
281             self._buffer.paste(buffer)
282             force_full_refresh = True
283
284         if force_full_refresh:
285             full_rect = RectangleStruct(0, 0, self._width, self._height)
286             self._refresh_display_area(full_rect)
287         else:
288             self._subrectangles = self._core.get_refresh_areas()
289             for area in self._subrectangles:
290                 self._refresh_display_area(area)
291
292         self._core.finish_refresh()
293
294         return True
295
296     def _refresh_loop(self):
297         while self._auto_refresh:
298             self.refresh()
299
300     def _refresh_display_area(self, rectangle):
301         """Loop through dirty rectangles and redraw that area."""
302         img = self._buffer.convert("RGB").crop(astuple(rectangle))
303         img = img.rotate(360 - self._rotation, expand=True)
304
305         display_rectangle = self._apply_rotation(rectangle)
306         img = img.crop(astuple(self._clip(display_rectangle)))
307
308         data = numpy.array(img).astype("uint16")
309         color = (
310             ((data[:, :, 0] & 0xF8) << 8)
311             | ((data[:, :, 1] & 0xFC) << 3)
312             | (data[:, :, 2] >> 3)
313         )
314
315         pixels = bytes(
316             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
317         )
318
319         self._send(
320             self._set_column_command,
321             self._encode_pos(
322                 display_rectangle.x1 + self._colstart,
323                 display_rectangle.x2 + self._colstart - 1,
324             ),
325         )
326         self._send(
327             self._set_row_command,
328             self._encode_pos(
329                 display_rectangle.y1 + self._rowstart,
330                 display_rectangle.y2 + self._rowstart - 1,
331             ),
332         )
333
334         self._core.begin_transaction()
335         self._send_pixels(pixels)
336         self._core.end_transaction()
337
338     def _clip(self, rectangle):
339         if self._rotation in (90, 270):
340             width, height = self._height, self._width
341         else:
342             width, height = self._width, self._height
343
344         rectangle.x1 = max(rectangle.x1, 0)
345         rectangle.y1 = max(rectangle.y1, 0)
346         rectangle.x2 = min(rectangle.x2, width)
347         rectangle.y2 = min(rectangle.y2, height)
348
349         return rectangle
350
351     def _apply_rotation(self, rectangle):
352         """Adjust the rectangle coordinates based on rotation"""
353         if self._rotation == 90:
354             return RectangleStruct(
355                 self._height - rectangle.y2,
356                 rectangle.x1,
357                 self._height - rectangle.y1,
358                 rectangle.x2,
359             )
360         if self._rotation == 180:
361             return RectangleStruct(
362                 self._width - rectangle.x2,
363                 self._height - rectangle.y2,
364                 self._width - rectangle.x1,
365                 self._height - rectangle.y1,
366             )
367         if self._rotation == 270:
368             return RectangleStruct(
369                 rectangle.y1,
370                 self._width - rectangle.x2,
371                 rectangle.y2,
372                 self._width - rectangle.x1,
373             )
374         return rectangle
375
376     def _encode_pos(self, x, y):
377         """Encode a postion into bytes."""
378         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
379
380     def fill_row(
381         self, y: int, buffer: circuitpython_typing.WriteableBuffer
382     ) -> circuitpython_typing.WriteableBuffer:
383         """Extract the pixels from a single row"""
384         for x in range(0, self._width):
385             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
386             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
387             buffer[x * 2 + 1] = _rgb_565 & 0xFF
388         return buffer
389
390     @property
391     def auto_refresh(self) -> bool:
392         """True when the display is refreshed automatically."""
393         return self._auto_refresh
394
395     @auto_refresh.setter
396     def auto_refresh(self, value: bool):
397         self._auto_refresh = value
398         if self._refresh_thread is None:
399             self._refresh_thread = threading.Thread(
400                 target=self._refresh_loop, daemon=True
401             )
402         if value and not self._refresh_thread.is_alive():
403             # Start the thread
404             self._refresh_thread.start()
405         elif not value and self._refresh_thread.is_alive():
406             # Stop the thread
407             self._refresh_thread.join()
408
409     @property
410     def brightness(self) -> float:
411         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
412         When `auto_brightness` is True, the value of `brightness` will change automatically.
413         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
414         """
415         return self._brightness
416
417     @brightness.setter
418     def brightness(self, value: float):
419         if 0 <= float(value) <= 1.0:
420             self._brightness = value
421             if self._backlight_type == BACKLIGHT_IN_OUT:
422                 self._backlight.value = round(self._brightness)
423             elif self._backlight_type == BACKLIGHT_PWM:
424                 self._backlight.duty_cycle = self._brightness * 65535
425             elif self._brightness_command is not None:
426                 self._send(self._brightness_command, round(value * 255))
427         else:
428             raise ValueError("Brightness must be between 0.0 and 1.0")
429
430     @property
431     def auto_brightness(self) -> bool:
432         """True when the display brightness is adjusted automatically, based on an ambient
433         light sensor or other method. Note that some displays may have this set to True by
434         default, but not actually implement automatic brightness adjustment.
435         `auto_brightness` is set to False if `brightness` is set manually.
436         """
437         return self._auto_brightness
438
439     @auto_brightness.setter
440     def auto_brightness(self, value: bool):
441         self._auto_brightness = value
442
443     @property
444     def width(self) -> int:
445         """Display Width"""
446         return self._core.get_width()
447
448     @property
449     def height(self) -> int:
450         """Display Height"""
451         return self._core.get_height()
452
453     @property
454     def rotation(self) -> int:
455         """The rotation of the display as an int in degrees."""
456         return self._core.get_rotation()
457
458     @rotation.setter
459     def rotation(self, value: int):
460         self._core.set_rotation(value)
461
462     @property
463     def bus(self) -> _DisplayBus:
464         """Current Display Bus"""
465         return self._core.get_bus()
466
467     @property
468     def root_group(self) -> Group:
469         """The root group on the display."""
470         return self._core.get_root_group()
471
472     @root_group.setter
473     def root_group(self, new_group):
474         """Switches to displaying the given group of layers. When group is None,
475         a blank screen will be shown.
476         """
477         self._core.set_root_group(new_group)