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