]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
Added typing and missing CP7 functions
[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 import digitalio
24 from PIL import Image
25 import numpy
26 import microcontroller
27 from recordclass import recordclass
28 from ._colorconverter import ColorConverter
29 import _typing
30 from ._group import Group
31 from displayio import _DisplayBus
32 from typing import Optional
33 from ._constants import (
34     CHIP_SELECT_TOGGLE_EVERY_BYTE,
35     CHIP_SELECT_UNTOUCHED,
36     DISPLAY_COMMAND,
37     DISPLAY_DATA,
38 )
39
40 __version__ = "0.0.0-auto.0"
41 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
42
43 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
44 displays = []
45
46 BACKLIGHT_IN_OUT = 1
47 BACKLIGHT_PWM = 2
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         set_column_command: int = 0x2A,
76         set_row_command: int = 0x2B,
77         write_ram_command: int = 0x2C,
78         backlight_pin: Optional[microcontroller.Pin] = None,
79         brightness_command: Optional[int] = None,
80         brightness: float = 1.0,
81         auto_brightness: bool = False,
82         single_byte_bounds: bool = False,
83         data_as_commands: bool = False,
84         auto_refresh: bool = True,
85         native_frames_per_second: int = 60,
86         backlight_on_high: bool = True,
87         SH1107_addressing: bool = False,
88         set_vertical_scroll: int = 0,
89     ):
90         # pylint: disable=unused-argument,too-many-locals
91         """Create a Display object on the given display bus (`displayio.FourWire` or
92         `paralleldisplay.ParallelBus`).
93
94         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
95         with a command byte followed by a byte to determine the parameter count and if a
96         delay is need after. When the top bit of the second byte is 1, the next byte will be
97         the delay time in milliseconds. The remaining 7 bits are the parameter count
98         excluding any delay byte. The third through final bytes are the remaining command
99         parameters. The next byte will begin a new command definition. Here is a portion of
100         ILI9341 init code:
101
102         .. code-block:: python
103
104             init_sequence = (
105                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
106 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
107                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
108                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
109             )
110             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
111
112         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
113         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
114         Multiple byte literals (b”“) are merged together on load. The parens are needed to
115         allow byte literals on subsequent lines.
116
117         The initialization sequence should always leave the display memory access inline with
118         the scan of the display to minimize tearing artifacts.
119         """
120         self._bus = display_bus
121         self._set_column_command = set_column_command
122         self._set_row_command = set_row_command
123         self._write_ram_command = write_ram_command
124         self._brightness_command = brightness_command
125         self._data_as_commands = data_as_commands
126         self._single_byte_bounds = single_byte_bounds
127         self._width = width
128         self._height = height
129         self._colstart = colstart
130         self._rowstart = rowstart
131         self._rotation = rotation
132         self._auto_brightness = auto_brightness
133         self._brightness = 1.0
134         self._auto_refresh = auto_refresh
135         self._initialize(init_sequence)
136         self._buffer = Image.new("RGB", (width, height))
137         self._subrectangles = []
138         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
139         self._current_group = None
140         displays.append(self)
141         self._refresh_thread = None
142         if self._auto_refresh:
143             self.auto_refresh = True
144         self._colorconverter = ColorConverter()
145
146         self._backlight_type = None
147         if backlight_pin is not None:
148             try:
149                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
150
151                 # 100Hz looks decent and doesn't keep the CPU too busy
152                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
153                 self._backlight_type = BACKLIGHT_PWM
154             except ImportError:
155                 # PWMOut not implemented on this platform
156                 pass
157             if self._backlight_type is None:
158                 self._backlight_type = BACKLIGHT_IN_OUT
159                 self._backlight = digitalio.DigitalInOut(backlight_pin)
160                 self._backlight.switch_to_output()
161             self.brightness = brightness
162
163     def _initialize(self, init_sequence):
164         i = 0
165         while i < len(init_sequence):
166             command = init_sequence[i]
167             data_size = init_sequence[i + 1]
168             delay = (data_size & 0x80) > 0
169             data_size &= ~0x80
170
171             self._send(command, init_sequence[i + 2 : i + 2 + data_size])
172             delay_time_ms = 10
173             if delay:
174                 data_size += 1
175                 delay_time_ms = init_sequence[i + 1 + data_size]
176                 if delay_time_ms == 255:
177                     delay_time_ms = 500
178             time.sleep(delay_time_ms / 1000)
179             i += 2 + data_size
180
181     def _send(self, command, data):
182         self._bus._begin_transaction()  # pylint: disable=protected-access
183         if self._data_as_commands:
184             self._bus._send(
185                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
186             )
187         else:
188             self._bus._send(
189                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
190             )
191             self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
192         self._bus._end_transaction()  # pylint: disable=protected-access
193
194     def _send_pixels(self, data):
195         if not self._data_as_commands:
196             self._bus._send(
197                 DISPLAY_COMMAND,
198                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
199                 bytes([self._write_ram_command]),
200             )
201         self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
202
203     def _release(self):
204         self._bus._release()  # pylint: disable=protected-access
205         self._bus = None
206
207     def show(self, group: Group) -> None:
208         """Switches to displaying the given group of layers. When group is None, the
209         default CircuitPython terminal will be shown.
210         """
211         self._current_group = group
212
213     def refresh(
214         self,
215         *,
216         target_frames_per_second: Optional[int] = None,
217         minimum_frames_per_second: int = 0,
218     ) -> bool:
219         # pylint: disable=unused-argument
220         """When auto refresh is off, waits for the target frame rate and then refreshes the
221         display, returning True. If the call has taken too long since the last refresh call
222         for the given target frame rate, then the refresh returns False immediately without
223         updating the screen to hopefully help getting caught up.
224
225         If the time since the last successful refresh is below the minimum frame rate, then
226         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
227
228         When auto refresh is on, updates the display immediately. (The display will also
229         update without calls to this.)
230         """
231         self._subrectangles = []
232
233         # Go through groups and and add each to buffer
234         if self._current_group is not None:
235             buffer = Image.new("RGBA", (self._width, self._height))
236             # Recursively have everything draw to the image
237             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
238             # save image to buffer (or probably refresh buffer so we can compare)
239             self._buffer.paste(buffer)
240
241         if self._current_group is not None:
242             # Eventually calculate dirty rectangles here
243             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
244
245         for area in self._subrectangles:
246             self._refresh_display_area(area)
247
248         return True
249
250     def _refresh_loop(self):
251         while self._auto_refresh:
252             self.refresh()
253
254     def _refresh_display_area(self, rectangle):
255         """Loop through dirty rectangles and redraw that area."""
256
257         img = self._buffer.convert("RGB").crop(rectangle)
258         img = img.rotate(self._rotation, expand=True)
259
260         display_rectangle = self._apply_rotation(rectangle)
261         img = img.crop(self._clip(display_rectangle))
262
263         data = numpy.array(img).astype("uint16")
264         color = (
265             ((data[:, :, 0] & 0xF8) << 8)
266             | ((data[:, :, 1] & 0xFC) << 3)
267             | (data[:, :, 2] >> 3)
268         )
269
270         pixels = bytes(
271             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
272         )
273
274         self._send(
275             self._set_column_command,
276             self._encode_pos(
277                 display_rectangle.x1 + self._colstart,
278                 display_rectangle.x2 + self._colstart - 1,
279             ),
280         )
281         self._send(
282             self._set_row_command,
283             self._encode_pos(
284                 display_rectangle.y1 + self._rowstart,
285                 display_rectangle.y2 + self._rowstart - 1,
286             ),
287         )
288
289         self._bus._begin_transaction()  # pylint: disable=protected-access
290         self._send_pixels(pixels)
291         self._bus._end_transaction()  # pylint: disable=protected-access
292
293     def _clip(self, rectangle):
294         if self._rotation in (90, 270):
295             width, height = self._height, self._width
296         else:
297             width, height = self._width, self._height
298
299         if rectangle.x1 < 0:
300             rectangle.x1 = 0
301         if rectangle.y1 < 0:
302             rectangle.y1 = 0
303         if rectangle.x2 > width:
304             rectangle.x2 = width
305         if rectangle.y2 > height:
306             rectangle.y2 = height
307
308         return rectangle
309
310     def _apply_rotation(self, rectangle):
311         """Adjust the rectangle coordinates based on rotation"""
312         if self._rotation == 90:
313             return Rectangle(
314                 self._height - rectangle.y2,
315                 rectangle.x1,
316                 self._height - rectangle.y1,
317                 rectangle.x2,
318             )
319         if self._rotation == 180:
320             return Rectangle(
321                 self._width - rectangle.x2,
322                 self._height - rectangle.y2,
323                 self._width - rectangle.x1,
324                 self._height - rectangle.y1,
325             )
326         if self._rotation == 270:
327             return Rectangle(
328                 rectangle.y1,
329                 self._width - rectangle.x2,
330                 rectangle.y2,
331                 self._width - rectangle.x1,
332             )
333         return rectangle
334
335     def _encode_pos(self, x, y):
336         """Encode a postion into bytes."""
337         return struct.pack(self._bounds_encoding, x, y)
338
339     def fill_row(
340         self, y: int, buffer: _typing.WriteableBuffer
341     ) -> _typing.WriteableBuffer:
342         """Extract the pixels from a single row"""
343         for x in range(0, self._width):
344             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
345             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
346             buffer[x * 2 + 1] = _rgb_565 & 0xFF
347         return buffer
348
349     @property
350     def auto_refresh(self) -> bool:
351         """True when the display is refreshed automatically."""
352         return self._auto_refresh
353
354     @auto_refresh.setter
355     def auto_refresh(self, value: bool):
356         self._auto_refresh = value
357         if self._refresh_thread is None:
358             self._refresh_thread = threading.Thread(
359                 target=self._refresh_loop, daemon=True
360             )
361         if value and not self._refresh_thread.is_alive():
362             # Start the thread
363             self._refresh_thread.start()
364         elif not value and self._refresh_thread.is_alive():
365             # Stop the thread
366             self._refresh_thread.join()
367
368     @property
369     def brightness(self) -> float:
370         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
371         When `auto_brightness` is True, the value of `brightness` will change automatically.
372         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
373         """
374         return self._brightness
375
376     @brightness.setter
377     def brightness(self, value: float):
378         if 0 <= float(value) <= 1.0:
379             self._brightness = value
380             if self._backlight_type == BACKLIGHT_IN_OUT:
381                 self._backlight.value = round(self._brightness)
382             elif self._backlight_type == BACKLIGHT_PWM:
383                 self._backlight.duty_cycle = self._brightness * 65535
384             elif self._brightness_command is not None:
385                 self._send(self._brightness_command, round(value * 255))
386         else:
387             raise ValueError("Brightness must be between 0.0 and 1.0")
388
389     @property
390     def auto_brightness(self) -> bool:
391         """True when the display brightness is adjusted automatically, based on an ambient
392         light sensor or other method. Note that some displays may have this set to True by
393         default, but not actually implement automatic brightness adjustment.
394         `auto_brightness` is set to False if `brightness` is set manually.
395         """
396         return self._auto_brightness
397
398     @auto_brightness.setter
399     def auto_brightness(self, value: bool):
400         self._auto_brightness = value
401
402     @property
403     def width(self) -> int:
404         """Display Width"""
405         return self._width
406
407     @property
408     def height(self) -> int:
409         """Display Height"""
410         return self._height
411
412     @property
413     def rotation(self) -> int:
414         """The rotation of the display as an int in degrees."""
415         return self._rotation
416
417     @rotation.setter
418     def rotation(self, value: int):
419         if value not in (0, 90, 180, 270):
420             raise ValueError("Rotation must be 0/90/180/270")
421         self._rotation = value
422
423     @property
424     def bus(self) -> _DisplayBus:
425         """Current Display Bus"""
426         return self._bus