1 # The MIT License (MIT)
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 ================================================================================
29 **Software and Dependencies:**
32 https://github.com/adafruit/Adafruit_Blinka/releases
34 * Author(s): Melissa LeBlanc-Williams
44 from recordclass import recordclass
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
55 # pylint: disable=unnecessary-pass, unused-argument
57 # pylint: disable=too-many-instance-attributes
59 """This initializes a display and connects it into CircuitPython. Unlike other objects
60 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
61 This is done so that CircuitPython can use the display itself.
63 Most people should not use this class directly. Use a specific display driver instead
64 that will contain the initialization sequence at minimum.
67 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
68 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
69 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
70 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
71 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
72 auto_refresh=True, native_frames_per_second=60)
75 # pylint: disable=too-many-locals
88 pixels_in_byte_share_row=True,
90 reverse_pixels_in_byte=False,
91 set_column_command=0x2A,
93 write_ram_command=0x2C,
94 set_vertical_scroll=0,
96 brightness_command=None,
98 auto_brightness=False,
99 single_byte_bounds=False,
100 data_as_commands=False,
102 native_frames_per_second=60
104 """Create a Display object on the given display bus (`displayio.FourWire` or
105 `displayio.ParallelBus`).
107 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
108 with a command byte followed by a byte to determine the parameter count and if a
109 delay is need after. When the top bit of the second byte is 1, the next byte will be
110 the delay time in milliseconds. The remaining 7 bits are the parameter count
111 excluding any delay byte. The third through final bytes are the remaining command
112 parameters. The next byte will begin a new command definition. Here is a portion of
114 .. code-block:: python
117 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
118 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
119 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
121 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
123 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
124 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
125 Multiple byte literals (b”“) are merged together on load. The parens are needed to
126 allow byte literals on subsequent lines.
128 The initialization sequence should always leave the display memory access inline with
129 the scan of the display to minimize tearing artifacts.
131 self._bus = display_bus
132 self._set_column_command = set_column_command
133 self._set_row_command = set_row_command
134 self._write_ram_command = write_ram_command
135 self._brightness_command = brightness_command
136 self._data_as_commands = data_as_commands
137 self._single_byte_bounds = single_byte_bounds
139 self._height = height
140 self._colstart = colstart
141 self._rowstart = rowstart
142 self._rotation = rotation
143 self._auto_brightness = auto_brightness
144 self._brightness = 1.0
145 self._auto_refresh = auto_refresh
146 self._initialize(init_sequence)
147 self._buffer = Image.new("RGB", (width, height))
148 self._subrectangles = []
149 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
150 self._current_group = None
151 displays.append(self)
152 self._refresh_thread = None
153 if self._auto_refresh:
154 self.auto_refresh = True
156 self._backlight_type = None
157 if backlight_pin is not None:
159 from pulseio import PWMOut # pylint: disable=import-outside-toplevel
161 # 100Hz looks decent and doesn't keep the CPU too busy
162 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
163 self._backlight_type = BACKLIGHT_PWM
165 # PWMOut not implemented on this platform
167 if self._backlight_type is None:
168 self._backlight_type = BACKLIGHT_IN_OUT
169 self._backlight = digitalio.DigitalInOut(backlight_pin)
170 self._backlight.switch_to_output()
171 self.brightness = brightness
173 # pylint: enable=too-many-locals
175 def _initialize(self, init_sequence):
177 while i < len(init_sequence):
178 command = init_sequence[i]
179 data_size = init_sequence[i + 1]
180 delay = (data_size & 0x80) > 0
183 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
187 delay_time_ms = init_sequence[i + 1 + data_size]
188 if delay_time_ms == 255:
190 time.sleep(delay_time_ms / 1000)
193 def _write(self, command, data):
194 self._bus.begin_transaction()
195 if self._data_as_commands:
196 if command is not None:
197 self._bus.send(True, bytes([command]), toggle_every_byte=True)
198 self._bus.send(command is not None, data)
200 self._bus.send(True, bytes([command]), toggle_every_byte=True)
201 self._bus.send(False, data)
202 self._bus.end_transaction()
205 self._bus._release() # pylint: disable=protected-access
208 def show(self, group):
209 """Switches to displaying the given group of layers. When group is None, the
210 default CircuitPython terminal will be shown.
212 self._current_group = group
214 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
215 """When auto refresh is off, waits for the target frame rate and then refreshes the
216 display, returning True. If the call has taken too long since the last refresh call
217 for the given target frame rate, then the refresh returns False immediately without
218 updating the screen to hopefully help getting caught up.
220 If the time since the last successful refresh is below the minimum frame rate, then
221 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
223 When auto refresh is on, updates the display immediately. (The display will also
224 update without calls to this.)
226 self._subrectangles = []
228 # Go through groups and and add each to buffer
229 if self._current_group is not None:
230 buffer = Image.new("RGBA", (self._width, self._height))
231 # Recursively have everything draw to the image
232 self._current_group._fill_area(buffer) # pylint: disable=protected-access
233 # save image to buffer (or probably refresh buffer so we can compare)
234 self._buffer.paste(buffer)
236 if self._current_group is not None:
237 # Eventually calculate dirty rectangles here
238 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
240 for area in self._subrectangles:
241 self._refresh_display_area(area)
243 def _refresh_loop(self):
244 while self._auto_refresh:
247 def _refresh_display_area(self, rectangle):
248 """Loop through dirty rectangles and redraw that area."""
250 img = self._buffer.convert("RGB").crop(rectangle)
251 img = img.rotate(self._rotation, expand=True)
253 display_rectangle = self._apply_rotation(rectangle)
254 img = img.crop(self._clip(display_rectangle))
256 data = numpy.array(img).astype("uint16")
258 ((data[:, :, 0] & 0xF8) << 8)
259 | ((data[:, :, 1] & 0xFC) << 3)
260 | (data[:, :, 2] >> 3)
264 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
268 self._set_column_command,
270 display_rectangle.x1 + self._colstart,
271 display_rectangle.x2 + self._colstart - 1,
275 self._set_row_command,
277 display_rectangle.y1 + self._rowstart,
278 display_rectangle.y2 + self._rowstart - 1,
282 if self._data_as_commands:
283 self._write(None, pixels)
285 self._write(self._write_ram_command, pixels)
287 def _clip(self, rectangle):
288 if self._rotation in (90, 270):
289 width, height = self._height, self._width
291 width, height = self._width, self._height
297 if rectangle.x2 > width:
299 if rectangle.y2 > height:
300 rectangle.y2 = height
304 def _apply_rotation(self, rectangle):
305 """Adjust the rectangle coordinates based on rotation"""
306 if self._rotation == 90:
308 self._height - rectangle.y2,
310 self._height - rectangle.y1,
313 if self._rotation == 180:
315 self._width - rectangle.x2,
316 self._height - rectangle.y2,
317 self._width - rectangle.x1,
318 self._height - rectangle.y1,
320 if self._rotation == 270:
323 self._width - rectangle.x2,
325 self._width - rectangle.x1,
329 def _encode_pos(self, x, y):
330 """Encode a postion into bytes."""
331 return struct.pack(self._bounds_encoding, x, y)
333 def _rgb_tuple_to_rgb565(self, color_tuple):
335 ((color_tuple[0] & 0x00F8) << 8)
336 | ((color_tuple[1] & 0x00FC) << 3)
337 | (color_tuple[2] & 0x00F8) >> 3
340 def fill_row(self, y, buffer):
341 """Extract the pixels from a single row"""
342 for x in range(0, self._width):
343 _rgb_565 = self._rgb_tuple_to_rgb565(self._buffer.getpixel((x, y)))
344 buffer[x * 2] = _rgb_565 >> 8
345 buffer[x * 2 + 1] = _rgb_565
346 #(data[i * 2] << 8) + data[i * 2 + 1]
350 def auto_refresh(self):
351 """True when the display is refreshed automatically."""
352 return self._auto_refresh
355 def auto_refresh(self, value):
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):
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):
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._write(self._brightness_command, round(value * 255))
387 raise ValueError("Brightness must be between 0.0 and 1.0")
390 def auto_brightness(self):
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):
400 self._auto_brightness = value
414 """The rotation of the display as an int in degrees."""
415 return self._rotation
418 def rotation(self, value):
419 if value not in (0, 90, 180, 270):
420 raise ValueError("Rotation must be 0/90/180/270")
421 self._rotation = value
425 """Current Display Bus"""
429 # pylint: enable=too-many-instance-attributes