]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - busdisplay/__init__.py
Fix import order so pylint is happy
[hackapet/Adafruit_Blinka_Displayio.git] / busdisplay / __init__.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 displayio._displaycore import _DisplayCore
26 from displayio._colorconverter import ColorConverter
27 from displayio._group import Group, circuitpython_splash
28 from displayio._area import Area
29 from displayio._constants import (
30     CHIP_SELECT_TOGGLE_EVERY_BYTE,
31     CHIP_SELECT_UNTOUCHED,
32     DISPLAY_COMMAND,
33     DISPLAY_DATA,
34     BACKLIGHT_IN_OUT,
35     BACKLIGHT_PWM,
36     NO_COMMAND,
37     DELAY,
38 )
39 from ._displaybus import _DisplayBus
40
41 __version__ = "0.0.0+auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
43
44
45 class BusDisplay:
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, NotImplementedError):
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 displayio 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     @staticmethod
246     def show(_group: Group) -> None:  # pylint: disable=missing-function-docstring
247         raise AttributeError(".show(x) removed. Use .root_group = x")
248
249     def _set_root_group(self, root_group: Group) -> None:
250         ok = self._core.set_root_group(root_group)
251         if not ok:
252             raise ValueError("Group already used")
253
254     def refresh(
255         self,
256         *,
257         target_frames_per_second: Optional[int] = None,
258         minimum_frames_per_second: int = 0,
259     ) -> bool:
260         """When auto refresh is off, waits for the target frame rate and then refreshes the
261         display, returning True. If the call has taken too long since the last refresh call
262         for the given target frame rate, then the refresh returns False immediately without
263         updating the screen to hopefully help getting caught up.
264
265         If the time since the last successful refresh is below the minimum frame rate, then
266         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
267
268         When auto refresh is on, updates the display immediately. (The display will also
269         update without calls to this.)
270         """
271         maximum_ms_per_real_frame = 0xFFFFFFFF
272         if minimum_frames_per_second > 0:
273             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
274
275         if target_frames_per_second is None:
276             target_ms_per_frame = 0xFFFFFFFF
277         else:
278             target_ms_per_frame = 1000 // target_frames_per_second
279
280         if (
281             not self._auto_refresh
282             and not self._first_manual_refresh
283             and target_ms_per_frame != 0xFFFFFFFF
284         ):
285             current_time = time.monotonic() * 1000
286             current_ms_since_real_refresh = current_time - self._core.last_refresh
287             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
288                 raise RuntimeError("Below minimum frame rate")
289             current_ms_since_last_call = current_time - self._last_refresh_call
290             self._last_refresh_call = current_time
291             if current_ms_since_last_call > target_ms_per_frame:
292                 return False
293
294             remaining_time = target_ms_per_frame - (
295                 current_ms_since_real_refresh % target_ms_per_frame
296             )
297             time.sleep(remaining_time / 1000)
298         self._first_manual_refresh = False
299         self._refresh_display()
300         return True
301
302     def _refresh_display(self):
303         if not self._core.start_refresh():
304             return False
305
306         areas_to_refresh = self._get_refresh_areas()
307         for area in areas_to_refresh:
308             self._refresh_area(area)
309
310         self._core.finish_refresh()
311
312         return True
313
314     def _get_refresh_areas(self) -> list[Area]:
315         """Get a list of areas to be refreshed"""
316         areas = []
317         if self._core.full_refresh:
318             areas.append(self._core.area)
319         elif self._core.current_group is not None:
320             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
321                 areas
322             )
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, too-many-branches
337
338         clipped = Area()
339         # Clip the area to the display by overlapping the areas.
340         # If there is no overlap then we're done.
341         if not self._core.clip_area(area, clipped):
342             return True
343
344         rows_per_buffer = clipped.height()
345         pixels_per_word = 32 // self._core.colorspace.depth
346         pixels_per_buffer = clipped.size()
347
348         # We should have lots of memory
349         buffer_size = clipped.size() // pixels_per_word
350
351         subrectangles = 1
352         # for SH1107 and other boundary constrained controllers
353         #      write one single row at a time
354         if self._core.sh1107_addressing:
355             subrectangles = rows_per_buffer // 8
356             rows_per_buffer = 8
357         elif clipped.size() > buffer_size * pixels_per_word:
358             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
359             if rows_per_buffer == 0:
360                 rows_per_buffer = 1
361             # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
362             if (
363                 self._core.colorspace.depth < 8
364                 and self._core.colorspace.pixels_in_byte_share_row
365             ):
366                 pixels_per_byte = 8 // self._core.colorspace.depth
367                 if rows_per_buffer % pixels_per_byte != 0:
368                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
369             subrectangles = clipped.height() // rows_per_buffer
370             if clipped.height() % rows_per_buffer != 0:
371                 subrectangles += 1
372             pixels_per_buffer = rows_per_buffer * clipped.width()
373             buffer_size = pixels_per_buffer // pixels_per_word
374             if pixels_per_buffer % pixels_per_word:
375                 buffer_size += 1
376         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
377         remaining_rows = clipped.height()
378
379         for subrect_index in range(subrectangles):
380             subrectangle = Area(
381                 x1=clipped.x1,
382                 y1=clipped.y1 + rows_per_buffer * subrect_index,
383                 x2=clipped.x2,
384                 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
385             )
386             if remaining_rows < rows_per_buffer:
387                 subrectangle.y2 = subrectangle.y1 + remaining_rows
388             remaining_rows -= rows_per_buffer
389             self._core.set_region_to_update(subrectangle)
390             if self._core.colorspace.depth >= 8:
391                 subrectangle_size_bytes = subrectangle.size() * (
392                     self._core.colorspace.depth // 8
393                 )
394             else:
395                 subrectangle_size_bytes = subrectangle.size() // (
396                     8 // self._core.colorspace.depth
397                 )
398
399             buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
400             mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
401             self._core.fill_area(subrectangle, mask, buffer)
402
403             # Can't acquire display bus; skip the rest of the data.
404             if not self._core.bus_free():
405                 return False
406
407             self._core.begin_transaction()
408             self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
409             self._core.end_transaction()
410         return True
411
412     def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
413         """Extract the pixels from a single row"""
414         if self._core.colorspace.depth != 16:
415             raise ValueError("Display must have a 16 bit colorspace.")
416
417         area = Area(0, y, self._core.width, y + 1)
418         pixels_per_word = 32 // self._core.colorspace.depth
419         buffer_size = self._core.width // pixels_per_word
420         pixels_per_buffer = area.size()
421         if pixels_per_buffer % pixels_per_word:
422             buffer_size += 1
423
424         buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
425         mask_length = (pixels_per_buffer // 32) + 1
426         mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
427         self._core.fill_area(area, mask, buffer)
428         return buffer
429
430     def _release(self) -> None:
431         """Release the display and free its resources"""
432         self.auto_refresh = False
433         self._core.release_display_core()
434
435     def _reset(self) -> None:
436         """Reset the display"""
437         self.auto_refresh = True
438         circuitpython_splash.x = 0
439         circuitpython_splash.y = 0
440         if not circuitpython_splash._in_group:  # pylint: disable=protected-access
441             self._set_root_group(circuitpython_splash)
442
443     @property
444     def auto_refresh(self) -> bool:
445         """True when the display is refreshed automatically."""
446         return self._auto_refresh
447
448     @auto_refresh.setter
449     def auto_refresh(self, value: bool):
450         self._first_manual_refresh = not value
451         self._auto_refresh = value
452
453     @property
454     def brightness(self) -> float:
455         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
456         return self._brightness
457
458     @brightness.setter
459     def brightness(self, value: float):
460         if 0 <= float(value) <= 1.0:
461             if not self._backlight_on_high:
462                 value = 1.0 - value
463
464             if self._backlight_type == BACKLIGHT_PWM:
465                 self._backlight.duty_cycle = value * 0xFFFF
466             elif self._backlight_type == BACKLIGHT_IN_OUT:
467                 self._backlight.value = value > 0.99
468             elif self._brightness_command is not None:
469                 okay = self._core.begin_transaction()
470                 if okay:
471                     if self._core.data_as_commands:
472                         self._core.send(
473                             DISPLAY_COMMAND,
474                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
475                             bytes([self._brightness_command, round(0xFF * value)]),
476                         )
477                     else:
478                         self._core.send(
479                             DISPLAY_COMMAND,
480                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
481                             bytes([self._brightness_command]),
482                         )
483                         self._core.send(
484                             DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
485                         )
486                     self._core.end_transaction()
487             self._brightness = value
488         else:
489             raise ValueError("Brightness must be between 0.0 and 1.0")
490
491     @property
492     def width(self) -> int:
493         """Display Width"""
494         return self._core.get_width()
495
496     @property
497     def height(self) -> int:
498         """Display Height"""
499         return self._core.get_height()
500
501     @property
502     def rotation(self) -> int:
503         """The rotation of the display as an int in degrees."""
504         return self._core.get_rotation()
505
506     @rotation.setter
507     def rotation(self, value: int):
508         if value % 90 != 0:
509             raise ValueError("Display rotation must be in 90 degree increments")
510         transposed = self._core.rotation in (90, 270)
511         will_transposed = value in (90, 270)
512         if transposed != will_transposed:
513             self._core.width, self._core.height = self._core.height, self._core.width
514         self._core.set_rotation(value)
515         if self._core.current_group is not None:
516             self._core.current_group._update_transform(  # pylint: disable=protected-access
517                 self._core.transform
518             )
519
520     @property
521     def bus(self) -> _DisplayBus:
522         """Current Display Bus"""
523         return self._core.get_bus()
524
525     @property
526     def root_group(self) -> Group:
527         """
528         The root group on the display.
529         If the root group is set to `displayio.CIRCUITPYTHON_TERMINAL`, the default
530         CircuitPython terminal will be shown.
531         If the root group is set to ``None``, no output will be shown.
532         """
533         return self._core.current_group
534
535     @root_group.setter
536     def root_group(self, new_group: Group) -> None:
537         self._set_root_group(new_group)