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
27 import microcontroller
28 from recordclass import recordclass
30 from ._displaybus import _DisplayBus
31 from ._colorconverter import ColorConverter
32 from ._group import Group
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,invalid-name
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 # pylint: disable=protected-access
183 self._bus._begin_transaction()
184 if self._data_as_commands:
186 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
190 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
192 self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
193 self._bus._end_transaction()
195 def _send_pixels(self, data):
196 # pylint: disable=protected-access
197 if not self._data_as_commands:
200 CHIP_SELECT_TOGGLE_EVERY_BYTE,
201 bytes([self._write_ram_command]),
203 self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
206 self._bus._release() # pylint: disable=protected-access
209 def show(self, group: Group) -> None:
210 """Switches to displaying the given group of layers. When group is None, the
211 default CircuitPython terminal will be shown.
213 self._current_group = group
218 target_frames_per_second: Optional[int] = None,
219 minimum_frames_per_second: int = 0,
221 # pylint: disable=unused-argument
222 """When auto refresh is off, waits for the target frame rate and then refreshes the
223 display, returning True. If the call has taken too long since the last refresh call
224 for the given target frame rate, then the refresh returns False immediately without
225 updating the screen to hopefully help getting caught up.
227 If the time since the last successful refresh is below the minimum frame rate, then
228 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
230 When auto refresh is on, updates the display immediately. (The display will also
231 update without calls to this.)
233 self._subrectangles = []
235 # Go through groups and and add each to buffer
236 if self._current_group is not None:
237 buffer = Image.new("RGBA", (self._width, self._height))
238 # Recursively have everything draw to the image
239 self._current_group._fill_area(buffer) # pylint: disable=protected-access
240 # save image to buffer (or probably refresh buffer so we can compare)
241 self._buffer.paste(buffer)
243 if self._current_group is not None:
244 # Eventually calculate dirty rectangles here
245 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
247 for area in self._subrectangles:
248 self._refresh_display_area(area)
252 def _refresh_loop(self):
253 while self._auto_refresh:
256 def _refresh_display_area(self, rectangle):
257 """Loop through dirty rectangles and redraw that area."""
259 img = self._buffer.convert("RGB").crop(rectangle)
260 img = img.rotate(self._rotation, expand=True)
262 display_rectangle = self._apply_rotation(rectangle)
263 img = img.crop(self._clip(display_rectangle))
265 data = numpy.array(img).astype("uint16")
267 ((data[:, :, 0] & 0xF8) << 8)
268 | ((data[:, :, 1] & 0xFC) << 3)
269 | (data[:, :, 2] >> 3)
273 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
277 self._set_column_command,
279 display_rectangle.x1 + self._colstart,
280 display_rectangle.x2 + self._colstart - 1,
284 self._set_row_command,
286 display_rectangle.y1 + self._rowstart,
287 display_rectangle.y2 + self._rowstart - 1,
291 self._bus._begin_transaction() # pylint: disable=protected-access
292 self._send_pixels(pixels)
293 self._bus._end_transaction() # pylint: disable=protected-access
295 def _clip(self, rectangle):
296 if self._rotation in (90, 270):
297 width, height = self._height, self._width
299 width, height = self._width, self._height
301 rectangle.x1 = max(rectangle.x1, 0)
302 rectangle.y1 = max(rectangle.y1, 0)
303 rectangle.x2 = min(rectangle.x2, width)
304 rectangle.y2 = min(rectangle.y2, height)
308 def _apply_rotation(self, rectangle):
309 """Adjust the rectangle coordinates based on rotation"""
310 if self._rotation == 90:
312 self._height - rectangle.y2,
314 self._height - rectangle.y1,
317 if self._rotation == 180:
319 self._width - rectangle.x2,
320 self._height - rectangle.y2,
321 self._width - rectangle.x1,
322 self._height - rectangle.y1,
324 if self._rotation == 270:
327 self._width - rectangle.x2,
329 self._width - rectangle.x1,
333 def _encode_pos(self, x, y):
334 """Encode a postion into bytes."""
335 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
338 self, y: int, buffer: _typing.WriteableBuffer
339 ) -> _typing.WriteableBuffer:
340 """Extract the pixels from a single row"""
341 for x in range(0, self._width):
342 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
343 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
344 buffer[x * 2 + 1] = _rgb_565 & 0xFF
348 def auto_refresh(self) -> bool:
349 """True when the display is refreshed automatically."""
350 return self._auto_refresh
353 def auto_refresh(self, value: bool):
354 self._auto_refresh = value
355 if self._refresh_thread is None:
356 self._refresh_thread = threading.Thread(
357 target=self._refresh_loop, daemon=True
359 if value and not self._refresh_thread.is_alive():
361 self._refresh_thread.start()
362 elif not value and self._refresh_thread.is_alive():
364 self._refresh_thread.join()
367 def brightness(self) -> float:
368 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
369 When `auto_brightness` is True, the value of `brightness` will change automatically.
370 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
372 return self._brightness
375 def brightness(self, value: float):
376 if 0 <= float(value) <= 1.0:
377 self._brightness = value
378 if self._backlight_type == BACKLIGHT_IN_OUT:
379 self._backlight.value = round(self._brightness)
380 elif self._backlight_type == BACKLIGHT_PWM:
381 self._backlight.duty_cycle = self._brightness * 65535
382 elif self._brightness_command is not None:
383 self._send(self._brightness_command, round(value * 255))
385 raise ValueError("Brightness must be between 0.0 and 1.0")
388 def auto_brightness(self) -> bool:
389 """True when the display brightness is adjusted automatically, based on an ambient
390 light sensor or other method. Note that some displays may have this set to True by
391 default, but not actually implement automatic brightness adjustment.
392 `auto_brightness` is set to False if `brightness` is set manually.
394 return self._auto_brightness
396 @auto_brightness.setter
397 def auto_brightness(self, value: bool):
398 self._auto_brightness = value
401 def width(self) -> int:
406 def height(self) -> int:
411 def rotation(self) -> int:
412 """The rotation of the display as an int in degrees."""
413 return self._rotation
416 def rotation(self, value: int):
417 if value not in (0, 90, 180, 270):
418 raise ValueError("Rotation must be 0/90/180/270")
419 self._rotation = value
422 def bus(self) -> _DisplayBus:
423 """Current Display Bus"""