]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/display.py
trying to implement fill_row function
[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             try:
159                 from pulseio import PWMOut  # pylint: disable=import-outside-toplevel
160
161                 # 100Hz looks decent and doesn't keep the CPU too busy
162                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
163                 self._backlight_type = BACKLIGHT_PWM
164             except ImportError:
165                 # PWMOut not implemented on this platform
166                 pass
167             if self._backlight_type is None:
168                 self._backlight_type = BACKLIGHT_IN_OUT
169                 self._backlight = digitalio.DigitalInOut(backlight_pin)
170                 self._backlight.switch_to_output()
171             self.brightness = brightness
172
173     # pylint: enable=too-many-locals
174
175     def _initialize(self, init_sequence):
176         i = 0
177         while i < len(init_sequence):
178             command = init_sequence[i]
179             data_size = init_sequence[i + 1]
180             delay = (data_size & 0x80) > 0
181             data_size &= ~0x80
182
183             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
184             delay_time_ms = 10
185             if delay:
186                 data_size += 1
187                 delay_time_ms = init_sequence[i + 1 + data_size]
188                 if delay_time_ms == 255:
189                     delay_time_ms = 500
190             time.sleep(delay_time_ms / 1000)
191             i += 2 + data_size
192
193     def _write(self, command, data):
194         self._bus.begin_transaction()
195         if self._data_as_commands:
196             if command is not None:
197                 self._bus.send(True, bytes([command]), toggle_every_byte=True)
198             self._bus.send(command is not None, data)
199         else:
200             self._bus.send(True, bytes([command]), toggle_every_byte=True)
201             self._bus.send(False, data)
202         self._bus.end_transaction()
203
204     def _release(self):
205         self._bus._release()  # pylint: disable=protected-access
206         self._bus = None
207
208     def show(self, group):
209         """Switches to displaying the given group of layers. When group is None, the
210         default CircuitPython terminal will be shown.
211         """
212         self._current_group = group
213
214     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
215         """When auto refresh is off, waits for the target frame rate and then refreshes the
216         display, returning True. If the call has taken too long since the last refresh call
217         for the given target frame rate, then the refresh returns False immediately without
218         updating the screen to hopefully help getting caught up.
219
220         If the time since the last successful refresh is below the minimum frame rate, then
221         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
222
223         When auto refresh is on, updates the display immediately. (The display will also
224         update without calls to this.)
225         """
226         self._subrectangles = []
227
228         # Go through groups and and add each to buffer
229         if self._current_group is not None:
230             buffer = Image.new("RGBA", (self._width, self._height))
231             # Recursively have everything draw to the image
232             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
233             # save image to buffer (or probably refresh buffer so we can compare)
234             self._buffer.paste(buffer)
235
236         if self._current_group is not None:
237             # Eventually calculate dirty rectangles here
238             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
239
240         for area in self._subrectangles:
241             self._refresh_display_area(area)
242
243     def _refresh_loop(self):
244         while self._auto_refresh:
245             self.refresh()
246
247     def _refresh_display_area(self, rectangle):
248         """Loop through dirty rectangles and redraw that area."""
249
250         img = self._buffer.convert("RGB").crop(rectangle)
251         img = img.rotate(self._rotation, expand=True)
252
253         display_rectangle = self._apply_rotation(rectangle)
254         img = img.crop(self._clip(display_rectangle))
255
256         data = numpy.array(img).astype("uint16")
257         color = (
258             ((data[:, :, 0] & 0xF8) << 8)
259             | ((data[:, :, 1] & 0xFC) << 3)
260             | (data[:, :, 2] >> 3)
261         )
262
263         pixels = list(
264             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
265         )
266
267         self._write(
268             self._set_column_command,
269             self._encode_pos(
270                 display_rectangle.x1 + self._colstart,
271                 display_rectangle.x2 + self._colstart - 1,
272             ),
273         )
274         self._write(
275             self._set_row_command,
276             self._encode_pos(
277                 display_rectangle.y1 + self._rowstart,
278                 display_rectangle.y2 + self._rowstart - 1,
279             ),
280         )
281
282         if self._data_as_commands:
283             self._write(None, pixels)
284         else:
285             self._write(self._write_ram_command, pixels)
286
287     def _clip(self, rectangle):
288         if self._rotation in (90, 270):
289             width, height = self._height, self._width
290         else:
291             width, height = self._width, self._height
292
293         if rectangle.x1 < 0:
294             rectangle.x1 = 0
295         if rectangle.y1 < 0:
296             rectangle.y1 = 0
297         if rectangle.x2 > width:
298             rectangle.x2 = width
299         if rectangle.y2 > height:
300             rectangle.y2 = height
301
302         return rectangle
303
304     def _apply_rotation(self, rectangle):
305         """Adjust the rectangle coordinates based on rotation"""
306         if self._rotation == 90:
307             return Rectangle(
308                 self._height - rectangle.y2,
309                 rectangle.x1,
310                 self._height - rectangle.y1,
311                 rectangle.x2,
312             )
313         if self._rotation == 180:
314             return Rectangle(
315                 self._width - rectangle.x2,
316                 self._height - rectangle.y2,
317                 self._width - rectangle.x1,
318                 self._height - rectangle.y1,
319             )
320         if self._rotation == 270:
321             return Rectangle(
322                 rectangle.y1,
323                 self._width - rectangle.x2,
324                 rectangle.y2,
325                 self._width - rectangle.x1,
326             )
327         return rectangle
328
329     def _encode_pos(self, x, y):
330         """Encode a postion into bytes."""
331         return struct.pack(self._bounds_encoding, x, y)
332
333     def _rgb_tuple_to_rgb565(self, color_tuple):
334         return (
335             ((color_tuple[0] & 0x00F8) << 8)
336             | ((color_tuple[1] & 0x00FC) << 3)
337             | (color_tuple[2] & 0x00F8) >> 3
338         )
339
340     def fill_row(self, y, buffer):
341         """Extract the pixels from a single row"""
342         for x in range(0, self._width):
343             _rgb_565 = self._rgb_tuple_to_rgb565(self._buffer.getpixel((x, y)))
344             buffer[x * 2] = _rgb_565 >> 8
345             buffer[x * 2 + 1] = _rgb_565
346             #(data[i * 2] << 8) + data[i * 2 + 1]
347         return buffer
348
349     @property
350     def auto_refresh(self):
351         """True when the display is refreshed automatically."""
352         return self._auto_refresh
353
354     @auto_refresh.setter
355     def auto_refresh(self, value):
356         self._auto_refresh = value
357         if self._refresh_thread is None:
358             self._refresh_thread = threading.Thread(
359                 target=self._refresh_loop, daemon=True
360             )
361         if value and not self._refresh_thread.is_alive():
362             # Start the thread
363             self._refresh_thread.start()
364         elif not value and self._refresh_thread.is_alive():
365             # Stop the thread
366             self._refresh_thread.join()
367
368     @property
369     def brightness(self):
370         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
371         When `auto_brightness` is True, the value of `brightness` will change automatically.
372         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
373         """
374         return self._brightness
375
376     @brightness.setter
377     def brightness(self, value):
378         if 0 <= float(value) <= 1.0:
379             self._brightness = value
380             if self._backlight_type == BACKLIGHT_IN_OUT:
381                 self._backlight.value = round(self._brightness)
382             elif self._backlight_type == BACKLIGHT_PWM:
383                 self._backlight.duty_cycle = self._brightness * 65535
384             elif self._brightness_command is not None:
385                 self._write(self._brightness_command, round(value * 255))
386         else:
387             raise ValueError("Brightness must be between 0.0 and 1.0")
388
389     @property
390     def auto_brightness(self):
391         """True when the display brightness is adjusted automatically, based on an ambient
392         light sensor or other method. Note that some displays may have this set to True by
393         default, but not actually implement automatic brightness adjustment.
394         `auto_brightness` is set to False if `brightness` is set manually.
395         """
396         return self._auto_brightness
397
398     @auto_brightness.setter
399     def auto_brightness(self, value):
400         self._auto_brightness = value
401
402     @property
403     def width(self):
404         """Display Width"""
405         return self._width
406
407     @property
408     def height(self):
409         """Display Height"""
410         return self._height
411
412     @property
413     def rotation(self):
414         """The rotation of the display as an int in degrees."""
415         return self._rotation
416
417     @rotation.setter
418     def rotation(self, value):
419         if value not in (0, 90, 180, 270):
420             raise ValueError("Rotation must be 0/90/180/270")
421         self._rotation = value
422
423     @property
424     def bus(self):
425         """Current Display Bus"""
426         return self._bus
427
428
429 # pylint: enable=too-many-instance-attributes