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
28 import microcontroller
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: _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"
161 self._current_group = None
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.show(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 # Go through groups and and add each to buffer
267 if self._core._current_group is not None:
268 buffer = Image.new("RGBA", (self._core._width, self._core._height))
269 # Recursively have everything draw to the image
270 self._core._current_group._fill_area(
272 ) # pylint: disable=protected-access
273 # save image to buffer (or probably refresh buffer so we can compare)
274 self._buffer.paste(buffer)
276 self._subrectangles = self._core.get_refresh_areas()
278 for area in self._subrectangles:
279 self._refresh_display_area(area)
281 self._core.finish_refresh()
285 def _refresh_loop(self):
286 while self._auto_refresh:
289 def _refresh_display_area(self, rectangle):
290 """Loop through dirty rectangles and redraw that area."""
291 img = self._buffer.convert("RGB").crop(astuple(rectangle))
292 img = img.rotate(self._rotation, expand=True)
294 display_rectangle = self._apply_rotation(rectangle)
295 img = img.crop(astuple(self._clip(display_rectangle)))
297 data = numpy.array(img).astype("uint16")
299 ((data[:, :, 0] & 0xF8) << 8)
300 | ((data[:, :, 1] & 0xFC) << 3)
301 | (data[:, :, 2] >> 3)
305 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
309 self._set_column_command,
311 display_rectangle.x1 + self._colstart,
312 display_rectangle.x2 + self._colstart - 1,
316 self._set_row_command,
318 display_rectangle.y1 + self._rowstart,
319 display_rectangle.y2 + self._rowstart - 1,
323 self._core.begin_transaction()
324 self._send_pixels(pixels)
325 self._core.end_transaction()
327 def _clip(self, rectangle):
328 if self._rotation in (90, 270):
329 width, height = self._height, self._width
331 width, height = self._width, self._height
333 rectangle.x1 = max(rectangle.x1, 0)
334 rectangle.y1 = max(rectangle.y1, 0)
335 rectangle.x2 = min(rectangle.x2, width)
336 rectangle.y2 = min(rectangle.y2, height)
340 def _apply_rotation(self, rectangle):
341 """Adjust the rectangle coordinates based on rotation"""
342 if self._rotation == 90:
343 return RectangleStruct(
344 self._height - rectangle.y2,
346 self._height - rectangle.y1,
349 if self._rotation == 180:
350 return RectangleStruct(
351 self._width - rectangle.x2,
352 self._height - rectangle.y2,
353 self._width - rectangle.x1,
354 self._height - rectangle.y1,
356 if self._rotation == 270:
357 return RectangleStruct(
359 self._width - rectangle.x2,
361 self._width - rectangle.x1,
365 def _encode_pos(self, x, y):
366 """Encode a postion into bytes."""
367 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
370 self, y: int, buffer: _typing.WriteableBuffer
371 ) -> _typing.WriteableBuffer:
372 """Extract the pixels from a single row"""
373 for x in range(0, self._width):
374 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
375 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
376 buffer[x * 2 + 1] = _rgb_565 & 0xFF
380 def auto_refresh(self) -> bool:
381 """True when the display is refreshed automatically."""
382 return self._auto_refresh
385 def auto_refresh(self, value: bool):
386 self._auto_refresh = value
387 if self._refresh_thread is None:
388 self._refresh_thread = threading.Thread(
389 target=self._refresh_loop, daemon=True
391 if value and not self._refresh_thread.is_alive():
393 self._refresh_thread.start()
394 elif not value and self._refresh_thread.is_alive():
396 self._refresh_thread.join()
399 def brightness(self) -> float:
400 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
401 When `auto_brightness` is True, the value of `brightness` will change automatically.
402 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
404 return self._brightness
407 def brightness(self, value: float):
408 if 0 <= float(value) <= 1.0:
409 self._brightness = value
410 if self._backlight_type == BACKLIGHT_IN_OUT:
411 self._backlight.value = round(self._brightness)
412 elif self._backlight_type == BACKLIGHT_PWM:
413 self._backlight.duty_cycle = self._brightness * 65535
414 elif self._brightness_command is not None:
415 self._send(self._brightness_command, round(value * 255))
417 raise ValueError("Brightness must be between 0.0 and 1.0")
420 def auto_brightness(self) -> bool:
421 """True when the display brightness is adjusted automatically, based on an ambient
422 light sensor or other method. Note that some displays may have this set to True by
423 default, but not actually implement automatic brightness adjustment.
424 `auto_brightness` is set to False if `brightness` is set manually.
426 return self._auto_brightness
428 @auto_brightness.setter
429 def auto_brightness(self, value: bool):
430 self._auto_brightness = value
433 def width(self) -> int:
435 return self._core.get_width()
438 def height(self) -> int:
440 return self._core.get_height()
443 def rotation(self) -> int:
444 """The rotation of the display as an int in degrees."""
445 return self._core.get_rotation()
448 def rotation(self, value: int):
449 self._core.set_rotation(value)
452 def bus(self) -> _DisplayBus:
453 """Current Display Bus"""
454 return self._core.get_bus()