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