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
23 from typing import Optional
24 from dataclasses import astuple
26 from PIL import Image, ImageDraw
28 import microcontroller
29 import circuitpython_typing
30 from ._displaycore import _DisplayCore
31 from ._displaybus import _DisplayBus
32 from ._colorconverter import ColorConverter
33 from ._group import Group
34 from ._structs import RectangleStruct
35 from ._constants import (
36 CHIP_SELECT_TOGGLE_EVERY_BYTE,
37 CHIP_SELECT_UNTOUCHED,
44 __version__ = "0.0.0+auto.0"
45 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
51 # pylint: disable=too-many-instance-attributes
52 """This initializes a display and connects it into CircuitPython. Unlike other objects
53 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
54 This is done so that CircuitPython can use the display itself.
56 Most people should not use this class directly. Use a specific display driver instead
57 that will contain the initialization sequence at minimum.
62 display_bus: _DisplayBus,
63 init_sequence: circuitpython_typing.ReadableBuffer,
70 color_depth: int = 16,
71 grayscale: bool = False,
72 pixels_in_byte_share_row: bool = True,
73 bytes_per_cell: int = 1,
74 reverse_pixels_in_byte: bool = False,
75 reverse_bytes_in_word: bool = True,
76 set_column_command: int = 0x2A,
77 set_row_command: int = 0x2B,
78 write_ram_command: int = 0x2C,
79 backlight_pin: Optional[microcontroller.Pin] = None,
80 brightness_command: Optional[int] = None,
81 brightness: float = 1.0,
82 auto_brightness: bool = False,
83 single_byte_bounds: bool = False,
84 data_as_commands: bool = False,
85 auto_refresh: bool = True,
86 native_frames_per_second: int = 60,
87 backlight_on_high: bool = True,
88 SH1107_addressing: bool = False,
89 set_vertical_scroll: int = 0,
91 # pylint: disable=unused-argument,too-many-locals,invalid-name
92 """Create a Display object on the given display bus (`displayio.FourWire` or
93 `paralleldisplay.ParallelBus`).
95 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
96 with a command byte followed by a byte to determine the parameter count and if a
97 delay is need after. When the top bit of the second byte is 1, the next byte will be
98 the delay time in milliseconds. The remaining 7 bits are the parameter count
99 excluding any delay byte. The third through final bytes are the remaining command
100 parameters. The next byte will begin a new command definition. Here is a portion of
103 .. code-block:: python
106 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
107 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
108 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
109 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
111 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
113 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
114 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
115 Multiple byte literals (b”“) are merged together on load. The parens are needed to
116 allow byte literals on subsequent lines.
118 The initialization sequence should always leave the display memory access inline with
119 the scan of the display to minimize tearing artifacts.
123 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,
143 self._set_column_command = set_column_command
144 self._set_row_command = set_row_command
145 self._write_ram_command = write_ram_command
146 self._brightness_command = brightness_command
147 self._data_as_commands = data_as_commands
148 self._single_byte_bounds = single_byte_bounds
150 self._height = height
151 self._colstart = colstart
152 self._rowstart = rowstart
153 self._rotation = rotation
154 self._auto_brightness = auto_brightness
155 self._brightness = 1.0
156 self._auto_refresh = auto_refresh
157 self._initialize(init_sequence)
158 self._buffer = Image.new("RGB", (width, height))
159 self._subrectangles = []
160 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
162 displays.append(self)
163 self._refresh_thread = None
164 if self._auto_refresh:
165 self.auto_refresh = True
166 self._colorconverter = ColorConverter()
168 self._backlight_type = None
169 if backlight_pin is not None:
171 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
173 # 100Hz looks decent and doesn't keep the CPU too busy
174 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
175 self._backlight_type = BACKLIGHT_PWM
177 # PWMOut not implemented on this platform
179 if self._backlight_type is None:
180 self._backlight_type = BACKLIGHT_IN_OUT
181 self._backlight = digitalio.DigitalInOut(backlight_pin)
182 self._backlight.switch_to_output()
183 self.brightness = brightness
185 def _initialize(self, init_sequence):
187 while i < len(init_sequence):
188 command = init_sequence[i]
189 data_size = init_sequence[i + 1]
190 delay = (data_size & 0x80) > 0
193 if self._data_as_commands:
196 CHIP_SELECT_TOGGLE_EVERY_BYTE,
197 bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
201 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
205 CHIP_SELECT_UNTOUCHED,
206 init_sequence[i + 2 : i + 2 + data_size],
211 delay_time_ms = init_sequence[i + 1 + data_size]
212 if delay_time_ms == 255:
214 time.sleep(delay_time_ms / 1000)
217 def _send(self, command, data):
218 self._core.begin_transaction()
219 if self._data_as_commands:
221 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
225 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
227 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
228 self._core.end_transaction()
230 def _send_pixels(self, data):
231 if not self._data_as_commands:
234 CHIP_SELECT_TOGGLE_EVERY_BYTE,
235 bytes([self._write_ram_command]),
237 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
239 def show(self, group: Group) -> None:
240 """Switches to displaying the given group of layers. When group is None, the
241 default CircuitPython terminal will be shown.
243 self._core.set_root_group(group)
248 target_frames_per_second: Optional[int] = None,
249 minimum_frames_per_second: int = 0,
251 # pylint: disable=unused-argument, protected-access
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 if not self._core.start_refresh():
266 force_full_refresh = False
267 # Go through groups and and add each to buffer
268 if self._core._current_group is not None:
269 buffer = Image.new("RGBA", (self._core._width, self._core._height))
270 # Recursively have everything draw to the image
271 self._core._current_group._fill_area(
273 ) # pylint: disable=protected-access
274 # save image to buffer (or probably refresh buffer so we can compare)
275 self._buffer.paste(buffer)
278 buffer = Image.new("RGBA", (self._core._width, self._core._height))
279 draw = ImageDraw.Draw(buffer)
280 draw.rectangle([(0, 0), buffer.size], fill=(0, 0, 0))
281 self._buffer.paste(buffer)
282 force_full_refresh = True
284 if force_full_refresh:
285 full_rect = RectangleStruct(0, 0, self._width, self._height)
286 self._refresh_display_area(full_rect)
288 self._subrectangles = self._core.get_refresh_areas()
289 for area in self._subrectangles:
290 self._refresh_display_area(area)
292 self._core.finish_refresh()
296 def _refresh_loop(self):
297 while self._auto_refresh:
300 def _refresh_display_area(self, rectangle):
301 """Loop through dirty rectangles and redraw that area."""
302 img = self._buffer.convert("RGB").crop(astuple(rectangle))
303 img = img.rotate(360 - self._rotation, expand=True)
305 display_rectangle = self._apply_rotation(rectangle)
306 img = img.crop(astuple(self._clip(display_rectangle)))
308 data = numpy.array(img).astype("uint16")
310 ((data[:, :, 0] & 0xF8) << 8)
311 | ((data[:, :, 1] & 0xFC) << 3)
312 | (data[:, :, 2] >> 3)
316 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
320 self._set_column_command,
322 display_rectangle.x1 + self._colstart,
323 display_rectangle.x2 + self._colstart - 1,
327 self._set_row_command,
329 display_rectangle.y1 + self._rowstart,
330 display_rectangle.y2 + self._rowstart - 1,
334 self._core.begin_transaction()
335 self._send_pixels(pixels)
336 self._core.end_transaction()
338 def _clip(self, rectangle):
339 if self._rotation in (90, 270):
340 width, height = self._height, self._width
342 width, height = self._width, self._height
344 rectangle.x1 = max(rectangle.x1, 0)
345 rectangle.y1 = max(rectangle.y1, 0)
346 rectangle.x2 = min(rectangle.x2, width)
347 rectangle.y2 = min(rectangle.y2, height)
351 def _apply_rotation(self, rectangle):
352 """Adjust the rectangle coordinates based on rotation"""
353 if self._rotation == 90:
354 return RectangleStruct(
355 self._height - rectangle.y2,
357 self._height - rectangle.y1,
360 if self._rotation == 180:
361 return RectangleStruct(
362 self._width - rectangle.x2,
363 self._height - rectangle.y2,
364 self._width - rectangle.x1,
365 self._height - rectangle.y1,
367 if self._rotation == 270:
368 return RectangleStruct(
370 self._width - rectangle.x2,
372 self._width - rectangle.x1,
376 def _encode_pos(self, x, y):
377 """Encode a postion into bytes."""
378 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
381 self, y: int, buffer: circuitpython_typing.WriteableBuffer
382 ) -> circuitpython_typing.WriteableBuffer:
383 """Extract the pixels from a single row"""
384 for x in range(0, self._width):
385 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
386 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
387 buffer[x * 2 + 1] = _rgb_565 & 0xFF
391 def auto_refresh(self) -> bool:
392 """True when the display is refreshed automatically."""
393 return self._auto_refresh
396 def auto_refresh(self, value: bool):
397 self._auto_refresh = value
398 if self._refresh_thread is None:
399 self._refresh_thread = threading.Thread(
400 target=self._refresh_loop, daemon=True
402 if value and not self._refresh_thread.is_alive():
404 self._refresh_thread.start()
405 elif not value and self._refresh_thread.is_alive():
407 self._refresh_thread.join()
410 def brightness(self) -> float:
411 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
412 When `auto_brightness` is True, the value of `brightness` will change automatically.
413 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
415 return self._brightness
418 def brightness(self, value: float):
419 if 0 <= float(value) <= 1.0:
420 self._brightness = value
421 if self._backlight_type == BACKLIGHT_IN_OUT:
422 self._backlight.value = round(self._brightness)
423 elif self._backlight_type == BACKLIGHT_PWM:
424 self._backlight.duty_cycle = self._brightness * 65535
425 elif self._brightness_command is not None:
426 self._send(self._brightness_command, round(value * 255))
428 raise ValueError("Brightness must be between 0.0 and 1.0")
431 def auto_brightness(self) -> bool:
432 """True when the display brightness is adjusted automatically, based on an ambient
433 light sensor or other method. Note that some displays may have this set to True by
434 default, but not actually implement automatic brightness adjustment.
435 `auto_brightness` is set to False if `brightness` is set manually.
437 return self._auto_brightness
439 @auto_brightness.setter
440 def auto_brightness(self, value: bool):
441 self._auto_brightness = value
444 def width(self) -> int:
446 return self._core.get_width()
449 def height(self) -> int:
451 return self._core.get_height()
454 def rotation(self) -> int:
455 """The rotation of the display as an int in degrees."""
456 return self._core.get_rotation()
459 def rotation(self, value: int):
460 self._core.set_rotation(value)
463 def bus(self) -> _DisplayBus:
464 """Current Display Bus"""
465 return self._core.get_bus()
468 def root_group(self) -> Group:
469 """The root group on the display."""
470 return self._core.get_root_group()
473 def root_group(self, new_group):
474 """Switches to displaying the given group of layers. When group is None,
475 a blank screen will be shown.
477 self._core.set_root_group(new_group)