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
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"
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)
277 print("show nothing")
278 buffer = Image.new("RGBA", (self._core._width, self._core._height))
279 self._buffer.paste(buffer)
281 self._subrectangles = self._core.get_refresh_areas()
283 for area in self._subrectangles:
284 self._refresh_display_area(area)
286 self._core.finish_refresh()
290 def _refresh_loop(self):
291 while self._auto_refresh:
294 def _refresh_display_area(self, rectangle):
295 """Loop through dirty rectangles and redraw that area."""
296 img = self._buffer.convert("RGB").crop(astuple(rectangle))
297 img = img.rotate(360 - self._rotation, expand=True)
299 display_rectangle = self._apply_rotation(rectangle)
300 img = img.crop(astuple(self._clip(display_rectangle)))
302 data = numpy.array(img).astype("uint16")
304 ((data[:, :, 0] & 0xF8) << 8)
305 | ((data[:, :, 1] & 0xFC) << 3)
306 | (data[:, :, 2] >> 3)
310 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
314 self._set_column_command,
316 display_rectangle.x1 + self._colstart,
317 display_rectangle.x2 + self._colstart - 1,
321 self._set_row_command,
323 display_rectangle.y1 + self._rowstart,
324 display_rectangle.y2 + self._rowstart - 1,
328 self._core.begin_transaction()
329 self._send_pixels(pixels)
330 self._core.end_transaction()
332 def _clip(self, rectangle):
333 if self._rotation in (90, 270):
334 width, height = self._height, self._width
336 width, height = self._width, self._height
338 rectangle.x1 = max(rectangle.x1, 0)
339 rectangle.y1 = max(rectangle.y1, 0)
340 rectangle.x2 = min(rectangle.x2, width)
341 rectangle.y2 = min(rectangle.y2, height)
345 def _apply_rotation(self, rectangle):
346 """Adjust the rectangle coordinates based on rotation"""
347 if self._rotation == 90:
348 return RectangleStruct(
349 self._height - rectangle.y2,
351 self._height - rectangle.y1,
354 if self._rotation == 180:
355 return RectangleStruct(
356 self._width - rectangle.x2,
357 self._height - rectangle.y2,
358 self._width - rectangle.x1,
359 self._height - rectangle.y1,
361 if self._rotation == 270:
362 return RectangleStruct(
364 self._width - rectangle.x2,
366 self._width - rectangle.x1,
370 def _encode_pos(self, x, y):
371 """Encode a postion into bytes."""
372 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
375 self, y: int, buffer: circuitpython_typing.WriteableBuffer
376 ) -> circuitpython_typing.WriteableBuffer:
377 """Extract the pixels from a single row"""
378 for x in range(0, self._width):
379 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
380 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
381 buffer[x * 2 + 1] = _rgb_565 & 0xFF
385 def auto_refresh(self) -> bool:
386 """True when the display is refreshed automatically."""
387 return self._auto_refresh
390 def auto_refresh(self, value: bool):
391 self._auto_refresh = value
392 if self._refresh_thread is None:
393 self._refresh_thread = threading.Thread(
394 target=self._refresh_loop, daemon=True
396 if value and not self._refresh_thread.is_alive():
398 self._refresh_thread.start()
399 elif not value and self._refresh_thread.is_alive():
401 self._refresh_thread.join()
404 def brightness(self) -> float:
405 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
406 When `auto_brightness` is True, the value of `brightness` will change automatically.
407 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
409 return self._brightness
412 def brightness(self, value: float):
413 if 0 <= float(value) <= 1.0:
414 self._brightness = value
415 if self._backlight_type == BACKLIGHT_IN_OUT:
416 self._backlight.value = round(self._brightness)
417 elif self._backlight_type == BACKLIGHT_PWM:
418 self._backlight.duty_cycle = self._brightness * 65535
419 elif self._brightness_command is not None:
420 self._send(self._brightness_command, round(value * 255))
422 raise ValueError("Brightness must be between 0.0 and 1.0")
425 def auto_brightness(self) -> bool:
426 """True when the display brightness is adjusted automatically, based on an ambient
427 light sensor or other method. Note that some displays may have this set to True by
428 default, but not actually implement automatic brightness adjustment.
429 `auto_brightness` is set to False if `brightness` is set manually.
431 return self._auto_brightness
433 @auto_brightness.setter
434 def auto_brightness(self, value: bool):
435 self._auto_brightness = value
438 def width(self) -> int:
440 return self._core.get_width()
443 def height(self) -> int:
445 return self._core.get_height()
448 def rotation(self) -> int:
449 """The rotation of the display as an int in degrees."""
450 return self._core.get_rotation()
453 def rotation(self, value: int):
454 self._core.set_rotation(value)
457 def bus(self) -> _DisplayBus:
458 """Current Display Bus"""
459 return self._core.get_bus()
462 def root_group(self) -> Group:
463 return self._current_group
466 def root_group(self, new_group):
467 self._current_group = new_group