]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
only convert command to bytes
[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         rectangle.x1 = max(rectangle.x1, 0)
302         rectangle.y1 = max(rectangle.y1, 0)
303         rectangle.x2 = min(rectangle.x2, width)
304         rectangle.y2 = min(rectangle.y2, height)
305
306         return rectangle
307
308     def _apply_rotation(self, rectangle):
309         """Adjust the rectangle coordinates based on rotation"""
310         if self._rotation == 90:
311             return Rectangle(
312                 self._height - rectangle.y2,
313                 rectangle.x1,
314                 self._height - rectangle.y1,
315                 rectangle.x2,
316             )
317         if self._rotation == 180:
318             return Rectangle(
319                 self._width - rectangle.x2,
320                 self._height - rectangle.y2,
321                 self._width - rectangle.x1,
322                 self._height - rectangle.y1,
323             )
324         if self._rotation == 270:
325             return Rectangle(
326                 rectangle.y1,
327                 self._width - rectangle.x2,
328                 rectangle.y2,
329                 self._width - rectangle.x1,
330             )
331         return rectangle
332
333     def _encode_pos(self, x, y):
334         """Encode a postion into bytes."""
335         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
336
337     def fill_row(
338         self, y: int, buffer: _typing.WriteableBuffer
339     ) -> _typing.WriteableBuffer:
340         """Extract the pixels from a single row"""
341         for x in range(0, self._width):
342             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
343             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
344             buffer[x * 2 + 1] = _rgb_565 & 0xFF
345         return buffer
346
347     @property
348     def auto_refresh(self) -> bool:
349         """True when the display is refreshed automatically."""
350         return self._auto_refresh
351
352     @auto_refresh.setter
353     def auto_refresh(self, value: bool):
354         self._auto_refresh = value
355         if self._refresh_thread is None:
356             self._refresh_thread = threading.Thread(
357                 target=self._refresh_loop, daemon=True
358             )
359         if value and not self._refresh_thread.is_alive():
360             # Start the thread
361             self._refresh_thread.start()
362         elif not value and self._refresh_thread.is_alive():
363             # Stop the thread
364             self._refresh_thread.join()
365
366     @property
367     def brightness(self) -> float:
368         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
369         When `auto_brightness` is True, the value of `brightness` will change automatically.
370         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
371         """
372         return self._brightness
373
374     @brightness.setter
375     def brightness(self, value: float):
376         if 0 <= float(value) <= 1.0:
377             self._brightness = value
378             if self._backlight_type == BACKLIGHT_IN_OUT:
379                 self._backlight.value = round(self._brightness)
380             elif self._backlight_type == BACKLIGHT_PWM:
381                 self._backlight.duty_cycle = self._brightness * 65535
382             elif self._brightness_command is not None:
383                 self._send(self._brightness_command, round(value * 255))
384         else:
385             raise ValueError("Brightness must be between 0.0 and 1.0")
386
387     @property
388     def auto_brightness(self) -> bool:
389         """True when the display brightness is adjusted automatically, based on an ambient
390         light sensor or other method. Note that some displays may have this set to True by
391         default, but not actually implement automatic brightness adjustment.
392         `auto_brightness` is set to False if `brightness` is set manually.
393         """
394         return self._auto_brightness
395
396     @auto_brightness.setter
397     def auto_brightness(self, value: bool):
398         self._auto_brightness = value
399
400     @property
401     def width(self) -> int:
402         """Display Width"""
403         return self._width
404
405     @property
406     def height(self) -> int:
407         """Display Height"""
408         return self._height
409
410     @property
411     def rotation(self) -> int:
412         """The rotation of the display as an int in degrees."""
413         return self._rotation
414
415     @rotation.setter
416     def rotation(self, value: int):
417         if value not in (0, 90, 180, 270):
418             raise ValueError("Rotation must be 0/90/180/270")
419         self._rotation = value
420
421     @property
422     def bus(self) -> _DisplayBus:
423         """Current Display Bus"""
424         return self._bus