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