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