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
 
  21 from typing import Optional
 
  23 import microcontroller
 
  24 from circuitpython_typing import WriteableBuffer, ReadableBuffer
 
  25 from displayio._displaycore import _DisplayCore
 
  26 from displayio._colorconverter import ColorConverter
 
  27 from displayio._group import Group, circuitpython_splash
 
  28 from displayio._area import Area
 
  29 from displayio._constants import (
 
  30     CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
  31     CHIP_SELECT_UNTOUCHED,
 
  39 from ._displaybus import _DisplayBus
 
  41 __version__ = "0.0.0+auto.0"
 
  42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  46     # pylint: disable=too-many-instance-attributes, too-many-statements
 
  47     """This initializes a display and connects it into CircuitPython. Unlike other objects
 
  48     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
 
  49     This is done so that CircuitPython can use the display itself.
 
  51     Most people should not use this class directly. Use a specific display driver instead
 
  52     that will contain the initialization sequence at minimum.
 
  57         display_bus: _DisplayBus,
 
  58         init_sequence: ReadableBuffer,
 
  65         color_depth: int = 16,
 
  66         grayscale: bool = False,
 
  67         pixels_in_byte_share_row: bool = True,
 
  68         bytes_per_cell: int = 1,
 
  69         reverse_pixels_in_byte: bool = False,
 
  70         reverse_bytes_in_word: bool = True,
 
  71         set_column_command: int = 0x2A,
 
  72         set_row_command: int = 0x2B,
 
  73         write_ram_command: int = 0x2C,
 
  74         backlight_pin: Optional[microcontroller.Pin] = None,
 
  75         brightness_command: Optional[int] = None,
 
  76         brightness: float = 1.0,
 
  77         single_byte_bounds: bool = False,
 
  78         data_as_commands: bool = False,
 
  79         auto_refresh: bool = True,
 
  80         native_frames_per_second: int = 60,
 
  81         backlight_on_high: bool = True,
 
  82         SH1107_addressing: bool = False,
 
  84         # pylint: disable=too-many-locals,invalid-name, too-many-branches
 
  85         """Create a Display object on the given display bus (`fourwire.FourWire` or
 
  86         `paralleldisplaybus.ParallelBus`).
 
  88         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
 
  89         with a command byte followed by a byte to determine the parameter count and if a
 
  90         delay is need after. When the top bit of the second byte is 1, the next byte will be
 
  91         the delay time in milliseconds. The remaining 7 bits are the parameter count
 
  92         excluding any delay byte. The third through final bytes are the remaining command
 
  93         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\
 
 100 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
 
 101                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
 
 102                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
 
 104             display = busdisplay.BusDisplay(display_bus, init_sequence, width=320, height=240)
 
 106         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
 
 107         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
 
 108         Multiple byte literals (b”“) are merged together on load. The parens are needed to
 
 109         allow byte literals on subsequent lines.
 
 111         The initialization sequence should always leave the display memory access inline with
 
 112         the scan of the display to minimize tearing artifacts.
 
 115         if rotation % 90 != 0:
 
 116             raise ValueError("Display rotation must be in 90 degree increments")
 
 118         if SH1107_addressing and color_depth != 1:
 
 119             raise ValueError("color_depth must be 1 when SH1107_addressing is True")
 
 121         # Turn off auto-refresh as we init
 
 122         self._auto_refresh = False
 
 125         if single_byte_bounds:
 
 129         self._core = _DisplayCore(
 
 134             ram_height=ram_height,
 
 138             color_depth=color_depth,
 
 140             pixels_in_byte_share_row=pixels_in_byte_share_row,
 
 141             bytes_per_cell=bytes_per_cell,
 
 142             reverse_pixels_in_byte=reverse_pixels_in_byte,
 
 143             reverse_bytes_in_word=reverse_bytes_in_word,
 
 144             column_command=set_column_command,
 
 145             row_command=set_row_command,
 
 146             set_current_column_command=NO_COMMAND,
 
 147             set_current_row_command=NO_COMMAND,
 
 148             data_as_commands=data_as_commands,
 
 149             always_toggle_chip_select=False,
 
 150             sh1107_addressing=(SH1107_addressing and color_depth == 1),
 
 151             address_little_endian=False,
 
 154         self._write_ram_command = write_ram_command
 
 155         self._brightness_command = brightness_command
 
 156         self._first_manual_refresh = not auto_refresh
 
 157         self._backlight_on_high = backlight_on_high
 
 159         self._native_frames_per_second = native_frames_per_second
 
 160         self._native_ms_per_frame = 1000 // native_frames_per_second
 
 162         self._brightness = brightness
 
 163         self._auto_refresh = auto_refresh
 
 166         while i < len(init_sequence):
 
 167             command = init_sequence[i]
 
 168             data_size = init_sequence[i + 1]
 
 169             delay = (data_size & DELAY) != 0
 
 171             while self._core.begin_transaction():
 
 174             if self._core.data_as_commands:
 
 175                 full_command = bytearray(data_size + 1)
 
 176                 full_command[0] = command
 
 177                 full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
 
 180                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
 185                     DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
 
 189                     CHIP_SELECT_UNTOUCHED,
 
 190                     init_sequence[i + 2 : i + 2 + data_size],
 
 192             self._core.end_transaction()
 
 196                 delay_time_ms = init_sequence[i + 1 + data_size]
 
 197                 if delay_time_ms == 255:
 
 199             time.sleep(delay_time_ms / 1000)
 
 202         self._current_group = None
 
 203         self._last_refresh_call = 0
 
 204         self._refresh_thread = None
 
 205         self._colorconverter = ColorConverter()
 
 207         self._backlight_type = None
 
 208         if backlight_pin is not None:
 
 210                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
 
 212                 # 100Hz looks decent and doesn't keep the CPU too busy
 
 213                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
 
 214                 self._backlight_type = BACKLIGHT_PWM
 
 215             except (ImportError, NotImplementedError):
 
 216                 # PWMOut not implemented on this platform
 
 218             if self._backlight_type is None:
 
 219                 self._backlight_type = BACKLIGHT_IN_OUT
 
 220                 self._backlight = digitalio.DigitalInOut(backlight_pin)
 
 221                 self._backlight.switch_to_output()
 
 222         self.brightness = brightness
 
 223         if not circuitpython_splash._in_group:
 
 224             self._set_root_group(circuitpython_splash)
 
 225         self.auto_refresh = auto_refresh
 
 227     def __new__(cls, *args, **kwargs):
 
 228         from displayio import (  # pylint: disable=import-outside-toplevel, cyclic-import
 
 232         display_instance = super().__new__(cls)
 
 233         allocate_display(display_instance)
 
 234         return display_instance
 
 236     def _send_pixels(self, pixels):
 
 237         if not self._core.data_as_commands:
 
 240                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
 241                 bytes([self._write_ram_command]),
 
 243         self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
 
 246     def show(_group: Group) -> None:  # pylint: disable=missing-function-docstring
 
 247         raise AttributeError(".show(x) removed. Use .root_group = x")
 
 249     def _set_root_group(self, root_group: Group) -> None:
 
 250         ok = self._core.set_root_group(root_group)
 
 252             raise ValueError("Group already used")
 
 257         target_frames_per_second: Optional[int] = None,
 
 258         minimum_frames_per_second: int = 0,
 
 260         """When auto refresh is off, waits for the target frame rate and then refreshes the
 
 261         display, returning True. If the call has taken too long since the last refresh call
 
 262         for the given target frame rate, then the refresh returns False immediately without
 
 263         updating the screen to hopefully help getting caught up.
 
 265         If the time since the last successful refresh is below the minimum frame rate, then
 
 266         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
 
 268         When auto refresh is on, updates the display immediately. (The display will also
 
 269         update without calls to this.)
 
 271         maximum_ms_per_real_frame = 0xFFFFFFFF
 
 272         if minimum_frames_per_second > 0:
 
 273             maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
 
 275         if target_frames_per_second is None:
 
 276             target_ms_per_frame = 0xFFFFFFFF
 
 278             target_ms_per_frame = 1000 // target_frames_per_second
 
 281             not self._auto_refresh
 
 282             and not self._first_manual_refresh
 
 283             and target_ms_per_frame != 0xFFFFFFFF
 
 285             current_time = time.monotonic() * 1000
 
 286             current_ms_since_real_refresh = current_time - self._core.last_refresh
 
 287             if current_ms_since_real_refresh > maximum_ms_per_real_frame:
 
 288                 raise RuntimeError("Below minimum frame rate")
 
 289             current_ms_since_last_call = current_time - self._last_refresh_call
 
 290             self._last_refresh_call = current_time
 
 291             if current_ms_since_last_call > target_ms_per_frame:
 
 294             remaining_time = target_ms_per_frame - (
 
 295                 current_ms_since_real_refresh % target_ms_per_frame
 
 297             time.sleep(remaining_time / 1000)
 
 298         self._first_manual_refresh = False
 
 299         self._refresh_display()
 
 302     def _refresh_display(self):
 
 303         if not self._core.start_refresh():
 
 306         areas_to_refresh = self._get_refresh_areas()
 
 307         for area in areas_to_refresh:
 
 308             self._refresh_area(area)
 
 310         self._core.finish_refresh()
 
 314     def _get_refresh_areas(self) -> list[Area]:
 
 315         """Get a list of areas to be refreshed"""
 
 317         if self._core.full_refresh:
 
 318             areas.append(self._core.area)
 
 319         elif self._core.current_group is not None:
 
 320             self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
 
 325     def _background(self):
 
 326         """Run background refresh tasks. Do not call directly"""
 
 329             and (time.monotonic() * 1000 - self._core.last_refresh)
 
 330             > self._native_ms_per_frame
 
 334     def _refresh_area(self, area) -> bool:
 
 335         """Loop through dirty areas and redraw that area."""
 
 336         # pylint: disable=too-many-locals, too-many-branches
 
 339         # Clip the area to the display by overlapping the areas.
 
 340         # If there is no overlap then we're done.
 
 341         if not self._core.clip_area(area, clipped):
 
 344         rows_per_buffer = clipped.height()
 
 345         pixels_per_word = 32 // self._core.colorspace.depth
 
 346         pixels_per_buffer = clipped.size()
 
 348         # We should have lots of memory
 
 349         buffer_size = clipped.size() // pixels_per_word
 
 352         # for SH1107 and other boundary constrained controllers
 
 353         #      write one single row at a time
 
 354         if self._core.sh1107_addressing:
 
 355             subrectangles = rows_per_buffer // 8
 
 357         elif clipped.size() > buffer_size * pixels_per_word:
 
 358             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
 
 359             if rows_per_buffer == 0:
 
 361             # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
 
 363                 self._core.colorspace.depth < 8
 
 364                 and self._core.colorspace.pixels_in_byte_share_row
 
 366                 pixels_per_byte = 8 // self._core.colorspace.depth
 
 367                 if rows_per_buffer % pixels_per_byte != 0:
 
 368                     rows_per_buffer -= rows_per_buffer % pixels_per_byte
 
 369             subrectangles = clipped.height() // rows_per_buffer
 
 370             if clipped.height() % rows_per_buffer != 0:
 
 372             pixels_per_buffer = rows_per_buffer * clipped.width()
 
 373             buffer_size = pixels_per_buffer // pixels_per_word
 
 374             if pixels_per_buffer % pixels_per_word:
 
 376         mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
 
 377         remaining_rows = clipped.height()
 
 379         for subrect_index in range(subrectangles):
 
 382                 y1=clipped.y1 + rows_per_buffer * subrect_index,
 
 384                 y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
 
 386             if remaining_rows < rows_per_buffer:
 
 387                 subrectangle.y2 = subrectangle.y1 + remaining_rows
 
 388             remaining_rows -= rows_per_buffer
 
 389             self._core.set_region_to_update(subrectangle)
 
 390             if self._core.colorspace.depth >= 8:
 
 391                 subrectangle_size_bytes = subrectangle.size() * (
 
 392                     self._core.colorspace.depth // 8
 
 395                 subrectangle_size_bytes = subrectangle.size() // (
 
 396                     8 // self._core.colorspace.depth
 
 399             buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
 
 400             mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
 
 401             self._core.fill_area(subrectangle, mask, buffer)
 
 403             # Can't acquire display bus; skip the rest of the data.
 
 404             if not self._core.bus_free():
 
 407             self._core.begin_transaction()
 
 408             self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
 
 409             self._core.end_transaction()
 
 412     def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
 
 413         """Extract the pixels from a single row"""
 
 414         if self._core.colorspace.depth != 16:
 
 415             raise ValueError("Display must have a 16 bit colorspace.")
 
 417         area = Area(0, y, self._core.width, y + 1)
 
 418         pixels_per_word = 32 // self._core.colorspace.depth
 
 419         buffer_size = self._core.width // pixels_per_word
 
 420         pixels_per_buffer = area.size()
 
 421         if pixels_per_buffer % pixels_per_word:
 
 424         buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
 
 425         mask_length = (pixels_per_buffer // 32) + 1
 
 426         mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
 
 427         self._core.fill_area(area, mask, buffer)
 
 430     def _release(self) -> None:
 
 431         """Release the display and free its resources"""
 
 432         self.auto_refresh = False
 
 433         self._core.release_display_core()
 
 435     def _reset(self) -> None:
 
 436         """Reset the display"""
 
 437         self.auto_refresh = True
 
 438         circuitpython_splash.x = 0
 
 439         circuitpython_splash.y = 0
 
 440         if not circuitpython_splash._in_group:  # pylint: disable=protected-access
 
 441             self._set_root_group(circuitpython_splash)
 
 444     def auto_refresh(self) -> bool:
 
 445         """True when the display is refreshed automatically."""
 
 446         return self._auto_refresh
 
 449     def auto_refresh(self, value: bool):
 
 450         self._first_manual_refresh = not value
 
 451         self._auto_refresh = value
 
 454     def brightness(self) -> float:
 
 455         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
 
 456         return self._brightness
 
 459     def brightness(self, value: float):
 
 460         if 0 <= float(value) <= 1.0:
 
 461             if not self._backlight_on_high:
 
 464             if self._backlight_type == BACKLIGHT_PWM:
 
 465                 self._backlight.duty_cycle = value * 0xFFFF
 
 466             elif self._backlight_type == BACKLIGHT_IN_OUT:
 
 467                 self._backlight.value = value > 0.99
 
 468             elif self._brightness_command is not None:
 
 469                 okay = self._core.begin_transaction()
 
 471                     if self._core.data_as_commands:
 
 474                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
 475                             bytes([self._brightness_command, round(0xFF * value)]),
 
 480                             CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
 481                             bytes([self._brightness_command]),
 
 484                             DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
 
 486                     self._core.end_transaction()
 
 487             self._brightness = value
 
 489             raise ValueError("Brightness must be between 0.0 and 1.0")
 
 492     def width(self) -> int:
 
 494         return self._core.get_width()
 
 497     def height(self) -> int:
 
 499         return self._core.get_height()
 
 502     def rotation(self) -> int:
 
 503         """The rotation of the display as an int in degrees."""
 
 504         return self._core.get_rotation()
 
 507     def rotation(self, value: int):
 
 509             raise ValueError("Display rotation must be in 90 degree increments")
 
 510         transposed = self._core.rotation in (90, 270)
 
 511         will_transposed = value in (90, 270)
 
 512         if transposed != will_transposed:
 
 513             self._core.width, self._core.height = self._core.height, self._core.width
 
 514         self._core.set_rotation(value)
 
 515         if self._core.current_group is not None:
 
 516             self._core.current_group._update_transform(  # pylint: disable=protected-access
 
 521     def bus(self) -> _DisplayBus:
 
 522         """Current Display Bus"""
 
 523         return self._core.get_bus()
 
 526     def root_group(self) -> Group:
 
 528         The root group on the display.
 
 529         If the root group is set to `displayio.CIRCUITPYTHON_TERMINAL`, the default
 
 530         CircuitPython terminal will be shown.
 
 531         If the root group is set to ``None``, no output will be shown.
 
 533         return self._core.current_group
 
 536     def root_group(self, new_group: Group) -> None:
 
 537         self._set_root_group(new_group)