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