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