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
305 if rectangle.x2 > width:
307 if rectangle.y2 > height:
308 rectangle.y2 = height
312 def _apply_rotation(self, rectangle):
313 """Adjust the rectangle coordinates based on rotation"""
314 if self._rotation == 90:
316 self._height - rectangle.y2,
318 self._height - rectangle.y1,
321 if self._rotation == 180:
323 self._width - rectangle.x2,
324 self._height - rectangle.y2,
325 self._width - rectangle.x1,
326 self._height - rectangle.y1,
328 if self._rotation == 270:
331 self._width - rectangle.x2,
333 self._width - rectangle.x1,
337 def _encode_pos(self, x, y):
338 """Encode a postion into bytes."""
339 return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
342 self, y: int, buffer: _typing.WriteableBuffer
343 ) -> _typing.WriteableBuffer:
344 """Extract the pixels from a single row"""
345 for x in range(0, self._width):
346 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
347 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
348 buffer[x * 2 + 1] = _rgb_565 & 0xFF
352 def auto_refresh(self) -> bool:
353 """True when the display is refreshed automatically."""
354 return self._auto_refresh
357 def auto_refresh(self, value: bool):
358 self._auto_refresh = value
359 if self._refresh_thread is None:
360 self._refresh_thread = threading.Thread(
361 target=self._refresh_loop, daemon=True
363 if value and not self._refresh_thread.is_alive():
365 self._refresh_thread.start()
366 elif not value and self._refresh_thread.is_alive():
368 self._refresh_thread.join()
371 def brightness(self) -> float:
372 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
373 When `auto_brightness` is True, the value of `brightness` will change automatically.
374 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
376 return self._brightness
379 def brightness(self, value: float):
380 if 0 <= float(value) <= 1.0:
381 self._brightness = value
382 if self._backlight_type == BACKLIGHT_IN_OUT:
383 self._backlight.value = round(self._brightness)
384 elif self._backlight_type == BACKLIGHT_PWM:
385 self._backlight.duty_cycle = self._brightness * 65535
386 elif self._brightness_command is not None:
387 self._send(self._brightness_command, round(value * 255))
389 raise ValueError("Brightness must be between 0.0 and 1.0")
392 def auto_brightness(self) -> bool:
393 """True when the display brightness is adjusted automatically, based on an ambient
394 light sensor or other method. Note that some displays may have this set to True by
395 default, but not actually implement automatic brightness adjustment.
396 `auto_brightness` is set to False if `brightness` is set manually.
398 return self._auto_brightness
400 @auto_brightness.setter
401 def auto_brightness(self, value: bool):
402 self._auto_brightness = value
405 def width(self) -> int:
410 def height(self) -> int:
415 def rotation(self) -> int:
416 """The rotation of the display as an int in degrees."""
417 return self._rotation
420 def rotation(self, value: int):
421 if value not in (0, 90, 180, 270):
422 raise ValueError("Rotation must be 0/90/180/270")
423 self._rotation = value
426 def bus(self) -> _DisplayBus:
427 """Current Display Bus"""