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