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