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
26 import microcontroller
27 from recordclass import recordclass
28 from ._colorconverter import ColorConverter
30 from ._group import Group
31 from displayio import _DisplayBus
32 from typing import Optional
33 from ._constants import (
34 CHIP_SELECT_TOGGLE_EVERY_BYTE,
35 CHIP_SELECT_UNTOUCHED,
40 __version__ = "0.0.0-auto.0"
41 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
43 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
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 set_column_command: int = 0x2A,
76 set_row_command: int = 0x2B,
77 write_ram_command: int = 0x2C,
78 backlight_pin: Optional[microcontroller.Pin] = None,
79 brightness_command: Optional[int] = None,
80 brightness: float = 1.0,
81 auto_brightness: bool = False,
82 single_byte_bounds: bool = False,
83 data_as_commands: bool = False,
84 auto_refresh: bool = True,
85 native_frames_per_second: int = 60,
86 backlight_on_high: bool = True,
87 SH1107_addressing: bool = False,
88 set_vertical_scroll: int = 0,
90 # pylint: disable=unused-argument,too-many-locals
91 """Create a Display object on the given display bus (`displayio.FourWire` or
92 `paralleldisplay.ParallelBus`).
94 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
95 with a command byte followed by a byte to determine the parameter count and if a
96 delay is need after. When the top bit of the second byte is 1, the next byte will be
97 the delay time in milliseconds. The remaining 7 bits are the parameter count
98 excluding any delay byte. The third through final bytes are the remaining command
99 parameters. The next byte will begin a new command definition. Here is a portion of
102 .. code-block:: python
105 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
106 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
107 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
108 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
110 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
112 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
113 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
114 Multiple byte literals (b”“) are merged together on load. The parens are needed to
115 allow byte literals on subsequent lines.
117 The initialization sequence should always leave the display memory access inline with
118 the scan of the display to minimize tearing artifacts.
120 self._bus = display_bus
121 self._set_column_command = set_column_command
122 self._set_row_command = set_row_command
123 self._write_ram_command = write_ram_command
124 self._brightness_command = brightness_command
125 self._data_as_commands = data_as_commands
126 self._single_byte_bounds = single_byte_bounds
128 self._height = height
129 self._colstart = colstart
130 self._rowstart = rowstart
131 self._rotation = rotation
132 self._auto_brightness = auto_brightness
133 self._brightness = 1.0
134 self._auto_refresh = auto_refresh
135 self._initialize(init_sequence)
136 self._buffer = Image.new("RGB", (width, height))
137 self._subrectangles = []
138 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
139 self._current_group = None
140 displays.append(self)
141 self._refresh_thread = None
142 if self._auto_refresh:
143 self.auto_refresh = True
144 self._colorconverter = ColorConverter()
146 self._backlight_type = None
147 if backlight_pin is not None:
149 from pwmio import PWMOut # pylint: disable=import-outside-toplevel
151 # 100Hz looks decent and doesn't keep the CPU too busy
152 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
153 self._backlight_type = BACKLIGHT_PWM
155 # PWMOut not implemented on this platform
157 if self._backlight_type is None:
158 self._backlight_type = BACKLIGHT_IN_OUT
159 self._backlight = digitalio.DigitalInOut(backlight_pin)
160 self._backlight.switch_to_output()
161 self.brightness = brightness
163 def _initialize(self, init_sequence):
165 while i < len(init_sequence):
166 command = init_sequence[i]
167 data_size = init_sequence[i + 1]
168 delay = (data_size & 0x80) > 0
171 self._send(command, init_sequence[i + 2 : i + 2 + data_size])
175 delay_time_ms = init_sequence[i + 1 + data_size]
176 if delay_time_ms == 255:
178 time.sleep(delay_time_ms / 1000)
181 def _send(self, command, data):
182 self._bus._begin_transaction() # pylint: disable=protected-access
183 if self._data_as_commands:
185 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
189 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
191 self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
192 self._bus._end_transaction() # pylint: disable=protected-access
194 def _send_pixels(self, data):
195 if not self._data_as_commands:
198 CHIP_SELECT_TOGGLE_EVERY_BYTE,
199 bytes([self._write_ram_command]),
201 self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
204 self._bus._release() # pylint: disable=protected-access
207 def show(self, group: Group) -> None:
208 """Switches to displaying the given group of layers. When group is None, the
209 default CircuitPython terminal will be shown.
211 self._current_group = group
216 target_frames_per_second: Optional[int] = None,
217 minimum_frames_per_second: int = 0,
219 # pylint: disable=unused-argument
220 """When auto refresh is off, waits for the target frame rate and then refreshes the
221 display, returning True. If the call has taken too long since the last refresh call
222 for the given target frame rate, then the refresh returns False immediately without
223 updating the screen to hopefully help getting caught up.
225 If the time since the last successful refresh is below the minimum frame rate, then
226 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
228 When auto refresh is on, updates the display immediately. (The display will also
229 update without calls to this.)
231 self._subrectangles = []
233 # Go through groups and and add each to buffer
234 if self._current_group is not None:
235 buffer = Image.new("RGBA", (self._width, self._height))
236 # Recursively have everything draw to the image
237 self._current_group._fill_area(buffer) # pylint: disable=protected-access
238 # save image to buffer (or probably refresh buffer so we can compare)
239 self._buffer.paste(buffer)
241 if self._current_group is not None:
242 # Eventually calculate dirty rectangles here
243 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
245 for area in self._subrectangles:
246 self._refresh_display_area(area)
250 def _refresh_loop(self):
251 while self._auto_refresh:
254 def _refresh_display_area(self, rectangle):
255 """Loop through dirty rectangles and redraw that area."""
257 img = self._buffer.convert("RGB").crop(rectangle)
258 img = img.rotate(self._rotation, expand=True)
260 display_rectangle = self._apply_rotation(rectangle)
261 img = img.crop(self._clip(display_rectangle))
263 data = numpy.array(img).astype("uint16")
265 ((data[:, :, 0] & 0xF8) << 8)
266 | ((data[:, :, 1] & 0xFC) << 3)
267 | (data[:, :, 2] >> 3)
271 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
275 self._set_column_command,
277 display_rectangle.x1 + self._colstart,
278 display_rectangle.x2 + self._colstart - 1,
282 self._set_row_command,
284 display_rectangle.y1 + self._rowstart,
285 display_rectangle.y2 + self._rowstart - 1,
289 self._bus._begin_transaction() # pylint: disable=protected-access
290 self._send_pixels(pixels)
291 self._bus._end_transaction() # pylint: disable=protected-access
293 def _clip(self, rectangle):
294 if self._rotation in (90, 270):
295 width, height = self._height, self._width
297 width, height = self._width, self._height
303 if rectangle.x2 > width:
305 if rectangle.y2 > height:
306 rectangle.y2 = height
310 def _apply_rotation(self, rectangle):
311 """Adjust the rectangle coordinates based on rotation"""
312 if self._rotation == 90:
314 self._height - rectangle.y2,
316 self._height - rectangle.y1,
319 if self._rotation == 180:
321 self._width - rectangle.x2,
322 self._height - rectangle.y2,
323 self._width - rectangle.x1,
324 self._height - rectangle.y1,
326 if self._rotation == 270:
329 self._width - rectangle.x2,
331 self._width - rectangle.x1,
335 def _encode_pos(self, x, y):
336 """Encode a postion into bytes."""
337 return struct.pack(self._bounds_encoding, x, y)
340 self, y: int, buffer: _typing.WriteableBuffer
341 ) -> _typing.WriteableBuffer:
342 """Extract the pixels from a single row"""
343 for x in range(0, self._width):
344 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
345 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
346 buffer[x * 2 + 1] = _rgb_565 & 0xFF
350 def auto_refresh(self) -> bool:
351 """True when the display is refreshed automatically."""
352 return self._auto_refresh
355 def auto_refresh(self, value: bool):
356 self._auto_refresh = value
357 if self._refresh_thread is None:
358 self._refresh_thread = threading.Thread(
359 target=self._refresh_loop, daemon=True
361 if value and not self._refresh_thread.is_alive():
363 self._refresh_thread.start()
364 elif not value and self._refresh_thread.is_alive():
366 self._refresh_thread.join()
369 def brightness(self) -> float:
370 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
371 When `auto_brightness` is True, the value of `brightness` will change automatically.
372 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
374 return self._brightness
377 def brightness(self, value: float):
378 if 0 <= float(value) <= 1.0:
379 self._brightness = value
380 if self._backlight_type == BACKLIGHT_IN_OUT:
381 self._backlight.value = round(self._brightness)
382 elif self._backlight_type == BACKLIGHT_PWM:
383 self._backlight.duty_cycle = self._brightness * 65535
384 elif self._brightness_command is not None:
385 self._send(self._brightness_command, round(value * 255))
387 raise ValueError("Brightness must be between 0.0 and 1.0")
390 def auto_brightness(self) -> bool:
391 """True when the display brightness is adjusted automatically, based on an ambient
392 light sensor or other method. Note that some displays may have this set to True by
393 default, but not actually implement automatic brightness adjustment.
394 `auto_brightness` is set to False if `brightness` is set manually.
396 return self._auto_brightness
398 @auto_brightness.setter
399 def auto_brightness(self, value: bool):
400 self._auto_brightness = value
403 def width(self) -> int:
408 def height(self) -> int:
413 def rotation(self) -> int:
414 """The rotation of the display as an int in degrees."""
415 return self._rotation
418 def rotation(self, value: int):
419 if value not in (0, 90, 180, 270):
420 raise ValueError("Rotation must be 0/90/180/270")
421 self._rotation = value
424 def bus(self) -> _DisplayBus:
425 """Current Display Bus"""