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 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
51 # pylint: disable=unnecessary-pass, unused-argument
53 # pylint: disable=too-many-instance-attributes
55 """This initializes a display and connects it into CircuitPython. Unlike other objects
56 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
57 This is done so that CircuitPython can use the display itself.
59 Most people should not use this class directly. Use a specific display driver instead
60 that will contain the initialization sequence at minimum.
63 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
64 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
65 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
66 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
67 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
68 auto_refresh=True, native_frames_per_second=60)
71 # pylint: disable=too-many-locals
84 pixels_in_byte_share_row=True,
86 reverse_pixels_in_byte=False,
87 set_column_command=0x2A,
89 write_ram_command=0x2C,
90 set_vertical_scroll=0,
92 brightness_command=None,
94 auto_brightness=False,
95 single_byte_bounds=False,
96 data_as_commands=False,
98 native_frames_per_second=60
100 """Create a Display object on the given display bus (`displayio.FourWire` or
101 `displayio.ParallelBus`).
103 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
104 with a command byte followed by a byte to determine the parameter count and if a
105 delay is need after. When the top bit of the second byte is 1, the next byte will be
106 the delay time in milliseconds. The remaining 7 bits are the parameter count
107 excluding any delay byte. The third through final bytes are the remaining command
108 parameters. The next byte will begin a new command definition. Here is a portion of
110 .. code-block:: python
113 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
114 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
115 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
117 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
119 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
120 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
121 Multiple byte literals (b”“) are merged together on load. The parens are needed to
122 allow byte literals on subsequent lines.
124 The initialization sequence should always leave the display memory access inline with
125 the scan of the display to minimize tearing artifacts.
127 self._bus = display_bus
128 self._set_column_command = set_column_command
129 self._set_row_command = set_row_command
130 self._write_ram_command = write_ram_command
131 self._brightness_command = brightness_command
132 self._data_as_commands = data_as_commands
133 self._single_byte_bounds = single_byte_bounds
135 self._height = height
136 self._colstart = colstart
137 self._rowstart = rowstart
139 self._auto_brightness = auto_brightness
140 self._brightness = brightness
141 self._auto_refresh = auto_refresh
142 self._initialize(init_sequence)
143 self._buffer = Image.new("RGB", (width, height))
144 self._subrectangles = []
145 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
146 self._current_group = None
147 self.rotation = rotation
148 displays.append(self)
149 self._refresh_thread = None
150 if self._auto_refresh:
151 self.auto_refresh = True
153 # pylint: enable=too-many-locals
155 def _initialize(self, init_sequence):
157 while i < len(init_sequence):
158 command = init_sequence[i]
159 data_size = init_sequence[i + 1]
160 delay = (data_size & 0x80) > 0
162 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
166 delay_time_ms = init_sequence[i + 1 + data_size]
167 if delay_time_ms == 255:
169 time.sleep(delay_time_ms / 1000)
172 def _write(self, command, data):
173 if self._single_byte_bounds:
174 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
176 self._bus.send(True, bytes([command]), toggle_every_byte=True)
177 self._bus.send(False, data)
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 """When auto refresh is off, waits for the target frame rate and then refreshes the
191 display, returning True. If the call has taken too long since the last refresh call
192 for the given target frame rate, then the refresh returns False immediately without
193 updating the screen to hopefully help getting caught up.
195 If the time since the last successful refresh is below the minimum frame rate, then
196 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
198 When auto refresh is on, updates the display immediately. (The display will also
199 update without calls to this.)
201 # Go through groups and and add each to buffer
202 if self._current_group is not None:
203 buffer = Image.new("RGBA", (self._width, self._height))
204 # Recursively have everything draw to the image
205 self._current_group._fill_area(buffer) # pylint: disable=protected-access
206 # save image to buffer (or probably refresh buffer so we can compare)
207 self._buffer.paste(buffer)
209 # Eventually calculate dirty rectangles here
210 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
212 for area in self._subrectangles:
213 self._refresh_display_area(area)
215 def _refresh_loop(self):
216 while self._auto_refresh:
219 def _refresh_display_area(self, rectangle):
220 """Loop through dirty rectangles and redraw that area."""
222 img = self._buffer.convert("RGB").crop(rectangle)
223 img = img.rotate(self._rotation, expand=True)
225 display_rectangle = self._apply_rotation(rectangle)
226 img = img.crop(self._clip(display_rectangle))
228 data = numpy.array(img).astype("uint16")
230 ((data[:, :, 0] & 0xF8) << 8)
231 | ((data[:, :, 1] & 0xFC) << 3)
232 | (data[:, :, 2] >> 3)
236 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
240 self._set_column_command,
242 display_rectangle.x1 + self._colstart,
243 display_rectangle.x2 + self._colstart - 1,
247 self._set_row_command,
249 display_rectangle.y1 + self._rowstart,
250 display_rectangle.y2 + self._rowstart - 1,
254 self._write(self._write_ram_command, pixels)
256 def _clip(self, rectangle):
257 if self._rotation in (90, 270):
258 width, height = self._height, self._width
260 width, height = self._width, self._height
266 if rectangle.x2 > width:
268 if rectangle.y2 > height:
269 rectangle.y2 = height
273 def _apply_rotation(self, rectangle):
274 """Adjust the rectangle coordinates based on rotation"""
275 if self._rotation == 90:
277 self._height - rectangle.y2,
279 self._height - rectangle.y1,
282 if self._rotation == 180:
284 self._width - rectangle.x2,
285 self._height - rectangle.y2,
286 self._width - rectangle.x1,
287 self._height - rectangle.y1,
289 if self._rotation == 270:
292 self._width - rectangle.x2,
294 self._width - rectangle.x1,
298 def _encode_pos(self, x, y):
299 """Encode a postion into bytes."""
300 return struct.pack(self._bounds_encoding, x, y)
302 def fill_row(self, y, buffer):
303 """Extract the pixels from a single row"""
307 def auto_refresh(self):
308 """True when the display is refreshed automatically."""
309 return self._auto_refresh
312 def auto_refresh(self, value):
313 self._auto_refresh = value
314 if self._refresh_thread is None:
315 self._refresh_thread = threading.Thread(
316 target=self._refresh_loop, daemon=True
318 if value and not self._refresh_thread.is_alive():
320 self._refresh_thread.start()
321 elif not value and self._refresh_thread.is_alive():
323 self._refresh_thread.join()
326 def brightness(self):
327 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
328 When `auto_brightness` is True, the value of `brightness` will change automatically.
329 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
331 return self._brightness
334 def brightness(self, value):
335 self._brightness = value
338 def auto_brightness(self):
339 """True when the display brightness is adjusted automatically, based on an ambient
340 light sensor or other method. Note that some displays may have this set to True by
341 default, but not actually implement automatic brightness adjustment.
342 `auto_brightness` is set to False if `brightness` is set manually.
344 return self._auto_brightness
346 @auto_brightness.setter
347 def auto_brightness(self, value):
348 self._auto_brightness = value
362 """The rotation of the display as an int in degrees."""
363 return self._rotation
366 def rotation(self, value):
367 if value not in (0, 90, 180, 270):
368 raise ValueError("Rotation must be 0/90/180/270")
369 self._rotation = value
373 """Current Display Bus"""
377 # pylint: enable=too-many-instance-attributes