]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_display.py
8551f8c87fded9cea51a104f207a773995c698ca
[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 from array import array
22 from typing import Optional
23 import digitalio
24 import microcontroller
25 from circuitpython_typing import WriteableBuffer, ReadableBuffer
26 from ._displaycore import _DisplayCore
27 from ._displaybus import _DisplayBus
28 from ._colorconverter import ColorConverter
29 from ._group import Group, circuitpython_splash
30 from ._area import Area
31 from ._constants import (
32     CHIP_SELECT_TOGGLE_EVERY_BYTE,
33     CHIP_SELECT_UNTOUCHED,
34     DISPLAY_COMMAND,
35     DISPLAY_DATA,
36     BACKLIGHT_IN_OUT,
37     BACKLIGHT_PWM,
38     NO_COMMAND,
39     DELAY,
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, too-many-statements
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, too-many-branches
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
116         if rotation % 90 != 0:
117             raise ValueError("Display rotation must be in 90 degree increments")
118
119         if SH1107_addressing and color_depth != 1:
120             raise ValueError("color_depth must be 1 when SH1107_addressing is True")
121
122         # Turn off auto-refresh as we init
123         self._auto_refresh = False
124         ram_width = 0x100
125         ram_height = 0x100
126         if single_byte_bounds:
127             ram_width = 0xFF
128             ram_height = 0xFF
129
130         self._core = _DisplayCore(
131             bus=display_bus,
132             width=width,
133             height=height,
134             ram_width=ram_width,
135             ram_height=ram_height,
136             colstart=colstart,
137             rowstart=rowstart,
138             rotation=rotation,
139             color_depth=color_depth,
140             grayscale=grayscale,
141             pixels_in_byte_share_row=pixels_in_byte_share_row,
142             bytes_per_cell=bytes_per_cell,
143             reverse_pixels_in_byte=reverse_pixels_in_byte,
144             reverse_bytes_in_word=reverse_bytes_in_word,
145             column_command=set_column_command,
146             row_command=set_row_command,
147             set_current_column_command=NO_COMMAND,
148             set_current_row_command=NO_COMMAND,
149             data_as_commands=data_as_commands,
150             always_toggle_chip_select=False,
151             sh1107_addressing=(SH1107_addressing and color_depth == 1),
152             address_little_endian=False,
153         )
154
155         self._write_ram_command = write_ram_command
156         self._brightness_command = brightness_command
157         self._first_manual_refresh = not auto_refresh
158         self._backlight_on_high = backlight_on_high
159
160         self._native_frames_per_second = native_frames_per_second
161         self._native_ms_per_frame = 1000 // native_frames_per_second
162
163         self._brightness = brightness
164         self._auto_refresh = auto_refresh
165
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 & DELAY) != 0
171             data_size &= ~DELAY
172             while self._core.begin_transaction():
173                 pass
174
175             if self._core.data_as_commands:
176                 full_command = bytearray(data_size + 1)
177                 full_command[0] = command
178                 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
179                 self._core.send(
180                     DISPLAY_COMMAND,
181                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
182                     full_command,
183                 )
184             else:
185                 self._core.send(
186                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
187                 )
188                 self._core.send(
189                     DISPLAY_DATA,
190                     CHIP_SELECT_UNTOUCHED,
191                     init_sequence[i + 2 : i + 2 + data_size],
192                 )
193             self._core.end_transaction()
194             delay_time_ms = 10
195             if delay:
196                 data_size += 1
197                 delay_time_ms = init_sequence[i + 1 + data_size]
198                 if delay_time_ms == 255:
199                     delay_time_ms = 500
200             time.sleep(delay_time_ms / 1000)
201             i += 2 + data_size
202
203         self._current_group = None
204         self._last_refresh_call = 0
205         self._refresh_thread = None
206         self._colorconverter = ColorConverter()
207
208         self._backlight_type = None
209         if backlight_pin is not None:
210             try:
211                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
212
213                 # 100Hz looks decent and doesn't keep the CPU too busy
214                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
215                 self._backlight_type = BACKLIGHT_PWM
216             except ImportError:
217                 # PWMOut not implemented on this platform
218                 pass
219             if self._backlight_type is None:
220                 self._backlight_type = BACKLIGHT_IN_OUT
221                 self._backlight = digitalio.DigitalInOut(backlight_pin)
222                 self._backlight.switch_to_output()
223         self.brightness = brightness
224         if not circuitpython_splash._in_group:
225             self._set_root_group(circuitpython_splash)
226         self.auto_refresh = auto_refresh
227
228     def __new__(cls, *args, **kwargs):
229         from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
230             allocate_display,
231         )
232
233         display_instance = super().__new__(cls)
234         allocate_display(display_instance)
235         return display_instance
236
237     def _send_pixels(self, pixels):
238         if not self._core.data_as_commands:
239             self._core.send(
240                 DISPLAY_COMMAND,
241                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
242                 bytes([self._write_ram_command]),
243             )
244         self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
245
246     def show(self, group: Group) -> None:
247         """Switches to displaying the given group of layers. When group is None, the
248         default CircuitPython terminal will be shown.
249         """
250         if group is None:
251             group = circuitpython_splash
252         self._core.set_root_group(group)
253
254     def _set_root_group(self, root_group: Group) -> None:
255         ok = self._core.set_root_group(root_group)
256         if not ok:
257             raise ValueError("Group already used")
258
259     def refresh(
260         self,
261         *,
262         target_frames_per_second: Optional[int] = None,
263         minimum_frames_per_second: int = 0,
264     ) -> bool:
265         """When auto refresh is off, waits for the target frame rate and then refreshes the
266         display, returning True. If the call has taken too long since the last refresh call
267         for the given target frame rate, then the refresh returns False immediately without
268         updating the screen to hopefully help getting caught up.
269
270         If the time since the last successful refresh is below the minimum frame rate, then
271         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
272
273         When auto refresh is on, updates the display immediately. (The display will also
274         update without calls to this.)
275         """
276         maximum_ms_per_real_frame = 0xFFFFFFFF
277         if minimum_frames_per_second > 0:
278             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
279
280         if target_frames_per_second is None:
281             target_ms_per_frame = 0xFFFFFFFF
282         else:
283             target_ms_per_frame = 1000 // target_frames_per_second
284
285         if (
286             not self._auto_refresh
287             and not self._first_manual_refresh
288             and target_ms_per_frame != 0xFFFFFFFF
289         ):
290             current_time = time.monotonic() * 1000
291             current_ms_since_real_refresh = current_time - self._core.last_refresh
292             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
293                 raise RuntimeError("Below minimum frame rate")
294             current_ms_since_last_call = current_time - self._last_refresh_call
295             self._last_refresh_call = current_time
296             if current_ms_since_last_call > target_ms_per_frame:
297                 return False
298
299             remaining_time = target_ms_per_frame - (
300                 current_ms_since_real_refresh % target_ms_per_frame
301             )
302             time.sleep(remaining_time / 1000)
303         self._first_manual_refresh = False
304         self._refresh_display()
305         return True
306
307     def _refresh_display(self):
308         if not self._core.start_refresh():
309             return False
310
311         areas_to_refresh = self._get_refresh_areas()
312         for area in areas_to_refresh:
313             self._refresh_area(area)
314
315         self._core.finish_refresh()
316
317         return True
318
319     def _get_refresh_areas(self) -> list[Area]:
320         """Get a list of areas to be refreshed"""
321         areas = []
322         if self._core.full_refresh:
323             areas.append(self._core.area)
324         elif self._core.current_group is not None:
325             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
326                 areas
327             )
328         return areas
329
330     def _background(self):
331         """Run background refresh tasks. Do not call directly"""
332         if (
333             self._auto_refresh
334             and (time.monotonic() * 1000 - self._core.last_refresh)
335             > self._native_ms_per_frame
336         ):
337             self.refresh()
338
339     def _refresh_area(self, area) -> bool:
340         """Loop through dirty areas and redraw that area."""
341         # pylint: disable=too-many-locals, too-many-branches
342
343         clipped = Area()
344         # Clip the area to the display by overlapping the areas.
345         # If there is no overlap then we're done.
346         if not self._core.clip_area(area, clipped):
347             return True
348
349         rows_per_buffer = clipped.height()
350         pixels_per_word = 32 // self._core.colorspace.depth
351         pixels_per_buffer = clipped.size()
352
353         # We should have lots of memory
354         buffer_size = clipped.size() // pixels_per_word
355
356         subrectangles = 1
357         # for SH1107 and other boundary constrained controllers
358         #      write one single row at a time
359         if self._core.sh1107_addressing:
360             subrectangles = rows_per_buffer // 8
361             rows_per_buffer = 8
362         elif clipped.size() > buffer_size * pixels_per_word:
363             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
364             if rows_per_buffer == 0:
365                 rows_per_buffer = 1
366             # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
367             if (
368                 self._core.colorspace.depth < 8
369                 and self._core.colorspace.pixels_in_byte_share_row
370             ):
371                 pixels_per_byte = 8 // self._core.colorspace.depth
372                 if rows_per_buffer % pixels_per_byte != 0:
373                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
374             subrectangles = clipped.height() // rows_per_buffer
375             if clipped.height() % rows_per_buffer != 0:
376                 subrectangles += 1
377             pixels_per_buffer = rows_per_buffer * clipped.width()
378             buffer_size = pixels_per_buffer // pixels_per_word
379             if pixels_per_buffer % pixels_per_word:
380                 buffer_size += 1
381         mask_length = (pixels_per_buffer // 8) + 1  # 1 bit per pixel + 1
382         remaining_rows = clipped.height()
383
384         for subrect_index in range(subrectangles):
385             subrectangle = Area(
386                 clipped.x1,
387                 clipped.y1 + rows_per_buffer * subrect_index,
388                 clipped.x2,
389                 clipped.y1 + rows_per_buffer * (subrect_index + 1),
390             )
391             if remaining_rows < rows_per_buffer:
392                 subrectangle.y2 = subrectangle.y1 + remaining_rows
393             remaining_rows -= rows_per_buffer
394             self._core.set_region_to_update(subrectangle)
395             if self._core.colorspace.depth >= 8:
396                 subrectangle_size_bytes = subrectangle.size() * (
397                     self._core.colorspace.depth // 8
398                 )
399             else:
400                 subrectangle_size_bytes = subrectangle.size() // (
401                     8 // self._core.colorspace.depth
402                 )
403
404             buffer = memoryview(bytearray([0] * (buffer_size * 4)))
405             mask = memoryview(bytearray([0] * mask_length))
406             self._core.fill_area(subrectangle, mask, buffer)
407
408             # Can't acquire display bus; skip the rest of the data.
409             if not self._core.bus_free():
410                 return False
411
412             self._core.begin_transaction()
413             self._send_pixels(buffer[:subrectangle_size_bytes])
414             self._core.end_transaction()
415         return True
416
417     def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
418         """Extract the pixels from a single row"""
419         if self._core.colorspace.depth != 16:
420             raise ValueError("Display must have a 16 bit colorspace.")
421
422         area = Area(0, y, self._core.width, y + 1)
423         pixels_per_word = 32 // self._core.colorspace.depth
424         buffer_size = self._core.width // pixels_per_word
425         pixels_per_buffer = area.size()
426         if pixels_per_buffer % pixels_per_word:
427             buffer_size += 1
428
429         buffer = bytearray([0] * (buffer_size * 4))
430         mask_length = (pixels_per_buffer // 32) + 1
431         mask = array("L", [0x00000000] * mask_length)
432         self._core.fill_area(area, mask, buffer)
433         return buffer
434
435     def _release(self) -> None:
436         """Release the display and free its resources"""
437         self.auto_refresh = False
438         self._core.release_display_core()
439
440     def _reset(self) -> None:
441         """Reset the display"""
442         self.auto_refresh = True
443         circuitpython_splash.x = 0
444         circuitpython_splash.y = 0
445         if not circuitpython_splash._in_group:  # pylint: disable=protected-access
446             self._set_root_group(circuitpython_splash)
447
448     @property
449     def auto_refresh(self) -> bool:
450         """True when the display is refreshed automatically."""
451         return self._auto_refresh
452
453     @auto_refresh.setter
454     def auto_refresh(self, value: bool):
455         self._first_manual_refresh = not value
456         self._auto_refresh = value
457
458     @property
459     def brightness(self) -> float:
460         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
461         return self._brightness
462
463     @brightness.setter
464     def brightness(self, value: float):
465         if 0 <= float(value) <= 1.0:
466             if not self._backlight_on_high:
467                 value = 1.0 - value
468
469             if self._backlight_type == BACKLIGHT_PWM:
470                 self._backlight.duty_cycle = value * 0xFFFF
471             elif self._backlight_type == BACKLIGHT_IN_OUT:
472                 self._backlight.value = value > 0.99
473             elif self._brightness_command is not None:
474                 okay = self._core.begin_transaction()
475                 if okay:
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, round(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 width(self) -> int:
498         """Display Width"""
499         return self._core.get_width()
500
501     @property
502     def height(self) -> int:
503         """Display Height"""
504         return self._core.get_height()
505
506     @property
507     def rotation(self) -> int:
508         """The rotation of the display as an int in degrees."""
509         return self._core.get_rotation()
510
511     @rotation.setter
512     def rotation(self, value: int):
513         if value % 90 != 0:
514             raise ValueError("Display rotation must be in 90 degree increments")
515         self._core.set_rotation(value)
516
517     @property
518     def bus(self) -> _DisplayBus:
519         """Current Display Bus"""
520         return self._core.get_bus()