]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
1e429954a9851a16196f783e506299cf0c9997e9
[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
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         self._current_group = None
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.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         # 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         # Go through groups and and add each to buffer
267         if self._core._current_group is not None:
268             buffer = Image.new("RGBA", (self._core._width, self._core._height))
269             # Recursively have everything draw to the image
270             self._core._current_group._fill_area(
271                 buffer
272             )  # pylint: disable=protected-access
273             # save image to buffer (or probably refresh buffer so we can compare)
274             self._buffer.paste(buffer)
275         else:
276             # show nothing
277             print("show nothing")
278             buffer = Image.new("RGBA", (self._core._width, self._core._height))
279             self._buffer.paste(buffer)
280
281         self._subrectangles = self._core.get_refresh_areas()
282
283         for area in self._subrectangles:
284             self._refresh_display_area(area)
285
286         self._core.finish_refresh()
287
288         return True
289
290     def _refresh_loop(self):
291         while self._auto_refresh:
292             self.refresh()
293
294     def _refresh_display_area(self, rectangle):
295         """Loop through dirty rectangles and redraw that area."""
296         img = self._buffer.convert("RGB").crop(astuple(rectangle))
297         img = img.rotate(360 - self._rotation, expand=True)
298
299         display_rectangle = self._apply_rotation(rectangle)
300         img = img.crop(astuple(self._clip(display_rectangle)))
301
302         data = numpy.array(img).astype("uint16")
303         color = (
304             ((data[:, :, 0] & 0xF8) << 8)
305             | ((data[:, :, 1] & 0xFC) << 3)
306             | (data[:, :, 2] >> 3)
307         )
308
309         pixels = bytes(
310             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
311         )
312
313         self._send(
314             self._set_column_command,
315             self._encode_pos(
316                 display_rectangle.x1 + self._colstart,
317                 display_rectangle.x2 + self._colstart - 1,
318             ),
319         )
320         self._send(
321             self._set_row_command,
322             self._encode_pos(
323                 display_rectangle.y1 + self._rowstart,
324                 display_rectangle.y2 + self._rowstart - 1,
325             ),
326         )
327
328         self._core.begin_transaction()
329         self._send_pixels(pixels)
330         self._core.end_transaction()
331
332     def _clip(self, rectangle):
333         if self._rotation in (90, 270):
334             width, height = self._height, self._width
335         else:
336             width, height = self._width, self._height
337
338         rectangle.x1 = max(rectangle.x1, 0)
339         rectangle.y1 = max(rectangle.y1, 0)
340         rectangle.x2 = min(rectangle.x2, width)
341         rectangle.y2 = min(rectangle.y2, height)
342
343         return rectangle
344
345     def _apply_rotation(self, rectangle):
346         """Adjust the rectangle coordinates based on rotation"""
347         if self._rotation == 90:
348             return RectangleStruct(
349                 self._height - rectangle.y2,
350                 rectangle.x1,
351                 self._height - rectangle.y1,
352                 rectangle.x2,
353             )
354         if self._rotation == 180:
355             return RectangleStruct(
356                 self._width - rectangle.x2,
357                 self._height - rectangle.y2,
358                 self._width - rectangle.x1,
359                 self._height - rectangle.y1,
360             )
361         if self._rotation == 270:
362             return RectangleStruct(
363                 rectangle.y1,
364                 self._width - rectangle.x2,
365                 rectangle.y2,
366                 self._width - rectangle.x1,
367             )
368         return rectangle
369
370     def _encode_pos(self, x, y):
371         """Encode a postion into bytes."""
372         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
373
374     def fill_row(
375         self, y: int, buffer: circuitpython_typing.WriteableBuffer
376     ) -> circuitpython_typing.WriteableBuffer:
377         """Extract the pixels from a single row"""
378         for x in range(0, self._width):
379             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
380             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
381             buffer[x * 2 + 1] = _rgb_565 & 0xFF
382         return buffer
383
384     @property
385     def auto_refresh(self) -> bool:
386         """True when the display is refreshed automatically."""
387         return self._auto_refresh
388
389     @auto_refresh.setter
390     def auto_refresh(self, value: bool):
391         self._auto_refresh = value
392         if self._refresh_thread is None:
393             self._refresh_thread = threading.Thread(
394                 target=self._refresh_loop, daemon=True
395             )
396         if value and not self._refresh_thread.is_alive():
397             # Start the thread
398             self._refresh_thread.start()
399         elif not value and self._refresh_thread.is_alive():
400             # Stop the thread
401             self._refresh_thread.join()
402
403     @property
404     def brightness(self) -> float:
405         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
406         When `auto_brightness` is True, the value of `brightness` will change automatically.
407         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
408         """
409         return self._brightness
410
411     @brightness.setter
412     def brightness(self, value: float):
413         if 0 <= float(value) <= 1.0:
414             self._brightness = value
415             if self._backlight_type == BACKLIGHT_IN_OUT:
416                 self._backlight.value = round(self._brightness)
417             elif self._backlight_type == BACKLIGHT_PWM:
418                 self._backlight.duty_cycle = self._brightness * 65535
419             elif self._brightness_command is not None:
420                 self._send(self._brightness_command, round(value * 255))
421         else:
422             raise ValueError("Brightness must be between 0.0 and 1.0")
423
424     @property
425     def auto_brightness(self) -> bool:
426         """True when the display brightness is adjusted automatically, based on an ambient
427         light sensor or other method. Note that some displays may have this set to True by
428         default, but not actually implement automatic brightness adjustment.
429         `auto_brightness` is set to False if `brightness` is set manually.
430         """
431         return self._auto_brightness
432
433     @auto_brightness.setter
434     def auto_brightness(self, value: bool):
435         self._auto_brightness = value
436
437     @property
438     def width(self) -> int:
439         """Display Width"""
440         return self._core.get_width()
441
442     @property
443     def height(self) -> int:
444         """Display Height"""
445         return self._core.get_height()
446
447     @property
448     def rotation(self) -> int:
449         """The rotation of the display as an int in degrees."""
450         return self._core.get_rotation()
451
452     @rotation.setter
453     def rotation(self, value: int):
454         self._core.set_rotation(value)
455
456     @property
457     def bus(self) -> _DisplayBus:
458         """Current Display Bus"""
459         return self._core.get_bus()
460
461     @property
462     def root_group(self) -> Group:
463         return self._current_group
464
465     @root_group.setter
466     def root_group(self, new_group):
467         self._current_group = new_group