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
 
  23 from typing import Optional
 
  27 import microcontroller
 
  28 from recordclass import recordclass
 
  30 from ._displaybus import _DisplayBus
 
  31 from ._colorconverter import ColorConverter
 
  32 from ._group import Group
 
  33 from ._constants import (
 
  34     CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
  35     CHIP_SELECT_UNTOUCHED,
 
  40 __version__ = "0.0.0-auto.0"
 
  41 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  43 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  51     # pylint: disable=too-many-instance-attributes
 
  52     """This initializes a display and connects it into CircuitPython. Unlike other objects
 
  53     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
 
  54     This is done so that CircuitPython can use the display itself.
 
  56     Most people should not use this class directly. Use a specific display driver instead
 
  57     that will contain the initialization sequence at minimum.
 
  62         display_bus: _DisplayBus,
 
  63         init_sequence: _typing.ReadableBuffer,
 
  70         color_depth: int = 16,
 
  71         grayscale: bool = False,
 
  72         pixels_in_byte_share_row: bool = True,
 
  73         bytes_per_cell: int = 1,
 
  74         reverse_pixels_in_byte: bool = False,
 
  75         set_column_command: int = 0x2A,
 
  76         set_row_command: int = 0x2B,
 
  77         write_ram_command: int = 0x2C,
 
  78         backlight_pin: Optional[microcontroller.Pin] = None,
 
  79         brightness_command: Optional[int] = None,
 
  80         brightness: float = 1.0,
 
  81         auto_brightness: bool = False,
 
  82         single_byte_bounds: bool = False,
 
  83         data_as_commands: bool = False,
 
  84         auto_refresh: bool = True,
 
  85         native_frames_per_second: int = 60,
 
  86         backlight_on_high: bool = True,
 
  87         SH1107_addressing: bool = False,
 
  88         set_vertical_scroll: int = 0,
 
  90         # pylint: disable=unused-argument,too-many-locals,invalid-name
 
  91         """Create a Display object on the given display bus (`displayio.FourWire` or
 
  92         `paralleldisplay.ParallelBus`).
 
  94         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
 
  95         with a command byte followed by a byte to determine the parameter count and if a
 
  96         delay is need after. When the top bit of the second byte is 1, the next byte will be
 
  97         the delay time in milliseconds. The remaining 7 bits are the parameter count
 
  98         excluding any delay byte. The third through final bytes are the remaining command
 
  99         parameters. The next byte will begin a new command definition. Here is a portion of
 
 102         .. code-block:: python
 
 105                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
 
 106 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
 
 107                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
 
 108                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
 
 110             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
 
 112         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
 
 113         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
 
 114         Multiple byte literals (b”“) are merged together on load. The parens are needed to
 
 115         allow byte literals on subsequent lines.
 
 117         The initialization sequence should always leave the display memory access inline with
 
 118         the scan of the display to minimize tearing artifacts.
 
 120         self._bus = display_bus
 
 121         self._set_column_command = set_column_command
 
 122         self._set_row_command = set_row_command
 
 123         self._write_ram_command = write_ram_command
 
 124         self._brightness_command = brightness_command
 
 125         self._data_as_commands = data_as_commands
 
 126         self._single_byte_bounds = single_byte_bounds
 
 128         self._height = height
 
 129         self._colstart = colstart
 
 130         self._rowstart = rowstart
 
 131         self._rotation = rotation
 
 132         self._auto_brightness = auto_brightness
 
 133         self._brightness = 1.0
 
 134         self._auto_refresh = auto_refresh
 
 135         self._initialize(init_sequence)
 
 136         self._buffer = Image.new("RGB", (width, height))
 
 137         self._subrectangles = []
 
 138         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
 
 139         self._current_group = None
 
 140         displays.append(self)
 
 141         self._refresh_thread = None
 
 142         if self._auto_refresh:
 
 143             self.auto_refresh = True
 
 144         self._colorconverter = ColorConverter()
 
 146         self._backlight_type = None
 
 147         if backlight_pin is not None:
 
 149                 from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
 
 151                 # 100Hz looks decent and doesn't keep the CPU too busy
 
 152                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
 
 153                 self._backlight_type = BACKLIGHT_PWM
 
 155                 # PWMOut not implemented on this platform
 
 157             if self._backlight_type is None:
 
 158                 self._backlight_type = BACKLIGHT_IN_OUT
 
 159                 self._backlight = digitalio.DigitalInOut(backlight_pin)
 
 160                 self._backlight.switch_to_output()
 
 161             self.brightness = brightness
 
 163     def _initialize(self, init_sequence):
 
 165         while i < len(init_sequence):
 
 166             command = init_sequence[i]
 
 167             data_size = init_sequence[i + 1]
 
 168             delay = (data_size & 0x80) > 0
 
 171             self._send(command, init_sequence[i + 2 : i + 2 + data_size])
 
 175                 delay_time_ms = init_sequence[i + 1 + data_size]
 
 176                 if delay_time_ms == 255:
 
 178             time.sleep(delay_time_ms / 1000)
 
 181     def _send(self, command, data):
 
 182         # pylint: disable=protected-access
 
 183         self._bus._begin_transaction()
 
 184         if self._data_as_commands:
 
 186                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
 
 190                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
 
 192             self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
 
 193         self._bus._end_transaction()
 
 195     def _send_pixels(self, data):
 
 196         # pylint: disable=protected-access
 
 197         if not self._data_as_commands:
 
 200                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
 
 201                 bytes([self._write_ram_command]),
 
 203         self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
 
 206         self._bus._release()  # pylint: disable=protected-access
 
 209     def show(self, group: Group) -> None:
 
 210         """Switches to displaying the given group of layers. When group is None, the
 
 211         default CircuitPython terminal will be shown.
 
 213         self._current_group = group
 
 218         target_frames_per_second: Optional[int] = None,
 
 219         minimum_frames_per_second: int = 0,
 
 221         # pylint: disable=unused-argument
 
 222         """When auto refresh is off, waits for the target frame rate and then refreshes the
 
 223         display, returning True. If the call has taken too long since the last refresh call
 
 224         for the given target frame rate, then the refresh returns False immediately without
 
 225         updating the screen to hopefully help getting caught up.
 
 227         If the time since the last successful refresh is below the minimum frame rate, then
 
 228         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
 
 230         When auto refresh is on, updates the display immediately. (The display will also
 
 231         update without calls to this.)
 
 233         self._subrectangles = []
 
 235         # Go through groups and and add each to buffer
 
 236         if self._current_group is not None:
 
 237             buffer = Image.new("RGBA", (self._width, self._height))
 
 238             # Recursively have everything draw to the image
 
 239             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
 
 240             # save image to buffer (or probably refresh buffer so we can compare)
 
 241             self._buffer.paste(buffer)
 
 243         if self._current_group is not None:
 
 244             # Eventually calculate dirty rectangles here
 
 245             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
 
 247         for area in self._subrectangles:
 
 248             self._refresh_display_area(area)
 
 252     def _refresh_loop(self):
 
 253         while self._auto_refresh:
 
 256     def _refresh_display_area(self, rectangle):
 
 257         """Loop through dirty rectangles and redraw that area."""
 
 259         img = self._buffer.convert("RGB").crop(rectangle)
 
 260         img = img.rotate(self._rotation, expand=True)
 
 262         display_rectangle = self._apply_rotation(rectangle)
 
 263         img = img.crop(self._clip(display_rectangle))
 
 265         data = numpy.array(img).astype("uint16")
 
 267             ((data[:, :, 0] & 0xF8) << 8)
 
 268             | ((data[:, :, 1] & 0xFC) << 3)
 
 269             | (data[:, :, 2] >> 3)
 
 273             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
 
 277             self._set_column_command,
 
 279                 display_rectangle.x1 + self._colstart,
 
 280                 display_rectangle.x2 + self._colstart - 1,
 
 284             self._set_row_command,
 
 286                 display_rectangle.y1 + self._rowstart,
 
 287                 display_rectangle.y2 + self._rowstart - 1,
 
 291         self._bus._begin_transaction()  # pylint: disable=protected-access
 
 292         self._send_pixels(pixels)
 
 293         self._bus._end_transaction()  # pylint: disable=protected-access
 
 295     def _clip(self, rectangle):
 
 296         if self._rotation in (90, 270):
 
 297             width, height = self._height, self._width
 
 299             width, height = self._width, self._height
 
 301         rectangle.x1 = max(rectangle.x1, 0)
 
 302         rectangle.y1 = max(rectangle.y1, 0)
 
 303         rectangle.x2 = min(rectangle.x2, width)
 
 304         rectangle.y2 = min(rectangle.y2, height)
 
 308     def _apply_rotation(self, rectangle):
 
 309         """Adjust the rectangle coordinates based on rotation"""
 
 310         if self._rotation == 90:
 
 312                 self._height - rectangle.y2,
 
 314                 self._height - rectangle.y1,
 
 317         if self._rotation == 180:
 
 319                 self._width - rectangle.x2,
 
 320                 self._height - rectangle.y2,
 
 321                 self._width - rectangle.x1,
 
 322                 self._height - rectangle.y1,
 
 324         if self._rotation == 270:
 
 327                 self._width - rectangle.x2,
 
 329                 self._width - rectangle.x1,
 
 333     def _encode_pos(self, x, y):
 
 334         """Encode a postion into bytes."""
 
 335         return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
 
 338         self, y: int, buffer: _typing.WriteableBuffer
 
 339     ) -> _typing.WriteableBuffer:
 
 340         """Extract the pixels from a single row"""
 
 341         for x in range(0, self._width):
 
 342             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
 
 343             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
 
 344             buffer[x * 2 + 1] = _rgb_565 & 0xFF
 
 348     def auto_refresh(self) -> bool:
 
 349         """True when the display is refreshed automatically."""
 
 350         return self._auto_refresh
 
 353     def auto_refresh(self, value: bool):
 
 354         self._auto_refresh = value
 
 355         if self._refresh_thread is None:
 
 356             self._refresh_thread = threading.Thread(
 
 357                 target=self._refresh_loop, daemon=True
 
 359         if value and not self._refresh_thread.is_alive():
 
 361             self._refresh_thread.start()
 
 362         elif not value and self._refresh_thread.is_alive():
 
 364             self._refresh_thread.join()
 
 367     def brightness(self) -> float:
 
 368         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
 
 369         When `auto_brightness` is True, the value of `brightness` will change automatically.
 
 370         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
 
 372         return self._brightness
 
 375     def brightness(self, value: float):
 
 376         if 0 <= float(value) <= 1.0:
 
 377             self._brightness = value
 
 378             if self._backlight_type == BACKLIGHT_IN_OUT:
 
 379                 self._backlight.value = round(self._brightness)
 
 380             elif self._backlight_type == BACKLIGHT_PWM:
 
 381                 self._backlight.duty_cycle = self._brightness * 65535
 
 382             elif self._brightness_command is not None:
 
 383                 self._send(self._brightness_command, round(value * 255))
 
 385             raise ValueError("Brightness must be between 0.0 and 1.0")
 
 388     def auto_brightness(self) -> bool:
 
 389         """True when the display brightness is adjusted automatically, based on an ambient
 
 390         light sensor or other method. Note that some displays may have this set to True by
 
 391         default, but not actually implement automatic brightness adjustment.
 
 392         `auto_brightness` is set to False if `brightness` is set manually.
 
 394         return self._auto_brightness
 
 396     @auto_brightness.setter
 
 397     def auto_brightness(self, value: bool):
 
 398         self._auto_brightness = value
 
 401     def width(self) -> int:
 
 406     def height(self) -> int:
 
 411     def rotation(self) -> int:
 
 412         """The rotation of the display as an int in degrees."""
 
 413         return self._rotation
 
 416     def rotation(self, value: int):
 
 417         if value not in (0, 90, 180, 270):
 
 418             raise ValueError("Rotation must be 0/90/180/270")
 
 419         self._rotation = value
 
 422     def bus(self) -> _DisplayBus:
 
 423         """Current Display Bus"""