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
23 from dataclasses import astuple
27 import microcontroller
28 import circuitpython_typing
29 from ._displaycore import _DisplayCore
30 from ._displaybus import _DisplayBus
31 from ._colorconverter import ColorConverter
32 from ._group import Group
33 from ._structs import RectangleStruct
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._subrectangles = []
167 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
168 self._current_group = None
169 self._last_refresh_call = 0
170 self._refresh_thread = None
171 if self._auto_refresh:
172 self.auto_refresh = True
173 self._colorconverter = ColorConverter()
175 self._backlight_type = None
176 if backlight_pin is not None:
178 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
180 # 100Hz looks decent and doesn't keep the CPU too busy
181 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
182 self._backlight_type = BACKLIGHT_PWM
184 # PWMOut not implemented on this platform
186 if self._backlight_type is None:
187 self._backlight_type = BACKLIGHT_IN_OUT
188 self._backlight = digitalio.DigitalInOut(backlight_pin)
189 self._backlight.switch_to_output()
190 self.brightness = brightness
192 def __new__(cls, *args, **kwargs):
193 from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
197 allocate_display(cls)
198 return super().__new__(cls)
200 def _initialize(self, init_sequence):
202 while i < len(init_sequence):
203 command = init_sequence[i]
204 data_size = init_sequence[i + 1]
205 delay = (data_size & 0x80) > 0
208 if self._core.data_as_commands:
211 CHIP_SELECT_TOGGLE_EVERY_BYTE,
212 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
216 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
220 CHIP_SELECT_UNTOUCHED,
221 init_sequence[i + 2 : i + 2 + data_size],
226 delay_time_ms = init_sequence[i + 1 + data_size]
227 if delay_time_ms == 255:
229 time.sleep(delay_time_ms / 1000)
232 def _send(self, command, data):
233 self._core.begin_transaction()
234 if self._core.data_as_commands:
236 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
240 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
242 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
243 self._core.end_transaction()
245 def _send_pixels(self, data):
246 if not self._core.data_as_commands:
249 CHIP_SELECT_TOGGLE_EVERY_BYTE,
250 bytes([self._write_ram_command]),
252 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
254 def show(self, group: Group) -> None:
255 """Switches to displaying the given group of layers. When group is None, the
256 default CircuitPython terminal will be shown.
258 self._core.show(group)
263 target_frames_per_second: Optional[int] = None,
264 minimum_frames_per_second: int = 0,
266 """When auto refresh is off, waits for the target frame rate and then refreshes the
267 display, returning True. If the call has taken too long since the last refresh call
268 for the given target frame rate, then the refresh returns False immediately without
269 updating the screen to hopefully help getting caught up.
271 If the time since the last successful refresh is below the minimum frame rate, then
272 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
274 When auto refresh is on, updates the display immediately. (The display will also
275 update without calls to this.)
277 maximum_ms_per_real_frame = 0xFFFFFFFF
278 if minimum_frames_per_second > 0:
279 maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
281 if target_frames_per_second is None:
282 target_ms_per_frame = 0xFFFFFFFF
284 target_ms_per_frame = 1000 // target_frames_per_second
287 not self._auto_refresh
288 and not self._first_manual_refresh
289 and target_ms_per_frame != 0xFFFFFFFF
291 current_time = time.monotonic() * 1000
292 current_ms_since_real_refresh = current_time - self._core.last_refresh
293 if current_ms_since_real_refresh > maximum_ms_per_real_frame:
294 raise RuntimeError("Below minimum frame rate")
295 current_ms_since_last_call = current_time - self._last_refresh_call
296 self._last_refresh_call = current_time
297 if current_ms_since_last_call > target_ms_per_frame:
300 remaining_time = target_ms_per_frame - (
301 current_ms_since_real_refresh % target_ms_per_frame
303 time.sleep(remaining_time / 1000)
304 self._first_manual_refresh = False
305 self._refresh_display()
308 def _refresh_display(self):
309 # pylint: disable=protected-access
310 if not self._core.start_refresh():
313 # Go through groups and and add each to buffer
314 if self._core.current_group is not None:
315 buffer = Image.new("RGBA", (self._core.width, self._core.height))
316 # Recursively have everything draw to the image
317 self._core.current_group._fill_area(
319 ) # pylint: disable=protected-access
320 # save image to buffer (or probably refresh buffer so we can compare)
321 self._buffer.paste(buffer)
323 self._subrectangles = self._core.get_refresh_areas()
325 for area in self._subrectangles:
326 self._refresh_display_area(area)
328 self._core.finish_refresh()
332 def _background(self):
335 and (time.monotonic() * 1000 - self._core.last_refresh)
336 > self._native_ms_per_frame
340 def _refresh_display_area(self, rectangle):
341 """Loop through dirty rectangles and redraw that area."""
342 img = self._buffer.convert("RGB").crop(astuple(rectangle))
343 img = img.rotate(360 - self._core.rotation, expand=True)
345 display_rectangle = self._apply_rotation(rectangle)
346 img = img.crop(astuple(self._clip(display_rectangle)))
348 data = numpy.array(img).astype("uint16")
350 ((data[:, :, 0] & 0xF8) << 8)
351 | ((data[:, :, 1] & 0xFC) << 3)
352 | (data[:, :, 2] >> 3)
356 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
360 self._core.column_command,
362 display_rectangle.x1 + self._core.colstart,
363 display_rectangle.x2 + self._core.colstart - 1,
367 self._core.row_command,
369 display_rectangle.y1 + self._core.rowstart,
370 display_rectangle.y2 + self._core.rowstart - 1,
374 self._core.begin_transaction()
375 self._send_pixels(pixels)
376 self._core.end_transaction()
378 def _clip(self, rectangle):
379 if self._core.rotation in (90, 270):
380 width, height = self._core.height, self._core.width
382 width, height = self._core.width, self._core.height
384 rectangle.x1 = max(rectangle.x1, 0)
385 rectangle.y1 = max(rectangle.y1, 0)
386 rectangle.x2 = min(rectangle.x2, width)
387 rectangle.y2 = min(rectangle.y2, height)
391 def _apply_rotation(self, rectangle):
392 """Adjust the rectangle coordinates based on rotation"""
393 if self._core.rotation == 90:
394 return RectangleStruct(
395 self._core.height - rectangle.y2,
397 self._core.height - rectangle.y1,
400 if self._core.rotation == 180:
401 return RectangleStruct(
402 self._core.width - rectangle.x2,
403 self._core.height - rectangle.y2,
404 self._core.width - rectangle.x1,
405 self._core.height - rectangle.y1,
407 if self._core.rotation == 270:
408 return RectangleStruct(
410 self._core.width - rectangle.x2,
412 self._core.width - rectangle.x1,
416 def _encode_pos(self, x, y):
417 """Encode a postion into bytes."""
418 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
421 self, y: int, buffer: circuitpython_typing.WriteableBuffer
422 ) -> circuitpython_typing.WriteableBuffer:
423 """Extract the pixels from a single row"""
424 for x in range(0, self._core.width):
425 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
426 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
427 buffer[x * 2 + 1] = _rgb_565 & 0xFF
431 def auto_refresh(self) -> bool:
432 """True when the display is refreshed automatically."""
433 return self._auto_refresh
436 def auto_refresh(self, value: bool):
437 self._auto_refresh = value
440 def brightness(self) -> float:
441 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
442 When `auto_brightness` is True, the value of `brightness` will change automatically.
443 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
445 return self._brightness
448 def brightness(self, value: float):
449 if 0 <= float(value) <= 1.0:
450 self._brightness = value
451 if self._backlight_type == BACKLIGHT_IN_OUT:
452 self._backlight.value = round(self._brightness)
453 elif self._backlight_type == BACKLIGHT_PWM:
454 self._backlight.duty_cycle = self._brightness * 65535
455 elif self._brightness_command is not None:
456 self._send(self._brightness_command, round(value * 255))
458 raise ValueError("Brightness must be between 0.0 and 1.0")
461 def auto_brightness(self) -> bool:
462 """True when the display brightness is adjusted automatically, based on an ambient
463 light sensor or other method. Note that some displays may have this set to True by
464 default, but not actually implement automatic brightness adjustment.
465 `auto_brightness` is set to False if `brightness` is set manually.
467 return self._auto_brightness
469 @auto_brightness.setter
470 def auto_brightness(self, value: bool):
471 self._auto_brightness = value
474 def width(self) -> int:
476 return self._core.get_width()
479 def height(self) -> int:
481 return self._core.get_height()
484 def rotation(self) -> int:
485 """The rotation of the display as an int in degrees."""
486 return self._core.get_rotation()
489 def rotation(self, value: int):
490 self._core.set_rotation(value)
493 def bus(self) -> _DisplayBus:
494 """Current Display Bus"""
495 return self._core.get_bus()