]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
Linted stuff
[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 import digitalio
25 from PIL import Image
26 import numpy
27 import microcontroller
28 from recordclass import recordclass
29 import _typing
30 from ._displaybus import _DisplayBus
31 from ._colorconverter import ColorConverter
32 from ._group import Group
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,invalid-name
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         # pylint: disable=protected-access
183         self._bus._begin_transaction()
184         if self._data_as_commands:
185             self._bus._send(
186                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
187             )
188         else:
189             self._bus._send(
190                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
191             )
192             self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
193         self._bus._end_transaction()
194
195     def _send_pixels(self, data):
196         # pylint: disable=protected-access
197         if not self._data_as_commands:
198             self._bus._send(
199                 DISPLAY_COMMAND,
200                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
201                 bytes([self._write_ram_command]),
202             )
203         self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
204
205     def _release(self):
206         self._bus._release()  # pylint: disable=protected-access
207         self._bus = None
208
209     def show(self, group: Group) -> None:
210         """Switches to displaying the given group of layers. When group is None, the
211         default CircuitPython terminal will be shown.
212         """
213         self._current_group = group
214
215     def refresh(
216         self,
217         *,
218         target_frames_per_second: Optional[int] = None,
219         minimum_frames_per_second: int = 0,
220     ) -> bool:
221         # pylint: disable=unused-argument
222         """When auto refresh is off, waits for the target frame rate and then refreshes the
223         display, returning True. If the call has taken too long since the last refresh call
224         for the given target frame rate, then the refresh returns False immediately without
225         updating the screen to hopefully help getting caught up.
226
227         If the time since the last successful refresh is below the minimum frame rate, then
228         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
229
230         When auto refresh is on, updates the display immediately. (The display will also
231         update without calls to this.)
232         """
233         self._subrectangles = []
234
235         # Go through groups and and add each to buffer
236         if self._current_group is not None:
237             buffer = Image.new("RGBA", (self._width, self._height))
238             # Recursively have everything draw to the image
239             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
240             # save image to buffer (or probably refresh buffer so we can compare)
241             self._buffer.paste(buffer)
242
243         if self._current_group is not None:
244             # Eventually calculate dirty rectangles here
245             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
246
247         for area in self._subrectangles:
248             self._refresh_display_area(area)
249
250         return True
251
252     def _refresh_loop(self):
253         while self._auto_refresh:
254             self.refresh()
255
256     def _refresh_display_area(self, rectangle):
257         """Loop through dirty rectangles and redraw that area."""
258
259         img = self._buffer.convert("RGB").crop(rectangle)
260         img = img.rotate(self._rotation, expand=True)
261
262         display_rectangle = self._apply_rotation(rectangle)
263         img = img.crop(self._clip(display_rectangle))
264
265         data = numpy.array(img).astype("uint16")
266         color = (
267             ((data[:, :, 0] & 0xF8) << 8)
268             | ((data[:, :, 1] & 0xFC) << 3)
269             | (data[:, :, 2] >> 3)
270         )
271
272         pixels = bytes(
273             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
274         )
275
276         self._send(
277             self._set_column_command,
278             self._encode_pos(
279                 display_rectangle.x1 + self._colstart,
280                 display_rectangle.x2 + self._colstart - 1,
281             ),
282         )
283         self._send(
284             self._set_row_command,
285             self._encode_pos(
286                 display_rectangle.y1 + self._rowstart,
287                 display_rectangle.y2 + self._rowstart - 1,
288             ),
289         )
290
291         self._bus._begin_transaction()  # pylint: disable=protected-access
292         self._send_pixels(pixels)
293         self._bus._end_transaction()  # pylint: disable=protected-access
294
295     def _clip(self, rectangle):
296         if self._rotation in (90, 270):
297             width, height = self._height, self._width
298         else:
299             width, height = self._width, self._height
300
301         if rectangle.x1 < 0:
302             rectangle.x1 = 0
303         if rectangle.y1 < 0:
304             rectangle.y1 = 0
305         if rectangle.x2 > width:
306             rectangle.x2 = width
307         if rectangle.y2 > height:
308             rectangle.y2 = height
309
310         return rectangle
311
312     def _apply_rotation(self, rectangle):
313         """Adjust the rectangle coordinates based on rotation"""
314         if self._rotation == 90:
315             return Rectangle(
316                 self._height - rectangle.y2,
317                 rectangle.x1,
318                 self._height - rectangle.y1,
319                 rectangle.x2,
320             )
321         if self._rotation == 180:
322             return Rectangle(
323                 self._width - rectangle.x2,
324                 self._height - rectangle.y2,
325                 self._width - rectangle.x1,
326                 self._height - rectangle.y1,
327             )
328         if self._rotation == 270:
329             return Rectangle(
330                 rectangle.y1,
331                 self._width - rectangle.x2,
332                 rectangle.y2,
333                 self._width - rectangle.x1,
334             )
335         return rectangle
336
337     def _encode_pos(self, x, y):
338         """Encode a postion into bytes."""
339         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
340
341     def fill_row(
342         self, y: int, buffer: _typing.WriteableBuffer
343     ) -> _typing.WriteableBuffer:
344         """Extract the pixels from a single row"""
345         for x in range(0, self._width):
346             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
347             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
348             buffer[x * 2 + 1] = _rgb_565 & 0xFF
349         return buffer
350
351     @property
352     def auto_refresh(self) -> bool:
353         """True when the display is refreshed automatically."""
354         return self._auto_refresh
355
356     @auto_refresh.setter
357     def auto_refresh(self, value: bool):
358         self._auto_refresh = value
359         if self._refresh_thread is None:
360             self._refresh_thread = threading.Thread(
361                 target=self._refresh_loop, daemon=True
362             )
363         if value and not self._refresh_thread.is_alive():
364             # Start the thread
365             self._refresh_thread.start()
366         elif not value and self._refresh_thread.is_alive():
367             # Stop the thread
368             self._refresh_thread.join()
369
370     @property
371     def brightness(self) -> float:
372         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
373         When `auto_brightness` is True, the value of `brightness` will change automatically.
374         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
375         """
376         return self._brightness
377
378     @brightness.setter
379     def brightness(self, value: float):
380         if 0 <= float(value) <= 1.0:
381             self._brightness = value
382             if self._backlight_type == BACKLIGHT_IN_OUT:
383                 self._backlight.value = round(self._brightness)
384             elif self._backlight_type == BACKLIGHT_PWM:
385                 self._backlight.duty_cycle = self._brightness * 65535
386             elif self._brightness_command is not None:
387                 self._send(self._brightness_command, round(value * 255))
388         else:
389             raise ValueError("Brightness must be between 0.0 and 1.0")
390
391     @property
392     def auto_brightness(self) -> bool:
393         """True when the display brightness is adjusted automatically, based on an ambient
394         light sensor or other method. Note that some displays may have this set to True by
395         default, but not actually implement automatic brightness adjustment.
396         `auto_brightness` is set to False if `brightness` is set manually.
397         """
398         return self._auto_brightness
399
400     @auto_brightness.setter
401     def auto_brightness(self, value: bool):
402         self._auto_brightness = value
403
404     @property
405     def width(self) -> int:
406         """Display Width"""
407         return self._width
408
409     @property
410     def height(self) -> int:
411         """Display Height"""
412         return self._height
413
414     @property
415     def rotation(self) -> int:
416         """The rotation of the display as an int in degrees."""
417         return self._rotation
418
419     @rotation.setter
420     def rotation(self, value: int):
421         if value not in (0, 90, 180, 270):
422             raise ValueError("Rotation must be 0/90/180/270")
423         self._rotation = value
424
425     @property
426     def bus(self) -> _DisplayBus:
427         """Current Display Bus"""
428         return self._bus