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