1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
26 from recordclass import recordclass
27 from displayio.colorconverter import ColorConverter
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
32 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
40 # pylint: disable=too-many-instance-attributes
41 """This initializes a display and connects it into CircuitPython. Unlike other objects
42 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
43 This is done so that CircuitPython can use the display itself.
45 Most people should not use this class directly. Use a specific display driver instead
46 that will contain the initialization sequence at minimum.
49 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
50 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
51 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
52 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
53 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
54 auto_refresh=True, native_frames_per_second=60)
69 pixels_in_byte_share_row=True,
71 reverse_pixels_in_byte=False,
72 set_column_command=0x2A,
74 write_ram_command=0x2C,
75 set_vertical_scroll=0,
77 brightness_command=None,
79 auto_brightness=False,
80 single_byte_bounds=False,
81 data_as_commands=False,
83 native_frames_per_second=60
85 # pylint: disable=unused-argument,too-many-locals
86 """Create a Display object on the given display bus (`displayio.FourWire` or
87 `displayio.ParallelBus`).
89 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
90 with a command byte followed by a byte to determine the parameter count and if a
91 delay is need after. When the top bit of the second byte is 1, the next byte will be
92 the delay time in milliseconds. The remaining 7 bits are the parameter count
93 excluding any delay byte. The third through final bytes are the remaining command
94 parameters. The next byte will begin a new command definition. Here is a portion of
96 .. code-block:: python
99 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
100 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
101 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
103 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
105 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
106 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
107 Multiple byte literals (b”“) are merged together on load. The parens are needed to
108 allow byte literals on subsequent lines.
110 The initialization sequence should always leave the display memory access inline with
111 the scan of the display to minimize tearing artifacts.
113 self._bus = display_bus
114 self._set_column_command = set_column_command
115 self._set_row_command = set_row_command
116 self._write_ram_command = write_ram_command
117 self._brightness_command = brightness_command
118 self._data_as_commands = data_as_commands
119 self._single_byte_bounds = single_byte_bounds
121 self._height = height
122 self._colstart = colstart
123 self._rowstart = rowstart
124 self._rotation = rotation
125 self._auto_brightness = auto_brightness
126 self._brightness = 1.0
127 self._auto_refresh = auto_refresh
128 self._initialize(init_sequence)
129 self._buffer = Image.new("RGB", (width, height))
130 self._subrectangles = []
131 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
132 self._current_group = None
133 displays.append(self)
134 self._refresh_thread = None
135 if self._auto_refresh:
136 self.auto_refresh = True
137 self._colorconverter = ColorConverter()
139 self._backlight_type = None
140 if backlight_pin is not None:
142 from pulseio import PWMOut # pylint: disable=import-outside-toplevel
144 # 100Hz looks decent and doesn't keep the CPU too busy
145 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
146 self._backlight_type = BACKLIGHT_PWM
148 # PWMOut not implemented on this platform
150 if self._backlight_type is None:
151 self._backlight_type = BACKLIGHT_IN_OUT
152 self._backlight = digitalio.DigitalInOut(backlight_pin)
153 self._backlight.switch_to_output()
154 self.brightness = brightness
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
164 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
168 delay_time_ms = init_sequence[i + 1 + data_size]
169 if delay_time_ms == 255:
171 time.sleep(delay_time_ms / 1000)
174 def _write(self, command, data):
175 self._bus.begin_transaction()
176 if self._data_as_commands:
177 if command is not None:
178 self._bus.send(True, bytes([command]), toggle_every_byte=True)
179 self._bus.send(command is not None, data)
181 self._bus.send(True, bytes([command]), toggle_every_byte=True)
182 self._bus.send(False, data)
183 self._bus.end_transaction()
186 self._bus._release() # pylint: disable=protected-access
189 def show(self, group):
190 """Switches to displaying the given group of layers. When group is None, the
191 default CircuitPython terminal will be shown.
193 self._current_group = group
195 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
196 # pylint: disable=unused-argument
197 """When auto refresh is off, waits for the target frame rate and then refreshes the
198 display, returning True. If the call has taken too long since the last refresh call
199 for the given target frame rate, then the refresh returns False immediately without
200 updating the screen to hopefully help getting caught up.
202 If the time since the last successful refresh is below the minimum frame rate, then
203 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
205 When auto refresh is on, updates the display immediately. (The display will also
206 update without calls to this.)
208 self._subrectangles = []
210 # Go through groups and and add each to buffer
211 if self._current_group is not None:
212 buffer = Image.new("RGBA", (self._width, self._height))
213 # Recursively have everything draw to the image
214 self._current_group._fill_area(buffer) # pylint: disable=protected-access
215 # save image to buffer (or probably refresh buffer so we can compare)
216 self._buffer.paste(buffer)
218 if self._current_group is not None:
219 # Eventually calculate dirty rectangles here
220 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
222 for area in self._subrectangles:
223 self._refresh_display_area(area)
225 def _refresh_loop(self):
226 while self._auto_refresh:
229 def _refresh_display_area(self, rectangle):
230 """Loop through dirty rectangles and redraw that area."""
232 img = self._buffer.convert("RGB").crop(rectangle)
233 img = img.rotate(self._rotation, expand=True)
235 display_rectangle = self._apply_rotation(rectangle)
236 img = img.crop(self._clip(display_rectangle))
238 data = numpy.array(img).astype("uint16")
240 ((data[:, :, 0] & 0xF8) << 8)
241 | ((data[:, :, 1] & 0xFC) << 3)
242 | (data[:, :, 2] >> 3)
246 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
250 self._set_column_command,
252 display_rectangle.x1 + self._colstart,
253 display_rectangle.x2 + self._colstart - 1,
257 self._set_row_command,
259 display_rectangle.y1 + self._rowstart,
260 display_rectangle.y2 + self._rowstart - 1,
264 if self._data_as_commands:
265 self._write(None, pixels)
267 self._write(self._write_ram_command, pixels)
269 def _clip(self, rectangle):
270 if self._rotation in (90, 270):
271 width, height = self._height, self._width
273 width, height = self._width, self._height
279 if rectangle.x2 > width:
281 if rectangle.y2 > height:
282 rectangle.y2 = height
286 def _apply_rotation(self, rectangle):
287 """Adjust the rectangle coordinates based on rotation"""
288 if self._rotation == 90:
290 self._height - rectangle.y2,
292 self._height - rectangle.y1,
295 if self._rotation == 180:
297 self._width - rectangle.x2,
298 self._height - rectangle.y2,
299 self._width - rectangle.x1,
300 self._height - rectangle.y1,
302 if self._rotation == 270:
305 self._width - rectangle.x2,
307 self._width - rectangle.x1,
311 def _encode_pos(self, x, y):
312 """Encode a postion into bytes."""
313 return struct.pack(self._bounds_encoding, x, y)
315 def fill_row(self, y, buffer):
316 """Extract the pixels from a single row"""
317 for x in range(0, self._width):
318 _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
319 buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
320 buffer[x * 2 + 1] = _rgb_565 & 0xFF
324 def auto_refresh(self):
325 """True when the display is refreshed automatically."""
326 return self._auto_refresh
329 def auto_refresh(self, value):
330 self._auto_refresh = value
331 if self._refresh_thread is None:
332 self._refresh_thread = threading.Thread(
333 target=self._refresh_loop, daemon=True
335 if value and not self._refresh_thread.is_alive():
337 self._refresh_thread.start()
338 elif not value and self._refresh_thread.is_alive():
340 self._refresh_thread.join()
343 def brightness(self):
344 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
345 When `auto_brightness` is True, the value of `brightness` will change automatically.
346 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
348 return self._brightness
351 def brightness(self, value):
352 if 0 <= float(value) <= 1.0:
353 self._brightness = value
354 if self._backlight_type == BACKLIGHT_IN_OUT:
355 self._backlight.value = round(self._brightness)
356 elif self._backlight_type == BACKLIGHT_PWM:
357 self._backlight.duty_cycle = self._brightness * 65535
358 elif self._brightness_command is not None:
359 self._write(self._brightness_command, round(value * 255))
361 raise ValueError("Brightness must be between 0.0 and 1.0")
364 def auto_brightness(self):
365 """True when the display brightness is adjusted automatically, based on an ambient
366 light sensor or other method. Note that some displays may have this set to True by
367 default, but not actually implement automatic brightness adjustment.
368 `auto_brightness` is set to False if `brightness` is set manually.
370 return self._auto_brightness
372 @auto_brightness.setter
373 def auto_brightness(self, value):
374 self._auto_brightness = value
388 """The rotation of the display as an int in degrees."""
389 return self._rotation
392 def rotation(self, value):
393 if value not in (0, 90, 180, 270):
394 raise ValueError("Rotation must be 0/90/180/270")
395 self._rotation = value
399 """Current Display Bus"""