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