]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio/_display.py
Bug fixes and optimizations. Super close now
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _display.py
index 8e33803a7293df2df6e27eac7650ac30c0638080..d8b49c7031cd3e6492dc1474c0f8a5f8ef310255 100644 (file)
@@ -18,17 +18,14 @@ displayio for Blinka
 """
 
 import time
 """
 
 import time
-import struct
 from typing import Optional
 import digitalio
 from typing import Optional
 import digitalio
-from PIL import Image
 import microcontroller
 import microcontroller
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer, ReadableBuffer
 from ._displaycore import _DisplayCore
 from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
 from ._displaycore import _DisplayCore
 from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
-from ._group import Group
-from ._structs import RectangleStruct
+from ._group import Group, circuitpython_splash
 from ._area import Area
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
 from ._area import Area
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
@@ -38,6 +35,7 @@ from ._constants import (
     BACKLIGHT_IN_OUT,
     BACKLIGHT_PWM,
     NO_COMMAND,
     BACKLIGHT_IN_OUT,
     BACKLIGHT_PWM,
     NO_COMMAND,
+    DELAY,
 )
 
 __version__ = "0.0.0+auto.0"
 )
 
 __version__ = "0.0.0+auto.0"
@@ -45,7 +43,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
 
 class Display:
 
 
 class Display:
-    # pylint: disable=too-many-instance-attributes
+    # pylint: disable=too-many-instance-attributes, too-many-statements
     """This initializes a display and connects it into CircuitPython. Unlike other objects
     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
     This is done so that CircuitPython can use the display itself.
     """This initializes a display and connects it into CircuitPython. Unlike other objects
     in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
     This is done so that CircuitPython can use the display itself.
@@ -57,7 +55,7 @@ class Display:
     def __init__(
         self,
         display_bus: _DisplayBus,
     def __init__(
         self,
         display_bus: _DisplayBus,
-        init_sequence: circuitpython_typing.ReadableBuffer,
+        init_sequence: ReadableBuffer,
         *,
         width: int,
         height: int,
         *,
         width: int,
         height: int,
@@ -76,16 +74,14 @@ class Display:
         backlight_pin: Optional[microcontroller.Pin] = None,
         brightness_command: Optional[int] = None,
         brightness: float = 1.0,
         backlight_pin: Optional[microcontroller.Pin] = None,
         brightness_command: Optional[int] = None,
         brightness: float = 1.0,
-        auto_brightness: bool = False,
         single_byte_bounds: bool = False,
         data_as_commands: bool = False,
         auto_refresh: bool = True,
         native_frames_per_second: int = 60,
         backlight_on_high: bool = True,
         SH1107_addressing: bool = False,
         single_byte_bounds: bool = False,
         data_as_commands: bool = False,
         auto_refresh: bool = True,
         native_frames_per_second: int = 60,
         backlight_on_high: bool = True,
         SH1107_addressing: bool = False,
-        set_vertical_scroll: int = 0,
     ):
     ):
-        # pylint: disable=unused-argument,too-many-locals,invalid-name
+        # pylint: disable=too-many-locals,invalid-name, too-many-branches
         """Create a Display object on the given display bus (`displayio.FourWire` or
         `paralleldisplay.ParallelBus`).
 
         """Create a Display object on the given display bus (`displayio.FourWire` or
         `paralleldisplay.ParallelBus`).
 
@@ -115,6 +111,13 @@ class Display:
         The initialization sequence should always leave the display memory access inline with
         the scan of the display to minimize tearing artifacts.
         """
         The initialization sequence should always leave the display memory access inline with
         the scan of the display to minimize tearing artifacts.
         """
+
+        if rotation % 90 != 0:
+            raise ValueError("Display rotation must be in 90 degree increments")
+
+        if SH1107_addressing and color_depth != 1:
+            raise ValueError("color_depth must be 1 when SH1107_addressing is True")
+
         # Turn off auto-refresh as we init
         self._auto_refresh = False
         ram_width = 0x100
         # Turn off auto-refresh as we init
         self._auto_refresh = False
         ram_width = 0x100
@@ -156,57 +159,26 @@ class Display:
         self._native_frames_per_second = native_frames_per_second
         self._native_ms_per_frame = 1000 // native_frames_per_second
 
         self._native_frames_per_second = native_frames_per_second
         self._native_ms_per_frame = 1000 // native_frames_per_second
 
-        self._auto_brightness = auto_brightness
         self._brightness = brightness
         self._auto_refresh = auto_refresh
 
         self._brightness = brightness
         self._auto_refresh = auto_refresh
 
-        self._initialize(init_sequence)
-        self._buffer = Image.new("RGB", (width, height))
-        self._current_group = None
-        self._last_refresh_call = 0
-        self._refresh_thread = None
-        if self._auto_refresh:
-            self.auto_refresh = True
-        self._colorconverter = ColorConverter()
-
-        self._backlight_type = None
-        if backlight_pin is not None:
-            try:
-                from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
-
-                # 100Hz looks decent and doesn't keep the CPU too busy
-                self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
-                self._backlight_type = BACKLIGHT_PWM
-            except ImportError:
-                # PWMOut not implemented on this platform
-                pass
-            if self._backlight_type is None:
-                self._backlight_type = BACKLIGHT_IN_OUT
-                self._backlight = digitalio.DigitalInOut(backlight_pin)
-                self._backlight.switch_to_output()
-            self.brightness = brightness
-
-    def __new__(cls, *args, **kwargs):
-        from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
-            allocate_display,
-        )
-
-        allocate_display(cls)
-        return super().__new__(cls)
-
-    def _initialize(self, init_sequence):
         i = 0
         while i < len(init_sequence):
             command = init_sequence[i]
             data_size = init_sequence[i + 1]
         i = 0
         while i < len(init_sequence):
             command = init_sequence[i]
             data_size = init_sequence[i + 1]
-            delay = (data_size & 0x80) > 0
-            data_size &= ~0x80
+            delay = (data_size & DELAY) != 0
+            data_size &= ~DELAY
+            while self._core.begin_transaction():
+                pass
 
             if self._core.data_as_commands:
 
             if self._core.data_as_commands:
+                full_command = bytearray(data_size + 1)
+                full_command[0] = command
+                full_command[1:] = init_sequence[i + 2 : i + 2 + data_size]
                 self._core.send(
                     DISPLAY_COMMAND,
                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
                 self._core.send(
                     DISPLAY_COMMAND,
                     CHIP_SELECT_TOGGLE_EVERY_BYTE,
-                    bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
+                    full_command,
                 )
             else:
                 self._core.send(
                 )
             else:
                 self._core.send(
@@ -217,6 +189,7 @@ class Display:
                     CHIP_SELECT_UNTOUCHED,
                     init_sequence[i + 2 : i + 2 + data_size],
                 )
                     CHIP_SELECT_UNTOUCHED,
                     init_sequence[i + 2 : i + 2 + data_size],
                 )
+            self._core.end_transaction()
             delay_time_ms = 10
             if delay:
                 data_size += 1
             delay_time_ms = 10
             if delay:
                 data_size += 1
@@ -226,6 +199,40 @@ class Display:
             time.sleep(delay_time_ms / 1000)
             i += 2 + data_size
 
             time.sleep(delay_time_ms / 1000)
             i += 2 + data_size
 
+        self._current_group = None
+        self._last_refresh_call = 0
+        self._refresh_thread = None
+        self._colorconverter = ColorConverter()
+
+        self._backlight_type = None
+        if backlight_pin is not None:
+            try:
+                from pwmio import PWMOut  # pylint: disable=import-outside-toplevel
+
+                # 100Hz looks decent and doesn't keep the CPU too busy
+                self._backlight = PWMOut(backlight_pin, frequency=100, duty_cycle=0)
+                self._backlight_type = BACKLIGHT_PWM
+            except ImportError:
+                # PWMOut not implemented on this platform
+                pass
+            if self._backlight_type is None:
+                self._backlight_type = BACKLIGHT_IN_OUT
+                self._backlight = digitalio.DigitalInOut(backlight_pin)
+                self._backlight.switch_to_output()
+        self.brightness = brightness
+        if not circuitpython_splash._in_group:
+            self._set_root_group(circuitpython_splash)
+        self.auto_refresh = auto_refresh
+
+    def __new__(cls, *args, **kwargs):
+        from . import (  # pylint: disable=import-outside-toplevel, cyclic-import
+            allocate_display,
+        )
+
+        display_instance = super().__new__(cls)
+        allocate_display(display_instance)
+        return display_instance
+
     def _send_pixels(self, pixels):
         if not self._core.data_as_commands:
             self._core.send(
     def _send_pixels(self, pixels):
         if not self._core.data_as_commands:
             self._core.send(
@@ -239,7 +246,14 @@ class Display:
         """Switches to displaying the given group of layers. When group is None, the
         default CircuitPython terminal will be shown.
         """
         """Switches to displaying the given group of layers. When group is None, the
         default CircuitPython terminal will be shown.
         """
-        self._core.show(group)
+        if group is None:
+            group = circuitpython_splash
+        self._core.set_root_group(group)
+
+    def _set_root_group(self, root_group: Group) -> None:
+        ok = self._core.set_root_group(root_group)
+        if not ok:
+            raise ValueError("Group already used")
 
     def refresh(
         self,
 
     def refresh(
         self,
@@ -290,23 +304,10 @@ class Display:
         return True
 
     def _refresh_display(self):
         return True
 
     def _refresh_display(self):
-        # pylint: disable=protected-access
         if not self._core.start_refresh():
             return False
 
         if not self._core.start_refresh():
             return False
 
-        # TODO: Likely move this to _refresh_area()
-        # Go through groups and and add each to buffer
-        if self._core.current_group is not None:
-            buffer = Image.new("RGBA", (self._core.width, self._core.height))
-            # Recursively have everything draw to the image
-            self._core.current_group._fill_area(
-                buffer
-            )  # pylint: disable=protected-access
-            # save image to buffer (or probably refresh buffer so we can compare)
-            self._buffer.paste(buffer)
-
         areas_to_refresh = self._get_refresh_areas()
         areas_to_refresh = self._get_refresh_areas()
-
         for area in areas_to_refresh:
             self._refresh_area(area)
 
         for area in areas_to_refresh:
             self._refresh_area(area)
 
@@ -317,12 +318,15 @@ class Display:
     def _get_refresh_areas(self) -> list[Area]:
         """Get a list of areas to be refreshed"""
         areas = []
     def _get_refresh_areas(self) -> list[Area]:
         """Get a list of areas to be refreshed"""
         areas = []
-        if self._core.current_group is not None:
-            # Eventually calculate dirty rectangles here
-            areas.append(Area(0, 0, self._core.width, self._core.height))
+        if self._core.full_refresh:
+            areas.append(self._core.area)
+        elif self._core.current_group is not None:
+            self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
+                areas
+            )
         return areas
 
         return areas
 
-    def background(self):
+    def _background(self):
         """Run background refresh tasks. Do not call directly"""
         if (
             self._auto_refresh
         """Run background refresh tasks. Do not call directly"""
         if (
             self._auto_refresh
@@ -333,19 +337,24 @@ class Display:
 
     def _refresh_area(self, area) -> bool:
         """Loop through dirty areas and redraw that area."""
 
     def _refresh_area(self, area) -> bool:
         """Loop through dirty areas and redraw that area."""
-        # pylint: disable=too-many-locals
-        buffer_size = 128
+        # pylint: disable=too-many-locals, too-many-branches
 
         clipped = Area()
 
         clipped = Area()
+        # Clip the area to the display by overlapping the areas.
+        # If there is no overlap then we're done.
         if not self._core.clip_area(area, clipped):
             return True
 
         rows_per_buffer = clipped.height()
         if not self._core.clip_area(area, clipped):
             return True
 
         rows_per_buffer = clipped.height()
-        pixels_per_word = (struct.calcsize("I") * 8) // self._core.colorspace.depth
+        pixels_per_word = 32 // self._core.colorspace.depth
         pixels_per_buffer = clipped.size()
 
         pixels_per_buffer = clipped.size()
 
-        subrectangles = 1
+        # We should have lots of memory
+        buffer_size = clipped.size() // pixels_per_word
 
 
+        subrectangles = 1
+        # for SH1107 and other boundary constrained controllers
+        #      write one single row at a time
         if self._core.sh1107_addressing:
             subrectangles = rows_per_buffer // 8
             rows_per_buffer = 8
         if self._core.sh1107_addressing:
             subrectangles = rows_per_buffer // 8
             rows_per_buffer = 8
@@ -353,6 +362,7 @@ class Display:
             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
             if rows_per_buffer == 0:
                 rows_per_buffer = 1
             rows_per_buffer = buffer_size * pixels_per_word // clipped.width()
             if rows_per_buffer == 0:
                 rows_per_buffer = 1
+            # If pixels are packed by column then ensure rows_per_buffer is on a byte boundary
             if (
                 self._core.colorspace.depth < 8
                 and self._core.colorspace.pixels_in_byte_share_row
             if (
                 self._core.colorspace.depth < 8
                 and self._core.colorspace.pixels_in_byte_share_row
@@ -367,21 +377,19 @@ class Display:
             buffer_size = pixels_per_buffer // pixels_per_word
             if pixels_per_buffer % pixels_per_word:
                 buffer_size += 1
             buffer_size = pixels_per_buffer // pixels_per_word
             if pixels_per_buffer % pixels_per_word:
                 buffer_size += 1
-
-        buffer = bytearray(buffer_size)
-        mask_length = (pixels_per_buffer // 32) + 1
-        mask = bytearray(mask_length)
+        mask_length = (pixels_per_buffer // 32) + 1  # 1 bit per pixel + 1
         remaining_rows = clipped.height()
 
         for subrect_index in range(subrectangles):
             subrectangle = Area(
         remaining_rows = clipped.height()
 
         for subrect_index in range(subrectangles):
             subrectangle = Area(
-                clipped.x1,
-                clipped.y1 + rows_per_buffer * subrect_index,
-                clipped.x2,
-                clipped.y1 + rows_per_buffer * (subrect_index + 1),
+                x1=clipped.x1,
+                y1=clipped.y1 + rows_per_buffer * subrect_index,
+                x2=clipped.x2,
+                y2=clipped.y1 + rows_per_buffer * (subrect_index + 1),
             )
             if remaining_rows < rows_per_buffer:
                 subrectangle.y2 = subrectangle.y1 + remaining_rows
             )
             if remaining_rows < rows_per_buffer:
                 subrectangle.y2 = subrectangle.y1 + remaining_rows
+            remaining_rows -= rows_per_buffer
             self._core.set_region_to_update(subrectangle)
             if self._core.colorspace.depth >= 8:
                 subrectangle_size_bytes = subrectangle.size() * (
             self._core.set_region_to_update(subrectangle)
             if self._core.colorspace.depth >= 8:
                 subrectangle_size_bytes = subrectangle.size() * (
@@ -392,56 +400,49 @@ class Display:
                     8 // self._core.colorspace.depth
                 )
 
                     8 // self._core.colorspace.depth
                 )
 
+            buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
+            mask = memoryview(bytearray([0] * mask_length)).cast("I")
             self._core.fill_area(subrectangle, mask, buffer)
 
             self._core.fill_area(subrectangle, mask, buffer)
 
+            # Can't acquire display bus; skip the rest of the data.
+            if not self._core.bus_free():
+                return False
+
             self._core.begin_transaction()
             self._core.begin_transaction()
-            self._send_pixels(buffer[:subrectangle_size_bytes])
+            self._send_pixels(buffer.tobytes()[:subrectangle_size_bytes])
             self._core.end_transaction()
         return True
 
             self._core.end_transaction()
         return True
 
-    def _apply_rotation(self, rectangle):
-        """Adjust the rectangle coordinates based on rotation"""
-        if self._core.rotation == 90:
-            return RectangleStruct(
-                self._core.height - rectangle.y2,
-                rectangle.x1,
-                self._core.height - rectangle.y1,
-                rectangle.x2,
-            )
-        if self._core.rotation == 180:
-            return RectangleStruct(
-                self._core.width - rectangle.x2,
-                self._core.height - rectangle.y2,
-                self._core.width - rectangle.x1,
-                self._core.height - rectangle.y1,
-            )
-        if self._core.rotation == 270:
-            return RectangleStruct(
-                rectangle.y1,
-                self._core.width - rectangle.x2,
-                rectangle.y2,
-                self._core.width - rectangle.x1,
-            )
-        return rectangle
-
-    def fill_row(
-        self, y: int, buffer: circuitpython_typing.WriteableBuffer
-    ) -> circuitpython_typing.WriteableBuffer:
+    def fill_row(self, y: int, buffer: WriteableBuffer) -> WriteableBuffer:
         """Extract the pixels from a single row"""
         """Extract the pixels from a single row"""
-        for x in range(0, self._core.width):
-            _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
-            buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
-            buffer[x * 2 + 1] = _rgb_565 & 0xFF
+        if self._core.colorspace.depth != 16:
+            raise ValueError("Display must have a 16 bit colorspace.")
+
+        area = Area(0, y, self._core.width, y + 1)
+        pixels_per_word = 32 // self._core.colorspace.depth
+        buffer_size = self._core.width // pixels_per_word
+        pixels_per_buffer = area.size()
+        if pixels_per_buffer % pixels_per_word:
+            buffer_size += 1
+
+        buffer = memoryview(bytearray([0] * (buffer_size * 4))).cast("I")
+        mask_length = (pixels_per_buffer // 32) + 1
+        mask = memoryview(bytearray([0] * (mask_length * 4))).cast("I")
+        self._core.fill_area(area, mask, buffer)
         return buffer
 
         return buffer
 
-    def release(self) -> None:
+    def _release(self) -> None:
         """Release the display and free its resources"""
         self.auto_refresh = False
         self._core.release_display_core()
 
         """Release the display and free its resources"""
         self.auto_refresh = False
         self._core.release_display_core()
 
-    def reset(self) -> None:
+    def _reset(self) -> None:
         """Reset the display"""
         self.auto_refresh = True
         """Reset the display"""
         self.auto_refresh = True
+        circuitpython_splash.x = 0
+        circuitpython_splash.y = 0
+        if not circuitpython_splash._in_group:  # pylint: disable=protected-access
+            self._set_root_group(circuitpython_splash)
 
     @property
     def auto_refresh(self) -> bool:
 
     @property
     def auto_refresh(self) -> bool:
@@ -455,10 +456,7 @@ class Display:
 
     @property
     def brightness(self) -> float:
 
     @property
     def brightness(self) -> float:
-        """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
-        When `auto_brightness` is True, the value of `brightness` will change automatically.
-        If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
-        """
+        """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`."""
         return self._brightness
 
     @brightness.setter
         return self._brightness
 
     @brightness.setter
@@ -472,40 +470,28 @@ class Display:
             elif self._backlight_type == BACKLIGHT_IN_OUT:
                 self._backlight.value = value > 0.99
             elif self._brightness_command is not None:
             elif self._backlight_type == BACKLIGHT_IN_OUT:
                 self._backlight.value = value > 0.99
             elif self._brightness_command is not None:
-                self._core.begin_transaction()
-                if self._core.data_as_commands:
-                    self._core.send(
-                        DISPLAY_COMMAND,
-                        CHIP_SELECT_TOGGLE_EVERY_BYTE,
-                        bytes([self._brightness_command, 0xFF * value]),
-                    )
-                else:
-                    self._core.send(
-                        DISPLAY_COMMAND,
-                        CHIP_SELECT_TOGGLE_EVERY_BYTE,
-                        bytes([self._brightness_command]),
-                    )
-                    self._core.send(
-                        DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
-                    )
-                self._core.end_transaction()
+                okay = self._core.begin_transaction()
+                if okay:
+                    if self._core.data_as_commands:
+                        self._core.send(
+                            DISPLAY_COMMAND,
+                            CHIP_SELECT_TOGGLE_EVERY_BYTE,
+                            bytes([self._brightness_command, round(0xFF * value)]),
+                        )
+                    else:
+                        self._core.send(
+                            DISPLAY_COMMAND,
+                            CHIP_SELECT_TOGGLE_EVERY_BYTE,
+                            bytes([self._brightness_command]),
+                        )
+                        self._core.send(
+                            DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, round(value * 255)
+                        )
+                    self._core.end_transaction()
             self._brightness = value
         else:
             raise ValueError("Brightness must be between 0.0 and 1.0")
 
             self._brightness = value
         else:
             raise ValueError("Brightness must be between 0.0 and 1.0")
 
-    @property
-    def auto_brightness(self) -> bool:
-        """True when the display brightness is adjusted automatically, based on an ambient
-        light sensor or other method. Note that some displays may have this set to True by
-        default, but not actually implement automatic brightness adjustment.
-        `auto_brightness` is set to False if `brightness` is set manually.
-        """
-        return self._auto_brightness
-
-    @auto_brightness.setter
-    def auto_brightness(self, value: bool):
-        self._auto_brightness = value
-
     @property
     def width(self) -> int:
         """Display Width"""
     @property
     def width(self) -> int:
         """Display Width"""
@@ -523,7 +509,17 @@ class Display:
 
     @rotation.setter
     def rotation(self, value: int):
 
     @rotation.setter
     def rotation(self, value: int):
+        if value % 90 != 0:
+            raise ValueError("Display rotation must be in 90 degree increments")
+        transposed = self._core.rotation in (90, 270)
+        will_transposed = value in (90, 270)
+        if transposed != will_transposed:
+            self._core.width, self._core.height = self._core.height, self._core.width
         self._core.set_rotation(value)
         self._core.set_rotation(value)
+        if self._core.current_group is not None:
+            self._core.current_group._update_transform(  # pylint: disable=protected-access
+                self._core.transform
+            )
 
     @property
     def bus(self) -> _DisplayBus:
 
     @property
     def bus(self) -> _DisplayBus: