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.
 
  61         pixels_in_byte_share_row=True,
 
  63         reverse_pixels_in_byte=False,
 
  64         set_column_command=0x2A,
 
  66         write_ram_command=0x2C,
 
  67         set_vertical_scroll=0,
 
  69         brightness_command=None,
 
  71         auto_brightness=False,
 
  72         single_byte_bounds=False,
 
  73         data_as_commands=False,
 
  75         native_frames_per_second=60
 
  77         # pylint: disable=unused-argument,too-many-locals
 
  78         """Create a Display object on the given display bus (`displayio.FourWire` or
 
  79         `displayio.ParallelBus`).
 
  81         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
 
  82         with a command byte followed by a byte to determine the parameter count and if a
 
  83         delay is need after. When the top bit of the second byte is 1, the next byte will be
 
  84         the delay time in milliseconds. The remaining 7 bits are the parameter count
 
  85         excluding any delay byte. The third through final bytes are the remaining command
 
  86         parameters. The next byte will begin a new command definition. Here is a portion of
 
  89         .. code-block:: python
 
  92                 b"\\xE1\\x0F\\x00\\x0E\\x14\\x03\\x11\\x07\\x31\
 
  93 \\xC1\\x48\\x08\\x0F\\x0C\\x31\\x36\\x0F"
 
  94                 b"\\x11\\x80\\x78"  # Exit Sleep then delay 0x78 (120ms)
 
  95                 b"\\x29\\x80\\x78"  # Display on then delay 0x78 (120ms)
 
  97             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
 
  99         The first command is 0xE1 with 15 (0x0F) parameters following. The second and third
 
 100         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
 
 101         Multiple byte literals (b”“) are merged together on load. The parens are needed to
 
 102         allow byte literals on subsequent lines.
 
 104         The initialization sequence should always leave the display memory access inline with
 
 105         the scan of the display to minimize tearing artifacts.
 
 107         self._bus = display_bus
 
 108         self._set_column_command = set_column_command
 
 109         self._set_row_command = set_row_command
 
 110         self._write_ram_command = write_ram_command
 
 111         self._brightness_command = brightness_command
 
 112         self._data_as_commands = data_as_commands
 
 113         self._single_byte_bounds = single_byte_bounds
 
 115         self._height = height
 
 116         self._colstart = colstart
 
 117         self._rowstart = rowstart
 
 118         self._rotation = rotation
 
 119         self._auto_brightness = auto_brightness
 
 120         self._brightness = 1.0
 
 121         self._auto_refresh = auto_refresh
 
 122         self._initialize(init_sequence)
 
 123         self._buffer = Image.new("RGB", (width, height))
 
 124         self._subrectangles = []
 
 125         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
 
 126         self._current_group = None
 
 127         displays.append(self)
 
 128         self._refresh_thread = None
 
 129         if self._auto_refresh:
 
 130             self.auto_refresh = True
 
 131         self._colorconverter = ColorConverter()
 
 133         self._backlight_type = None
 
 134         if backlight_pin is not None:
 
 136                 from pulseio import PWMOut  # pylint: disable=import-outside-toplevel
 
 138                 # 100Hz looks decent and doesn't keep the CPU too busy
 
 139                 self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
 
 140                 self._backlight_type = BACKLIGHT_PWM
 
 142                 # PWMOut not implemented on this platform
 
 144             if self._backlight_type is None:
 
 145                 self._backlight_type = BACKLIGHT_IN_OUT
 
 146                 self._backlight = digitalio.DigitalInOut(backlight_pin)
 
 147                 self._backlight.switch_to_output()
 
 148             self.brightness = brightness
 
 150     def _initialize(self, init_sequence):
 
 152         while i < len(init_sequence):
 
 153             command = init_sequence[i]
 
 154             data_size = init_sequence[i + 1]
 
 155             delay = (data_size & 0x80) > 0
 
 158             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
 
 162                 delay_time_ms = init_sequence[i + 1 + data_size]
 
 163                 if delay_time_ms == 255:
 
 165             time.sleep(delay_time_ms / 1000)
 
 168     def _write(self, command, data):
 
 169         self._bus.begin_transaction()
 
 170         if self._data_as_commands:
 
 171             if command is not None:
 
 172                 self._bus.send(True, bytes([command]), toggle_every_byte=True)
 
 173             self._bus.send(command is not None, data)
 
 175             self._bus.send(True, bytes([command]), toggle_every_byte=True)
 
 176             self._bus.send(False, data)
 
 177         self._bus.end_transaction()
 
 180         self._bus._release()  # pylint: disable=protected-access
 
 183     def show(self, group):
 
 184         """Switches to displaying the given group of layers. When group is None, the
 
 185         default CircuitPython terminal will be shown.
 
 187         self._current_group = group
 
 189     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
 
 190         # pylint: disable=unused-argument
 
 191         """When auto refresh is off, waits for the target frame rate and then refreshes the
 
 192         display, returning True. If the call has taken too long since the last refresh call
 
 193         for the given target frame rate, then the refresh returns False immediately without
 
 194         updating the screen to hopefully help getting caught up.
 
 196         If the time since the last successful refresh is below the minimum frame rate, then
 
 197         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
 
 199         When auto refresh is on, updates the display immediately. (The display will also
 
 200         update without calls to this.)
 
 202         self._subrectangles = []
 
 204         # Go through groups and and add each to buffer
 
 205         if self._current_group is not None:
 
 206             buffer = Image.new("RGBA", (self._width, self._height))
 
 207             # Recursively have everything draw to the image
 
 208             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
 
 209             # save image to buffer (or probably refresh buffer so we can compare)
 
 210             self._buffer.paste(buffer)
 
 212         if self._current_group is not None:
 
 213             # Eventually calculate dirty rectangles here
 
 214             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
 
 216         for area in self._subrectangles:
 
 217             self._refresh_display_area(area)
 
 219     def _refresh_loop(self):
 
 220         while self._auto_refresh:
 
 223     def _refresh_display_area(self, rectangle):
 
 224         """Loop through dirty rectangles and redraw that area."""
 
 226         img = self._buffer.convert("RGB").crop(rectangle)
 
 227         img = img.rotate(self._rotation, expand=True)
 
 229         display_rectangle = self._apply_rotation(rectangle)
 
 230         img = img.crop(self._clip(display_rectangle))
 
 232         data = numpy.array(img).astype("uint16")
 
 234             ((data[:, :, 0] & 0xF8) << 8)
 
 235             | ((data[:, :, 1] & 0xFC) << 3)
 
 236             | (data[:, :, 2] >> 3)
 
 240             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
 
 244             self._set_column_command,
 
 246                 display_rectangle.x1 + self._colstart,
 
 247                 display_rectangle.x2 + self._colstart - 1,
 
 251             self._set_row_command,
 
 253                 display_rectangle.y1 + self._rowstart,
 
 254                 display_rectangle.y2 + self._rowstart - 1,
 
 258         if self._data_as_commands:
 
 259             self._write(None, pixels)
 
 261             self._write(self._write_ram_command, pixels)
 
 263     def _clip(self, rectangle):
 
 264         if self._rotation in (90, 270):
 
 265             width, height = self._height, self._width
 
 267             width, height = self._width, self._height
 
 273         if rectangle.x2 > width:
 
 275         if rectangle.y2 > height:
 
 276             rectangle.y2 = height
 
 280     def _apply_rotation(self, rectangle):
 
 281         """Adjust the rectangle coordinates based on rotation"""
 
 282         if self._rotation == 90:
 
 284                 self._height - rectangle.y2,
 
 286                 self._height - rectangle.y1,
 
 289         if self._rotation == 180:
 
 291                 self._width - rectangle.x2,
 
 292                 self._height - rectangle.y2,
 
 293                 self._width - rectangle.x1,
 
 294                 self._height - rectangle.y1,
 
 296         if self._rotation == 270:
 
 299                 self._width - rectangle.x2,
 
 301                 self._width - rectangle.x1,
 
 305     def _encode_pos(self, x, y):
 
 306         """Encode a postion into bytes."""
 
 307         return struct.pack(self._bounds_encoding, x, y)
 
 309     def fill_row(self, y, buffer):
 
 310         """Extract the pixels from a single row"""
 
 311         for x in range(0, self._width):
 
 312             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
 
 313             buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
 
 314             buffer[x * 2 + 1] = _rgb_565 & 0xFF
 
 318     def auto_refresh(self):
 
 319         """True when the display is refreshed automatically."""
 
 320         return self._auto_refresh
 
 323     def auto_refresh(self, value):
 
 324         self._auto_refresh = value
 
 325         if self._refresh_thread is None:
 
 326             self._refresh_thread = threading.Thread(
 
 327                 target=self._refresh_loop, daemon=True
 
 329         if value and not self._refresh_thread.is_alive():
 
 331             self._refresh_thread.start()
 
 332         elif not value and self._refresh_thread.is_alive():
 
 334             self._refresh_thread.join()
 
 337     def brightness(self):
 
 338         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
 
 339         When `auto_brightness` is True, the value of `brightness` will change automatically.
 
 340         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
 
 342         return self._brightness
 
 345     def brightness(self, value):
 
 346         if 0 <= float(value) <= 1.0:
 
 347             self._brightness = value
 
 348             if self._backlight_type == BACKLIGHT_IN_OUT:
 
 349                 self._backlight.value = round(self._brightness)
 
 350             elif self._backlight_type == BACKLIGHT_PWM:
 
 351                 self._backlight.duty_cycle = self._brightness * 65535
 
 352             elif self._brightness_command is not None:
 
 353                 self._write(self._brightness_command, round(value * 255))
 
 355             raise ValueError("Brightness must be between 0.0 and 1.0")
 
 358     def auto_brightness(self):
 
 359         """True when the display brightness is adjusted automatically, based on an ambient
 
 360         light sensor or other method. Note that some displays may have this set to True by
 
 361         default, but not actually implement automatic brightness adjustment.
 
 362         `auto_brightness` is set to False if `brightness` is set manually.
 
 364         return self._auto_brightness
 
 366     @auto_brightness.setter
 
 367     def auto_brightness(self, value):
 
 368         self._auto_brightness = value
 
 382         """The rotation of the display as an int in degrees."""
 
 383         return self._rotation
 
 386     def rotation(self, value):
 
 387         if value not in (0, 90, 180, 270):
 
 388             raise ValueError("Rotation must be 0/90/180/270")
 
 389         self._rotation = value
 
 393         """Current Display Bus"""