]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blobdiff - displayio/_display.py
Merge pull request #85 from makermelissa/split-display-core
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _display.py
index 4e5da3f9b62cac322ad5c5f07223e26fbc02e23a..88900871b6f28f73f80a337c66b9b5d6edf834dc 100644 (file)
@@ -20,32 +20,32 @@ displayio for Blinka
 import time
 import struct
 import threading
+from typing import Optional
+from dataclasses import astuple
 import digitalio
 from PIL import Image
 import numpy
 import microcontroller
-from recordclass import recordclass
+import circuitpython_typing
+from ._displaycore import _DisplayCore
+from ._displaybus import _DisplayBus
 from ._colorconverter import ColorConverter
-import _typing
 from ._group import Group
-from displayio import _DisplayBus
-from typing import Optional
+from ._structs import RectangleStruct
 from ._constants import (
     CHIP_SELECT_TOGGLE_EVERY_BYTE,
     CHIP_SELECT_UNTOUCHED,
     DISPLAY_COMMAND,
     DISPLAY_DATA,
+    BACKLIGHT_IN_OUT,
+    BACKLIGHT_PWM,
 )
 
 __version__ = "0.0.0-auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
-Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 displays = []
 
-BACKLIGHT_IN_OUT = 1
-BACKLIGHT_PWM = 2
-
 
 class Display:
     # pylint: disable=too-many-instance-attributes
@@ -60,7 +60,7 @@ class Display:
     def __init__(
         self,
         display_bus: _DisplayBus,
-        init_sequence: _typing.ReadableBuffer,
+        init_sequence: circuitpython_typing.ReadableBuffer,
         *,
         width: int,
         height: int,
@@ -72,6 +72,7 @@ class Display:
         pixels_in_byte_share_row: bool = True,
         bytes_per_cell: int = 1,
         reverse_pixels_in_byte: bool = False,
+        reverse_bytes_in_word: bool = True,
         set_column_command: int = 0x2A,
         set_row_command: int = 0x2B,
         write_ram_command: int = 0x2C,
@@ -87,7 +88,7 @@ class Display:
         SH1107_addressing: bool = False,
         set_vertical_scroll: int = 0,
     ):
-        # pylint: disable=unused-argument,too-many-locals
+        # pylint: disable=unused-argument,too-many-locals,invalid-name
         """Create a Display object on the given display bus (`displayio.FourWire` or
         `paralleldisplay.ParallelBus`).
 
@@ -117,7 +118,28 @@ class Display:
         The initialization sequence should always leave the display memory access inline with
         the scan of the display to minimize tearing artifacts.
         """
-        self._bus = display_bus
+        ram_width = 0x100
+        ram_height = 0x100
+        if single_byte_bounds:
+            ram_width = 0xFF
+            ram_height = 0xFF
+        self._core = _DisplayCore(
+            bus=display_bus,
+            width=width,
+            height=height,
+            ram_width=ram_width,
+            ram_height=ram_height,
+            colstart=colstart,
+            rowstart=rowstart,
+            rotation=rotation,
+            color_depth=color_depth,
+            grayscale=grayscale,
+            pixels_in_byte_share_row=pixels_in_byte_share_row,
+            bytes_per_cell=bytes_per_cell,
+            reverse_pixels_in_byte=reverse_pixels_in_byte,
+            reverse_bytes_in_word=reverse_bytes_in_word,
+        )
+
         self._set_column_command = set_column_command
         self._set_row_command = set_row_command
         self._write_ram_command = write_ram_command
@@ -168,7 +190,21 @@ class Display:
             delay = (data_size & 0x80) > 0
             data_size &= ~0x80
 
-            self._send(command, init_sequence[i + 2 : i + 2 + data_size])
+            if self._data_as_commands:
+                self._core.send(
+                    DISPLAY_COMMAND,
+                    CHIP_SELECT_TOGGLE_EVERY_BYTE,
+                    bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
+                )
+            else:
+                self._core.send(
+                    DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
+                )
+                self._core.send(
+                    DISPLAY_DATA,
+                    CHIP_SELECT_UNTOUCHED,
+                    init_sequence[i + 2 : i + 2 + data_size],
+                )
             delay_time_ms = 10
             if delay:
                 data_size += 1
@@ -179,36 +215,32 @@ class Display:
             i += 2 + data_size
 
     def _send(self, command, data):
-        self._bus._begin_transaction()  # pylint: disable=protected-access
+        self._core.begin_transaction()
         if self._data_as_commands:
-            self._bus._send(
-                DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command] + data)
+            self._core.send(
+                DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
             )
         else:
-            self._bus._send(
+            self._core.send(
                 DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
             )
-            self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
-        self._bus._end_transaction()  # pylint: disable=protected-access
+            self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
+        self._core.end_transaction()
 
     def _send_pixels(self, data):
         if not self._data_as_commands:
-            self._bus._send(
+            self._core.send(
                 DISPLAY_COMMAND,
                 CHIP_SELECT_TOGGLE_EVERY_BYTE,
                 bytes([self._write_ram_command]),
             )
-        self._bus._send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
-
-    def _release(self):
-        self._bus._release()  # pylint: disable=protected-access
-        self._bus = None
+        self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
 
     def show(self, group: Group) -> None:
         """Switches to displaying the given group of layers. When group is None, the
         default CircuitPython terminal will be shown.
         """
-        self._current_group = group
+        self._core.show(group)
 
     def refresh(
         self,
@@ -216,7 +248,7 @@ class Display:
         target_frames_per_second: Optional[int] = None,
         minimum_frames_per_second: int = 0,
     ) -> bool:
-        # pylint: disable=unused-argument
+        # pylint: disable=unused-argument, protected-access
         """When auto refresh is off, waits for the target frame rate and then refreshes the
         display, returning True. If the call has taken too long since the last refresh call
         for the given target frame rate, then the refresh returns False immediately without
@@ -228,23 +260,26 @@ class Display:
         When auto refresh is on, updates the display immediately. (The display will also
         update without calls to this.)
         """
-        self._subrectangles = []
+        if not self._core.start_refresh():
+            return False
 
         # Go through groups and and add each to buffer
-        if self._current_group is not None:
-            buffer = Image.new("RGBA", (self._width, self._height))
+        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._current_group._fill_area(buffer)  # pylint: disable=protected-access
+            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)
 
-        if self._current_group is not None:
-            # Eventually calculate dirty rectangles here
-            self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
+        self._subrectangles = self._core.get_refresh_areas()
 
         for area in self._subrectangles:
             self._refresh_display_area(area)
 
+        self._core.finish_refresh()
+
         return True
 
     def _refresh_loop(self):
@@ -253,12 +288,11 @@ class Display:
 
     def _refresh_display_area(self, rectangle):
         """Loop through dirty rectangles and redraw that area."""
-
-        img = self._buffer.convert("RGB").crop(rectangle)
+        img = self._buffer.convert("RGB").crop(astuple(rectangle))
         img = img.rotate(self._rotation, expand=True)
 
         display_rectangle = self._apply_rotation(rectangle)
-        img = img.crop(self._clip(display_rectangle))
+        img = img.crop(astuple(self._clip(display_rectangle)))
 
         data = numpy.array(img).astype("uint16")
         color = (
@@ -286,9 +320,9 @@ class Display:
             ),
         )
 
-        self._bus._begin_transaction()  # pylint: disable=protected-access
+        self._core.begin_transaction()
         self._send_pixels(pixels)
-        self._bus._end_transaction()  # pylint: disable=protected-access
+        self._core.end_transaction()
 
     def _clip(self, rectangle):
         if self._rotation in (90, 270):
@@ -296,35 +330,31 @@ class Display:
         else:
             width, height = self._width, self._height
 
-        if rectangle.x1 < 0:
-            rectangle.x1 = 0
-        if rectangle.y1 < 0:
-            rectangle.y1 = 0
-        if rectangle.x2 > width:
-            rectangle.x2 = width
-        if rectangle.y2 > height:
-            rectangle.y2 = height
+        rectangle.x1 = max(rectangle.x1, 0)
+        rectangle.y1 = max(rectangle.y1, 0)
+        rectangle.x2 = min(rectangle.x2, width)
+        rectangle.y2 = min(rectangle.y2, height)
 
         return rectangle
 
     def _apply_rotation(self, rectangle):
         """Adjust the rectangle coordinates based on rotation"""
         if self._rotation == 90:
-            return Rectangle(
+            return RectangleStruct(
                 self._height - rectangle.y2,
                 rectangle.x1,
                 self._height - rectangle.y1,
                 rectangle.x2,
             )
         if self._rotation == 180:
-            return Rectangle(
+            return RectangleStruct(
                 self._width - rectangle.x2,
                 self._height - rectangle.y2,
                 self._width - rectangle.x1,
                 self._height - rectangle.y1,
             )
         if self._rotation == 270:
-            return Rectangle(
+            return RectangleStruct(
                 rectangle.y1,
                 self._width - rectangle.x2,
                 rectangle.y2,
@@ -334,11 +364,11 @@ class Display:
 
     def _encode_pos(self, x, y):
         """Encode a postion into bytes."""
-        return struct.pack(self._bounds_encoding, x, y)
+        return struct.pack(self._bounds_encoding, x, y)  # pylint: disable=no-member
 
     def fill_row(
-        self, y: int, buffer: _typing.WriteableBuffer
-    ) -> _typing.WriteableBuffer:
+        self, y: int, buffer: circuitpython_typing.WriteableBuffer
+    ) -> circuitpython_typing.WriteableBuffer:
         """Extract the pixels from a single row"""
         for x in range(0, self._width):
             _rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
@@ -402,25 +432,23 @@ class Display:
     @property
     def width(self) -> int:
         """Display Width"""
-        return self._width
+        return self._core.get_width()
 
     @property
     def height(self) -> int:
         """Display Height"""
-        return self._height
+        return self._core.get_height()
 
     @property
     def rotation(self) -> int:
         """The rotation of the display as an int in degrees."""
-        return self._rotation
+        return self._core.get_rotation()
 
     @rotation.setter
     def rotation(self, value: int):
-        if value not in (0, 90, 180, 270):
-            raise ValueError("Rotation must be 0/90/180/270")
-        self._rotation = value
+        self._core.set_rotation(value)
 
     @property
     def bus(self) -> _DisplayBus:
         """Current Display Bus"""
-        return self._bus
+        return self._core.get_bus()