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
 
  44 from recordclass import recordclass
 
  46 __version__ = "0.0.0-auto.0"
 
  47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  49 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  55 # pylint: disable=unnecessary-pass, unused-argument
 
  57 # pylint: disable=too-many-instance-attributes
 
  59     """This initializes a display and connects it into CircuitPython. Unlike other objects
 
  60     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
 
  61     This is done so that CircuitPython can use the display itself.
 
  63     Most people should not use this class directly. Use a specific display driver instead
 
  64     that will contain the initialization sequence at minimum.
 
  67         Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
 
  68         color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
 
  69         reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
 
  70         write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
 
  71         brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
 
  72         auto_refresh=True, native_frames_per_second=60)
 
  75     # pylint: disable=too-many-locals
 
  88         pixels_in_byte_share_row=True,
 
  90         reverse_pixels_in_byte=False,
 
  91         set_column_command=0x2A,
 
  93         write_ram_command=0x2C,
 
  94         set_vertical_scroll=0,
 
  96         brightness_command=None,
 
  98         auto_brightness=False,
 
  99         single_byte_bounds=False,
 
 100         data_as_commands=False,
 
 102         native_frames_per_second=60
 
 104         """Create a Display object on the given display bus (`displayio.FourWire` or
 
 105         `displayio.ParallelBus`).
 
 107         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
 
 108         with a command byte followed by a byte to determine the parameter count and if a
 
 109         delay is need after. When the top bit of the second byte is 1, the next byte will be
 
 110         the delay time in milliseconds. The remaining 7 bits are the parameter count
 
 111         excluding any delay byte. The third through final bytes are the remaining command
 
 112         parameters. The next byte will begin a new command definition. Here is a portion of
 
 114         .. code-block:: python
 
 117                 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
 
 118                 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
 
 119                 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
 
 121             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
 
 123         The first command is 0xe1 with 15 (0xf) parameters following. The second and third
 
 124         are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
 
 125         Multiple byte literals (b”“) are merged together on load. The parens are needed to
 
 126         allow byte literals on subsequent lines.
 
 128         The initialization sequence should always leave the display memory access inline with
 
 129         the scan of the display to minimize tearing artifacts.
 
 131         self._bus = display_bus
 
 132         self._set_column_command = set_column_command
 
 133         self._set_row_command = set_row_command
 
 134         self._write_ram_command = write_ram_command
 
 135         self._brightness_command = brightness_command
 
 136         self._data_as_commands = data_as_commands
 
 137         self._single_byte_bounds = single_byte_bounds
 
 139         self._height = height
 
 140         self._colstart = colstart
 
 141         self._rowstart = rowstart
 
 142         self._rotation = rotation
 
 143         self._auto_brightness = auto_brightness
 
 144         self._brightness = 1.0
 
 145         self._auto_refresh = auto_refresh
 
 146         self._initialize(init_sequence)
 
 147         self._buffer = Image.new("RGB", (width, height))
 
 148         self._subrectangles = []
 
 149         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
 
 150         self._current_group = None
 
 151         displays.append(self)
 
 152         self._refresh_thread = None
 
 153         if self._auto_refresh:
 
 154             self.auto_refresh = True
 
 156         self._backlight_type = None
 
 157         if backlight_pin is not 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     # pylint: enable=too-many-locals
 
 165     def _initialize(self, init_sequence):
 
 167         while i < len(init_sequence):
 
 168             command = init_sequence[i]
 
 169             data_size = init_sequence[i + 1]
 
 170             delay = (data_size & 0x80) > 0
 
 173             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
 
 177                 delay_time_ms = init_sequence[i + 1 + data_size]
 
 178                 if delay_time_ms == 255:
 
 180             time.sleep(delay_time_ms / 1000)
 
 183     def _write(self, command, data):
 
 184         self._bus.begin_transaction()
 
 185         if self._data_as_commands:
 
 186             if command is not None:
 
 187                 self._bus.send(True, bytes([command]), toggle_every_byte=True)
 
 188             self._bus.send(command is not None, data)
 
 190             self._bus.send(True, bytes([command]), toggle_every_byte=True)
 
 191             self._bus.send(False, data)
 
 192         self._bus.end_transaction()
 
 198     def show(self, group):
 
 199         """Switches to displaying the given group of layers. When group is None, the
 
 200         default CircuitPython terminal will be shown.
 
 202         self._current_group = group
 
 204     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
 
 205         """When auto refresh is off, waits for the target frame rate and then refreshes the
 
 206         display, returning True. If the call has taken too long since the last refresh call
 
 207         for the given target frame rate, then the refresh returns False immediately without
 
 208         updating the screen to hopefully help getting caught up.
 
 210         If the time since the last successful refresh is below the minimum frame rate, then
 
 211         an exception will be raised. Set minimum_frames_per_second to 0 to disable.
 
 213         When auto refresh is on, updates the display immediately. (The display will also
 
 214         update without calls to this.)
 
 216         self._subrectangles = []
 
 218         # Go through groups and and add each to buffer
 
 219         if self._current_group is not None:
 
 220             buffer = Image.new("RGBA", (self._width, self._height))
 
 221             # Recursively have everything draw to the image
 
 222             self._current_group._fill_area(buffer)  # pylint: disable=protected-access
 
 223             # save image to buffer (or probably refresh buffer so we can compare)
 
 224             self._buffer.paste(buffer)
 
 226         if self._current_group is not None:
 
 227             # Eventually calculate dirty rectangles here
 
 228             self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
 
 230         for area in self._subrectangles:
 
 231             self._refresh_display_area(area)
 
 233     def _refresh_loop(self):
 
 234         while self._auto_refresh:
 
 237     def _refresh_display_area(self, rectangle):
 
 238         """Loop through dirty rectangles and redraw that area."""
 
 240         img = self._buffer.convert("RGB").crop(rectangle)
 
 241         img = img.rotate(self._rotation, expand=True)
 
 243         display_rectangle = self._apply_rotation(rectangle)
 
 244         img = img.crop(self._clip(display_rectangle))
 
 246         data = numpy.array(img).astype("uint16")
 
 248             ((data[:, :, 0] & 0xF8) << 8)
 
 249             | ((data[:, :, 1] & 0xFC) << 3)
 
 250             | (data[:, :, 2] >> 3)
 
 254             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
 
 258             self._set_column_command,
 
 260                 display_rectangle.x1 + self._colstart,
 
 261                 display_rectangle.x2 + self._colstart - 1,
 
 265             self._set_row_command,
 
 267                 display_rectangle.y1 + self._rowstart,
 
 268                 display_rectangle.y2 + self._rowstart - 1,
 
 272         if self._data_as_commands:
 
 273             self._write(None, pixels)
 
 275             self._write(self._write_ram_command, pixels)
 
 277     def _clip(self, rectangle):
 
 278         if self._rotation in (90, 270):
 
 279             width, height = self._height, self._width
 
 281             width, height = self._width, self._height
 
 287         if rectangle.x2 > width:
 
 289         if rectangle.y2 > height:
 
 290             rectangle.y2 = height
 
 294     def _apply_rotation(self, rectangle):
 
 295         """Adjust the rectangle coordinates based on rotation"""
 
 296         if self._rotation == 90:
 
 298                 self._height - rectangle.y2,
 
 300                 self._height - rectangle.y1,
 
 303         if self._rotation == 180:
 
 305                 self._width - rectangle.x2,
 
 306                 self._height - rectangle.y2,
 
 307                 self._width - rectangle.x1,
 
 308                 self._height - rectangle.y1,
 
 310         if self._rotation == 270:
 
 313                 self._width - rectangle.x2,
 
 315                 self._width - rectangle.x1,
 
 319     def _encode_pos(self, x, y):
 
 320         """Encode a postion into bytes."""
 
 321         return struct.pack(self._bounds_encoding, x, y)
 
 323     def fill_row(self, y, buffer):
 
 324         """Extract the pixels from a single row"""
 
 328     def auto_refresh(self):
 
 329         """True when the display is refreshed automatically."""
 
 330         return self._auto_refresh
 
 333     def auto_refresh(self, value):
 
 334         self._auto_refresh = value
 
 335         if self._refresh_thread is None:
 
 336             self._refresh_thread = threading.Thread(
 
 337                 target=self._refresh_loop, daemon=True
 
 339         if value and not self._refresh_thread.is_alive():
 
 341             self._refresh_thread.start()
 
 342         elif not value and self._refresh_thread.is_alive():
 
 344             self._refresh_thread.join()
 
 347     def brightness(self):
 
 348         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
 
 349         When `auto_brightness` is True, the value of `brightness` will change automatically.
 
 350         If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
 
 352         return self._brightness
 
 355     def brightness(self, value):
 
 356         if 0 <= float(value) <= 1.0:
 
 357             self._brightness = value
 
 358             if self._backlight_type == BACKLIGHT_IN_OUT:
 
 359                 self._backlight.value = round(self._brightness)
 
 360             # PWM not currently implemented
 
 361             # Command-based brightness not implemented
 
 363             raise ValueError("Brightness must be between 0.0 and 1.0")
 
 366     def auto_brightness(self):
 
 367         """True when the display brightness is adjusted automatically, based on an ambient
 
 368         light sensor or other method. Note that some displays may have this set to True by
 
 369         default, but not actually implement automatic brightness adjustment.
 
 370         `auto_brightness` is set to False if `brightness` is set manually.
 
 372         return self._auto_brightness
 
 374     @auto_brightness.setter
 
 375     def auto_brightness(self, value):
 
 376         self._auto_brightness = value
 
 390         """The rotation of the display as an int in degrees."""
 
 391         return self._rotation
 
 394     def rotation(self, value):
 
 395         if value not in (0, 90, 180, 270):
 
 396             raise ValueError("Rotation must be 0/90/180/270")
 
 397         self._rotation = value
 
 401         """Current Display Bus"""
 
 405 # pylint: enable=too-many-instance-attributes