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