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"""