]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
More displayio code updates
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _display.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.display`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 import time
21 import struct
22 from typing import Optional
23 import digitalio
24 from PIL import Image
25 import microcontroller
26 import circuitpython_typing
27 from ._displaycore import _DisplayCore
28 from ._displaybus import _DisplayBus
29 from ._colorconverter import ColorConverter
30 from ._group import Group
31 from ._structs import RectangleStruct
32 from ._area import Area
33 from ._constants import (
34     CHIP_SELECT_TOGGLE_EVERY_BYTE,
35     CHIP_SELECT_UNTOUCHED,
36     DISPLAY_COMMAND,
37     DISPLAY_DATA,
38     BACKLIGHT_IN_OUT,
39     BACKLIGHT_PWM,
40     NO_COMMAND,
41 )
42
43 __version__ = "0.0.0+auto.0"
44 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
45
46
47 class Display:
48     # pylint: disable=too-many-instance-attributes
49     """This initializes a display and connects it into CircuitPython. Unlike other objects
50     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
51     This is done so that CircuitPython can use the display itself.
52
53     Most people should not use this class directly. Use a specific display driver instead
54     that will contain the initialization sequence at minimum.
55     """
56
57     def __init__(
58         self,
59         display_bus: _DisplayBus,
60         init_sequence: circuitpython_typing.ReadableBuffer,
61         *,
62         width: int,
63         height: int,
64         colstart: int = 0,
65         rowstart: int = 0,
66         rotation: int = 0,
67         color_depth: int = 16,
68         grayscale: bool = False,
69         pixels_in_byte_share_row: bool = True,
70         bytes_per_cell: int = 1,
71         reverse_pixels_in_byte: bool = False,
72         reverse_bytes_in_word: bool = True,
73         set_column_command: int = 0x2A,
74         set_row_command: int = 0x2B,
75         write_ram_command: int = 0x2C,
76         backlight_pin: Optional[microcontroller.Pin] = None,
77         brightness_command: Optional[int] = None,
78         brightness: float = 1.0,
79         auto_brightness: bool = False,
80         single_byte_bounds: bool = False,
81         data_as_commands: bool = False,
82         auto_refresh: bool = True,
83         native_frames_per_second: int = 60,
84         backlight_on_high: bool = True,
85         SH1107_addressing: bool = False,
86         set_vertical_scroll: int = 0,
87     ):
88         # pylint: disable=unused-argument,too-many-locals,invalid-name
89         """Create a Display object on the given display bus (`displayio.FourWire` or
90         `paralleldisplay.ParallelBus`).
91
92         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
93         with a command byte followed by a byte to determine the parameter count and if a
94         delay is need after. When the top bit of the second byte is 1, the next byte will be
95         the delay time in milliseconds. The remaining 7 bits are the parameter count
96         excluding any delay byte. The third through final bytes are the remaining command
97         parameters. The next byte will begin a new command definition. Here is a portion of
98         ILI9341 init code:
99
100         .. code-block:: python
101
102             init_sequence = (
103                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
104 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
105                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
106                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
107             )
108             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
109
110         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
111         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
112         Multiple byte literals (b”“) are merged together on load. The parens are needed to
113         allow byte literals on subsequent lines.
114
115         The initialization sequence should always leave the display memory access inline with
116         the scan of the display to minimize tearing artifacts.
117         """
118         # Turn off auto-refresh as we init
119         self._auto_refresh = False
120         ram_width = 0x100
121         ram_height = 0x100
122         if single_byte_bounds:
123             ram_width = 0xFF
124             ram_height = 0xFF
125
126         self._core = _DisplayCore(
127             bus=display_bus,
128             width=width,
129             height=height,
130             ram_width=ram_width,
131             ram_height=ram_height,
132             colstart=colstart,
133             rowstart=rowstart,
134             rotation=rotation,
135             color_depth=color_depth,
136             grayscale=grayscale,
137             pixels_in_byte_share_row=pixels_in_byte_share_row,
138             bytes_per_cell=bytes_per_cell,
139             reverse_pixels_in_byte=reverse_pixels_in_byte,
140             reverse_bytes_in_word=reverse_bytes_in_word,
141             column_command=set_column_command,
142             row_command=set_row_command,
143             set_current_column_command=NO_COMMAND,
144             set_current_row_command=NO_COMMAND,
145             data_as_commands=data_as_commands,
146             always_toggle_chip_select=False,
147             sh1107_addressing=(SH1107_addressing and color_depth == 1),
148             address_little_endian=False,
149         )
150
151         self._write_ram_command = write_ram_command
152         self._brightness_command = brightness_command
153         self._first_manual_refresh = not auto_refresh
154         self._backlight_on_high = backlight_on_high
155
156         self._native_frames_per_second = native_frames_per_second
157         self._native_ms_per_frame = 1000 // native_frames_per_second
158
159         self._auto_brightness = auto_brightness
160         self._brightness = brightness
161         self._auto_refresh = auto_refresh
162
163         self._initialize(init_sequence)
164         self._buffer = Image.new("RGB", (width, height))
165         self._current_group = None
166         self._last_refresh_call = 0
167         self._refresh_thread = None
168         if self._auto_refresh:
169             self.auto_refresh = True
170         self._colorconverter = ColorConverter()
171
172         self._backlight_type = None
173         if backlight_pin is not None:
174             try:
175                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
176
177                 # 100Hz looks decent and doesn't keep the CPU too busy
178                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
179                 self._backlight_type = BACKLIGHT_PWM
180             except ImportError:
181                 # PWMOut not implemented on this platform
182                 pass
183             if self._backlight_type is None:
184                 self._backlight_type = BACKLIGHT_IN_OUT
185                 self._backlight = digitalio.DigitalInOut(backlight_pin)
186                 self._backlight.switch_to_output()
187             self.brightness = brightness
188
189     def __new__(cls, *args, **kwargs):
190         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
191             allocate_display,
192         )
193
194         allocate_display(cls)
195         return super().__new__(cls)
196
197     def _initialize(self, init_sequence):
198         i = 0
199         while i < len(init_sequence):
200             command = init_sequence[i]
201             data_size = init_sequence[i + 1]
202             delay = (data_size & 0x80) > 0
203             data_size &= ~0x80
204
205             if self._core.data_as_commands:
206                 self._core.send(
207                     DISPLAY_COMMAND,
208                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
209                     bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
210                 )
211             else:
212                 self._core.send(
213                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
214                 )
215                 self._core.send(
216                     DISPLAY_DATA,
217                     CHIP_SELECT_UNTOUCHED,
218                     init_sequence[i + 2 : i + 2 + data_size],
219                 )
220             delay_time_ms = 10
221             if delay:
222                 data_size += 1
223                 delay_time_ms = init_sequence[i + 1 + data_size]
224                 if delay_time_ms == 255:
225                     delay_time_ms = 500
226             time.sleep(delay_time_ms / 1000)
227             i += 2 + data_size
228
229     def _send_pixels(self, pixels):
230         if not self._core.data_as_commands:
231             self._core.send(
232                 DISPLAY_COMMAND,
233                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
234                 bytes([self._write_ram_command]),
235             )
236         self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
237
238     def show(self, group: Group) -> None:
239         """Switches to displaying the given group of layers. When group is None, the
240         default CircuitPython terminal will be shown.
241         """
242         self._core.show(group)
243
244     def refresh(
245         self,
246         *,
247         target_frames_per_second: Optional[int] = None,
248         minimum_frames_per_second: int = 0,
249     ) -> bool:
250         """When auto refresh is off, waits for the target frame rate and then refreshes the
251         display, returning True. If the call has taken too long since the last refresh call
252         for the given target frame rate, then the refresh returns False immediately without
253         updating the screen to hopefully help getting caught up.
254
255         If the time since the last successful refresh is below the minimum frame rate, then
256         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
257
258         When auto refresh is on, updates the display immediately. (The display will also
259         update without calls to this.)
260         """
261         maximum_ms_per_real_frame = 0xFFFFFFFF
262         if minimum_frames_per_second > 0:
263             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
264
265         if target_frames_per_second is None:
266             target_ms_per_frame = 0xFFFFFFFF
267         else:
268             target_ms_per_frame = 1000 // target_frames_per_second
269
270         if (
271             not self._auto_refresh
272             and not self._first_manual_refresh
273             and target_ms_per_frame != 0xFFFFFFFF
274         ):
275             current_time = time.monotonic() * 1000
276             current_ms_since_real_refresh = current_time - self._core.last_refresh
277             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
278                 raise RuntimeError("Below minimum frame rate")
279             current_ms_since_last_call = current_time - self._last_refresh_call
280             self._last_refresh_call = current_time
281             if current_ms_since_last_call > target_ms_per_frame:
282                 return False
283
284             remaining_time = target_ms_per_frame - (
285                 current_ms_since_real_refresh % target_ms_per_frame
286             )
287             time.sleep(remaining_time / 1000)
288         self._first_manual_refresh = False
289         self._refresh_display()
290         return True
291
292     def _refresh_display(self):
293         # pylint: disable=protected-access
294         if not self._core.start_refresh():
295             return False
296
297         # TODO: Likely move this to _refresh_area()
298         # Go through groups and and add each to buffer
299         if self._core.current_group is not None:
300             buffer = Image.new("RGBA", (self._core.width, self._core.height))
301             # Recursively have everything draw to the image
302             self._core.current_group._fill_area(
303                 buffer
304             )  # pylint: disable=protected-access
305             # save image to buffer (or probably refresh buffer so we can compare)
306             self._buffer.paste(buffer)
307
308         areas_to_refresh = self._get_refresh_areas()
309
310         for area in areas_to_refresh:
311             self._refresh_area(area)
312
313         self._core.finish_refresh()
314
315         return True
316
317     def _get_refresh_areas(self) -> list[Area]:
318         """Get a list of areas to be refreshed"""
319         areas = []
320         if self._core.current_group is not None:
321             # Eventually calculate dirty rectangles here
322             areas.append(Area(0, 0, self._core.width, self._core.height))
323         return areas
324
325     def background(self):
326         """Run background refresh tasks. Do not call directly"""
327         if (
328             self._auto_refresh
329             and (time.monotonic() * 1000 - self._core.last_refresh)
330             > self._native_ms_per_frame
331         ):
332             self.refresh()
333
334     def _refresh_area(self, area) -> bool:
335         """Loop through dirty areas and redraw that area."""
336         # pylint: disable=too-many-locals
337         buffer_size = 128
338
339         clipped = Area()
340         if not self._core.clip_area(area, clipped):
341             return True
342
343         rows_per_buffer = clipped.height()
344         pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
345         pixels_per_buffer = clipped.size()
346
347         subrectangles = 1
348
349         if self._core.sh1107_addressing:
350             subrectangles = rows_per_buffer // 8
351             rows_per_buffer = 8
352         elif clipped.size() > buffer_size * pixels_per_word:
353             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
354             if rows_per_buffer == 0:
355                 rows_per_buffer = 1
356             if (
357                 self._core.colorspace.depth < 8
358                 and self._core.colorspace.pixels_in_byte_share_row
359             ):
360                 pixels_per_byte = 8 // self._core.colorspace.depth
361                 if rows_per_buffer % pixels_per_byte != 0:
362                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
363             subrectangles = clipped.height() // rows_per_buffer
364             if clipped.height() % rows_per_buffer != 0:
365                 subrectangles += 1
366             pixels_per_buffer = rows_per_buffer * clipped.width()
367             buffer_size = pixels_per_buffer // pixels_per_word
368             if pixels_per_buffer % pixels_per_word:
369                 buffer_size += 1
370
371         buffer = bytearray(buffer_size)
372         mask_length = (pixels_per_buffer // 32) + 1
373         mask = bytearray(mask_length)
374         remaining_rows = clipped.height()
375
376         for subrect_index in range(subrectangles):
377             subrectangle = Area(
378                 clipped.x1,
379                 clipped.y1 + rows_per_buffer * subrect_index,
380                 clipped.x2,
381                 clipped.y1 + rows_per_buffer * (subrect_index + 1),
382             )
383             if remaining_rows < rows_per_buffer:
384                 subrectangle.y2 = subrectangle.y1 + remaining_rows
385             self._core.set_region_to_update(subrectangle)
386             if self._core.colorspace.depth >= 8:
387                 subrectangle_size_bytes = subrectangle.size() * (
388                     self._core.colorspace.depth // 8
389                 )
390             else:
391                 subrectangle_size_bytes = subrectangle.size() // (
392                     8 // self._core.colorspace.depth
393                 )
394
395             self._core.fill_area(subrectangle, mask, buffer)
396
397             self._core.begin_transaction()
398             self._send_pixels(buffer[:subrectangle_size_bytes])
399             self._core.end_transaction()
400         return True
401
402     def _apply_rotation(self, rectangle):
403         """Adjust the rectangle coordinates based on rotation"""
404         if self._core.rotation == 90:
405             return RectangleStruct(
406                 self._core.height - rectangle.y2,
407                 rectangle.x1,
408                 self._core.height - rectangle.y1,
409                 rectangle.x2,
410             )
411         if self._core.rotation == 180:
412             return RectangleStruct(
413                 self._core.width - rectangle.x2,
414                 self._core.height - rectangle.y2,
415                 self._core.width - rectangle.x1,
416                 self._core.height - rectangle.y1,
417             )
418         if self._core.rotation == 270:
419             return RectangleStruct(
420                 rectangle.y1,
421                 self._core.width - rectangle.x2,
422                 rectangle.y2,
423                 self._core.width - rectangle.x1,
424             )
425         return rectangle
426
427     def fill_row(
428         self, y: int, buffer: circuitpython_typing.WriteableBuffer
429     ) -> circuitpython_typing.WriteableBuffer:
430         """Extract the pixels from a single row"""
431         for x in range(0, self._core.width):
432             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
433             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
434             buffer[x * 2 + 1] = _rgb_565 & 0xFF
435         return buffer
436
437     def release(self) -> None:
438         """Release the display and free its resources"""
439         self.auto_refresh = False
440         self._core.release_display_core()
441
442     def reset(self) -> None:
443         """Reset the display"""
444         self.auto_refresh = True
445
446     @property
447     def auto_refresh(self) -> bool:
448         """True when the display is refreshed automatically."""
449         return self._auto_refresh
450
451     @auto_refresh.setter
452     def auto_refresh(self, value: bool):
453         self._first_manual_refresh = not value
454         self._auto_refresh = value
455
456     @property
457     def brightness(self) -> float:
458         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
459         When `auto_brightness` is True, the value of `brightness` will change automatically.
460         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
461         """
462         return self._brightness
463
464     @brightness.setter
465     def brightness(self, value: float):
466         if 0 <= float(value) <= 1.0:
467             if not self._backlight_on_high:
468                 value = 1.0 - value
469
470             if self._backlight_type == BACKLIGHT_PWM:
471                 self._backlight.duty_cycle = value * 0xFFFF
472             elif self._backlight_type == BACKLIGHT_IN_OUT:
473                 self._backlight.value = value > 0.99
474             elif self._brightness_command is not None:
475                 self._core.begin_transaction()
476                 if self._core.data_as_commands:
477                     self._core.send(
478                         DISPLAY_COMMAND,
479                         CHIP_SELECT_TOGGLE_EVERY_BYTE,
480                         bytes([self._brightness_command, 0xFF * value]),
481                     )
482                 else:
483                     self._core.send(
484                         DISPLAY_COMMAND,
485                         CHIP_SELECT_TOGGLE_EVERY_BYTE,
486                         bytes([self._brightness_command]),
487                     )
488                     self._core.send(
489                         DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
490                     )
491                 self._core.end_transaction()
492             self._brightness = value
493         else:
494             raise ValueError("Brightness must be between 0.0 and 1.0")
495
496     @property
497     def auto_brightness(self) -> bool:
498         """True when the display brightness is adjusted automatically, based on an ambient
499         light sensor or other method. Note that some displays may have this set to True by
500         default, but not actually implement automatic brightness adjustment.
501         `auto_brightness` is set to False if `brightness` is set manually.
502         """
503         return self._auto_brightness
504
505     @auto_brightness.setter
506     def auto_brightness(self, value: bool):
507         self._auto_brightness = value
508
509     @property
510     def width(self) -> int:
511         """Display Width"""
512         return self._core.get_width()
513
514     @property
515     def height(self) -> int:
516         """Display Height"""
517         return self._core.get_height()
518
519     @property
520     def rotation(self) -> int:
521         """The rotation of the display as an int in degrees."""
522         return self._core.get_rotation()
523
524     @rotation.setter
525     def rotation(self, value: int):
526         self._core.set_rotation(value)
527
528     @property
529     def bus(self) -> _DisplayBus:
530         """Current Display Bus"""
531         return self._core.get_bus()