1 # The MIT License (MIT)
 
   3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   5 # Permission is hereby granted, free of charge, to any person obtaining a copy
 
   6 # of this software and associated documentation files (the "Software"), to deal
 
   7 # in the Software without restriction, including without limitation the rights
 
   8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 
   9 # copies of the Software, and to permit persons to whom the Software is
 
  10 # furnished to do so, subject to the following conditions:
 
  12 # The above copyright notice and this permission notice shall be included in
 
  13 # all copies or substantial portions of the Software.
 
  15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 
  16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 
  17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 
  18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 
  19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 
  20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
  25 ================================================================================
 
  29 **Software and Dependencies:**
 
  32   https://github.com/adafruit/Adafruit_Blinka/releases
 
  34 * Author(s): Melissa LeBlanc-Williams
 
  43 from recordclass import recordclass
 
  45 __version__ = "0.0.0-auto.0"
 
  46 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  48 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  51 # pylint: disable=unnecessary-pass, unused-argument
 
  53 # pylint: disable=too-many-instance-attributes
 
  55     """This initializes a display and connects it into CircuitPython. Unlike other objects
 
  56     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
 
  57     This is done so that CircuitPython can use the display itself.
 
  59     Most people should not use this class directly. Use a specific display driver instead
 
  60     that will contain the initialization sequence at minimum.
 
  63         Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
 
  64         color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
 
  65         reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
 
  66         write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
 
  67         brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
 
  68         auto_refresh=True, native_frames_per_second=60)
 
  71     # pylint: disable=too-many-locals
 
  84         pixels_in_byte_share_row=True,
 
  86         reverse_pixels_in_byte=False,
 
  87         set_column_command=0x2A,
 
  89         write_ram_command=0x2C,
 
  90         set_vertical_scroll=0,
 
  92         brightness_command=None,
 
  94         auto_brightness=False,
 
  95         single_byte_bounds=False,
 
  96         data_as_commands=False,
 
  98         native_frames_per_second=60
 
 100         """Create a Display object on the given display bus (`displayio.FourWire` or
 
 101         `displayio.ParallelBus`).
 
 103         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
 
 104         with a command byte followed by a byte to determine the parameter count and if a
 
 105         delay is need after. When the top bit of the second byte is 1, the next byte will be
 
 106         the delay time in milliseconds. The remaining 7 bits are the parameter count
 
 107         excluding any delay byte. The third through final bytes are the remaining command
 
 108         parameters. The next byte will begin a new command definition. Here is a portion of
 
 110         .. code-block:: python
 
 113                 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
 
 114                 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
 
 115                 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
 
 117             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
 
 119         The first command is 0xe1 with 15 (0xf) parameters following. The second and third
 
 120         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
 
 121         Multiple byte literals (b”“) are merged together on load. The parens are needed to
 
 122         allow byte literals on subsequent lines.
 
 124         The initialization sequence should always leave the display memory access inline with
 
 125         the scan of the display to minimize tearing artifacts.
 
 127         self._bus = display_bus
 
 128         self._set_column_command = set_column_command
 
 129         self._set_row_command = set_row_command
 
 130         self._write_ram_command = write_ram_command
 
 131         self._brightness_command = brightness_command
 
 132         self._data_as_commands = data_as_commands
 
 133         self._single_byte_bounds = single_byte_bounds
 
 135         self._height = height
 
 136         self._colstart = colstart
 
 137         self._rowstart = rowstart
 
 138         self._rotation = rotation
 
 139         self._auto_brightness = auto_brightness
 
 140         self._brightness = brightness
 
 141         self._auto_refresh = auto_refresh
 
 142         self._initialize(init_sequence)
 
 143         self._buffer = Image.new("RGB", (width, height))
 
 144         self._subrectangles = []
 
 145         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
 
 146         self._current_group = None
 
 147         displays.append(self)
 
 148         self._refresh_thread = None
 
 149         if self._auto_refresh:
 
 150             self.auto_refresh = True
 
 152     # pylint: enable=too-many-locals
 
 154     def _initialize(self, init_sequence):
 
 156         while i < len(init_sequence):
 
 157             command = init_sequence[i]
 
 158             data_size = init_sequence[i + 1]
 
 159             delay = (data_size & 0x80) > 0
 
 161             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
 
 165                 delay_time_ms = init_sequence[i + 1 + data_size]
 
 166                 if delay_time_ms == 255:
 
 168             time.sleep(delay_time_ms / 1000)
 
 171     def _write(self, command, data):
 
 172         if self._single_byte_bounds:
 
 173             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
 
 175             self._bus.send(True, bytes([command]), toggle_every_byte=True)
 
 176             self._bus.send(False, data)
 
 182     def show(self, group):
 
 183         """Switches to displaying the given group of layers. When group is None, the
 
 184         default CircuitPython terminal will be shown.
 
 186         self._current_group = group
 
 188     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
 
 189         """When auto refresh is off, waits for the target frame rate and then refreshes the
 
 190         display, returning True. If the call has taken too long since the last refresh call
 
 191         for the given target frame rate, then the refresh returns False immediately without
 
 192         updating the screen to hopefully help getting caught up.
 
 194         If the time since the last successful refresh is below the minimum frame rate, then
 
 195         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
 
 197         When auto refresh is on, updates the display immediately. (The display will also
 
 198         update without calls to this.)
 
 200         # Go through groups and and add each to buffer
 
 201         if self._current_group is not None:
 
 202             buffer = Image.new("RGBA", (self._width, self._height))
 
 203             # Recursively have everything draw to the image
 
 204             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
 
 205             # save image to buffer (or probably refresh buffer so we can compare)
 
 206             self._buffer.paste(buffer)
 
 208         # Eventually calculate dirty rectangles here
 
 209         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
 
 211         for area in self._subrectangles:
 
 212             self._refresh_display_area(area)
 
 214     def _refresh_loop(self):
 
 215         while self._auto_refresh:
 
 218     def _refresh_display_area(self, rectangle):
 
 219         """Loop through dirty rectangles and redraw that area."""
 
 221         img = self._buffer.convert("RGB").crop(rectangle)
 
 222         img = img.rotate(self._rotation, expand=True)
 
 224         display_rectangle = self._apply_rotation(rectangle)
 
 225         img = img.crop(self._clip(display_rectangle))
 
 227         data = numpy.array(img).astype("uint16")
 
 229             ((data[:, :, 0] & 0xF8) << 8)
 
 230             | ((data[:, :, 1] & 0xFC) << 3)
 
 231             | (data[:, :, 2] >> 3)
 
 235             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
 
 239             self._set_column_command,
 
 241                 display_rectangle.x1 + self._colstart,
 
 242                 display_rectangle.x2 + self._colstart - 1,
 
 246             self._set_row_command,
 
 248                 display_rectangle.y1 + self._rowstart,
 
 249                 display_rectangle.y2 + self._rowstart - 1,
 
 253         self._write(self._write_ram_command, pixels)
 
 255     def _clip(self, rectangle):
 
 256         if self._rotation in (90, 270):
 
 257             width, height = self._height, self._width
 
 259             width, height = self._width, self._height
 
 265         if rectangle.x2 > width:
 
 267         if rectangle.y2 > height:
 
 268             rectangle.y2 = height
 
 272     def _apply_rotation(self, rectangle):
 
 273         """Adjust the rectangle coordinates based on rotation"""
 
 274         if self._rotation == 90:
 
 276                 self._height - rectangle.y2,
 
 278                 self._height - rectangle.y1,
 
 281         if self._rotation == 180:
 
 283                 self._width - rectangle.x2,
 
 284                 self._height - rectangle.y2,
 
 285                 self._width - rectangle.x1,
 
 286                 self._height - rectangle.y1,
 
 288         if self._rotation == 270:
 
 291                 self._width - rectangle.x2,
 
 293                 self._width - rectangle.x1,
 
 297     def _encode_pos(self, x, y):
 
 298         """Encode a postion into bytes."""
 
 299         return struct.pack(self._bounds_encoding, x, y)
 
 301     def fill_row(self, y, buffer):
 
 302         """Extract the pixels from a single row"""
 
 306     def auto_refresh(self):
 
 307         """True when the display is refreshed automatically."""
 
 308         return self._auto_refresh
 
 311     def auto_refresh(self, value):
 
 312         self._auto_refresh = value
 
 313         if self._refresh_thread is None:
 
 314             self._refresh_thread = threading.Thread(
 
 315                 target=self._refresh_loop, daemon=True
 
 317         if value and not self._refresh_thread.is_alive():
 
 319             self._refresh_thread.start()
 
 320         elif not value and self._refresh_thread.is_alive():
 
 322             self._refresh_thread.join()
 
 325     def brightness(self):
 
 326         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
 
 327         When `auto_brightness` is True, the value of `brightness` will change automatically.
 
 328         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
 
 330         return self._brightness
 
 333     def brightness(self, value):
 
 334         self._brightness = value
 
 337     def auto_brightness(self):
 
 338         """True when the display brightness is adjusted automatically, based on an ambient
 
 339         light sensor or other method. Note that some displays may have this set to True by
 
 340         default, but not actually implement automatic brightness adjustment.
 
 341         `auto_brightness` is set to False if `brightness` is set manually.
 
 343         return self._auto_brightness
 
 345     @auto_brightness.setter
 
 346     def auto_brightness(self, value):
 
 347         self._auto_brightness = value
 
 361         """The rotation of the display as an int in degrees."""
 
 362         return self._rotation
 
 365     def rotation(self, value):
 
 366         if value not in (0, 90, 180, 270):
 
 367             raise ValueError("Rotation must be 0/90/180/270")
 
 368         self._rotation = value
 
 372         """Current Display Bus"""
 
 376 # pylint: enable=too-many-instance-attributes