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
56 # pylint: disable=too-many-instance-attributes
59 def _rgb_tuple_to_rgb565(color_tuple):
61 ((color_tuple[0] & 0x00F8) << 8)
62 | ((color_tuple[1] & 0x00FC) << 3)
63 | (color_tuple[2] & 0x00F8) >> 3
68 """This initializes a display and connects it into CircuitPython. Unlike other objects
69 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
70 This is done so that CircuitPython can use the display itself.
72 Most people should not use this class directly. Use a specific display driver instead
73 that will contain the initialization sequence at minimum.
76 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
77 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
78 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
79 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
80 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
81 auto_refresh=True, native_frames_per_second=60)
84 # pylint: disable=too-many-locals
97 pixels_in_byte_share_row=True,
99 reverse_pixels_in_byte=False,
100 set_column_command=0x2A,
101 set_row_command=0x2B,
102 write_ram_command=0x2C,
103 set_vertical_scroll=0,
105 brightness_command=None,
107 auto_brightness=False,
108 single_byte_bounds=False,
109 data_as_commands=False,
111 native_frames_per_second=60
113 """Create a Display object on the given display bus (`displayio.FourWire` or
114 `displayio.ParallelBus`).
116 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
117 with a command byte followed by a byte to determine the parameter count and if a
118 delay is need after. When the top bit of the second byte is 1, the next byte will be
119 the delay time in milliseconds. The remaining 7 bits are the parameter count
120 excluding any delay byte. The third through final bytes are the remaining command
121 parameters. The next byte will begin a new command definition. Here is a portion of
123 .. code-block:: python
126 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
127 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
128 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
130 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
132 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
133 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
134 Multiple byte literals (b”“) are merged together on load. The parens are needed to
135 allow byte literals on subsequent lines.
137 The initialization sequence should always leave the display memory access inline with
138 the scan of the display to minimize tearing artifacts.
140 self._bus = display_bus
141 self._set_column_command = set_column_command
142 self._set_row_command = set_row_command
143 self._write_ram_command = write_ram_command
144 self._brightness_command = brightness_command
145 self._data_as_commands = data_as_commands
146 self._single_byte_bounds = single_byte_bounds
148 self._height = height
149 self._colstart = colstart
150 self._rowstart = rowstart
151 self._rotation = rotation
152 self._auto_brightness = auto_brightness
153 self._brightness = 1.0
154 self._auto_refresh = auto_refresh
155 self._initialize(init_sequence)
156 self._buffer = Image.new("RGB", (width, height))
157 self._subrectangles = []
158 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
159 self._current_group = None
160 displays.append(self)
161 self._refresh_thread = None
162 if self._auto_refresh:
163 self.auto_refresh = True
165 self._backlight_type = None
166 if backlight_pin is not None:
168 from pulseio import PWMOut # pylint: disable=import-outside-toplevel
170 # 100Hz looks decent and doesn't keep the CPU too busy
171 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
172 self._backlight_type = BACKLIGHT_PWM
174 # PWMOut not implemented on this platform
176 if self._backlight_type is None:
177 self._backlight_type = BACKLIGHT_IN_OUT
178 self._backlight = digitalio.DigitalInOut(backlight_pin)
179 self._backlight.switch_to_output()
180 self.brightness = brightness
182 # pylint: enable=too-many-locals
184 def _initialize(self, init_sequence):
186 while i < len(init_sequence):
187 command = init_sequence[i]
188 data_size = init_sequence[i + 1]
189 delay = (data_size & 0x80) > 0
192 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
196 delay_time_ms = init_sequence[i + 1 + data_size]
197 if delay_time_ms == 255:
199 time.sleep(delay_time_ms / 1000)
202 def _write(self, command, data):
203 self._bus.begin_transaction()
204 if self._data_as_commands:
205 if command is not None:
206 self._bus.send(True, bytes([command]), toggle_every_byte=True)
207 self._bus.send(command is not None, data)
209 self._bus.send(True, bytes([command]), toggle_every_byte=True)
210 self._bus.send(False, data)
211 self._bus.end_transaction()
214 self._bus._release() # pylint: disable=protected-access
217 def show(self, group):
218 """Switches to displaying the given group of layers. When group is None, the
219 default CircuitPython terminal will be shown.
221 self._current_group = group
223 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
224 """When auto refresh is off, waits for the target frame rate and then refreshes the
225 display, returning True. If the call has taken too long since the last refresh call
226 for the given target frame rate, then the refresh returns False immediately without
227 updating the screen to hopefully help getting caught up.
229 If the time since the last successful refresh is below the minimum frame rate, then
230 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
232 When auto refresh is on, updates the display immediately. (The display will also
233 update without calls to this.)
235 self._subrectangles = []
237 # Go through groups and and add each to buffer
238 if self._current_group is not None:
239 buffer = Image.new("RGBA", (self._width, self._height))
240 # Recursively have everything draw to the image
241 self._current_group._fill_area(buffer) # pylint: disable=protected-access
242 # save image to buffer (or probably refresh buffer so we can compare)
243 self._buffer.paste(buffer)
245 if self._current_group is not None:
246 # Eventually calculate dirty rectangles here
247 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
249 for area in self._subrectangles:
250 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 if self._data_as_commands:
292 self._write(None, pixels)
294 self._write(self._write_ram_command, pixels)
296 def _clip(self, rectangle):
297 if self._rotation in (90, 270):
298 width, height = self._height, self._width
300 width, height = self._width, self._height
306 if rectangle.x2 > width:
308 if rectangle.y2 > height:
309 rectangle.y2 = height
313 def _apply_rotation(self, rectangle):
314 """Adjust the rectangle coordinates based on rotation"""
315 if self._rotation == 90:
317 self._height - rectangle.y2,
319 self._height - rectangle.y1,
322 if self._rotation == 180:
324 self._width - rectangle.x2,
325 self._height - rectangle.y2,
326 self._width - rectangle.x1,
327 self._height - rectangle.y1,
329 if self._rotation == 270:
332 self._width - rectangle.x2,
334 self._width - rectangle.x1,
338 def _encode_pos(self, x, y):
339 """Encode a postion into bytes."""
340 return struct.pack(self._bounds_encoding, x, y)
342 def fill_row(self, y, buffer):
343 """Extract the pixels from a single row"""
344 for x in range(0, self._width):
345 _rgb_565 = _rgb_tuple_to_rgb565(self._buffer.getpixel((x, y)))
346 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
347 buffer[x * 2 + 1] = _rgb_565 & 0xFF
351 def auto_refresh(self):
352 """True when the display is refreshed automatically."""
353 return self._auto_refresh
356 def auto_refresh(self, value):
357 self._auto_refresh = value
358 if self._refresh_thread is None:
359 self._refresh_thread = threading.Thread(
360 target=self._refresh_loop, daemon=True
362 if value and not self._refresh_thread.is_alive():
364 self._refresh_thread.start()
365 elif not value and self._refresh_thread.is_alive():
367 self._refresh_thread.join()
370 def brightness(self):
371 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
372 When `auto_brightness` is True, the value of `brightness` will change automatically.
373 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
375 return self._brightness
378 def brightness(self, value):
379 if 0 <= float(value) <= 1.0:
380 self._brightness = value
381 if self._backlight_type == BACKLIGHT_IN_OUT:
382 self._backlight.value = round(self._brightness)
383 elif self._backlight_type == BACKLIGHT_PWM:
384 self._backlight.duty_cycle = self._brightness * 65535
385 elif self._brightness_command is not None:
386 self._write(self._brightness_command, round(value * 255))
388 raise ValueError("Brightness must be between 0.0 and 1.0")
391 def auto_brightness(self):
392 """True when the display brightness is adjusted automatically, based on an ambient
393 light sensor or other method. Note that some displays may have this set to True by
394 default, but not actually implement automatic brightness adjustment.
395 `auto_brightness` is set to False if `brightness` is set manually.
397 return self._auto_brightness
399 @auto_brightness.setter
400 def auto_brightness(self, value):
401 self._auto_brightness = value
415 """The rotation of the display as an int in degrees."""
416 return self._rotation
419 def rotation(self, value):
420 if value not in (0, 90, 180, 270):
421 raise ValueError("Rotation must be 0/90/180/270")
422 self._rotation = value
426 """Current Display Bus"""
430 # pylint: enable=too-many-instance-attributes