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
43 from recordclass import recordclass
45 __version__ = "0.0.0-auto.0"
46 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
49 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
52 # pylint: disable=unnecessary-pass, unused-argument
54 # pylint: disable=too-many-instance-attributes
56 """This initializes a display and connects it into CircuitPython. Unlike other objects
57 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
58 This is done so that CircuitPython can use the display itself.
60 Most people should not use this class directly. Use a specific display driver instead
61 that will contain the initialization sequence at minimum.
64 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
65 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
66 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
67 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
68 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
69 auto_refresh=True, native_frames_per_second=60)
72 # pylint: disable=too-many-locals
85 pixels_in_byte_share_row=True,
87 reverse_pixels_in_byte=False,
88 set_column_command=0x2A,
90 write_ram_command=0x2C,
91 set_vertical_scroll=0,
93 brightness_command=None,
95 auto_brightness=False,
96 single_byte_bounds=False,
97 data_as_commands=False,
99 native_frames_per_second=60
101 """Create a Display object on the given display bus (`displayio.FourWire` or
102 `displayio.ParallelBus`).
104 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
105 with a command byte followed by a byte to determine the parameter count and if a
106 delay is need after. When the top bit of the second byte is 1, the next byte will be
107 the delay time in milliseconds. The remaining 7 bits are the parameter count
108 excluding any delay byte. The third through final bytes are the remaining command
109 parameters. The next byte will begin a new command definition. Here is a portion of
111 .. code-block:: python
114 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
115 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
116 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
118 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
120 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
121 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
122 Multiple byte literals (b”“) are merged together on load. The parens are needed to
123 allow byte literals on subsequent lines.
125 The initialization sequence should always leave the display memory access inline with
126 the scan of the display to minimize tearing artifacts.
128 self._bus = display_bus
129 self._set_column_command = set_column_command
130 self._set_row_command = set_row_command
131 self._write_ram_command = write_ram_command
132 self._brightness_command = brightness_command
133 self._data_as_commands = data_as_commands
134 self._single_byte_bounds = single_byte_bounds
136 self._height = height
137 self._colstart = colstart
138 self._rowstart = rowstart
140 self._auto_brightness = auto_brightness
141 self._brightness = brightness
142 self._auto_refresh = auto_refresh
143 self._initialize(init_sequence)
144 self._buffer = Image.new("RGB", (width, height))
145 self._subrectangles = []
146 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
147 self._current_group = None
148 self.rotation = rotation
149 displays.append(self)
150 self._refresh_thread = None
151 if self._auto_refresh:
152 self.auto_refresh = True
154 # pylint: enable=too-many-locals
156 def _initialize(self, init_sequence):
158 while i < len(init_sequence):
159 command = init_sequence[i]
160 data_size = init_sequence[i + 1]
161 delay = (data_size & 0x80) > 0
163 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
167 delay_time_ms = init_sequence[i + 1 + data_size]
168 if delay_time_ms == 255:
170 time.sleep(delay_time_ms / 1000)
173 def _write(self, command, data):
174 if self._single_byte_bounds:
175 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
177 self._bus.send(True, bytes([command]), toggle_every_byte=True)
178 self._bus.send(False, data)
184 def show(self, group):
185 """Switches to displaying the given group of layers. When group is None, the
186 default CircuitPython terminal will be shown.
188 self._current_group = group
190 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
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 # Go through groups and and add each to buffer
203 if self._current_group is not None:
204 buffer = Image.new("RGBA", (self._width, self._height))
205 # Recursively have everything draw to the image
206 self._current_group._fill_area(buffer) # pylint: disable=protected-access
207 # save image to buffer (or probably refresh buffer so we can compare)
208 self._buffer.paste(buffer)
210 # Eventually calculate dirty rectangles here
211 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
213 for area in self._subrectangles:
214 self._refresh_display_area(area)
216 def _refresh_loop(self):
217 while self._auto_refresh:
220 def _refresh_display_area(self, rectangle):
221 """Loop through dirty rectangles and redraw that area."""
223 img = self._buffer.convert("RGB").crop(rectangle)
224 img = img.rotate(self._rotation, expand=True)
226 display_rectangle = self._apply_rotation(rectangle)
227 img = img.crop(self._clip(display_rectangle))
229 data = numpy.array(img).astype("uint16")
231 ((data[:, :, 0] & 0xF8) << 8)
232 | ((data[:, :, 1] & 0xFC) << 3)
233 | (data[:, :, 2] >> 3)
237 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
241 self._set_column_command,
243 display_rectangle.x1 + self._colstart,
244 display_rectangle.x2 + self._colstart - 1,
248 self._set_row_command,
250 display_rectangle.y1 + self._rowstart,
251 display_rectangle.y2 + self._rowstart - 1,
255 self._write(self._write_ram_command, pixels)
257 def _clip(self, rectangle):
258 if self._rotation in (90, 270):
259 width, height = self._height, self._width
261 width, height = self._width, self._height
267 if rectangle.x2 > width:
269 if rectangle.y2 > height:
270 rectangle.y2 = height
274 def _apply_rotation(self, rectangle):
275 """Adjust the rectangle coordinates based on rotation"""
276 if self._rotation == 90:
278 self._height - rectangle.y2,
280 self._height - rectangle.y1,
283 if self._rotation == 180:
285 self._width - rectangle.x2,
286 self._height - rectangle.y2,
287 self._width - rectangle.x1,
288 self._height - rectangle.y1,
290 if self._rotation == 270:
293 self._width - rectangle.x2,
295 self._width - rectangle.x1,
299 def _encode_pos(self, x, y):
300 """Encode a postion into bytes."""
301 return struct.pack(self._bounds_encoding, x, y)
303 def fill_row(self, y, buffer):
304 """Extract the pixels from a single row"""
308 def auto_refresh(self):
309 """True when the display is refreshed automatically."""
310 return self._auto_refresh
313 def auto_refresh(self, value):
314 self._auto_refresh = value
315 if self._refresh_thread is None:
316 self._refresh_thread = threading.Thread(
317 target=self._refresh_loop, daemon=True
319 if value and not self._refresh_thread.is_alive():
321 self._refresh_thread.start()
322 elif not value and self._refresh_thread.is_alive():
324 self._refresh_thread.join()
327 def brightness(self):
328 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
329 When `auto_brightness` is True, the value of `brightness` will change automatically.
330 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
332 return self._brightness
335 def brightness(self, value):
336 self._brightness = value
339 def auto_brightness(self):
340 """True when the display brightness is adjusted automatically, based on an ambient
341 light sensor or other method. Note that some displays may have this set to True by
342 default, but not actually implement automatic brightness adjustment.
343 `auto_brightness` is set to False if `brightness` is set manually.
345 return self._auto_brightness
347 @auto_brightness.setter
348 def auto_brightness(self, value):
349 self._auto_brightness = value
363 """The rotation of the display as an int in degrees."""
364 return self._rotation
367 def rotation(self, value):
368 if value not in (0, 90, 180, 270):
369 raise ValueError("Rotation must be 0/90/180/270")
370 self._rotation = value
374 """Current Display Bus"""
378 # pylint: enable=too-many-instance-attributes