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