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 typing import Optional
25 import microcontroller
26 import circuitpython_typing
27 from ._displaycore import _DisplayCore
28 from ._displaybus import _DisplayBus
29 from ._colorconverter import ColorConverter
30 from ._group import Group
31 from ._structs import RectangleStruct
32 from ._area import Area
33 from ._constants import (
34 CHIP_SELECT_TOGGLE_EVERY_BYTE,
35 CHIP_SELECT_UNTOUCHED,
43 __version__ = "0.0.0+auto.0"
44 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48 # pylint: disable=too-many-instance-attributes
49 """This initializes a display and connects it into CircuitPython. Unlike other objects
50 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
51 This is done so that CircuitPython can use the display itself.
53 Most people should not use this class directly. Use a specific display driver instead
54 that will contain the initialization sequence at minimum.
59 display_bus: _DisplayBus,
60 init_sequence: circuitpython_typing.ReadableBuffer,
67 color_depth: int = 16,
68 grayscale: bool = False,
69 pixels_in_byte_share_row: bool = True,
70 bytes_per_cell: int = 1,
71 reverse_pixels_in_byte: bool = False,
72 reverse_bytes_in_word: bool = True,
73 set_column_command: int = 0x2A,
74 set_row_command: int = 0x2B,
75 write_ram_command: int = 0x2C,
76 backlight_pin: Optional[microcontroller.Pin] = None,
77 brightness_command: Optional[int] = None,
78 brightness: float = 1.0,
79 auto_brightness: bool = False,
80 single_byte_bounds: bool = False,
81 data_as_commands: bool = False,
82 auto_refresh: bool = True,
83 native_frames_per_second: int = 60,
84 backlight_on_high: bool = True,
85 SH1107_addressing: bool = False,
86 set_vertical_scroll: int = 0,
88 # pylint: disable=unused-argument,too-many-locals,invalid-name
89 """Create a Display object on the given display bus (`displayio.FourWire` or
90 `paralleldisplay.ParallelBus`).
92 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
93 with a command byte followed by a byte to determine the parameter count and if a
94 delay is need after. When the top bit of the second byte is 1, the next byte will be
95 the delay time in milliseconds. The remaining 7 bits are the parameter count
96 excluding any delay byte. The third through final bytes are the remaining command
97 parameters. The next byte will begin a new command definition. Here is a portion of
100 .. code-block:: python
103 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
104 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
105 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
106 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
108 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
110 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
111 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
112 Multiple byte literals (b”“) are merged together on load. The parens are needed to
113 allow byte literals on subsequent lines.
115 The initialization sequence should always leave the display memory access inline with
116 the scan of the display to minimize tearing artifacts.
118 # Turn off auto-refresh as we init
119 self._auto_refresh = False
122 if single_byte_bounds:
126 self._core = _DisplayCore(
131 ram_height=ram_height,
135 color_depth=color_depth,
137 pixels_in_byte_share_row=pixels_in_byte_share_row,
138 bytes_per_cell=bytes_per_cell,
139 reverse_pixels_in_byte=reverse_pixels_in_byte,
140 reverse_bytes_in_word=reverse_bytes_in_word,
141 column_command=set_column_command,
142 row_command=set_row_command,
143 set_current_column_command=NO_COMMAND,
144 set_current_row_command=NO_COMMAND,
145 data_as_commands=data_as_commands,
146 always_toggle_chip_select=False,
147 sh1107_addressing=(SH1107_addressing and color_depth == 1),
148 address_little_endian=False,
151 self._write_ram_command = write_ram_command
152 self._brightness_command = brightness_command
153 self._first_manual_refresh = not auto_refresh
154 self._backlight_on_high = backlight_on_high
156 self._native_frames_per_second = native_frames_per_second
157 self._native_ms_per_frame = 1000 // native_frames_per_second
159 self._auto_brightness = auto_brightness
160 self._brightness = brightness
161 self._auto_refresh = auto_refresh
163 self._initialize(init_sequence)
164 self._buffer = Image.new("RGB", (width, height))
165 self._current_group = None
166 self._last_refresh_call = 0
167 self._refresh_thread = None
168 if self._auto_refresh:
169 self.auto_refresh = True
170 self._colorconverter = ColorConverter()
172 self._backlight_type = None
173 if backlight_pin is not None:
175 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
177 # 100Hz looks decent and doesn't keep the CPU too busy
178 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
179 self._backlight_type = BACKLIGHT_PWM
181 # PWMOut not implemented on this platform
183 if self._backlight_type is None:
184 self._backlight_type = BACKLIGHT_IN_OUT
185 self._backlight = digitalio.DigitalInOut(backlight_pin)
186 self._backlight.switch_to_output()
187 self.brightness = brightness
189 def __new__(cls, *args, **kwargs):
190 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
194 allocate_display(cls)
195 return super().__new__(cls)
197 def _initialize(self, init_sequence):
199 while i < len(init_sequence):
200 command = init_sequence[i]
201 data_size = init_sequence[i + 1]
202 delay = (data_size & 0x80) > 0
205 if self._core.data_as_commands:
208 CHIP_SELECT_TOGGLE_EVERY_BYTE,
209 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
213 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
217 CHIP_SELECT_UNTOUCHED,
218 init_sequence[i + 2 : i + 2 + data_size],
223 delay_time_ms = init_sequence[i + 1 + data_size]
224 if delay_time_ms == 255:
226 time.sleep(delay_time_ms / 1000)
229 def _send_pixels(self, pixels):
230 if not self._core.data_as_commands:
233 CHIP_SELECT_TOGGLE_EVERY_BYTE,
234 bytes([self._write_ram_command]),
236 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
238 def show(self, group: Group) -> None:
239 """Switches to displaying the given group of layers. When group is None, the
240 default CircuitPython terminal will be shown.
242 self._core.show(group)
247 target_frames_per_second: Optional[int] = None,
248 minimum_frames_per_second: int = 0,
250 """When auto refresh is off, waits for the target frame rate and then refreshes the
251 display, returning True. If the call has taken too long since the last refresh call
252 for the given target frame rate, then the refresh returns False immediately without
253 updating the screen to hopefully help getting caught up.
255 If the time since the last successful refresh is below the minimum frame rate, then
256 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
258 When auto refresh is on, updates the display immediately. (The display will also
259 update without calls to this.)
261 maximum_ms_per_real_frame = 0xFFFFFFFF
262 if minimum_frames_per_second > 0:
263 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
265 if target_frames_per_second is None:
266 target_ms_per_frame = 0xFFFFFFFF
268 target_ms_per_frame = 1000 // target_frames_per_second
271 not self._auto_refresh
272 and not self._first_manual_refresh
273 and target_ms_per_frame != 0xFFFFFFFF
275 current_time = time.monotonic() * 1000
276 current_ms_since_real_refresh = current_time - self._core.last_refresh
277 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
278 raise RuntimeError("Below minimum frame rate")
279 current_ms_since_last_call = current_time - self._last_refresh_call
280 self._last_refresh_call = current_time
281 if current_ms_since_last_call > target_ms_per_frame:
284 remaining_time = target_ms_per_frame - (
285 current_ms_since_real_refresh % target_ms_per_frame
287 time.sleep(remaining_time / 1000)
288 self._first_manual_refresh = False
289 self._refresh_display()
292 def _refresh_display(self):
293 # pylint: disable=protected-access
294 if not self._core.start_refresh():
297 # TODO: Likely move this to _refresh_area()
298 # Go through groups and and add each to buffer
299 if self._core.current_group is not None:
300 buffer = Image.new("RGBA", (self._core.width, self._core.height))
301 # Recursively have everything draw to the image
302 self._core.current_group._fill_area(
304 ) # pylint: disable=protected-access
305 # save image to buffer (or probably refresh buffer so we can compare)
306 self._buffer.paste(buffer)
308 areas_to_refresh = self._get_refresh_areas()
310 for area in areas_to_refresh:
311 self._refresh_area(area)
313 self._core.finish_refresh()
317 def _get_refresh_areas(self) -> list[Area]:
318 """Get a list of areas to be refreshed"""
320 if self._core.current_group is not None:
321 # Eventually calculate dirty rectangles here
322 areas.append(Area(0, 0, self._core.width, self._core.height))
325 def background(self):
326 """Run background refresh tasks. Do not call directly"""
329 and (time.monotonic() * 1000 - self._core.last_refresh)
330 > self._native_ms_per_frame
334 def _refresh_area(self, area) -> bool:
335 """Loop through dirty areas and redraw that area."""
336 # pylint: disable=too-many-locals
340 if not self._core.clip_area(area, clipped):
343 rows_per_buffer = clipped.height()
344 pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
345 pixels_per_buffer = clipped.size()
349 if self._core.sh1107_addressing:
350 subrectangles = rows_per_buffer // 8
352 elif clipped.size() > buffer_size * pixels_per_word:
353 rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
354 if rows_per_buffer == 0:
357 self._core.colorspace.depth < 8
358 and self._core.colorspace.pixels_in_byte_share_row
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:
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:
371 buffer = bytearray(buffer_size)
372 mask_length = (pixels_per_buffer // 32) + 1
373 mask = bytearray(mask_length)
374 remaining_rows = clipped.height()
376 for subrect_index in range(subrectangles):
379 clipped.y1 + rows_per_buffer * subrect_index,
381 clipped.y1 + rows_per_buffer * (subrect_index + 1),
383 if remaining_rows < rows_per_buffer:
384 subrectangle.y2 = subrectangle.y1 + remaining_rows
385 self._core.set_region_to_update(subrectangle)
386 if self._core.colorspace.depth >= 8:
387 subrectangle_size_bytes = subrectangle.size() * (
388 self._core.colorspace.depth // 8
391 subrectangle_size_bytes = subrectangle.size() // (
392 8 // self._core.colorspace.depth
395 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()
402 def _apply_rotation(self, rectangle):
403 """Adjust the rectangle coordinates based on rotation"""
404 if self._core.rotation == 90:
405 return RectangleStruct(
406 self._core.height - rectangle.y2,
408 self._core.height - rectangle.y1,
411 if self._core.rotation == 180:
412 return RectangleStruct(
413 self._core.width - rectangle.x2,
414 self._core.height - rectangle.y2,
415 self._core.width - rectangle.x1,
416 self._core.height - rectangle.y1,
418 if self._core.rotation == 270:
419 return RectangleStruct(
421 self._core.width - rectangle.x2,
423 self._core.width - rectangle.x1,
428 self, y: int, buffer: circuitpython_typing.WriteableBuffer
429 ) -> circuitpython_typing.WriteableBuffer:
430 """Extract the pixels from a single row"""
431 for x in range(0, self._core.width):
432 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
433 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
434 buffer[x * 2 + 1] = _rgb_565 & 0xFF
437 def release(self) -> None:
438 """Release the display and free its resources"""
439 self.auto_refresh = False
440 self._core.release_display_core()
442 def reset(self) -> None:
443 """Reset the display"""
444 self.auto_refresh = True
447 def auto_refresh(self) -> bool:
448 """True when the display is refreshed automatically."""
449 return self._auto_refresh
452 def auto_refresh(self, value: bool):
453 self._first_manual_refresh = not value
454 self._auto_refresh = value
457 def brightness(self) -> float:
458 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
459 When `auto_brightness` is True, the value of `brightness` will change automatically.
460 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
462 return self._brightness
465 def brightness(self, value: float):
466 if 0 <= float(value) <= 1.0:
467 if not self._backlight_on_high:
470 if self._backlight_type == BACKLIGHT_PWM:
471 self._backlight.duty_cycle = value * 0xFFFF
472 elif self._backlight_type == BACKLIGHT_IN_OUT:
473 self._backlight.value = value > 0.99
474 elif self._brightness_command is not None:
475 self._core.begin_transaction()
476 if self._core.data_as_commands:
479 CHIP_SELECT_TOGGLE_EVERY_BYTE,
480 bytes([self._brightness_command, 0xFF * value]),
485 CHIP_SELECT_TOGGLE_EVERY_BYTE,
486 bytes([self._brightness_command]),
489 DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
491 self._core.end_transaction()
492 self._brightness = value
494 raise ValueError("Brightness must be between 0.0 and 1.0")
497 def auto_brightness(self) -> bool:
498 """True when the display brightness is adjusted automatically, based on an ambient
499 light sensor or other method. Note that some displays may have this set to True by
500 default, but not actually implement automatic brightness adjustment.
501 `auto_brightness` is set to False if `brightness` is set manually.
503 return self._auto_brightness
505 @auto_brightness.setter
506 def auto_brightness(self, value: bool):
507 self._auto_brightness = value
510 def width(self) -> int:
512 return self._core.get_width()
515 def height(self) -> int:
517 return self._core.get_height()
520 def rotation(self) -> int:
521 """The rotation of the display as an int in degrees."""
522 return self._core.get_rotation()
525 def rotation(self, value: int):
526 self._core.set_rotation(value)
529 def bus(self) -> _DisplayBus:
530 """Current Display Bus"""
531 return self._core.get_bus()