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