1 # SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
2 # SPDX-FileCopyrightText: 2021 James Carr
4 # SPDX-License-Identifier: MIT
7 `displayio._displaycore`
8 ================================================================================
10 Super class of the display classes
12 **Software and Dependencies:**
15 https://github.com/adafruit/Adafruit_Blinka/releases
17 * Author(s): James Carr, Melissa LeBlanc-Williams
21 __version__ = "0.0.0+auto.0"
22 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
27 import circuitpython_typing
28 from paralleldisplay import ParallelBus
29 from ._fourwire import FourWire
30 from ._group import Group
31 from ._i2cdisplay import I2CDisplay
32 from ._structs import ColorspaceStruct, TransformStruct
33 from ._area import Area
34 from ._displaybus import _DisplayBus
35 from ._helpers import bswap16
36 from ._constants import (
37 CHIP_SELECT_UNTOUCHED,
38 CHIP_SELECT_TOGGLE_EVERY_BYTE,
46 # pylint: disable=too-many-arguments, too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements
60 pixels_in_byte_share_row: bool,
62 reverse_pixels_in_byte: bool,
63 reverse_bytes_in_word: bool,
66 set_current_column_command: int,
67 set_current_row_command: int,
68 data_as_commands: bool,
69 always_toggle_chip_select: bool,
70 sh1107_addressing: bool,
71 address_little_endian: bool,
73 self.colorspace = ColorspaceStruct(
76 grayscale_bit=8 - color_depth,
77 pixels_in_byte_share_row=pixels_in_byte_share_row,
78 bytes_per_cell=bytes_per_cell,
79 reverse_pixels_in_byte=reverse_pixels_in_byte,
80 reverse_bytes_in_word=reverse_bytes_in_word,
83 self.current_group = None
84 self.colstart = colstart
85 self.rowstart = rowstart
88 self.column_command = column_command
89 self.row_command = row_command
90 self.set_current_column_command = set_current_column_command
91 self.set_current_row_command = set_current_row_command
92 self.data_as_commands = data_as_commands
93 self.always_toggle_chip_select = always_toggle_chip_select
94 self.sh1107_addressing = sh1107_addressing
95 self.address_little_endian = address_little_endian
97 self.refresh_in_progress = False
98 self.full_refresh = False
102 if isinstance(bus, (FourWire, I2CDisplay, ParallelBus)):
103 self._bus_reset = bus.reset
104 self._begin_transaction = bus._begin_transaction
105 self._send = bus._send
106 self._end_transaction = bus._end_transaction
108 raise ValueError("Unsupported display bus type")
111 self.area = Area(0, 0, width, height)
115 self.ram_width = ram_width
116 self.ram_height = ram_height
117 self.rotation = rotation
118 self.transform = TransformStruct()
120 def set_rotation(self, rotation: int) -> None:
122 Sets the rotation of the display as an int in degrees.
124 # pylint: disable=protected-access, too-many-branches
125 transposed = self.rotation in (90, 270)
126 will_be_transposed = rotation in (90, 270)
127 if transposed != will_be_transposed:
128 self.width, self.height = self.height, self.width
134 self.rotation = rotation
137 self.transform.scale = 1
138 self.transform.mirror_x = False
139 self.transform.mirror_y = False
140 self.transform.transpose_xy = False
142 if rotation in (0, 180):
144 self.transform.mirror_x = True
145 self.transform.mirror_y = True
147 self.transform.transpose_xy = True
149 self.transform.mirror_y = True
151 self.transform.mirror_x = True
155 self.area.next = None
157 self.transform.dx = 1
158 self.transform.dy = 1
159 if self.transform.transpose_xy:
160 self.area.x2 = height
162 if self.transform.mirror_x:
163 self.transform.x = height
164 self.transform.dx = -1
165 if self.transform.mirror_y:
166 self.transform.y = width
167 self.transform.dy = -1
170 self.area.y2 = height
171 if self.transform.mirror_x:
172 self.transform.x = width
173 self.transform.dx = -1
174 if self.transform.mirror_y:
175 self.transform.y = height
176 self.transform.dy = -1
178 if self.current_group is not None:
179 self.current_group._update_transform(self.transform)
181 def show(self, root_group: Group) -> bool:
182 # pylint: disable=protected-access
185 Switches to displaying the given group of layers. When group is `None`, the
186 default CircuitPython terminal will be shown.
188 :param Optional[displayio.Group] root_group: The group to show.
192 # TODO: Implement Supervisor
193 if root_group is None:
194 circuitpython_splash = _Supervisor().circuitpython_splash
195 if not circuitpython_splash._in_group:
196 root_group = circuitpython_splash
197 elif self.current_group == circuitpython_splash:
201 if root_group == self.current_group:
204 if root_group is not None and root_group._in_group:
207 if self.current_group is not None:
208 self.current_group._in_group = False
210 if root_group is not None:
211 root_group._update_transform(self.transform)
212 root_group._in_group = True
214 self.current_group = root_group
215 self.full_refresh = True
219 def start_refresh(self) -> bool:
220 # pylint: disable=protected-access
221 """Mark the display core as currently being refreshed"""
223 if self.refresh_in_progress:
226 self.refresh_in_progress = True
227 self.last_refresh = time.monotonic() * 1000
230 def finish_refresh(self) -> None:
231 # pylint: disable=protected-access
232 """Unmark the display core as currently being refreshed"""
234 if self.current_group is not None:
235 self.current_group._finish_refresh()
237 self.full_refresh = False
238 self.refresh_in_progress = False
239 self.last_refresh = time.monotonic() * 1000
241 def release_display_core(self) -> None:
242 """Release the display from the current group"""
243 # pylint: disable=protected-access
245 if self.current_group is not None:
246 self.current_group._in_group = False
251 mask: circuitpython_typing.WriteableBuffer,
252 buffer: circuitpython_typing.WriteableBuffer,
254 """Call the current group's fill area function"""
255 if self.current_group is not None:
256 return self.current_group._fill_area( # pylint: disable=protected-access
257 self.colorspace, area, mask, buffer
262 def _clip(self, rectangle):
263 if self._core.rotation in (90, 270):
264 width, height = self._core.height, self._core.width
266 width, height = self._core.width, self._core.height
268 rectangle.x1 = max(rectangle.x1, 0)
269 rectangle.y1 = max(rectangle.y1, 0)
270 rectangle.x2 = min(rectangle.x2, width)
271 rectangle.y2 = min(rectangle.y2, height)
276 def clip_area(self, area: Area, clipped: Area) -> bool:
277 """Shrink the area to the region shared by the two areas"""
279 overlaps = self.area.compute_overlap(area, clipped)
283 # Expand the area if we have multiple pixels per byte and we need to byte align the bounds
284 if self.colorspace.depth < 8:
286 8 // self.colorspace.depth * self.colorspace.bytes_per_cell
288 if self.colorspace.pixels_in_byte_share_row:
289 if clipped.x1 % pixels_per_byte != 0:
290 clipped.x1 -= clipped.x1 % pixels_per_byte
291 if clipped.x2 % pixels_per_byte != 0:
292 clipped.x2 += pixels_per_byte - clipped.x2 % pixels_per_byte
294 if clipped.y1 % pixels_per_byte != 0:
295 clipped.y1 -= clipped.y1 % pixels_per_byte
296 if clipped.y2 % pixels_per_byte != 0:
297 clipped.y2 += pixels_per_byte - clipped.y2 % pixels_per_byte
301 def set_region_to_update(self, area: Area) -> None:
302 """Set the region to update"""
303 region_x1 = area.x1 + self.colstart
304 region_x2 = area.x2 + self.colstart
305 region_y1 = area.y1 + self.rowstart
306 region_y2 = area.y2 + self.rowstart
308 if self.colorspace.depth < 8:
309 pixels_per_byte = 8 // self.colorspace.depth
310 if self.colorspace.pixels_in_byte_share_row:
311 region_x1 /= pixels_per_byte * self.colorspace.bytes_per_cell
312 region_x2 /= pixels_per_byte * self.colorspace.bytes_per_cell
314 region_y1 /= pixels_per_byte * self.colorspace.bytes_per_cell
315 region_y2 /= pixels_per_byte * self.colorspace.bytes_per_cell
320 chip_select = CHIP_SELECT_UNTOUCHED
321 if self.always_toggle_chip_select or self.data_as_commands:
322 chip_select = CHIP_SELECT_TOGGLE_EVERY_BYTE
325 self.begin_transaction()
326 data_type = DISPLAY_DATA
327 if not self.data_as_commands:
329 DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, bytes([self.column_command])
332 data_type = DISPLAY_COMMAND
335 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
337 self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
340 if self.ram_width < 0x100: # Single Byte Bounds
341 data = struct.pack(">BB", region_x1, region_x2)
343 if self.address_little_endian:
344 region_x1 = bswap16(region_x1)
345 region_x2 = bswap16(region_x2)
346 data = struct.pack(">HH", region_x1, region_x2)
348 # Quirk for SH1107 "SH1107_addressing"
349 # Column lower command = 0x00, Column upper command = 0x10
350 if self.sh1107_addressing:
353 ((region_x1 >> 4) & 0xF0) | 0x10, # 0x10 to 0x17
354 region_x1 & 0x0F, # 0x00 to 0x0F
357 self.send(data_type, chip_select, data)
358 self.end_transaction()
360 if self.set_current_column_command != NO_COMMAND:
361 self.begin_transaction()
363 DISPLAY_COMMAND, chip_select, bytes([self.set_current_column_command])
365 # Only send the first half of data because it is the first coordinate.
366 self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
367 self.end_transaction()
370 self.begin_transaction()
372 if not self.data_as_commands:
373 self.send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, bytes([self.row_command]))
375 if self.ram_width < 0x100: # Single Byte Bounds
376 data = struct.pack(">BB", region_y1, region_y2)
378 if self.address_little_endian:
379 region_y1 = bswap16(region_y1)
380 region_y2 = bswap16(region_y2)
381 data = struct.pack(">HH", region_y1, region_y2)
383 # Quirk for SH1107 "SH1107_addressing"
384 # Page address command = 0xB0
385 if self.sh1107_addressing:
386 data = struct.pack(">B", 0xB0 | region_y1)
388 self.send(data_type, chip_select, data)
389 self.end_transaction()
391 if self.set_current_row_command != NO_COMMAND:
392 self.begin_transaction()
394 DISPLAY_COMMAND, chip_select, bytes([self.set_current_row_command])
396 # Only send the first half of data because it is the first coordinate.
397 self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
398 self.end_transaction()
401 img = self._buffer.convert("RGB").crop(astuple(area))
402 img = img.rotate(360 - self._core.rotation, expand=True)
404 display_area = self._apply_rotation(area)
406 img = img.crop(astuple(display_area))
408 data = numpy.array(img).astype("uint16")
410 ((data[:, :, 0] & 0xF8) << 8)
411 | ((data[:, :, 1] & 0xFC) << 3)
412 | (data[:, :, 2] >> 3)
416 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
424 data: circuitpython_typing.ReadableBuffer,
427 Send the data to the current bus
429 self._send(data_type, chip_select, data)
431 def begin_transaction(self) -> None:
433 Begin Bus Transaction
435 self._begin_transaction()
437 def end_transaction(self) -> None:
441 self._end_transaction()
443 def get_width(self) -> int:
445 Gets the width of the display in pixels.
449 def get_height(self) -> int:
451 Gets the height of the display in pixels.
455 def get_rotation(self) -> int:
457 Gets the rotation of the display as an int in degrees.
461 def get_bus(self) -> _DisplayBus:
463 The bus being used by the display. [readonly]