]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/display.py
implemented basic backlight functionality
[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             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
173             delay_time_ms = 10
174             if delay:
175                 data_size += 1
176                 delay_time_ms = init_sequence[i + 1 + data_size]
177                 if delay_time_ms == 255:
178                     delay_time_ms = 500
179             time.sleep(delay_time_ms / 1000)
180             i += 2 + data_size
181
182     def _write(self, command, data):
183         if self._single_byte_bounds:
184             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
185         else:
186             self._bus.send(True, bytes([command]), toggle_every_byte=True)
187             self._bus.send(False, data)
188
189     def _release(self):
190         self._bus.release()
191         self._bus = None
192
193     def show(self, group):
194         """Switches to displaying the given group of layers. When group is None, the
195         default CircuitPython terminal will be shown.
196         """
197         self._current_group = group
198
199     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
200         """When auto refresh is off, waits for the target frame rate and then refreshes the
201         display, returning True. If the call has taken too long since the last refresh call
202         for the given target frame rate, then the refresh returns False immediately without
203         updating the screen to hopefully help getting caught up.
204
205         If the time since the last successful refresh is below the minimum frame rate, then
206         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
207
208         When auto refresh is on, updates the display immediately. (The display will also
209         update without calls to this.)
210         """
211         # Go through groups and and add each to buffer
212         if self._current_group is not None:
213             buffer = Image.new("RGBA", (self._width, self._height))
214             # Recursively have everything draw to the image
215             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
216             # save image to buffer (or probably refresh buffer so we can compare)
217             self._buffer.paste(buffer)
218
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         self._write(self._write_ram_command, pixels)
265
266     def _clip(self, rectangle):
267         if self._rotation in (90, 270):
268             width, height = self._height, self._width
269         else:
270             width, height = self._width, self._height
271
272         if rectangle.x1 < 0:
273             rectangle.x1 = 0
274         if rectangle.y1 < 0:
275             rectangle.y1 = 0
276         if rectangle.x2 > width:
277             rectangle.x2 = width
278         if rectangle.y2 > height:
279             rectangle.y2 = height
280
281         return rectangle
282
283     def _apply_rotation(self, rectangle):
284         """Adjust the rectangle coordinates based on rotation"""
285         if self._rotation == 90:
286             return Rectangle(
287                 self._height - rectangle.y2,
288                 rectangle.x1,
289                 self._height - rectangle.y1,
290                 rectangle.x2,
291             )
292         if self._rotation == 180:
293             return Rectangle(
294                 self._width - rectangle.x2,
295                 self._height - rectangle.y2,
296                 self._width - rectangle.x1,
297                 self._height - rectangle.y1,
298             )
299         if self._rotation == 270:
300             return Rectangle(
301                 rectangle.y1,
302                 self._width - rectangle.x2,
303                 rectangle.y2,
304                 self._width - rectangle.x1,
305             )
306         return rectangle
307
308     def _encode_pos(self, x, y):
309         """Encode a postion into bytes."""
310         return struct.pack(self._bounds_encoding, x, y)
311
312     def fill_row(self, y, buffer):
313         """Extract the pixels from a single row"""
314         pass
315
316     @property
317     def auto_refresh(self):
318         """True when the display is refreshed automatically."""
319         return self._auto_refresh
320
321     @auto_refresh.setter
322     def auto_refresh(self, value):
323         self._auto_refresh = value
324         if self._refresh_thread is None:
325             self._refresh_thread = threading.Thread(
326                 target=self._refresh_loop, daemon=True
327             )
328         if value and not self._refresh_thread.is_alive():
329             # Start the thread
330             self._refresh_thread.start()
331         elif not value and self._refresh_thread.is_alive():
332             # Stop the thread
333             self._refresh_thread.join()
334
335     @property
336     def brightness(self):
337         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
338         When `auto_brightness` is True, the value of `brightness` will change automatically.
339         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
340         """
341         return self._brightness
342
343     @brightness.setter
344     def brightness(self, value):
345         print("Brightness set")
346         if 0 <= float(value) <= 1.0:
347             self._brightness = value
348             if self._backlight_type == BACKLIGHT_IN_OUT:
349                 self._backlight.value = int(round(self._brightness))
350                 print(int(round(self._brightness)))
351             # PWM not currently implemented
352         else:
353             raise ValueError("Brightness must be between 0.0 and 1.0")
354
355     @property
356     def auto_brightness(self):
357         """True when the display brightness is adjusted automatically, based on an ambient
358         light sensor or other method. Note that some displays may have this set to True by
359         default, but not actually implement automatic brightness adjustment.
360         `auto_brightness` is set to False if `brightness` is set manually.
361         """
362         return self._auto_brightness
363
364     @auto_brightness.setter
365     def auto_brightness(self, value):
366         self._auto_brightness = value
367
368     @property
369     def width(self):
370         """Display Width"""
371         return self._width
372
373     @property
374     def height(self):
375         """Display Height"""
376         return self._height
377
378     @property
379     def rotation(self):
380         """The rotation of the display as an int in degrees."""
381         return self._rotation
382
383     @rotation.setter
384     def rotation(self, value):
385         if value not in (0, 90, 180, 270):
386             raise ValueError("Rotation must be 0/90/180/270")
387         self._rotation = value
388
389     @property
390     def bus(self):
391         """Current Display Bus"""
392         return self._bus
393
394
395 # pylint: enable=too-many-instance-attributes