]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
eb3655d9ed2405598182f389c42a188d41cefc23
[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 from dataclasses import astuple
24 import digitalio
25 from PIL import Image
26 import numpy
27 import microcontroller
28 import circuitpython_typing
29 from ._displaycore import _DisplayCore
30 from ._displaybus import _DisplayBus
31 from ._colorconverter import ColorConverter
32 from ._group import Group
33 from ._structs import RectangleStruct
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._subrectangles = []
167         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
168         self._current_group = None
169         self._last_refresh_call = 0
170         self._refresh_thread = None
171         if self._auto_refresh:
172             self.auto_refresh = True
173         self._colorconverter = ColorConverter()
174
175         self._backlight_type = None
176         if backlight_pin is not None:
177             try:
178                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
179
180                 # 100Hz looks decent and doesn't keep the CPU too busy
181                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
182                 self._backlight_type = BACKLIGHT_PWM
183             except ImportError:
184                 # PWMOut not implemented on this platform
185                 pass
186             if self._backlight_type is None:
187                 self._backlight_type = BACKLIGHT_IN_OUT
188                 self._backlight = digitalio.DigitalInOut(backlight_pin)
189                 self._backlight.switch_to_output()
190             self.brightness = brightness
191
192     def __new__(cls, *args, **kwargs):
193         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
194             allocate_display,
195         )
196
197         allocate_display(cls)
198         return super().__new__(cls)
199
200     def _initialize(self, init_sequence):
201         i = 0
202         while i < len(init_sequence):
203             command = init_sequence[i]
204             data_size = init_sequence[i + 1]
205             delay = (data_size & 0x80) > 0
206             data_size &= ~0x80
207
208             if self._core.data_as_commands:
209                 self._core.send(
210                     DISPLAY_COMMAND,
211                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
212                     bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
213                 )
214             else:
215                 self._core.send(
216                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
217                 )
218                 self._core.send(
219                     DISPLAY_DATA,
220                     CHIP_SELECT_UNTOUCHED,
221                     init_sequence[i + 2 : i + 2 + data_size],
222                 )
223             delay_time_ms = 10
224             if delay:
225                 data_size += 1
226                 delay_time_ms = init_sequence[i + 1 + data_size]
227                 if delay_time_ms == 255:
228                     delay_time_ms = 500
229             time.sleep(delay_time_ms / 1000)
230             i += 2 + data_size
231
232     def _send(self, command, data):
233         self._core.begin_transaction()
234         if self._core.data_as_commands:
235             self._core.send(
236                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
237             )
238         else:
239             self._core.send(
240                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
241             )
242             self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
243         self._core.end_transaction()
244
245     def _send_pixels(self, data):
246         if not self._core.data_as_commands:
247             self._core.send(
248                 DISPLAY_COMMAND,
249                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
250                 bytes([self._write_ram_command]),
251             )
252         self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
253
254     def show(self, group: Group) -> None:
255         """Switches to displaying the given group of layers. When group is None, the
256         default CircuitPython terminal will be shown.
257         """
258         self._core.show(group)
259
260     def refresh(
261         self,
262         *,
263         target_frames_per_second: Optional[int] = None,
264         minimum_frames_per_second: int = 0,
265     ) -> bool:
266         """When auto refresh is off, waits for the target frame rate and then refreshes the
267         display, returning True. If the call has taken too long since the last refresh call
268         for the given target frame rate, then the refresh returns False immediately without
269         updating the screen to hopefully help getting caught up.
270
271         If the time since the last successful refresh is below the minimum frame rate, then
272         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
273
274         When auto refresh is on, updates the display immediately. (The display will also
275         update without calls to this.)
276         """
277         maximum_ms_per_real_frame = 0xFFFFFFFF
278         if minimum_frames_per_second > 0:
279             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
280
281         if target_frames_per_second is None:
282             target_ms_per_frame = 0xFFFFFFFF
283         else:
284             target_ms_per_frame = 1000 // target_frames_per_second
285
286         if (
287             not self._auto_refresh
288             and not self._first_manual_refresh
289             and target_ms_per_frame != 0xFFFFFFFF
290         ):
291             current_time = time.monotonic() * 1000
292             current_ms_since_real_refresh = current_time - self._core.last_refresh
293             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
294                 raise RuntimeError("Below minimum frame rate")
295             current_ms_since_last_call = current_time - self._last_refresh_call
296             self._last_refresh_call = current_time
297             if current_ms_since_last_call > target_ms_per_frame:
298                 return False
299
300             remaining_time = target_ms_per_frame - (
301                 current_ms_since_real_refresh % target_ms_per_frame
302             )
303             time.sleep(remaining_time / 1000)
304         self._first_manual_refresh = False
305         self._refresh_display()
306         return True
307
308     def _refresh_display(self):
309         # pylint: disable=protected-access
310         if not self._core.start_refresh():
311             return False
312
313         # Go through groups and and add each to buffer
314         if self._core.current_group is not None:
315             buffer = Image.new("RGBA", (self._core.width, self._core.height))
316             # Recursively have everything draw to the image
317             self._core.current_group._fill_area(
318                 buffer
319             )  # pylint: disable=protected-access
320             # save image to buffer (or probably refresh buffer so we can compare)
321             self._buffer.paste(buffer)
322
323         self._subrectangles = self._core.get_refresh_areas()
324
325         for area in self._subrectangles:
326             self._refresh_display_area(area)
327
328         self._core.finish_refresh()
329
330         return True
331
332     def _background(self):
333         if (
334             self._auto_refresh
335             and (time.monotonic() * 1000 - self._core.last_refresh)
336             > self._native_ms_per_frame
337         ):
338             self.refresh()
339
340     def _refresh_display_area(self, rectangle):
341         """Loop through dirty rectangles and redraw that area."""
342         img = self._buffer.convert("RGB").crop(astuple(rectangle))
343         img = img.rotate(360 - self._core.rotation, expand=True)
344
345         display_rectangle = self._apply_rotation(rectangle)
346         img = img.crop(astuple(self._clip(display_rectangle)))
347
348         data = numpy.array(img).astype("uint16")
349         color = (
350             ((data[:, :, 0] & 0xF8) << 8)
351             | ((data[:, :, 1] & 0xFC) << 3)
352             | (data[:, :, 2] >> 3)
353         )
354
355         pixels = bytes(
356             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
357         )
358
359         self._send(
360             self._core.column_command,
361             self._encode_pos(
362                 display_rectangle.x1 + self._core.colstart,
363                 display_rectangle.x2 + self._core.colstart - 1,
364             ),
365         )
366         self._send(
367             self._core.row_command,
368             self._encode_pos(
369                 display_rectangle.y1 + self._core.rowstart,
370                 display_rectangle.y2 + self._core.rowstart - 1,
371             ),
372         )
373
374         self._core.begin_transaction()
375         self._send_pixels(pixels)
376         self._core.end_transaction()
377
378     def _clip(self, rectangle):
379         if self._core.rotation in (90, 270):
380             width, height = self._core.height, self._core.width
381         else:
382             width, height = self._core.width, self._core.height
383
384         rectangle.x1 = max(rectangle.x1, 0)
385         rectangle.y1 = max(rectangle.y1, 0)
386         rectangle.x2 = min(rectangle.x2, width)
387         rectangle.y2 = min(rectangle.y2, height)
388
389         return rectangle
390
391     def _apply_rotation(self, rectangle):
392         """Adjust the rectangle coordinates based on rotation"""
393         if self._core.rotation == 90:
394             return RectangleStruct(
395                 self._core.height - rectangle.y2,
396                 rectangle.x1,
397                 self._core.height - rectangle.y1,
398                 rectangle.x2,
399             )
400         if self._core.rotation == 180:
401             return RectangleStruct(
402                 self._core.width - rectangle.x2,
403                 self._core.height - rectangle.y2,
404                 self._core.width - rectangle.x1,
405                 self._core.height - rectangle.y1,
406             )
407         if self._core.rotation == 270:
408             return RectangleStruct(
409                 rectangle.y1,
410                 self._core.width - rectangle.x2,
411                 rectangle.y2,
412                 self._core.width - rectangle.x1,
413             )
414         return rectangle
415
416     def _encode_pos(self, x, y):
417         """Encode a postion into bytes."""
418         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
419
420     def fill_row(
421         self, y: int, buffer: circuitpython_typing.WriteableBuffer
422     ) -> circuitpython_typing.WriteableBuffer:
423         """Extract the pixels from a single row"""
424         for x in range(0, self._core.width):
425             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
426             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
427             buffer[x * 2 + 1] = _rgb_565 & 0xFF
428         return buffer
429
430     @property
431     def auto_refresh(self) -> bool:
432         """True when the display is refreshed automatically."""
433         return self._auto_refresh
434
435     @auto_refresh.setter
436     def auto_refresh(self, value: bool):
437         self._auto_refresh = value
438
439     @property
440     def brightness(self) -> float:
441         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
442         When `auto_brightness` is True, the value of `brightness` will change automatically.
443         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
444         """
445         return self._brightness
446
447     @brightness.setter
448     def brightness(self, value: float):
449         if 0 <= float(value) <= 1.0:
450             self._brightness = value
451             if self._backlight_type == BACKLIGHT_IN_OUT:
452                 self._backlight.value = round(self._brightness)
453             elif self._backlight_type == BACKLIGHT_PWM:
454                 self._backlight.duty_cycle = self._brightness * 65535
455             elif self._brightness_command is not None:
456                 self._send(self._brightness_command, round(value * 255))
457         else:
458             raise ValueError("Brightness must be between 0.0 and 1.0")
459
460     @property
461     def auto_brightness(self) -> bool:
462         """True when the display brightness is adjusted automatically, based on an ambient
463         light sensor or other method. Note that some displays may have this set to True by
464         default, but not actually implement automatic brightness adjustment.
465         `auto_brightness` is set to False if `brightness` is set manually.
466         """
467         return self._auto_brightness
468
469     @auto_brightness.setter
470     def auto_brightness(self, value: bool):
471         self._auto_brightness = value
472
473     @property
474     def width(self) -> int:
475         """Display Width"""
476         return self._core.get_width()
477
478     @property
479     def height(self) -> int:
480         """Display Height"""
481         return self._core.get_height()
482
483     @property
484     def rotation(self) -> int:
485         """The rotation of the display as an int in degrees."""
486         return self._core.get_rotation()
487
488     @rotation.setter
489     def rotation(self, value: int):
490         self._core.set_rotation(value)
491
492     @property
493     def bus(self) -> _DisplayBus:
494         """Current Display Bus"""
495         return self._core.get_bus()