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