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
45 from displayio.colorconverter import ColorConverter
47 __version__ = "0.0.0-auto.0"
48 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
50 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
56 # pylint: disable=unnecessary-pass, unused-argument
57 # pylint: disable=too-many-instance-attributes
61 """This initializes a display and connects it into CircuitPython. Unlike other objects
62 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
63 This is done so that CircuitPython can use the display itself.
65 Most people should not use this class directly. Use a specific display driver instead
66 that will contain the initialization sequence at minimum.
69 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
70 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
71 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
72 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
73 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
74 auto_refresh=True, native_frames_per_second=60)
77 # pylint: disable=too-many-locals
90 pixels_in_byte_share_row=True,
92 reverse_pixels_in_byte=False,
93 set_column_command=0x2A,
95 write_ram_command=0x2C,
96 set_vertical_scroll=0,
98 brightness_command=None,
100 auto_brightness=False,
101 single_byte_bounds=False,
102 data_as_commands=False,
104 native_frames_per_second=60
106 """Create a Display object on the given display bus (`displayio.FourWire` or
107 `displayio.ParallelBus`).
109 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
110 with a command byte followed by a byte to determine the parameter count and if a
111 delay is need after. When the top bit of the second byte is 1, the next byte will be
112 the delay time in milliseconds. The remaining 7 bits are the parameter count
113 excluding any delay byte. The third through final bytes are the remaining command
114 parameters. The next byte will begin a new command definition. Here is a portion of
116 .. code-block:: python
119 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
120 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
121 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
123 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
125 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
126 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
127 Multiple byte literals (b”“) are merged together on load. The parens are needed to
128 allow byte literals on subsequent lines.
130 The initialization sequence should always leave the display memory access inline with
131 the scan of the display to minimize tearing artifacts.
133 self._bus = display_bus
134 self._set_column_command = set_column_command
135 self._set_row_command = set_row_command
136 self._write_ram_command = write_ram_command
137 self._brightness_command = brightness_command
138 self._data_as_commands = data_as_commands
139 self._single_byte_bounds = single_byte_bounds
141 self._height = height
142 self._colstart = colstart
143 self._rowstart = rowstart
144 self._rotation = rotation
145 self._auto_brightness = auto_brightness
146 self._brightness = 1.0
147 self._auto_refresh = auto_refresh
148 self._initialize(init_sequence)
149 self._buffer = Image.new("RGB", (width, height))
150 self._subrectangles = []
151 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
152 self._current_group = None
153 displays.append(self)
154 self._refresh_thread = None
155 if self._auto_refresh:
156 self.auto_refresh = True
157 self._colorconverter = ColorConverter()
159 self._backlight_type = None
160 if backlight_pin is not None:
162 from pulseio import PWMOut # pylint: disable=import-outside-toplevel
164 # 100Hz looks decent and doesn't keep the CPU too busy
165 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
166 self._backlight_type = BACKLIGHT_PWM
168 # PWMOut not implemented on this platform
170 if self._backlight_type is None:
171 self._backlight_type = BACKLIGHT_IN_OUT
172 self._backlight = digitalio.DigitalInOut(backlight_pin)
173 self._backlight.switch_to_output()
174 self.brightness = brightness
176 # pylint: enable=too-many-locals
178 def _initialize(self, init_sequence):
180 while i < len(init_sequence):
181 command = init_sequence[i]
182 data_size = init_sequence[i + 1]
183 delay = (data_size & 0x80) > 0
186 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
190 delay_time_ms = init_sequence[i + 1 + data_size]
191 if delay_time_ms == 255:
193 time.sleep(delay_time_ms / 1000)
196 def _write(self, command, data):
197 self._bus.begin_transaction()
198 if self._data_as_commands:
199 if command is not None:
200 self._bus.send(True, bytes([command]), toggle_every_byte=True)
201 self._bus.send(command is not None, data)
203 self._bus.send(True, bytes([command]), toggle_every_byte=True)
204 self._bus.send(False, data)
205 self._bus.end_transaction()
208 self._bus._release() # pylint: disable=protected-access
211 def show(self, group):
212 """Switches to displaying the given group of layers. When group is None, the
213 default CircuitPython terminal will be shown.
215 self._current_group = group
217 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
218 """When auto refresh is off, waits for the target frame rate and then refreshes the
219 display, returning True. If the call has taken too long since the last refresh call
220 for the given target frame rate, then the refresh returns False immediately without
221 updating the screen to hopefully help getting caught up.
223 If the time since the last successful refresh is below the minimum frame rate, then
224 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
226 When auto refresh is on, updates the display immediately. (The display will also
227 update without calls to this.)
229 self._subrectangles = []
231 # Go through groups and and add each to buffer
232 if self._current_group is not None:
233 buffer = Image.new("RGBA", (self._width, self._height))
234 # Recursively have everything draw to the image
235 self._current_group._fill_area(buffer) # pylint: disable=protected-access
236 # save image to buffer (or probably refresh buffer so we can compare)
237 self._buffer.paste(buffer)
239 if self._current_group is not None:
240 # Eventually calculate dirty rectangles here
241 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
243 for area in self._subrectangles:
244 self._refresh_display_area(area)
246 def _refresh_loop(self):
247 while self._auto_refresh:
250 def _refresh_display_area(self, rectangle):
251 """Loop through dirty rectangles and redraw that area."""
253 img = self._buffer.convert("RGB").crop(rectangle)
254 img = img.rotate(self._rotation, expand=True)
256 display_rectangle = self._apply_rotation(rectangle)
257 img = img.crop(self._clip(display_rectangle))
259 data = numpy.array(img).astype("uint16")
261 ((data[:, :, 0] & 0xF8) << 8)
262 | ((data[:, :, 1] & 0xFC) << 3)
263 | (data[:, :, 2] >> 3)
267 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
271 self._set_column_command,
273 display_rectangle.x1 + self._colstart,
274 display_rectangle.x2 + self._colstart - 1,
278 self._set_row_command,
280 display_rectangle.y1 + self._rowstart,
281 display_rectangle.y2 + self._rowstart - 1,
285 if self._data_as_commands:
286 self._write(None, pixels)
288 self._write(self._write_ram_command, pixels)
290 def _clip(self, rectangle):
291 if self._rotation in (90, 270):
292 width, height = self._height, self._width
294 width, height = self._width, self._height
300 if rectangle.x2 > width:
302 if rectangle.y2 > height:
303 rectangle.y2 = height
307 def _apply_rotation(self, rectangle):
308 """Adjust the rectangle coordinates based on rotation"""
309 if self._rotation == 90:
311 self._height - rectangle.y2,
313 self._height - rectangle.y1,
316 if self._rotation == 180:
318 self._width - rectangle.x2,
319 self._height - rectangle.y2,
320 self._width - rectangle.x1,
321 self._height - rectangle.y1,
323 if self._rotation == 270:
326 self._width - rectangle.x2,
328 self._width - rectangle.x1,
332 def _encode_pos(self, x, y):
333 """Encode a postion into bytes."""
334 return struct.pack(self._bounds_encoding, x, y)
336 def fill_row(self, y, buffer):
337 """Extract the pixels from a single row"""
338 for x in range(0, self._width):
339 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
340 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
341 buffer[x * 2 + 1] = _rgb_565 & 0xFF
345 def auto_refresh(self):
346 """True when the display is refreshed automatically."""
347 return self._auto_refresh
350 def auto_refresh(self, value):
351 self._auto_refresh = value
352 if self._refresh_thread is None:
353 self._refresh_thread = threading.Thread(
354 target=self._refresh_loop, daemon=True
356 if value and not self._refresh_thread.is_alive():
358 self._refresh_thread.start()
359 elif not value and self._refresh_thread.is_alive():
361 self._refresh_thread.join()
364 def brightness(self):
365 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
366 When `auto_brightness` is True, the value of `brightness` will change automatically.
367 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
369 return self._brightness
372 def brightness(self, value):
373 if 0 <= float(value) <= 1.0:
374 self._brightness = value
375 if self._backlight_type == BACKLIGHT_IN_OUT:
376 self._backlight.value = round(self._brightness)
377 elif self._backlight_type == BACKLIGHT_PWM:
378 self._backlight.duty_cycle = self._brightness * 65535
379 elif self._brightness_command is not None:
380 self._write(self._brightness_command, round(value * 255))
382 raise ValueError("Brightness must be between 0.0 and 1.0")
385 def auto_brightness(self):
386 """True when the display brightness is adjusted automatically, based on an ambient
387 light sensor or other method. Note that some displays may have this set to True by
388 default, but not actually implement automatic brightness adjustment.
389 `auto_brightness` is set to False if `brightness` is set manually.
391 return self._auto_brightness
393 @auto_brightness.setter
394 def auto_brightness(self, value):
395 self._auto_brightness = value
409 """The rotation of the display as an int in degrees."""
410 return self._rotation
413 def rotation(self, value):
414 if value not in (0, 90, 180, 270):
415 raise ValueError("Rotation must be 0/90/180/270")
416 self._rotation = value
420 """Current Display Bus"""
424 # pylint: enable=too-many-instance-attributes