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 from recordclass import recordclass
27 from displayio.colorconverter import ColorConverter
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
32 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
40 # pylint: disable=too-many-instance-attributes
41 """This initializes a display and connects it into CircuitPython. Unlike other objects
42 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
43 This is done so that CircuitPython can use the display itself.
45 Most people should not use this class directly. Use a specific display driver instead
46 that will contain the initialization sequence at minimum.
61 pixels_in_byte_share_row=True,
63 reverse_pixels_in_byte=False,
64 set_column_command=0x2A,
66 write_ram_command=0x2C,
67 set_vertical_scroll=0,
69 brightness_command=None,
71 auto_brightness=False,
72 single_byte_bounds=False,
73 data_as_commands=False,
75 native_frames_per_second=60
77 # pylint: disable=unused-argument,too-many-locals
78 """Create a Display object on the given display bus (`displayio.FourWire` or
79 `displayio.ParallelBus`).
81 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
82 with a command byte followed by a byte to determine the parameter count and if a
83 delay is need after. When the top bit of the second byte is 1, the next byte will be
84 the delay time in milliseconds. The remaining 7 bits are the parameter count
85 excluding any delay byte. The third through final bytes are the remaining command
86 parameters. The next byte will begin a new command definition. Here is a portion of
89 .. code-block:: python
92 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
93 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
94 b"\\x11\\x80\\x78" # Exit Sleep then delay 0x78 (120ms)
95 b"\\x29\\x80\\x78" # Display on then delay 0x78 (120ms)
97 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
99 The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
100 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
101 Multiple byte literals (b”“) are merged together on load. The parens are needed to
102 allow byte literals on subsequent lines.
104 The initialization sequence should always leave the display memory access inline with
105 the scan of the display to minimize tearing artifacts.
107 self._bus = display_bus
108 self._set_column_command = set_column_command
109 self._set_row_command = set_row_command
110 self._write_ram_command = write_ram_command
111 self._brightness_command = brightness_command
112 self._data_as_commands = data_as_commands
113 self._single_byte_bounds = single_byte_bounds
115 self._height = height
116 self._colstart = colstart
117 self._rowstart = rowstart
118 self._rotation = rotation
119 self._auto_brightness = auto_brightness
120 self._brightness = 1.0
121 self._auto_refresh = auto_refresh
122 self._initialize(init_sequence)
123 self._buffer = Image.new("RGB", (width, height))
124 self._subrectangles = []
125 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
126 self._current_group = None
127 displays.append(self)
128 self._refresh_thread = None
129 if self._auto_refresh:
130 self.auto_refresh = True
131 self._colorconverter = ColorConverter()
133 self._backlight_type = None
134 if backlight_pin is not None:
136 from pulseio import PWMOut # pylint: disable=import-outside-toplevel
138 # 100Hz looks decent and doesn't keep the CPU too busy
139 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
140 self._backlight_type = BACKLIGHT_PWM
142 # PWMOut not implemented on this platform
144 if self._backlight_type is None:
145 self._backlight_type = BACKLIGHT_IN_OUT
146 self._backlight = digitalio.DigitalInOut(backlight_pin)
147 self._backlight.switch_to_output()
148 self.brightness = brightness
150 def _initialize(self, init_sequence):
152 while i < len(init_sequence):
153 command = init_sequence[i]
154 data_size = init_sequence[i + 1]
155 delay = (data_size & 0x80) > 0
158 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
162 delay_time_ms = init_sequence[i + 1 + data_size]
163 if delay_time_ms == 255:
165 time.sleep(delay_time_ms / 1000)
168 def _write(self, command, data):
169 self._bus.begin_transaction()
170 if self._data_as_commands:
171 if command is not None:
172 self._bus.send(True, bytes([command]), toggle_every_byte=True)
173 self._bus.send(command is not None, data)
175 self._bus.send(True, bytes([command]), toggle_every_byte=True)
176 self._bus.send(False, data)
177 self._bus.end_transaction()
180 self._bus._release() # pylint: disable=protected-access
183 def show(self, group):
184 """Switches to displaying the given group of layers. When group is None, the
185 default CircuitPython terminal will be shown.
187 self._current_group = group
189 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
190 # pylint: disable=unused-argument
191 """When auto refresh is off, waits for the target frame rate and then refreshes the
192 display, returning True. If the call has taken too long since the last refresh call
193 for the given target frame rate, then the refresh returns False immediately without
194 updating the screen to hopefully help getting caught up.
196 If the time since the last successful refresh is below the minimum frame rate, then
197 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
199 When auto refresh is on, updates the display immediately. (The display will also
200 update without calls to this.)
202 self._subrectangles = []
204 # Go through groups and and add each to buffer
205 if self._current_group is not None:
206 buffer = Image.new("RGBA", (self._width, self._height))
207 # Recursively have everything draw to the image
208 self._current_group._fill_area(buffer) # pylint: disable=protected-access
209 # save image to buffer (or probably refresh buffer so we can compare)
210 self._buffer.paste(buffer)
212 if self._current_group is not None:
213 # Eventually calculate dirty rectangles here
214 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
216 for area in self._subrectangles:
217 self._refresh_display_area(area)
219 def _refresh_loop(self):
220 while self._auto_refresh:
223 def _refresh_display_area(self, rectangle):
224 """Loop through dirty rectangles and redraw that area."""
226 img = self._buffer.convert("RGB").crop(rectangle)
227 img = img.rotate(self._rotation, expand=True)
229 display_rectangle = self._apply_rotation(rectangle)
230 img = img.crop(self._clip(display_rectangle))
232 data = numpy.array(img).astype("uint16")
234 ((data[:, :, 0] & 0xF8) << 8)
235 | ((data[:, :, 1] & 0xFC) << 3)
236 | (data[:, :, 2] >> 3)
240 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
244 self._set_column_command,
246 display_rectangle.x1 + self._colstart,
247 display_rectangle.x2 + self._colstart - 1,
251 self._set_row_command,
253 display_rectangle.y1 + self._rowstart,
254 display_rectangle.y2 + self._rowstart - 1,
258 if self._data_as_commands:
259 self._write(None, pixels)
261 self._write(self._write_ram_command, pixels)
263 def _clip(self, rectangle):
264 if self._rotation in (90, 270):
265 width, height = self._height, self._width
267 width, height = self._width, self._height
273 if rectangle.x2 > width:
275 if rectangle.y2 > height:
276 rectangle.y2 = height
280 def _apply_rotation(self, rectangle):
281 """Adjust the rectangle coordinates based on rotation"""
282 if self._rotation == 90:
284 self._height - rectangle.y2,
286 self._height - rectangle.y1,
289 if self._rotation == 180:
291 self._width - rectangle.x2,
292 self._height - rectangle.y2,
293 self._width - rectangle.x1,
294 self._height - rectangle.y1,
296 if self._rotation == 270:
299 self._width - rectangle.x2,
301 self._width - rectangle.x1,
305 def _encode_pos(self, x, y):
306 """Encode a postion into bytes."""
307 return struct.pack(self._bounds_encoding, x, y)
309 def fill_row(self, y, buffer):
310 """Extract the pixels from a single row"""
311 for x in range(0, self._width):
312 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
313 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
314 buffer[x * 2 + 1] = _rgb_565 & 0xFF
318 def auto_refresh(self):
319 """True when the display is refreshed automatically."""
320 return self._auto_refresh
323 def auto_refresh(self, value):
324 self._auto_refresh = value
325 if self._refresh_thread is None:
326 self._refresh_thread = threading.Thread(
327 target=self._refresh_loop, daemon=True
329 if value and not self._refresh_thread.is_alive():
331 self._refresh_thread.start()
332 elif not value and self._refresh_thread.is_alive():
334 self._refresh_thread.join()
337 def brightness(self):
338 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
339 When `auto_brightness` is True, the value of `brightness` will change automatically.
340 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
342 return self._brightness
345 def brightness(self, value):
346 if 0 <= float(value) <= 1.0:
347 self._brightness = value
348 if self._backlight_type == BACKLIGHT_IN_OUT:
349 self._backlight.value = round(self._brightness)
350 elif self._backlight_type == BACKLIGHT_PWM:
351 self._backlight.duty_cycle = self._brightness * 65535
352 elif self._brightness_command is not None:
353 self._write(self._brightness_command, round(value * 255))
355 raise ValueError("Brightness must be between 0.0 and 1.0")
358 def auto_brightness(self):
359 """True when the display brightness is adjusted automatically, based on an ambient
360 light sensor or other method. Note that some displays may have this set to True by
361 default, but not actually implement automatic brightness adjustment.
362 `auto_brightness` is set to False if `brightness` is set manually.
364 return self._auto_brightness
366 @auto_brightness.setter
367 def auto_brightness(self, value):
368 self._auto_brightness = value
382 """The rotation of the display as an int in degrees."""
383 return self._rotation
386 def rotation(self, value):
387 if value not in (0, 90, 180, 270):
388 raise ValueError("Rotation must be 0/90/180/270")
389 self._rotation = value
393 """Current Display Bus"""