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