1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
22 from array import array
23 from typing import Optional
26 import microcontroller
27 import circuitpython_typing
28 from ._displaycore import _DisplayCore
29 from ._displaybus import _DisplayBus
30 from ._colorconverter import ColorConverter
31 from ._group import Group
32 from ._structs import RectangleStruct
33 from ._area import Area
34 from ._constants import (
35 CHIP_SELECT_TOGGLE_EVERY_BYTE,
36 CHIP_SELECT_UNTOUCHED,
44 __version__ = "0.0.0+auto.0"
45 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49 # pylint: disable=too-many-instance-attributes
50 """This initializes a display and connects it into CircuitPython. Unlike other objects
51 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
52 This is done so that CircuitPython can use the display itself.
54 Most people should not use this class directly. Use a specific display driver instead
55 that will contain the initialization sequence at minimum.
60 display_bus: _DisplayBus,
61 init_sequence: circuitpython_typing.ReadableBuffer,
68 color_depth: int = 16,
69 grayscale: bool = False,
70 pixels_in_byte_share_row: bool = True,
71 bytes_per_cell: int = 1,
72 reverse_pixels_in_byte: bool = False,
73 reverse_bytes_in_word: bool = True,
74 set_column_command: int = 0x2A,
75 set_row_command: int = 0x2B,
76 write_ram_command: int = 0x2C,
77 backlight_pin: Optional[microcontroller.Pin] = None,
78 brightness_command: Optional[int] = None,
79 brightness: float = 1.0,
80 auto_brightness: bool = False,
81 single_byte_bounds: bool = False,
82 data_as_commands: bool = False,
83 auto_refresh: bool = True,
84 native_frames_per_second: int = 60,
85 backlight_on_high: bool = True,
86 SH1107_addressing: bool = False,
87 set_vertical_scroll: int = 0,
89 # pylint: disable=unused-argument,too-many-locals,invalid-name
90 """Create a Display object on the given display bus (`displayio.FourWire` or
91 `paralleldisplay.ParallelBus`).
93 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
94 with a command byte followed by a byte to determine the parameter count and if a
95 delay is need after. When the top bit of the second byte is 1, the next byte will be
96 the delay time in milliseconds. The remaining 7 bits are the parameter count
97 excluding any delay byte. The third through final bytes are the remaining command
98 parameters. The next byte will begin a new command definition. Here is a portion of
101 .. code-block:: python
104 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
105 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
106 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
107 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
109 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
111 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
112 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
113 Multiple byte literals (b”“) are merged together on load. The parens are needed to
114 allow byte literals on subsequent lines.
116 The initialization sequence should always leave the display memory access inline with
117 the scan of the display to minimize tearing artifacts.
119 # Turn off auto-refresh as we init
120 self._auto_refresh = False
123 if single_byte_bounds:
127 self._core = _DisplayCore(
132 ram_height=ram_height,
136 color_depth=color_depth,
138 pixels_in_byte_share_row=pixels_in_byte_share_row,
139 bytes_per_cell=bytes_per_cell,
140 reverse_pixels_in_byte=reverse_pixels_in_byte,
141 reverse_bytes_in_word=reverse_bytes_in_word,
142 column_command=set_column_command,
143 row_command=set_row_command,
144 set_current_column_command=NO_COMMAND,
145 set_current_row_command=NO_COMMAND,
146 data_as_commands=data_as_commands,
147 always_toggle_chip_select=False,
148 sh1107_addressing=(SH1107_addressing and color_depth == 1),
149 address_little_endian=False,
152 self._write_ram_command = write_ram_command
153 self._brightness_command = brightness_command
154 self._first_manual_refresh = not auto_refresh
155 self._backlight_on_high = backlight_on_high
157 self._native_frames_per_second = native_frames_per_second
158 self._native_ms_per_frame = 1000 // native_frames_per_second
160 self._auto_brightness = auto_brightness
161 self._brightness = brightness
162 self._auto_refresh = auto_refresh
164 self._initialize(init_sequence)
165 self._buffer = Image.new("RGB", (width, height))
166 self._current_group = None
167 self._last_refresh_call = 0
168 self._refresh_thread = None
169 if self._auto_refresh:
170 self.auto_refresh = True
171 self._colorconverter = ColorConverter()
173 self._backlight_type = None
174 if backlight_pin is not None:
176 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
178 # 100Hz looks decent and doesn't keep the CPU too busy
179 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
180 self._backlight_type = BACKLIGHT_PWM
182 # PWMOut not implemented on this platform
184 if self._backlight_type is None:
185 self._backlight_type = BACKLIGHT_IN_OUT
186 self._backlight = digitalio.DigitalInOut(backlight_pin)
187 self._backlight.switch_to_output()
188 self.brightness = brightness
190 def __new__(cls, *args, **kwargs):
191 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
195 display_instance = super().__new__(cls)
196 allocate_display(display_instance)
197 return display_instance
199 def _initialize(self, init_sequence):
201 while i < len(init_sequence):
202 command = init_sequence[i]
203 data_size = init_sequence[i + 1]
204 delay = (data_size & 0x80) > 0
207 if self._core.data_as_commands:
210 CHIP_SELECT_TOGGLE_EVERY_BYTE,
211 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
215 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
219 CHIP_SELECT_UNTOUCHED,
220 init_sequence[i + 2 : i + 2 + data_size],
225 delay_time_ms = init_sequence[i + 1 + data_size]
226 if delay_time_ms == 255:
228 time.sleep(delay_time_ms / 1000)
231 def _send_pixels(self, pixels):
232 if not self._core.data_as_commands:
235 CHIP_SELECT_TOGGLE_EVERY_BYTE,
236 bytes([self._write_ram_command]),
238 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
240 def show(self, group: Group) -> None:
241 """Switches to displaying the given group of layers. When group is None, the
242 default CircuitPython terminal will be shown.
244 self._core.show(group)
249 target_frames_per_second: Optional[int] = None,
250 minimum_frames_per_second: int = 0,
252 """When auto refresh is off, waits for the target frame rate and then refreshes the
253 display, returning True. If the call has taken too long since the last refresh call
254 for the given target frame rate, then the refresh returns False immediately without
255 updating the screen to hopefully help getting caught up.
257 If the time since the last successful refresh is below the minimum frame rate, then
258 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
260 When auto refresh is on, updates the display immediately. (The display will also
261 update without calls to this.)
263 maximum_ms_per_real_frame = 0xFFFFFFFF
264 if minimum_frames_per_second > 0:
265 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
267 if target_frames_per_second is None:
268 target_ms_per_frame = 0xFFFFFFFF
270 target_ms_per_frame = 1000 // target_frames_per_second
273 not self._auto_refresh
274 and not self._first_manual_refresh
275 and target_ms_per_frame != 0xFFFFFFFF
277 current_time = time.monotonic() * 1000
278 current_ms_since_real_refresh = current_time - self._core.last_refresh
279 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
280 raise RuntimeError("Below minimum frame rate")
281 current_ms_since_last_call = current_time - self._last_refresh_call
282 self._last_refresh_call = current_time
283 if current_ms_since_last_call > target_ms_per_frame:
286 remaining_time = target_ms_per_frame - (
287 current_ms_since_real_refresh % target_ms_per_frame
289 time.sleep(remaining_time / 1000)
290 self._first_manual_refresh = False
291 self._refresh_display()
294 def _refresh_display(self):
295 if not self._core.start_refresh():
298 # TODO: Likely move this to _refresh_area()
299 # Go through groups and and add each to buffer
301 if self._core.current_group is not None:
302 buffer = Image.new("RGBA", (self._core.width, self._core.height))
303 # Recursively have everything draw to the image
304 self._core.current_group._fill_area(
306 ) # pylint: disable=protected-access
307 # save image to buffer (or probably refresh buffer so we can compare)
308 self._buffer.paste(buffer)
310 areas_to_refresh = self._get_refresh_areas()
312 for area in areas_to_refresh:
313 self._refresh_area(area)
315 self._core.finish_refresh()
319 def _get_refresh_areas(self) -> list[Area]:
320 """Get a list of areas to be refreshed"""
322 if self._core.full_refresh:
323 areas.append(self._core.area)
324 elif self._core.current_group is not None:
325 self._core.current_group._get_refresh_areas( # pylint: disable=protected-access
330 def background(self):
331 """Run background refresh tasks. Do not call directly"""
334 and (time.monotonic() * 1000 - self._core.last_refresh)
335 > self._native_ms_per_frame
339 def _refresh_area(self, area) -> bool:
340 """Loop through dirty areas and redraw that area."""
341 # pylint: disable=too-many-locals
345 if not self._core.clip_area(area, clipped):
348 rows_per_buffer = clipped.height()
349 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
350 pixels_per_buffer = clipped.size()
354 if self._core.sh1107_addressing:
355 subrectangles = 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:
362 self._core.colorspace.depth < 8
363 and self._core.colorspace.pixels_in_byte_share_row
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:
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:
376 buffer = bytearray([0] * (buffer_size * struct.calcsize("I")))
377 mask_length = (pixels_per_buffer // 32) + 1
378 mask = array("L", [0] * mask_length)
379 remaining_rows = clipped.height()
381 for subrect_index in range(subrectangles):
384 clipped.y1 + rows_per_buffer * subrect_index,
386 clipped.y1 + rows_per_buffer * (subrect_index + 1),
388 if remaining_rows < rows_per_buffer:
389 subrectangle.y2 = subrectangle.y1 + remaining_rows
390 self._core.set_region_to_update(subrectangle)
391 if self._core.colorspace.depth >= 8:
392 subrectangle_size_bytes = subrectangle.size() * (
393 self._core.colorspace.depth // 8
396 subrectangle_size_bytes = subrectangle.size() // (
397 8 // self._core.colorspace.depth
400 self._core.fill_area(subrectangle, mask, buffer)
402 self._core.begin_transaction()
403 self._send_pixels(buffer[:subrectangle_size_bytes])
404 self._core.end_transaction()
407 def _apply_rotation(self, rectangle):
408 """Adjust the rectangle coordinates based on rotation"""
409 if self._core.rotation == 90:
410 return RectangleStruct(
411 self._core.height - rectangle.y2,
413 self._core.height - rectangle.y1,
416 if self._core.rotation == 180:
417 return RectangleStruct(
418 self._core.width - rectangle.x2,
419 self._core.height - rectangle.y2,
420 self._core.width - rectangle.x1,
421 self._core.height - rectangle.y1,
423 if self._core.rotation == 270:
424 return RectangleStruct(
426 self._core.width - rectangle.x2,
428 self._core.width - rectangle.x1,
433 self, y: int, buffer: circuitpython_typing.WriteableBuffer
434 ) -> circuitpython_typing.WriteableBuffer:
435 """Extract the pixels from a single row"""
436 for x in range(0, self._core.width):
437 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
438 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
439 buffer[x * 2 + 1] = _rgb_565 & 0xFF
442 def release(self) -> None:
443 """Release the display and free its resources"""
444 self.auto_refresh = False
445 self._core.release_display_core()
447 def reset(self) -> None:
448 """Reset the display"""
449 self.auto_refresh = True
452 def auto_refresh(self) -> bool:
453 """True when the display is refreshed automatically."""
454 return self._auto_refresh
457 def auto_refresh(self, value: bool):
458 self._first_manual_refresh = not value
459 self._auto_refresh = value
462 def brightness(self) -> float:
463 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
464 When `auto_brightness` is True, the value of `brightness` will change automatically.
465 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
467 return self._brightness
470 def brightness(self, value: float):
471 if 0 <= float(value) <= 1.0:
472 if not self._backlight_on_high:
475 if self._backlight_type == BACKLIGHT_PWM:
476 self._backlight.duty_cycle = value * 0xFFFF
477 elif self._backlight_type == BACKLIGHT_IN_OUT:
478 self._backlight.value = value > 0.99
479 elif self._brightness_command is not None:
480 self._core.begin_transaction()
481 if self._core.data_as_commands:
484 CHIP_SELECT_TOGGLE_EVERY_BYTE,
485 bytes([self._brightness_command, 0xFF * value]),
490 CHIP_SELECT_TOGGLE_EVERY_BYTE,
491 bytes([self._brightness_command]),
494 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
496 self._core.end_transaction()
497 self._brightness = value
499 raise ValueError("Brightness must be between 0.0 and 1.0")
502 def auto_brightness(self) -> bool:
503 """True when the display brightness is adjusted automatically, based on an ambient
504 light sensor or other method. Note that some displays may have this set to True by
505 default, but not actually implement automatic brightness adjustment.
506 `auto_brightness` is set to False if `brightness` is set manually.
508 return self._auto_brightness
510 @auto_brightness.setter
511 def auto_brightness(self, value: bool):
512 self._auto_brightness = value
515 def width(self) -> int:
517 return self._core.get_width()
520 def height(self) -> int:
522 return self._core.get_height()
525 def rotation(self) -> int:
526 """The rotation of the display as an int in degrees."""
527 return self._core.get_rotation()
530 def rotation(self, value: int):
531 self._core.set_rotation(value)
534 def bus(self) -> _DisplayBus:
535 """Current Display Bus"""
536 return self._core.get_bus()