# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
* Author(s): Melissa LeBlanc-Williams
"""
-
+import threading
from typing import Union
from ._fourwire import FourWire
from ._i2cdisplay import I2CDisplay
from ._palette import Palette
from ._shape import Shape
from ._tilegrid import TileGrid
-from ._display import displays
from ._displaybus import _DisplayBus
+from ._constants import CIRCUITPY_DISPLAY_LIMIT
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+displays = []
+display_buses = []
+
+
+def _background():
+ """Main thread function to loop through all displays and update them"""
+ while True:
+ for display in displays:
+ display._background() # pylint: disable=protected-access
+
+
def release_displays() -> None:
"""Releases any actively used displays so their busses and pins can be used again.
Use this once in your code.py if you initialize a display. Place it right before the
initialization so the display is active as long as possible.
"""
- for _disp in displays:
- _disp._release() # pylint: disable=protected-access
+ for display in displays:
+ display._release() # pylint: disable=protected-access
displays.clear()
+
+ for display_bus in display_buses:
+ display_bus.deinit()
+ display_buses.clear()
+
+
+def allocate_display(new_display: Union[Display, EPaperDisplay]) -> None:
+ """Add a display to the displays pool and return the new display"""
+ if len(displays) >= CIRCUITPY_DISPLAY_LIMIT:
+ raise RuntimeError("Too many displays")
+ displays.append(new_display)
+
+
+def allocate_display_bus(new_display_bus: _DisplayBus) -> None:
+ """Add a display bus to the display_buses pool and return the new display bus"""
+ if len(display_buses) >= CIRCUITPY_DISPLAY_LIMIT:
+ raise RuntimeError(
+ "Too many display busses; forgot displayio.release_displays() ?"
+ )
+ display_buses.append(new_display_bus)
+
+
+background_thread = threading.Thread(target=_background, daemon=True)
+
+
+# Start the background thread
+def _start_background():
+ if not background_thread.is_alive():
+ background_thread.start()
+
+
+def _stop_background():
+ if background_thread.is_alive():
+ # Stop the thread
+ background_thread.join()
+
+
+_start_background()
class Area:
- # pylint: disable=invalid-name,missing-function-docstring
- """Area Class to represent an area to be updated. Currently not used."""
+ """Area Class to represent an area to be updated."""
+ # pylint: disable=invalid-name
def __init__(self, x1: int = 0, y1: int = 0, x2: int = 0, y2: int = 0):
self.x1 = x1
self.y1 = y1
def __str__(self):
return f"Area TL({self.x1},{self.y1}) BR({self.x2},{self.y2})"
- def _copy_into(self, dst) -> None:
+ def copy_into(self, dst) -> None:
+ """Copy the area into another area."""
dst.x1 = self.x1
dst.y1 = self.y1
dst.x2 = self.x2
dst.y2 = self.y2
- def _scale(self, scale: int) -> None:
+ def scale(self, scale: int) -> None:
+ """Scale the area by scale."""
self.x1 *= scale
self.y1 *= scale
self.x2 *= scale
self.y2 *= scale
- def _shift(self, dx: int, dy: int) -> None:
+ def shift(self, dx: int, dy: int) -> None:
+ """Shift the area by dx and dy."""
self.x1 += dx
self.y1 += dy
self.x2 += dx
self.y2 += dy
- def _compute_overlap(self, other, overlap) -> bool:
+ def compute_overlap(self, other, overlap) -> bool:
+ """Compute the overlap between two areas. Returns True if there is an overlap."""
a = self
overlap.x1 = max(a.x1, other.x1)
overlap.x2 = min(a.x2, other.x2)
return overlap.y1 < overlap.y2
- def _empty(self):
+ def empty(self):
+ """Return True if the area is empty."""
return (self.x1 == self.x2) or (self.y1 == self.y2)
- def _canon(self):
+ def canon(self):
+ """Make sure the area is in canonical form."""
if self.x1 > self.x2:
self.x1, self.x2 = self.x2, self.x1
if self.y1 > self.y2:
self.y1, self.y2 = self.y2, self.y1
- def _union(self, other, union):
- # pylint: disable=protected-access
- if self._empty():
- self._copy_into(union)
+ def union(self, other, union):
+ """Combine this area along with another into union"""
+ if self.empty():
+ self.copy_into(union)
return
- if other._empty():
- other._copy_into(union)
+ if other.empty():
+ other.copy_into(union)
return
union.x1 = min(self.x1, other.x1)
union.y2 = max(self.y2, other.y2)
def width(self) -> int:
+ """Return the width of the area."""
return self.x2 - self.x1
def height(self) -> int:
+ """Return the height of the area."""
return self.y2 - self.y1
def size(self) -> int:
+ """Return the size of the area."""
return self.width() * self.height()
def __eq__(self, other):
)
@staticmethod
- def _transform_within(
+ def transform_within(
mirror_x: bool,
mirror_y: bool,
transpose_xy: bool,
whole: Area,
transformed: Area,
):
+ """Transform an area within a larger area."""
# pylint: disable=too-many-arguments
# Original and whole must be in the same coordinate space.
if mirror_x:
"""
from __future__ import annotations
+import struct
+from array import array
from typing import Union, Tuple
-from PIL import Image
-from ._structs import RectangleStruct
+from circuitpython_typing import WriteableBuffer
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+def stride(width: int, bits_per_pixel: int) -> int:
+ """Return the number of bytes per row of a bitmap with the given width and bits per pixel."""
+ row_width = width * bits_per_pixel
+ return (row_width + (31)) // 32
+
+
class Bitmap:
- """Stores values of a certain size in a 2D array"""
+ """Stores values of a certain size in a 2D array
+
+ Bitmaps can be treated as read-only buffers. If the number of bits in a pixel is 8, 16,
+ or 32; and the number of bytes per row is a multiple of 4, then the resulting memoryview
+ will correspond directly with the bitmap's contents. Otherwise, the bitmap data is packed
+ into the memoryview with unspecified padding.
+
+ A Bitmap can be treated as a buffer, allowing its content to be
+ viewed and modified using e.g., with ``ulab.numpy.frombuffer``,
+ but the `displayio.Bitmap.dirty` method must be used to inform
+ displayio when a bitmap was modified through the buffer interface.
+
+ `bitmaptools.arrayblit` can also be useful to move data efficiently
+ into a Bitmap."""
def __init__(self, width: int, height: int, value_count: int):
"""Create a Bitmap object with the given fixed size. Each pixel stores a value that is
share the underlying Bitmap. value_count is used to minimize the memory used to store
the Bitmap.
"""
- self._bmp_width = width
- self._bmp_height = height
- self._read_only = False
- if value_count < 0:
- raise ValueError("value_count must be > 0")
+ if not 1 <= value_count <= 65535:
+ raise ValueError("value_count must be in the range of 1-65535")
bits = 1
while (value_count - 1) >> bits:
else:
bits += 8
- self._bits_per_value = bits
+ self._from_buffer(width, height, bits, None, False)
+
+ def _from_buffer(
+ self,
+ width: int,
+ height: int,
+ bits_per_value: int,
+ data: WriteableBuffer,
+ read_only: bool,
+ ) -> None:
+ # pylint: disable=too-many-arguments
+ self._bmp_width = width
+ self._bmp_height = height
+ self._stride = stride(width, bits_per_value)
+ self._data_alloc = False
+
+ if data is None or len(data) == 0:
+ data = array("L", [0] * self._stride * height)
+ self._data_alloc = True
+ self._data = data
+ self._read_only = read_only
+ self._bits_per_value = bits_per_value
if (
self._bits_per_value > 8
):
raise NotImplementedError("Invalid bits per value")
- self._image = Image.new("P", (width, height), 0)
- self._dirty_area = RectangleStruct(0, 0, width, height)
+ # Division and modulus can be slow because it has to handle any integer. We know
+ # bits_per_value is a power of two. We divide and mod by bits_per_value to compute
+ # the offset into the byte array. So, we can the offset computation to simplify to
+ # a shift for division and mask for mod.
+
+ # Used to divide the index by the number of pixels per word. It's
+ # used in a shift which effectively divides by 2 ** x_shift.
+ self._x_shift = 0
+
+ power_of_two = 1
+ while power_of_two < 32 // bits_per_value:
+ self._x_shift += 1
+ power_of_two = power_of_two << 1
+
+ self._x_mask = (1 << self._x_shift) - 1 # Used as a modulus on the x value
+ self._bitmask = (1 << bits_per_value) - 1
+ self._dirty_area = Area(0, 0, width, height)
def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
"""
else:
raise TypeError("Index is not an int, list, or tuple")
- if x > self._image.width or y > self._image.height:
+ if x > self._bmp_width or x < 0 or y > self._bmp_height or y < 0:
raise ValueError(f"Index {index} is out of range")
- return self._image.getpixel((x, y))
+ return self._get_pixel(x, y)
+
+ def _get_pixel(self, x: int, y: int) -> int:
+ if x >= self._bmp_width or x < 0 or y >= self._bmp_height or y < 0:
+ return 0
+ row_start = y * self._stride
+ bytes_per_value = self._bits_per_value // 8
+ if bytes_per_value < 1:
+ word = self._data[row_start + (x >> self._x_shift)]
+ return (
+ word >> (32 - ((x & self._x_mask) + 1) * self._bits_per_value)
+ ) & self._bitmask
+ row = memoryview(self._data)[row_start : row_start + self._stride]
+ if bytes_per_value == 1:
+ return row[x]
+ if bytes_per_value == 2:
+ return struct.unpack_from("<H", row, x * 2)[0]
+ if bytes_per_value == 4:
+ return struct.unpack_from("<I", row, x * 4)[0]
+ return 0
def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
"""
elif isinstance(index, int):
x = index % self._bmp_width
y = index // self._bmp_width
- self._image.putpixel((x, y), value)
- if self._dirty_area.x1 == self._dirty_area.x2:
- self._dirty_area.x1 = x
- self._dirty_area.x2 = x + 1
- self._dirty_area.y1 = y
- self._dirty_area.y2 = y + 1
+ # update the dirty region
+ self._set_dirty_area(Area(x, y, x + 1, y + 1))
+ self._write_pixel(x, y, value)
+
+ def _write_pixel(self, x: int, y: int, value: int) -> None:
+ if self._read_only:
+ raise RuntimeError("Read-only")
+
+ # Writes the color index value into a pixel position
+ # Must update the dirty area separately
+
+ # Don't write if out of area
+ if x < 0 or x >= self._bmp_width or y < 0 or y >= self._bmp_height:
+ return
+
+ # Update one pixel of data
+ row_start = y * self._stride
+ bytes_per_value = self._bits_per_value // 8
+ if bytes_per_value < 1:
+ bit_position = 32 - ((x & self._x_mask) + 1) * self._bits_per_value
+ index = row_start + (x >> self._x_shift)
+ word = self._data[index]
+ word &= ~(self._bitmask << bit_position)
+ word |= (value & self._bitmask) << bit_position
+ self._data[index] = word
else:
- if x < self._dirty_area.x1:
- self._dirty_area.x1 = x
- elif x >= self._dirty_area.x2:
- self._dirty_area.x2 = x + 1
- if y < self._dirty_area.y1:
- self._dirty_area.y1 = y
- elif y >= self._dirty_area.y2:
- self._dirty_area.y2 = y + 1
+ row = memoryview(self._data)[row_start : row_start + self._stride]
+ if bytes_per_value == 1:
+ row[x] = value
+ elif bytes_per_value == 2:
+ struct.pack_into("<H", row, x * 2, value)
+ elif bytes_per_value == 4:
+ struct.pack_into("<I", row, x * 4, value)
def _finish_refresh(self):
self._dirty_area.x1 = 0
def fill(self, value: int) -> None:
"""Fills the bitmap with the supplied palette index value."""
- self._image = Image.new("P", (self._bmp_width, self._bmp_height), value)
- self._dirty_area = RectangleStruct(0, 0, self._bmp_width, self._bmp_height)
+ if self._read_only:
+ raise RuntimeError("Read-only")
+ self._set_dirty_area(Area(0, 0, self._bmp_width, self._bmp_height))
+
+ # build the packed word
+ word = 0
+ for i in range(32 // self._bits_per_value):
+ word |= (value & self._bitmask) << (32 - ((i + 1) * self._bits_per_value))
+
+ # copy it in
+ for i in range(self._stride * self._bmp_height):
+ self._data[i] = word
def blit(
self,
y2: int,
skip_index: int,
) -> None:
- # pylint: disable=unnecessary-pass, invalid-name
"""Inserts the source_bitmap region defined by rectangular boundaries"""
+ # pylint: disable=invalid-name
if x2 is None:
x2 = source_bitmap.width
if y2 is None:
break
def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None:
- # pylint: disable=unnecessary-pass, invalid-name
"""Inform displayio of bitmap updates done via the buffer protocol."""
- pass
+ # pylint: disable=invalid-name
+ if x2 == -1:
+ x2 = self._bmp_width
+ if y2 == -1:
+ y2 = self._bmp_height
+ self._set_dirty_area(Area(x1, y1, x2, y2))
+
+ def _set_dirty_area(self, dirty_area: Area) -> None:
+ if self._read_only:
+ raise RuntimeError("Read-only")
+
+ area = dirty_area
+ area.canon()
+ area.union(self._dirty_area, area)
+ bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
+ area.compute_overlap(bitmap_area, self._dirty_area)
+
+ def _finish_refresh(self):
+ if self._read_only:
+ return
+ self._dirty_area.x1 = 0
+ self._dirty_area.x2 = 0
+
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
+ return
+ areas.append(self._dirty_area)
@property
def width(self) -> int:
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
from ._colorspace import Colorspace
+from ._structs import ColorspaceStruct, InputPixelStruct, OutputPixelStruct
+from ._helpers import clamp, bswap16
class ColorConverter:
:param bool dither: Adds random noise to dither the output image
"""
self._dither = dither
- self._depth = 16
self._transparent_color = None
- self._rgba = False
+ self._rgba = False # Todo set Output colorspace depth to 32 maybe?
self._input_colorspace = input_colorspace
+ self._output_colorspace = ColorspaceStruct(16)
+ self._cached_colorspace = None
+ self._cached_input_pixel = None
+ self._cached_output_color = None
+ self._needs_refresh = False
- def _compute_rgb565(self, color: int):
- self._depth = 16
- return (color[0] & 0xF8) << 8 | (color[1] & 0xFC) << 3 | color[2] >> 3
+ @staticmethod
+ def _dither_noise_1(noise):
+ noise = (noise >> 13) ^ noise
+ more_noise = (
+ noise * (noise * noise * 60493 + 19990303) + 1376312589
+ ) & 0x7FFFFFFF
+ return clamp(int((more_noise / (1073741824.0 * 2)) * 255), 0, 0xFFFFFFFF)
+
+ @staticmethod
+ def _dither_noise_2(x, y):
+ return ColorConverter._dither_noise_1(x + y * 0xFFFF)
+
+ @staticmethod
+ def _compute_rgb565(color_rgb888: int):
+ red5 = color_rgb888 >> 19
+ grn6 = (color_rgb888 >> 10) & 0x3F
+ blu5 = (color_rgb888 >> 3) & 0x1F
+ return red5 << 11 | grn6 << 5 | blu5
@staticmethod
- def _compute_luma(color: int):
- red = color >> 16
- green = (color >> 8) & 0xFF
- blue = color & 0xFF
- return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
+ def _compute_rgb332(color_rgb888: int):
+ red3 = color_rgb888 >> 21
+ grn2 = (color_rgb888 >> 13) & 0x7
+ blu2 = (color_rgb888 >> 6) & 0x3
+ return red3 << 5 | grn2 << 3 | blu2
@staticmethod
- def _compute_chroma(color: int):
- red = color >> 16
- green = (color >> 8) & 0xFF
- blue = color & 0xFF
- return max(red, green, blue) - min(red, green, blue)
-
- def _compute_hue(self, color: int):
- red = color >> 16
- green = (color >> 8) & 0xFF
- blue = color & 0xFF
- max_color = max(red, green, blue)
- chroma = self._compute_chroma(color)
+ def _compute_rgbd(color_rgb888: int):
+ red1 = (color_rgb888 >> 23) & 0x1
+ grn1 = (color_rgb888 >> 15) & 0x1
+ blu1 = (color_rgb888 >> 7) & 0x1
+ return red1 << 3 | grn1 << 2 | blu1 << 1 # | dummy
+
+ @staticmethod
+ def _compute_luma(color_rgb888: int):
+ red8 = color_rgb888 >> 16
+ grn8 = (color_rgb888 >> 8) & 0xFF
+ blu8 = color_rgb888 & 0xFF
+ return (red8 * 19 + grn8 * 182 + blu8 + 54) // 255
+
+ @staticmethod
+ def _compute_chroma(color_rgb888: int):
+ red8 = color_rgb888 >> 16
+ grn8 = (color_rgb888 >> 8) & 0xFF
+ blu8 = color_rgb888 & 0xFF
+ return max(red8, grn8, blu8) - min(red8, grn8, blu8)
+
+ @staticmethod
+ def _compute_hue(color_rgb888: int):
+ red8 = color_rgb888 >> 16
+ grn8 = (color_rgb888 >> 8) & 0xFF
+ blu8 = color_rgb888 & 0xFF
+ max_color = max(red8, grn8, blu8)
+ chroma = max_color - min(red8, grn8, blu8)
if chroma == 0:
return 0
hue = 0
- if max_color == red:
- hue = (((green - blue) * 40) / chroma) % 240
- elif max_color == green:
- hue = (((blue - red) + (2 * chroma)) * 40) / chroma
- elif max_color == blue:
- hue = (((red - green) + (4 * chroma)) * 40) / chroma
+ if max_color == red8:
+ hue = (((grn8 - blu8) * 40) // chroma) % 240
+ elif max_color == grn8:
+ hue = (((blu8 - red8) + (2 * chroma)) * 40) // chroma
+ elif max_color == blu8:
+ hue = (((red8 - grn8) + (4 * chroma)) * 40) // chroma
if hue < 0:
hue += 240
return hue
@staticmethod
- def _dither_noise_1(noise):
- noise = (noise >> 13) ^ noise
- more_noise = (
- noise * (noise * noise * 60493 + 19990303) + 1376312589
- ) & 0x7FFFFFFF
- return (more_noise / (1073741824.0 * 2)) * 255
-
- def _dither_noise_2(self, x, y):
- return self._dither_noise_1(x + y * 0xFFFF)
+ def _compute_sevencolor(color_rgb888: int):
+ # pylint: disable=too-many-return-statements
+ chroma = ColorConverter._compute_chroma(color_rgb888)
+ if chroma >= 64:
+ hue = ColorConverter._compute_hue(color_rgb888)
+ # Red 0
+ if hue < 10:
+ return 0x4
+ # Orange 21
+ if hue < 21 + 10:
+ return 0x6
+ # Yellow 42
+ if hue < 42 + 21:
+ return 0x5
+ # Green 85
+ if hue < 85 + 42:
+ return 0x2
+ # Blue 170
+ if hue < 170 + 42:
+ return 0x3
+ # The rest is red to 255
+ return 0x4
+ luma = ColorConverter._compute_luma(color_rgb888)
+ if luma >= 128:
+ return 0x1 # White
+ return 0x0 # Black
- def _compute_tricolor(self):
- pass
+ @staticmethod
+ def _compute_tricolor(
+ colorspace: ColorspaceStruct, pixel_hue: int, color: int
+ ) -> int:
+ hue_diff = colorspace.tricolor_hue - pixel_hue
+ if -10 <= hue_diff <= 10 or hue_diff <= -220 or hue_diff >= 220:
+ if colorspace.grayscale:
+ color = 0
+ else:
+ color = 1
+ elif not colorspace.grayscale:
+ color = 0
+ return color
def convert(self, color: int) -> int:
"Converts the given rgb888 color to RGB565"
else:
raise ValueError("Color must be an integer or 3 or 4 value tuple")
- if self._dither:
- return color # To Do: return a dithered color
- if self._rgba:
- return color
- return self._compute_rgb565(color)
+ input_pixel = InputPixelStruct(color)
+ output_pixel = OutputPixelStruct()
+
+ self._convert(self._output_colorspace, input_pixel, output_pixel)
+
+ return output_pixel.pixel
+
+ def _convert(
+ self,
+ colorspace: Colorspace,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
+ ) -> None:
+ pixel = input_pixel.pixel
+
+ if self._transparent_color == pixel:
+ output_color.opaque = False
+ return
+
+ if (
+ not self._dither
+ and self._cached_colorspace == colorspace
+ and self._cached_input_pixel == input_pixel.pixel
+ ):
+ output_color = self._cached_output_color
+ return
+
+ rgb888_pixel = input_pixel
+ rgb888_pixel.pixel = self._convert_pixel(
+ self._input_colorspace, input_pixel.pixel
+ )
+ self._convert_color(colorspace, self._dither, rgb888_pixel, output_color)
+
+ if not self._dither:
+ self._cached_colorspace = colorspace
+ self._cached_input_pixel = input_pixel.pixel
+ self._cached_output_color = output_color.pixel
+
+ @staticmethod
+ def _convert_pixel(colorspace: Colorspace, pixel: int) -> int:
+ pixel = clamp(pixel, 0, 0xFFFFFFFF)
+ if colorspace in (
+ Colorspace.RGB565_SWAPPED,
+ Colorspace.RGB555_SWAPPED,
+ Colorspace.BGR565_SWAPPED,
+ Colorspace.BGR555_SWAPPED,
+ ):
+ pixel = bswap16(pixel)
+ if colorspace in (Colorspace.RGB565, Colorspace.RGB565_SWAPPED):
+ red8 = (pixel >> 11) << 3
+ grn8 = ((pixel >> 5) << 2) & 0xFF
+ blu8 = (pixel << 3) & 0xFF
+ return (red8 << 16) | (grn8 << 8) | blu8
+ if colorspace in (Colorspace.RGB555, Colorspace.RGB555_SWAPPED):
+ red8 = (pixel >> 10) << 3
+ grn8 = ((pixel >> 5) << 3) & 0xFF
+ blu8 = (pixel << 3) & 0xFF
+ return (red8 << 16) | (grn8 << 8) | blu8
+ if colorspace in (Colorspace.BGR565, Colorspace.BGR565_SWAPPED):
+ blu8 = (pixel >> 11) << 3
+ grn8 = ((pixel >> 5) << 2) & 0xFF
+ red8 = (pixel << 3) & 0xFF
+ return (red8 << 16) | (grn8 << 8) | blu8
+ if colorspace in (Colorspace.BGR555, Colorspace.BGR555_SWAPPED):
+ blu8 = (pixel >> 10) << 3
+ grn8 = ((pixel >> 5) << 3) & 0xFF
+ red8 = (pixel << 3) & 0xFF
+ return (red8 << 16) | (grn8 << 8) | blu8
+ if colorspace == Colorspace.L8:
+ return (pixel & 0xFF) & 0x01010101
+ return pixel
+
+ @staticmethod
+ def _convert_color(
+ colorspace: ColorspaceStruct,
+ dither: bool,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
+ ) -> None:
+ # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
+ pixel = input_pixel.pixel
+ if dither:
+ rand_red = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y)
+ rand_grn = ColorConverter._dither_noise_2(input_pixel.x + 33, input_pixel.y)
+ rand_blu = ColorConverter._dither_noise_2(input_pixel.x, input_pixel.y + 33)
+
+ red8 = pixel >> 16
+ grn8 = (pixel >> 8) & 0xFF
+ blu8 = pixel & 0xFF
+
+ if colorspace.depth == 16:
+ blu8 = min(255, blu8 + (rand_blu & 0x07))
+ red8 = min(255, red8 + (rand_red & 0x07))
+ grn8 = min(255, grn8 + (rand_grn & 0x03))
+ else:
+ bitmask = 0xFF >> colorspace.depth
+ blu8 = min(255, blu8 + (rand_blu & bitmask))
+ red8 = min(255, red8 + (rand_red & bitmask))
+ grn8 = min(255, grn8 + (rand_grn & bitmask))
+ pixel = (red8 << 16) | (grn8 << 8) | blu8
+
+ if colorspace.depth == 16:
+ packed = ColorConverter._compute_rgb565(pixel)
+ if colorspace.reverse_bytes_in_word:
+ packed = bswap16(packed)
+ output_color.pixel = packed
+ output_color.opaque = True
+ return
+ if colorspace.tricolor:
+ output_color.pixel = ColorConverter._compute_luma(pixel) >> (
+ 8 - colorspace.depth
+ )
+ if ColorConverter._compute_chroma(pixel) <= 16:
+ if not colorspace.grayscale:
+ output_color.pixel = 0
+ output_color.opaque = True
+ return
+ pixel_hue = ColorConverter._compute_hue(pixel)
+ output_color.pixel = ColorConverter._compute_tricolor(
+ colorspace, pixel_hue, output_color.pixel
+ )
+ return
+ if colorspace.grayscale and colorspace.depth <= 8:
+ bitmask = (1 << colorspace.depth) - 1
+ output_color.pixel = (
+ ColorConverter._compute_luma(pixel) >> colorspace.grayscale_bit
+ ) & bitmask
+ output_color.opaque = True
+ return
+ if colorspace.depth == 32:
+ output_color.pixel = pixel
+ output_color.opaque = True
+ return
+ if colorspace.depth == 8 and colorspace.grayscale:
+ packed = ColorConverter._compute_rgb332(pixel)
+ output_color.pixel = packed
+ output_color.opaque = True
+ return
+ if colorspace.depth == 4:
+ if colorspace.sevencolor:
+ packed = ColorConverter._compute_sevencolor(pixel)
+ else:
+ packed = ColorConverter._compute_rgbd(pixel)
+ output_color.pixel = packed
+ output_color.opaque = True
+ return
+ output_color.opaque = False
def make_transparent(self, color: int) -> None:
"""Set the transparent color or index for the ColorConverter. This will
"""
self._transparent_color = color
- def make_opaque(self, color: int) -> None:
- # pylint: disable=unused-argument
+ def make_opaque(self, _color: int) -> None:
"""Make the ColorConverter be opaque and have no transparent pixels."""
self._transparent_color = None
+ def _finish_refresh(self) -> None:
+ pass
+
@property
def dither(self) -> bool:
"""When true the color converter dithers the output by adding
BACKLIGHT_IN_OUT = 1
BACKLIGHT_PWM = 2
+
+NO_COMMAND = 0x100
+CIRCUITPY_DISPLAY_LIMIT = 1
+
+DELAY = 0x80
"""
import time
-import struct
-import threading
+from array import array
from typing import Optional
-from dataclasses import astuple
import digitalio
-from PIL import Image
-import numpy
import microcontroller
-import circuitpython_typing
+from circuitpython_typing import WriteableBuffer, ReadableBuffer
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,
CHIP_SELECT_UNTOUCHED,
DISPLAY_DATA,
BACKLIGHT_IN_OUT,
BACKLIGHT_PWM,
+ NO_COMMAND,
+ DELAY,
)
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
-displays = []
-
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.
def __init__(
self,
display_bus: _DisplayBus,
- init_sequence: circuitpython_typing.ReadableBuffer,
+ init_sequence: ReadableBuffer,
*,
width: int,
height: int,
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,
- 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`).
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
ram_height = 0x100
if single_byte_bounds:
ram_width = 0xFF
ram_height = 0xFF
+
self._core = _DisplayCore(
bus=display_bus,
width=width,
bytes_per_cell=bytes_per_cell,
reverse_pixels_in_byte=reverse_pixels_in_byte,
reverse_bytes_in_word=reverse_bytes_in_word,
+ column_command=set_column_command,
+ row_command=set_row_command,
+ set_current_column_command=NO_COMMAND,
+ set_current_row_command=NO_COMMAND,
+ data_as_commands=data_as_commands,
+ always_toggle_chip_select=False,
+ sh1107_addressing=(SH1107_addressing and color_depth == 1),
+ address_little_endian=False,
)
- self._set_column_command = set_column_command
- self._set_row_command = set_row_command
self._write_ram_command = write_ram_command
self._brightness_command = brightness_command
- self._data_as_commands = data_as_commands
- self._single_byte_bounds = single_byte_bounds
- self._width = width
- self._height = height
- self._colstart = colstart
- self._rowstart = rowstart
- self._rotation = rotation
- self._auto_brightness = auto_brightness
- self._brightness = 1.0
- self._auto_refresh = auto_refresh
- self._initialize(init_sequence)
- self._buffer = Image.new("RGB", (width, height))
- self._subrectangles = []
- self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
- self._current_group = None
- displays.append(self)
- self._refresh_thread = None
- if self._auto_refresh:
- self.auto_refresh = True
- self._colorconverter = ColorConverter()
+ self._first_manual_refresh = not auto_refresh
+ self._backlight_on_high = backlight_on_high
- self._backlight_type = None
- if backlight_pin is not None:
- try:
- from pwmio import PWMOut # pylint: disable=import-outside-toplevel
+ self._native_frames_per_second = native_frames_per_second
+ self._native_ms_per_frame = 1000 // native_frames_per_second
- # 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
+ self._brightness = brightness
+ self._auto_refresh = auto_refresh
- def _initialize(self, init_sequence):
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._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,
- bytes([command]) + init_sequence[i + 2 : i + 2 + data_size],
+ full_command,
)
else:
self._core.send(
CHIP_SELECT_UNTOUCHED,
init_sequence[i + 2 : i + 2 + data_size],
)
+ self._core.end_transaction()
delay_time_ms = 10
if delay:
data_size += 1
time.sleep(delay_time_ms / 1000)
i += 2 + data_size
- def _send(self, command, data):
- self._core.begin_transaction()
- if self._data_as_commands:
- self._core.send(
- DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
- )
- else:
- self._core.send(
- DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command])
- )
- self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
- self._core.end_transaction()
+ 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,
+ )
- def _send_pixels(self, data):
- if not self._data_as_commands:
+ 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(
DISPLAY_COMMAND,
CHIP_SELECT_TOGGLE_EVERY_BYTE,
bytes([self._write_ram_command]),
)
- self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
+ self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, pixels)
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._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,
target_frames_per_second: Optional[int] = None,
minimum_frames_per_second: int = 0,
) -> bool:
- # 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
When auto refresh is on, updates the display immediately. (The display will also
update without calls to this.)
"""
- if not self._core.start_refresh():
- return False
+ maximum_ms_per_real_frame = 0xFFFFFFFF
+ if minimum_frames_per_second > 0:
+ maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
- # 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)
+ if target_frames_per_second is None:
+ target_ms_per_frame = 0xFFFFFFFF
+ else:
+ target_ms_per_frame = 1000 // target_frames_per_second
+
+ if (
+ not self._auto_refresh
+ and not self._first_manual_refresh
+ and target_ms_per_frame != 0xFFFFFFFF
+ ):
+ current_time = time.monotonic() * 1000
+ current_ms_since_real_refresh = current_time - self._core.last_refresh
+ if current_ms_since_real_refresh > maximum_ms_per_real_frame:
+ raise RuntimeError("Below minimum frame rate")
+ current_ms_since_last_call = current_time - self._last_refresh_call
+ self._last_refresh_call = current_time
+ if current_ms_since_last_call > target_ms_per_frame:
+ return False
+
+ remaining_time = target_ms_per_frame - (
+ current_ms_since_real_refresh % target_ms_per_frame
+ )
+ time.sleep(remaining_time / 1000)
+ self._first_manual_refresh = False
+ self._refresh_display()
+ return True
- self._subrectangles = self._core.get_refresh_areas()
+ def _refresh_display(self):
+ if not self._core.start_refresh():
+ return False
- for area in self._subrectangles:
- self._refresh_display_area(area)
+ areas_to_refresh = self._get_refresh_areas()
+ for area in areas_to_refresh:
+ self._refresh_area(area)
self._core.finish_refresh()
return True
- def _refresh_loop(self):
- while self._auto_refresh:
+ def _get_refresh_areas(self) -> list[Area]:
+ """Get a list of areas to be refreshed"""
+ areas = []
+ 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
+
+ def _background(self):
+ """Run background refresh tasks. Do not call directly"""
+ if (
+ self._auto_refresh
+ and (time.monotonic() * 1000 - self._core.last_refresh)
+ > self._native_ms_per_frame
+ ):
self.refresh()
- def _refresh_display_area(self, rectangle):
- """Loop through dirty rectangles and redraw that area."""
- img = self._buffer.convert("RGB").crop(astuple(rectangle))
- img = img.rotate(360 - self._rotation, expand=True)
-
- display_rectangle = self._apply_rotation(rectangle)
- img = img.crop(astuple(self._clip(display_rectangle)))
-
- data = numpy.array(img).astype("uint16")
- color = (
- ((data[:, :, 0] & 0xF8) << 8)
- | ((data[:, :, 1] & 0xFC) << 3)
- | (data[:, :, 2] >> 3)
- )
-
- pixels = bytes(
- numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
- )
-
- self._send(
- self._set_column_command,
- self._encode_pos(
- display_rectangle.x1 + self._colstart,
- display_rectangle.x2 + self._colstart - 1,
- ),
- )
- self._send(
- self._set_row_command,
- self._encode_pos(
- display_rectangle.y1 + self._rowstart,
- display_rectangle.y2 + self._rowstart - 1,
- ),
- )
+ def _refresh_area(self, area) -> bool:
+ """Loop through dirty areas and redraw that area."""
+ # pylint: disable=too-many-locals, too-many-branches
+
+ 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()
+ pixels_per_word = 32 // self._core.colorspace.depth
+ pixels_per_buffer = clipped.size()
+
+ # 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
+ elif clipped.size() > buffer_size * pixels_per_word:
+ 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
+ ):
+ pixels_per_byte = 8 // self._core.colorspace.depth
+ if rows_per_buffer % pixels_per_byte != 0:
+ rows_per_buffer -= rows_per_buffer % pixels_per_byte
+ subrectangles = clipped.height() // rows_per_buffer
+ if clipped.height() % rows_per_buffer != 0:
+ subrectangles += 1
+ pixels_per_buffer = rows_per_buffer * clipped.width()
+ buffer_size = pixels_per_buffer // pixels_per_word
+ if pixels_per_buffer % pixels_per_word:
+ buffer_size += 1
+ mask_length = (pixels_per_buffer // 8) + 1 # 1 bit per pixel + 1
+ 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),
+ )
+ 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.colorspace.depth // 8
+ )
+ else:
+ subrectangle_size_bytes = subrectangle.size() // (
+ 8 // self._core.colorspace.depth
+ )
- self._core.begin_transaction()
- self._send_pixels(pixels)
- self._core.end_transaction()
+ buffer = memoryview(bytearray([0] * (buffer_size * 4)))
+ mask = memoryview(bytearray([0] * mask_length))
+ self._core.fill_area(subrectangle, mask, buffer)
- def _clip(self, rectangle):
- if self._rotation in (90, 270):
- width, height = self._height, self._width
- else:
- width, height = self._width, self._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 RectangleStruct(
- self._height - rectangle.y2,
- rectangle.x1,
- self._height - rectangle.y1,
- rectangle.x2,
- )
- if self._rotation == 180:
- return RectangleStruct(
- self._width - rectangle.x2,
- self._height - rectangle.y2,
- self._width - rectangle.x1,
- self._height - rectangle.y1,
- )
- if self._rotation == 270:
- return RectangleStruct(
- rectangle.y1,
- self._width - rectangle.x2,
- rectangle.y2,
- self._width - rectangle.x1,
- )
- return rectangle
+ # Can't acquire display bus; skip the rest of the data.
+ if not self._core.bus_free():
+ return False
- def _encode_pos(self, x, y):
- """Encode a postion into bytes."""
- return struct.pack(self._bounds_encoding, x, y) # pylint: disable=no-member
+ self._core.begin_transaction()
+ self._send_pixels(buffer[:subrectangle_size_bytes])
+ self._core.end_transaction()
+ return True
- 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"""
- for x in range(0, self._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 = bytearray([0] * (buffer_size * 4))
+ mask_length = (pixels_per_buffer // 32) + 1
+ mask = array("L", [0x00000000] * mask_length)
+ self._core.fill_area(area, mask, buffer)
return buffer
+ def _release(self) -> None:
+ """Release the display and free its resources"""
+ self.auto_refresh = False
+ self._core.release_display_core()
+
+ def _reset(self) -> None:
+ """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:
"""True when the display is refreshed automatically."""
@auto_refresh.setter
def auto_refresh(self, value: bool):
+ self._first_manual_refresh = not value
self._auto_refresh = value
- if self._refresh_thread is None:
- self._refresh_thread = threading.Thread(
- target=self._refresh_loop, daemon=True
- )
- if value and not self._refresh_thread.is_alive():
- # Start the thread
- self._refresh_thread.start()
- elif not value and self._refresh_thread.is_alive():
- # Stop the thread
- self._refresh_thread.join()
- self._refresh_thread = None
@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
def brightness(self, value: float):
if 0 <= float(value) <= 1.0:
- self._brightness = value
- if self._backlight_type == BACKLIGHT_IN_OUT:
- self._backlight.value = round(self._brightness)
- elif self._backlight_type == BACKLIGHT_PWM:
- self._backlight.duty_cycle = self._brightness * 65535
+ if not self._backlight_on_high:
+ value = 1.0 - value
+
+ if self._backlight_type == BACKLIGHT_PWM:
+ self._backlight.duty_cycle = value * 0xFFFF
+ elif self._backlight_type == BACKLIGHT_IN_OUT:
+ self._backlight.value = value > 0.99
elif self._brightness_command is not None:
- self._send(self._brightness_command, round(value * 255))
+ 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")
- @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"""
@rotation.setter
def rotation(self, value: int):
+ if value % 90 != 0:
+ raise ValueError("Display rotation must be in 90 degree increments")
self._core.set_rotation(value)
@property
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
-from typing import Union
-import circuitpython_typing
+import time
+import struct
+from circuitpython_typing import WriteableBuffer, ReadableBuffer
from paralleldisplay import ParallelBus
from ._fourwire import FourWire
from ._group import Group
from ._i2cdisplay import I2CDisplay
-from ._structs import ColorspaceStruct, TransformStruct, RectangleStruct
+from ._structs import ColorspaceStruct, TransformStruct
from ._area import Area
-
-displays = []
+from ._displaybus import _DisplayBus
+from ._helpers import bswap16
+from ._constants import (
+ CHIP_SELECT_UNTOUCHED,
+ CHIP_SELECT_TOGGLE_EVERY_BYTE,
+ DISPLAY_COMMAND,
+ DISPLAY_DATA,
+ NO_COMMAND,
+)
class _DisplayCore:
- # pylint: disable=too-many-arguments, too-many-instance-attributes
+ # pylint: disable=too-many-arguments, too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements
def __init__(
self,
bytes_per_cell: int,
reverse_pixels_in_byte: bool,
reverse_bytes_in_word: bool,
+ column_command: int,
+ row_command: int,
+ set_current_column_command: int,
+ set_current_row_command: int,
+ data_as_commands: bool,
+ always_toggle_chip_select: bool,
+ sh1107_addressing: bool,
+ address_little_endian: bool,
):
- self._colorspace = ColorspaceStruct(
+ self.colorspace = ColorspaceStruct(
depth=color_depth,
grayscale=grayscale,
grayscale_bit=8 - color_depth,
reverse_bytes_in_word=reverse_bytes_in_word,
dither=False,
)
- self._current_group = None
- self._colstart = colstart
- self._rowstart = rowstart
- self._last_refresh = 0
- self._refresh_in_progress = False
- self._full_refresh = False
+ self.current_group = None
+ self.colstart = colstart
+ self.rowstart = rowstart
+ self.last_refresh = 0
+
+ self.column_command = column_command
+ self.row_command = row_command
+ self.set_current_column_command = set_current_column_command
+ self.set_current_row_command = set_current_row_command
+ self.data_as_commands = data_as_commands
+ self.always_toggle_chip_select = always_toggle_chip_select
+ self.sh1107_addressing = sh1107_addressing
+ self.address_little_endian = address_little_endian
+
+ self.refresh_in_progress = False
+ self.full_refresh = False
+ self.last_refresh = 0
if bus:
if isinstance(bus, (FourWire, I2CDisplay, ParallelBus)):
self._bus_reset = bus.reset
+ self._bus_free = bus._free
self._begin_transaction = bus._begin_transaction
self._send = bus._send
self._end_transaction = bus._end_transaction
raise ValueError("Unsupported display bus type")
self._bus = bus
- self._area = Area(0, 0, width, height)
+ self.area = Area(0, 0, width, height)
- self._width = width
- self._height = height
- self._ram_width = ram_width
- self._ram_height = ram_height
- self._rotation = rotation
- self._transform = TransformStruct()
+ self.width = width
+ self.height = height
+ self.ram_width = ram_width
+ self.ram_height = ram_height
+ self.rotation = rotation
+ self.transform = TransformStruct()
def set_rotation(self, rotation: int) -> None:
"""
Sets the rotation of the display as an int in degrees.
"""
# pylint: disable=protected-access, too-many-branches
- transposed = self._rotation in (90, 270)
+ transposed = self.rotation in (90, 270)
will_be_transposed = rotation in (90, 270)
if transposed != will_be_transposed:
- self._width, self._height = self._height, self._width
+ self.width, self.height = self.height, self.width
- height = self._height
- width = self._width
+ height = self.height
+ width = self.width
rotation %= 360
- self._rotation = rotation
- self._transform.x = 0
- self._transform.y = 0
- self._transform.scale = 1
- self._transform.mirror_x = False
- self._transform.mirror_y = False
- self._transform.transpose_xy = False
+ self.rotation = rotation
+ self.transform.x = 0
+ self.transform.y = 0
+ self.transform.scale = 1
+ self.transform.mirror_x = False
+ self.transform.mirror_y = False
+ self.transform.transpose_xy = False
if rotation in (0, 180):
if rotation == 180:
- self._transform.mirror_x = True
- self._transform.mirror_y = True
+ self.transform.mirror_x = True
+ self.transform.mirror_y = True
else:
- self._transform.transpose_xy = True
+ self.transform.transpose_xy = True
if rotation == 270:
- self._transform.mirror_y = True
+ self.transform.mirror_y = True
else:
- self._transform.mirror_x = True
-
- self._area.x1 = 0
- self._area.y1 = 0
- self._area.next = None
-
- self._transform.dx = 1
- self._transform.dy = 1
- if self._transform.transpose_xy:
- self._area.x2 = height
- self._area.y2 = width
- if self._transform.mirror_x:
- self._transform.x = height
- self._transform.dx = -1
- if self._transform.mirror_y:
- self._transform.y = width
- self._transform.dy = -1
+ self.transform.mirror_x = True
+
+ self.area.x1 = 0
+ self.area.y1 = 0
+ self.area.next = None
+
+ self.transform.dx = 1
+ self.transform.dy = 1
+ if self.transform.transpose_xy:
+ self.area.x2 = height
+ self.area.y2 = width
+ if self.transform.mirror_x:
+ self.transform.x = height
+ self.transform.dx = -1
+ if self.transform.mirror_y:
+ self.transform.y = width
+ self.transform.dy = -1
else:
- self._area.x2 = width
- self._area.y2 = height
- if self._transform.mirror_x:
- self._transform.x = width
- self._transform.dx = -1
- if self._transform.mirror_y:
- self._transform.y = height
- self._transform.dy = -1
-
- if self._current_group is not None:
- self._current_group._update_transform(self._transform)
-
- def show(self, root_group: Group) -> bool:
- # pylint: disable=protected-access
-
+ self.area.x2 = width
+ self.area.y2 = height
+ if self.transform.mirror_x:
+ self.transform.x = width
+ self.transform.dx = -1
+ if self.transform.mirror_y:
+ self.transform.y = height
+ self.transform.dy = -1
+
+ if self.current_group is not None:
+ self.current_group._update_transform(self.transform)
+
+ def set_root_group(self, root_group: Group) -> bool:
"""
Switches to displaying the given group of layers. When group is `None`, the
default CircuitPython terminal will be shown.
:param Optional[displayio.Group] root_group: The group to show.
"""
+ # pylint: disable=protected-access
- """
- # TODO: Implement Supervisor
- if root_group is None:
- circuitpython_splash = _Supervisor().circuitpython_splash
- if not circuitpython_splash._in_group:
- root_group = circuitpython_splash
- elif self._current_group == circuitpython_splash:
- return True
- """
-
- if root_group == self._current_group:
+ if root_group == self.current_group:
return True
if root_group is not None and root_group._in_group:
return False
- if self._current_group is not None:
- self._current_group._in_group = False
+ if self.current_group is not None:
+ self.current_group._in_group = False
if root_group is not None:
- root_group._update_transform(self._transform)
+ root_group._update_transform(self.transform)
root_group._in_group = True
- self._current_group = root_group
- self._full_refresh = True
+ self.current_group = root_group
+ self.full_refresh = True
return True
# pylint: disable=protected-access
"""Mark the display core as currently being refreshed"""
- if self._refresh_in_progress:
+ if self.refresh_in_progress:
return False
- self._refresh_in_progress = True
- # self._last_refresh = _Supervisor()._ticks_ms64()
+ self.refresh_in_progress = True
+ self.last_refresh = time.monotonic() * 1000
return True
def finish_refresh(self) -> None:
# pylint: disable=protected-access
"""Unmark the display core as currently being refreshed"""
- if self._current_group is not None:
- self._current_group._finish_refresh()
+ if self.current_group is not None:
+ self.current_group._finish_refresh()
- self._full_refresh = False
- self._refresh_in_progress = False
- # self._last_refresh = _Supervisor()._ticks_ms64()
+ self.full_refresh = False
+ self.refresh_in_progress = False
+ self.last_refresh = time.monotonic() * 1000
- def get_refresh_areas(self) -> list:
- """Get a list of areas to be refreshed"""
- subrectangles = []
- if self._current_group is not None:
- # Eventually calculate dirty rectangles here
- subrectangles.append(RectangleStruct(0, 0, self._width, self._height))
- return subrectangles
-
- def release(self) -> None:
+ def release_display_core(self) -> None:
"""Release the display from the current group"""
# pylint: disable=protected-access
- if self._current_group is not None:
- self._current_group._in_group = False
+ if self.current_group is not None:
+ self.current_group._in_group = False
def fill_area(
self,
area: Area,
- mask: circuitpython_typing.WriteableBuffer,
- buffer: circuitpython_typing.WriteableBuffer,
+ mask: WriteableBuffer,
+ buffer: WriteableBuffer,
) -> bool:
- # pylint: disable=protected-access
"""Call the current group's fill area function"""
-
- return self._current_group._fill_area(self._colorspace, area, mask, buffer)
+ if self.current_group is not None:
+ return self.current_group._fill_area( # pylint: disable=protected-access
+ self.colorspace, area, mask, buffer
+ )
+ return False
def clip_area(self, area: Area, clipped: Area) -> bool:
"""Shrink the area to the region shared by the two areas"""
- # pylint: disable=protected-access
- overlaps = self._area._compute_overlap(area, clipped)
+ overlaps = self.area.compute_overlap(area, clipped)
if not overlaps:
return False
- # Expand the area if we have multiple pixels per byte and we need to byte align the bounds
- if self._colorspace.depth < 8:
+ # Expand the area if we have multiple pixels per byte and we need to byte
+ # align the bounds
+ if self.colorspace.depth < 8:
pixels_per_byte = (
- 8 // self._colorspace.depth * self._colorspace.bytes_per_cell
+ 8 // self.colorspace.depth * self.colorspace.bytes_per_cell
)
- if self._colorspace.pixels_in_byte_share_row:
+ if self.colorspace.pixels_in_byte_share_row:
if clipped.x1 % pixels_per_byte != 0:
clipped.x1 -= clipped.x1 % pixels_per_byte
if clipped.x2 % pixels_per_byte != 0:
return True
+ def set_region_to_update(self, area: Area) -> None:
+ """Set the region to update"""
+ region_x1 = area.x1 + self.colstart
+ region_x2 = area.x2 + self.colstart
+ region_y1 = area.y1 + self.rowstart
+ region_y2 = area.y2 + self.rowstart
+
+ if self.colorspace.depth < 8:
+ pixels_per_byte = 8 // self.colorspace.depth
+ if self.colorspace.pixels_in_byte_share_row:
+ region_x1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_x2 //= pixels_per_byte * self.colorspace.bytes_per_cell
+ else:
+ region_y1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+ region_y2 //= pixels_per_byte * self.colorspace.bytes_per_cell
+
+ region_x2 -= 1
+ region_y2 -= 1
+
+ chip_select = CHIP_SELECT_UNTOUCHED
+ if self.always_toggle_chip_select or self.data_as_commands:
+ chip_select = CHIP_SELECT_TOGGLE_EVERY_BYTE
+
+ # Set column
+ self.begin_transaction()
+ data = bytearray([self.column_command])
+ data_type = DISPLAY_DATA
+ if not self.data_as_commands:
+ self.send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, data)
+ data = bytearray(0)
+ else:
+ data_type = DISPLAY_COMMAND
+
+ if self.ram_width < 0x100: # Single Byte Bounds
+ data += struct.pack(">BB", region_x1, region_x2)
+ else:
+ if self.address_little_endian:
+ region_x1 = bswap16(region_x1)
+ region_x2 = bswap16(region_x2)
+ data += struct.pack(">HH", region_x1, region_x2)
+
+ # Quirk for SH1107 "SH1107_addressing"
+ # Column lower command = 0x00, Column upper command = 0x10
+ if self.sh1107_addressing:
+ data = struct.pack(
+ ">BB",
+ ((region_x1 >> 4) & 0xF0) | 0x10, # 0x10 to 0x17
+ region_x1 & 0x0F, # 0x00 to 0x0F
+ )
+
+ self.send(data_type, chip_select, data)
+ self.end_transaction()
+
+ if self.set_current_column_command != NO_COMMAND:
+ self.begin_transaction()
+ self.send(
+ DISPLAY_COMMAND, chip_select, bytes([self.set_current_column_command])
+ )
+ # Only send the first half of data because it is the first coordinate.
+ self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
+ self.end_transaction()
+
+ # Set row
+ self.begin_transaction()
+ data = bytearray([self.row_command])
+
+ if not self.data_as_commands:
+ self.send(DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, data)
+ data = bytearray(0)
+ if self.ram_height < 0x100: # Single Byte Bounds
+ data += struct.pack(">BB", region_y1, region_y2)
+ else:
+ if self.address_little_endian:
+ region_y1 = bswap16(region_y1)
+ region_y2 = bswap16(region_y2)
+ data += struct.pack(">HH", region_y1, region_y2)
+
+ # Quirk for SH1107 "SH1107_addressing"
+ # Page address command = 0xB0
+ if self.sh1107_addressing:
+ data = struct.pack(">B", 0xB0 | region_y1)
+
+ self.send(data_type, chip_select, data)
+ self.end_transaction()
+
+ if self.set_current_row_command != NO_COMMAND:
+ self.begin_transaction()
+ self.send(
+ DISPLAY_COMMAND, chip_select, bytes([self.set_current_row_command])
+ )
+ # Only send the first half of data because it is the first coordinate.
+ self.send(DISPLAY_DATA, chip_select, data[: len(data) // 2])
+ self.end_transaction()
+
def send(
self,
data_type: int,
chip_select: int,
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
) -> None:
"""
Send the data to the current bus
"""
self._send(data_type, chip_select, data)
- def begin_transaction(self) -> None:
+ def bus_free(self) -> bool:
+ """
+ Check if the bus is free
+ """
+ return self._bus_free()
+
+ def begin_transaction(self) -> bool:
"""
Begin Bus Transaction
"""
- self._begin_transaction()
+ return self._begin_transaction()
def end_transaction(self) -> None:
"""
"""
Gets the width of the display in pixels.
"""
- return self._width
+ return self.width
def get_height(self) -> int:
"""
Gets the height of the display in pixels.
"""
- return self._height
+ return self.height
def get_rotation(self) -> int:
"""
Gets the rotation of the display as an int in degrees.
"""
- return self._rotation
+ return self.rotation
- def get_bus(self) -> Union[FourWire, ParallelBus, I2CDisplay]:
+ def get_bus(self) -> _DisplayBus:
"""
The bus being used by the display. [readonly]
"""
from typing import Optional
import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
from ._group import Group
from ._displaybus import _DisplayBus
def __init__(
self,
display_bus: _DisplayBus,
- start_sequence: circuitpython_typing.ReadableBuffer,
- stop_sequence: circuitpython_typing.ReadableBuffer,
+ start_sequence: ReadableBuffer,
+ stop_sequence: ReadableBuffer,
*,
width: int,
height: int,
:param display_bus: The bus that the display is connected to
:type _DisplayBus: displayio.FourWire or displayio.ParallelBus
- :param ~circuitpython_typing.ReadableBuffer start_sequence: Byte-packed
+ :param ~ReadableBuffer start_sequence: Byte-packed
initialization sequence.
- :param ~circuitpython_typing.ReadableBuffer stop_sequence: Byte-packed
+ :param ~ReadableBuffer stop_sequence: Byte-packed
initialization sequence.
:param int width: Width in pixels
:param int height: Height in pixels
import digitalio
import busio
import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
from ._constants import (
CHIP_SELECT_TOGGLE_EVERY_BYTE,
CHIP_SELECT_UNTOUCHED,
def send(
self,
command,
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
*,
toggle_every_byte: bool = False,
) -> None:
self,
data_type: int,
chip_select: int,
- data: circuitpython_typing.ReadableBuffer,
+ data: ReadableBuffer,
):
self._dc.value = data_type == DISPLAY_DATA
if chip_select == CHIP_SELECT_TOGGLE_EVERY_BYTE:
else:
self._spi.write(data)
- def _begin_transaction(self):
+ def _free(self) -> bool:
+ """Attempt to free the bus and return False if busy"""
+ if not self._spi.try_lock():
+ return False
+ self._spi.unlock()
+ return True
+
+ def _begin_transaction(self) -> bool:
"""Begin the SPI transaction by locking, configuring, and setting Chip Select"""
- while not self._spi.try_lock():
- pass
+ if not self._spi.try_lock():
+ return False
self._spi.configure(
baudrate=self._frequency, polarity=self._polarity, phase=self._phase
)
self._chip_select.value = False
+ return True
- def _end_transaction(self):
+ def _end_transaction(self) -> None:
"""End the SPI transaction by unlocking and setting Chip Select"""
self._chip_select.value = True
self._spi.unlock()
from __future__ import annotations
from typing import Union, Callable
+from circuitpython_typing import WriteableBuffer
from ._structs import TransformStruct
from ._tilegrid import TileGrid
+from ._colorspace import Colorspace
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
class Group:
+ # pylint: disable=too-many-instance-attributes
"""
Manage a group of sprites and groups and how they are inter-related.
if not isinstance(scale, int) or scale < 1:
raise ValueError("Scale must be >= 1")
self._scale = 1 # Use the setter below to actually set the scale
+ self._name = "Group"
self._group_x = x
self._group_y = y
self._hidden_group = False
+ self._hidden_by_parent = False
self._layers = []
self._supported_types = (TileGrid, Group)
self._in_group = False
+ self._item_removed = False
self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
self._set_scale(scale) # Set the scale via the setter
"""Deletes the value at the given index."""
del self._layers[index]
- def _fill_area(self, buffer):
- if self._hidden_group:
- return
-
- for layer in self._layers:
- if isinstance(layer, (Group, TileGrid)):
- layer._fill_area(buffer) # pylint: disable=protected-access
+ def _fill_area(
+ self,
+ colorspace: Colorspace,
+ area: Area,
+ mask: WriteableBuffer,
+ buffer: WriteableBuffer,
+ ) -> bool:
+ if not self._hidden_group:
+ for layer in reversed(self._layers):
+ if isinstance(layer, (Group, TileGrid)):
+ if layer._fill_area( # pylint: disable=protected-access
+ colorspace, area, mask, buffer
+ ):
+ return True
+ return False
def sort(self, key: Callable, reverse: bool) -> None:
"""Sort the members of the group."""
self._layers.sort(key=key, reverse=reverse)
def _finish_refresh(self):
- for layer in self._layers:
+ for layer in reversed(self._layers):
if isinstance(layer, (Group, TileGrid)):
layer._finish_refresh() # pylint: disable=protected-access
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ # pylint: disable=protected-access
+ for layer in reversed(self._layers):
+ if isinstance(layer, Group):
+ layer._get_refresh_areas(areas)
+ elif isinstance(layer, TileGrid):
+ if not layer._get_rendered_hidden():
+ layer._get_refresh_areas(areas)
+
+ def _set_hidden(self, hidden: bool) -> None:
+ if self._hidden_group == hidden:
+ return
+ self._hidden_group = hidden
+ if self._hidden_by_parent:
+ return
+ for layer in self._layers:
+ if isinstance(layer, (Group, TileGrid)):
+ layer._set_hidden_by_parent(hidden) # pylint: disable=protected-access
+
+ def _set_hidden_by_parent(self, hidden: bool) -> None:
+ if self._hidden_by_parent == hidden:
+ return
+ self._hidden_by_parent = hidden
+ if self._hidden_group:
+ return
+ for layer in self._layers:
+ if isinstance(layer, (Group, TileGrid)):
+ layer._set_hidden_by_parent(hidden) # pylint: disable=protected-access
+
@property
def hidden(self) -> bool:
"""True when the Group and all of it's layers are not visible. When False, the
return self._hidden_group
@hidden.setter
- def hidden(self, value: bool):
+ def hidden(self, value: bool) -> None:
if not isinstance(value, (bool, int)):
raise ValueError("Expecting a boolean or integer value")
- self._hidden_group = bool(value)
+ value = bool(value)
+ self._set_hidden(value)
@property
def scale(self) -> int:
self._absolute_transform.y += dy_value * (value - self._group_y)
self._group_y = value
self._update_child_transforms()
+
+
+circuitpython_splash = Group(scale=2, x=0, y=0)
--- /dev/null
+# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+
+"""
+`displayio.helpers`
+================================================================================
+
+displayio for Blinka
+
+**Software and Dependencies:**
+
+* Adafruit Blinka:
+ https://github.com/adafruit/Adafruit_Blinka/releases
+
+* Author(s): Melissa LeBlanc-Williams
+
+"""
+
+__version__ = "0.0.0+auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+
+
+def clamp(value, min_value, max_value):
+ """Clamp a value between a minimum and maximum value"""
+ return max(min(max_value, value), min_value)
+
+
+def bswap16(value):
+ """Swap the bytes in a 16 bit value"""
+ return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
+
+
+def read_word(header: memoryview, index: int) -> int:
+ """Read a 32-bit value from a memoryview cast as 16-bit values"""
+ return header[index] | header[index + 1] << 16
import time
import busio
import digitalio
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
from ._constants import CHIP_SELECT_UNTOUCHED, DISPLAY_COMMAND
__version__ = "0.0.0+auto.0"
self._i2c = i2c_bus
self._dev_addr = device_address
+ def __new__(cls, *args, **kwargs):
+ from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
+ allocate_display_bus,
+ )
+
+ display_bus_instance = super().__new__(cls)
+ allocate_display_bus(display_bus_instance)
+ return display_bus_instance
+
def _release(self):
self.reset()
self._i2c.deinit()
time.sleep(0.0001)
self._reset.value = True
- def _begin_transaction(self) -> None:
- """Lock the bus before sending data."""
- while not self._i2c.try_lock():
- pass
-
- def send(self, command: int, data: circuitpython_typing.ReadableBuffer) -> None:
+ def send(self, command: int, data: ReadableBuffer) -> None:
"""
Sends the given command value followed by the full set of data. Display state,
such as vertical scroll, set via ``send`` may or may not be reset once the code is
def _send(
self,
data_type: int,
- chip_select: int,
- data: circuitpython_typing.ReadableBuffer,
+ _chip_select: int, # Chip select behavior
+ data: ReadableBuffer,
):
- # pylint: disable=unused-argument
if data_type == DISPLAY_COMMAND:
n = len(data)
if n > 0:
command_bytes[2 * i] = 0x80
command_bytes[2 * i + 1] = data[i]
- self._i2c.writeto(self._dev_addr, buffer=command_bytes, stop=True)
+ try:
+ self._i2c.writeto(self._dev_addr, buffer=command_bytes)
+ except OSError as error:
+ if error.errno == 121:
+ raise RuntimeError(
+ f"I2C write error to 0x{self._dev_addr:02x}"
+ ) from error
+ raise error
else:
data_bytes = bytearray(len(data) + 1)
data_bytes[0] = 0x40
data_bytes[1:] = data
- self._i2c.writeto(self._dev_addr, buffer=data_bytes, stop=True)
+ try:
+ self._i2c.writeto(self._dev_addr, buffer=data_bytes)
+ except OSError as error:
+ if error.errno == 121:
+ raise RuntimeError(
+ f"I2C write error to 0x{self._dev_addr:02x}"
+ ) from error
+ raise error
+
+ def _free(self) -> bool:
+ """Attempt to free the bus and return False if busy"""
+ if not self._i2c.try_lock():
+ return False
+ self._i2c.unlock()
+ return True
+
+ def _begin_transaction(self) -> bool:
+ """Lock the bus before sending data."""
+ return self._i2c.try_lock()
def _end_transaction(self) -> None:
"""Release the bus after sending data."""
# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
+# SPDX-FileCopyrightText: 2021 James Carr
#
# SPDX-License-Identifier: MIT
* Adafruit Blinka:
https://github.com/adafruit/Adafruit_Blinka/releases
-* Author(s): Melissa LeBlanc-Williams
+* Author(s): Melissa LeBlanc-Williams, James Carr
"""
from typing import Union, BinaryIO
-from PIL import Image
+from ._helpers import read_word
from ._colorconverter import ColorConverter
from ._palette import Palette
class OnDiskBitmap:
+ # pylint: disable=too-many-instance-attributes
"""
- Loads values straight from disk. This minimizes memory use but can lead to much slower
- pixel load times. These load times may result in frame tearing where only part of the
- image is visible."""
+ Loads values straight from disk. This minimizes memory use but can lead to much slower pixel
+ load times. These load times may result in frame tearing where only part of the image is
+ visible.
- def __init__(self, file: Union[str, BinaryIO]):
- self._image = Image.open(file).convert("RGBA")
+ It's easiest to use on a board with a built in display such as the `Hallowing M0 Express
+ <https://www.adafruit.com/product/3900>`_.
+
+ .. code-block:: Python
+
+ import board
+ import displayio
+ import time
+ import pulseio
+
+ board.DISPLAY.auto_brightness = False
+ board.DISPLAY.brightness = 0
+ splash = displayio.Group()
+ board.DISPLAY.show(splash)
+
+ odb = displayio.OnDiskBitmap(\'/sample.bmp\')
+ face = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
+ splash.append(face)
+ # Wait for the image to load.
+ board.DISPLAY.refresh(target_frames_per_second=60)
+
+ # Fade up the backlight
+ for i in range(100):
+ board.DISPLAY.brightness = 0.01 * i
+ time.sleep(0.05)
+
+ # Wait forever
+ while True:
+ pass
+
+ """
+
+ def __init__(self, file: Union[str, BinaryIO]) -> None:
+ # pylint: disable=too-many-locals, too-many-branches, too-many-statements
+ """
+ Create an OnDiskBitmap object with the given file.
+
+ :param file file: The name of the bitmap file. For backwards compatibility, a file opened
+ in binary mode may also be passed.
+
+ Older versions of CircuitPython required a file opened in binary mode. CircuitPython 7.0
+ modified OnDiskBitmap so that it takes a filename instead, and opens the file internally.
+ A future version of CircuitPython will remove the ability to pass in an opened file.
+ """
+
+ if isinstance(file, str):
+ file = open(file, "rb") # pylint: disable=consider-using-with
+
+ if not (file.readable() and file.seekable()):
+ raise TypeError("file must be a file opened in byte mode")
+
+ self._pixel_shader_base: Union[ColorConverter, Palette, None] = None
+
+ try:
+ self._file = file
+ file.seek(0)
+ bmp_header = memoryview(file.read(138)).cast(
+ "H"
+ ) # cast as unsigned 16-bit int
+
+ if len(bmp_header.tobytes()) != 138 or bmp_header.tobytes()[0:2] != b"BM":
+ raise ValueError("Invalid BMP file")
+
+ self._data_offset = read_word(bmp_header, 5)
+
+ header_size = read_word(bmp_header, 7)
+ bits_per_pixel = bmp_header[14]
+ compression = read_word(bmp_header, 15)
+ number_of_colors = read_word(bmp_header, 23)
+
+ indexed = bits_per_pixel <= 8
+ self._bitfield_compressed = compression == 3
+ self._bits_per_pixel = bits_per_pixel
+ self._width = read_word(bmp_header, 9)
+ self._height = read_word(bmp_header, 11)
+
+ self._colorconverter = ColorConverter()
+
+ if bits_per_pixel == 16:
+ if header_size >= 56 or self._bitfield_compressed:
+ self._r_bitmask = read_word(bmp_header, 27)
+ self._g_bitmask = read_word(bmp_header, 29)
+ self._b_bitmask = read_word(bmp_header, 31)
+ else:
+ # No compression or short header mean 5:5:5
+ self._r_bitmask = 0x7C00
+ self._g_bitmask = 0x03E0
+ self._b_bitmask = 0x001F
+ elif indexed:
+ if number_of_colors == 0:
+ number_of_colors = 1 << bits_per_pixel
+
+ palette = Palette(number_of_colors)
+
+ if number_of_colors > 1:
+ palette_size = number_of_colors * 4
+ palette_offset = 0xE + header_size
+
+ file.seek(palette_offset)
+
+ palette_data = memoryview(file.read(palette_size)).cast(
+ "I"
+ ) # cast as unsigned 32-bit int
+ if len(palette_data.tobytes()) != palette_size:
+ raise ValueError("Unable to read color palette data")
+
+ for i in range(number_of_colors):
+ palette[i] = palette_data[i]
+ else:
+ palette[0] = 0x000000
+ palette[1] = 0xFFFFFF
+ self._palette = palette
+ elif header_size not in (12, 40, 108, 124):
+ raise ValueError(
+ "Only Windows format, uncompressed BMP supported: "
+ f"given header size is {header_size}"
+ )
+
+ if bits_per_pixel == 8 and number_of_colors == 0:
+ raise ValueError(
+ "Only monochrome, indexed 4bpp or 8bpp, and 16bpp or greater BMPs supported: "
+ f"{bits_per_pixel} bpp given"
+ )
+
+ bytes_per_pixel = (
+ self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
+ )
+ pixels_per_byte = 8 // self._bits_per_pixel
+ if pixels_per_byte == 0:
+ self._stride = self._width * bytes_per_pixel
+ if self._stride % 4 != 0:
+ self._stride += 4 - self._stride % 4
+ else:
+ bit_stride = self._width * self._bits_per_pixel
+ if bit_stride % 32 != 0:
+ bit_stride += 32 - bit_stride % 32
+ self._stride = bit_stride // 8
+ except IOError as error:
+ raise OSError from error
@property
def width(self) -> int:
- """Width of the bitmap. (read only)"""
- return self._image.width
+ """
+ Width of the bitmap. (read only)
+
+ :type: int
+ """
+
+ return self._width
@property
def height(self) -> int:
- """Height of the bitmap. (read only)"""
- return self._image.height
+ """
+ Height of the bitmap. (read only)
+
+ :type: int
+ """
+
+ return self._height
@property
def pixel_shader(self) -> Union[ColorConverter, Palette]:
- """The ColorConverter or Palette for this image. (read only)"""
- return self._image.getpalette()
-
- def __getitem__(self, index: Union[tuple, list, int]) -> int:
"""
- Returns the value at the given index. The index can either be
- an x,y tuple or an int equal to `y * width + x`.
+ The image's pixel_shader. The type depends on the underlying `Bitmap`'s structure. The
+ pixel shader can be modified (e.g., to set the transparent pixel or, for paletted images,
+ to update the palette)
+
+ :type: Union[ColorConverter, Palette]
"""
- if isinstance(index, (tuple, list)):
- x = index[0]
- y = index[1]
- elif isinstance(index, int):
- x = index % self._image._width
- y = index // self._image._width
- if not 0 <= x < self._image.width or not 0 <= y < self._image.height:
+
+ return self._pixel_shader_base
+
+ @property
+ def _colorconverter(self) -> ColorConverter:
+ return self._pixel_shader_base
+
+ @_colorconverter.setter
+ def _colorconverter(self, colorconverter: ColorConverter) -> None:
+ self._pixel_shader_base = colorconverter
+
+ @property
+ def _palette(self) -> Palette:
+ return self._pixel_shader_base
+
+ @_palette.setter
+ def _palette(self, palette: Palette) -> None:
+ self._pixel_shader_base = palette
+
+ def _get_pixel(self, x: int, y: int) -> int:
+ if not (0 <= x < self.width and 0 <= y < self.height):
return 0
- return self._image.getpixel((x, y))
+ bytes_per_pixel = (
+ self._bits_per_pixel // 8 if (self._bits_per_pixel // 8) else 1
+ )
+ pixels_per_byte = 8 // self._bits_per_pixel
+ if pixels_per_byte == 0:
+ location = (
+ self._data_offset
+ + (self.height - y - 1) * self._stride
+ + x * bytes_per_pixel
+ )
+ else:
+ location = (
+ self._data_offset
+ + (self.height - y - 1) * self._stride
+ + x // pixels_per_byte
+ )
+
+ self._file.seek(location)
+
+ pixel_data = memoryview(self._file.read(4)).cast(
+ "I"
+ ) # cast as unsigned 32-bit int
+ pixel_data = pixel_data[0] # We only need a single 32-bit uint
+ if bytes_per_pixel == 1:
+ offset = (x % pixels_per_byte) * self._bits_per_pixel
+ mask = (1 << self._bits_per_pixel) - 1
+ return (pixel_data >> ((8 - self._bits_per_pixel) - offset)) & mask
+ if bytes_per_pixel == 2:
+ if self._g_bitmask == 0x07E0: # 565
+ red = (pixel_data & self._r_bitmask) >> 11
+ green = (pixel_data & self._g_bitmask) >> 5
+ blue = pixel_data & self._b_bitmask
+ else: # 555
+ red = (pixel_data & self._r_bitmask) >> 10
+ green = (pixel_data & self._g_bitmask) >> 4
+ blue = pixel_data & self._b_bitmask
+ return red << 19 | green << 10 | blue << 3
+ if bytes_per_pixel == 4 and self._bitfield_compressed:
+ return pixel_data & 0x00FFFFFF
+ return pixel_data
+
+ def _finish_refresh(self) -> None:
+ pass
"""
from typing import Optional, Union, Tuple
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
+from ._colorconverter import ColorConverter
+from ._colorspace import Colorspace
+from ._structs import InputPixelStruct, OutputPixelStruct, ColorStruct
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
class Palette:
- """Map a pixel palette_index to a full color. Colors are transformed to the display’s
+ """Map a pixel palette_index to a full color. Colors are transformed to the display's
format internally to save memory.
"""
- def __init__(self, color_count: int):
- """Create a Palette object to store a set number of colors."""
+ def __init__(self, color_count: int, *, dither: bool = False):
+ """Create a Palette object to store a set number of colors.
+
+ :param int color_count: The number of colors in the Palette
+ :param bool dither: When true, dither the RGB color before converting to the
+ display's color space
+ """
self._needs_refresh = False
+ self._dither = dither
self._colors = []
for _ in range(color_count):
self._colors.append(self._make_color(0))
- self._update_rgba(len(self._colors) - 1)
-
- def _update_rgba(self, index):
- color = self._colors[index]["rgb888"]
- transparent = self._colors[index]["transparent"]
- self._colors[index]["rgba"] = (
- color >> 16,
- (color >> 8) & 0xFF,
- color & 0xFF,
- 0 if transparent else 0xFF,
- )
- def _make_color(self, value, transparent=False):
- color = {
- "transparent": transparent,
- "rgb888": 0,
- "rgba": (0, 0, 0, 255),
- }
+ @staticmethod
+ def _make_color(value, transparent=False):
+ color = ColorStruct(transparent=transparent)
+
if isinstance(value, (tuple, list, bytes, bytearray)):
value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
elif isinstance(value, int):
raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
else:
raise TypeError("Color buffer must be a buffer, tuple, list, or int")
- color["rgb888"] = value
- self._needs_refresh = True
+ color.rgb888 = value
return color
def __setitem__(
self,
index: int,
- value: Union[int, circuitpython_typing.ReadableBuffer, Tuple[int, int, int]],
+ value: Union[int, ReadableBuffer, Tuple[int, int, int]],
) -> None:
"""Sets the pixel color at the given index. The index should be
an integer in the range 0 to color_count-1.
(to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
"""
- if self._colors[index]["rgb888"] != value:
- self._colors[index] = self._make_color(value)
- self._update_rgba(index)
+ if self._colors[index].rgb888 == value:
+ return
+ self._colors[index] = self._make_color(value)
+ self._colors[index].cached_colorspace = None
+ self._needs_refresh = True
def __getitem__(self, index: int) -> Optional[int]:
if not 0 <= index < len(self._colors):
raise ValueError("Palette index out of range")
- return self._colors[index]["rgb888"]
+ return self._colors[index].rgb888
def make_transparent(self, palette_index: int) -> None:
"""Set the palette index to be a transparent color"""
- self._colors[palette_index]["transparent"] = True
- self._update_rgba(palette_index)
+ self._colors[palette_index].transparent = True
+ self._needs_refresh = True
def make_opaque(self, palette_index: int) -> None:
"""Set the palette index to be an opaque color"""
- self._colors[palette_index]["transparent"] = False
- self._update_rgba(palette_index)
-
- def _get_palette(self):
- """Generate a palette for use with PIL"""
- palette = []
- for color in self._colors:
- palette += color["rgba"][0:3]
- return palette
-
- def _get_alpha_palette(self):
- """Generate an alpha channel palette with white being
- opaque and black being transparent"""
- palette = []
- for color in self._colors:
- for _ in range(3):
- palette += [0 if color["transparent"] else 255]
- return palette
+ self._colors[palette_index].transparent = False
+ self._needs_refresh = True
+
+ def _get_color(
+ self,
+ colorspace: Colorspace,
+ input_pixel: InputPixelStruct,
+ output_color: OutputPixelStruct,
+ ):
+ palette_index = input_pixel.pixel
+ if palette_index > len(self._colors) or self._colors[palette_index].transparent:
+ output_color.opaque = False
+ return
+
+ color = self._colors[palette_index]
+ if (
+ not self._dither
+ and color.cached_colorspace == colorspace
+ and color.cached_colorspace_grayscale_bit == colorspace.grayscale_bit
+ and color.cached_colorspace_grayscale == colorspace.grayscale
+ ):
+ output_color.pixel = self._colors[palette_index].cached_color
+ return
+
+ rgb888_pixel = input_pixel
+ rgb888_pixel.pixel = self._colors[palette_index].rgb888
+ ColorConverter._convert_color( # pylint: disable=protected-access
+ colorspace, self._dither, rgb888_pixel, output_color
+ )
+ if not self._dither:
+ color.cached_colorspace = colorspace
+ color.cached_color = output_color.pixel
+ color.cached_colorspace_grayscale = colorspace.grayscale
+ color.cached_colorspace_grayscale_bit = colorspace.grayscale_bit
def is_transparent(self, palette_index: int) -> bool:
"""Returns True if the palette index is transparent. Returns False if opaque."""
- return self._colors[palette_index]["transparent"]
+ return self._colors[palette_index].transparent
+
+ def _finish_refresh(self):
+ self._needs_refresh = False
+
+ @property
+ def dither(self) -> bool:
+ """When true the palette dithers the output by adding
+ random noise when truncating to display bitdepth
+ """
+ return self._dither
+
+ @dither.setter
+ def dither(self, value: bool):
+ if not isinstance(value, bool):
+ raise ValueError("Value should be boolean")
+ self._dither = value
"""
from ._bitmap import Bitmap
+from ._area import Area
+from ._helpers import clamp
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
def __init__(
self, width: int, height: int, *, mirror_x: bool = False, mirror_y: bool = False
):
- # pylint: disable=unused-argument
"""Create a Shape object with the given fixed size. Each pixel is one bit and is
stored by the column boundaries of the shape on each row. Each row’s boundary
defaults to the full row.
"""
+ self._mirror_x = mirror_x
+ self._mirror_y = mirror_y
+ self._width = width
+ self._height = height
+ if self._mirror_x:
+ width //= 2
+ width += self._width % 2
+ self._half_width = width
+ if self._mirror_y:
+ height //= 2
+ height += self._height % 2
+ self._half_height = height
+ self._data = bytearray(height * 4)
+ for i in range(height):
+ self._data[2 * i] = 0
+ self._data[2 * i + 1] = width
+
+ self._dirty_area = Area(0, 0, width, height)
super().__init__(width, height, 2)
def set_boundary(self, y: int, start_x: int, end_x: int) -> None:
- # pylint: disable=unnecessary-pass
"""Loads pre-packed data into the given row."""
- pass
+ max_y = self._height - 1
+ if self._mirror_y:
+ max_y = self._half_height - 1
+ y = clamp(y, 0, max_y)
+ max_x = self._width - 1
+ if self._mirror_x:
+ max_x = self._half_width - 1
+ start_x = clamp(start_x, 0, max_x)
+ end_x = clamp(end_x, 0, max_x)
+
+ # find x-boundaries for updating based on current data and start_x, end_x, and mirror_x
+ lower_x = min(start_x, self._data[2 * y])
+
+ if self._mirror_x:
+ upper_x = (
+ self._width - lower_x + 1
+ ) # dirty rectangles are treated with max value exclusive
+ else:
+ upper_x = max(
+ end_x, self._data[2 * y + 1]
+ ) # dirty rectangles are treated with max value exclusive
+
+ # find y-boundaries based on y and mirror_y
+ lower_y = y
+
+ if self._mirror_y:
+ upper_y = (
+ self._height - lower_y + 1
+ ) # dirty rectangles are treated with max value exclusive
+ else:
+ upper_y = y + 1 # dirty rectangles are treated with max value exclusive
+
+ self._data[2 * y] = start_x # update the data array with the new boundaries
+ self._data[2 * y + 1] = end_x
+
+ if self._dirty_area.x1 == self._dirty_area.x2: # dirty region is empty
+ self._dirty_area.x1 = lower_x
+ self._dirty_area.x2 = upper_x
+ self._dirty_area.y1 = lower_y
+ self._dirty_area.y2 = upper_y
+ else:
+ self._dirty_area.x1 = min(lower_x, self._dirty_area.x1)
+ self._dirty_area.x2 = max(upper_x, self._dirty_area.x2)
+ self._dirty_area.y1 = min(lower_y, self._dirty_area.y1)
+ self._dirty_area.y2 = max(upper_y, self._dirty_area.y2)
+
+ def _get_pixel(self, x: int, y: int) -> int:
+ if x >= self._width or x < 0 or y >= self._height or y < 0:
+ return 0
+ if self._mirror_x and x >= self._half_width:
+ x = self._width - x - 1
+ if self._mirror_y and y >= self._half_height:
+ y = self._height - y - 1
+ start_x = self._data[2 * y]
+ end_x = self._data[2 * y + 1]
+ if x < start_x or x >= end_x:
+ return 0
+ return 1
+
+ def _finish_refresh(self):
+ self._dirty_area.x1 = 0
+ self._dirty_area.x2 = 0
+
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ if self._dirty_area.x1 != self._dirty_area.x2:
+ areas.append(self._dirty_area)
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
-@dataclass
-class RectangleStruct:
- # pylint: disable=invalid-name
- """Rectangle Struct Dataclass. To eventually be replaced by Area."""
- x1: int
- y1: int
- x2: int
- y2: int
-
-
@dataclass
class TransformStruct:
# pylint: disable=invalid-name
dx: int = 1
dy: int = 1
scale: int = 1
- transpose_xy: bool = False
+ width: int = 0
+ height: int = 0
mirror_x: bool = False
mirror_y: bool = False
+ transpose_xy: bool = False
@dataclass
class ColorspaceStruct:
- # pylint: disable=invalid-name
+ # pylint: disable=invalid-name, too-many-instance-attributes
"""Colorspace Struct Dataclass"""
depth: int
bytes_per_cell: int = 0
grayscale_bit: int = 0
grayscale: bool = False
tricolor: bool = False
+ sevencolor: bool = False # Acep e-ink screens.
pixels_in_byte_share_row: bool = False
reverse_pixels_in_byte: bool = False
reverse_bytes_in_word: bool = False
dither: bool = False
+
+
+@dataclass
+class InputPixelStruct:
+ """InputPixel Struct Dataclass"""
+
+ pixel: int = 0
+ x: int = 0
+ y: int = 0
+ tile: int = 0
+ tile_x: int = 0
+ tile_y: int = 0
+
+
+@dataclass
+class OutputPixelStruct:
+ """OutputPixel Struct Dataclass"""
+
+ pixel: int = 0
+ opaque: bool = False
+
+
+@dataclass
+class ColorStruct:
+ """Color Struct Dataclass"""
+
+ rgb888: int = 0
+ cached_colorspace: ColorspaceStruct = None
+ cached_color: int = 0
+ cached_colorspace_grayscale_bit: int = 0
+ cached_colorspace_grayscale: bool = False
+ transparent: bool = False
+
+ def rgba(self) -> tuple[int, int, int, int]:
+ """Return the color as a tuple of red, green, blue, alpha"""
+ return (
+ self.rgb888 >> 16,
+ (self.rgb888 >> 8) & 0xFF,
+ self.rgb888 & 0xFF,
+ 0 if self.transparent else 0xFF,
+ )
+
+
+null_transform = TransformStruct() # Use defaults
"""
+import struct
from typing import Union, Optional, Tuple
-from PIL import Image
+from circuitpython_typing import WriteableBuffer
from ._bitmap import Bitmap
from ._colorconverter import ColorConverter
from ._ondiskbitmap import OnDiskBitmap
from ._shape import Shape
from ._palette import Palette
-from ._structs import RectangleStruct, TransformStruct
+from ._structs import (
+ InputPixelStruct,
+ OutputPixelStruct,
+ null_transform,
+)
+from ._colorspace import Colorspace
+from ._area import Area
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
class TileGrid:
- # pylint: disable=too-many-instance-attributes
+ # pylint: disable=too-many-instance-attributes, too-many-statements
"""Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
grids can share bitmaps and pixel shaders.
if isinstance(self._pixel_shader, ColorConverter):
self._pixel_shader._rgba = True # pylint: disable=protected-access
self._hidden_tilegrid = False
+ self._hidden_by_parent = False
+ self._rendered_hidden = False
+ self._name = "Tilegrid"
self._x = x
self._y = y
- self._width = width # Number of Tiles Wide
- self._height = height # Number of Tiles High
+ self._width_in_tiles = width
+ self._height_in_tiles = height
self._transpose_xy = False
self._flip_x = False
self._flip_y = False
raise ValueError("Default Tile is out of range")
self._pixel_width = width * tile_width
self._pixel_height = height * tile_height
- self._tiles = (self._width * self._height) * [default_tile]
- self._in_group = False
- self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
- self._current_area = RectangleStruct(
- 0, 0, self._pixel_width, self._pixel_height
+ self._tiles = bytearray(
+ (self._width_in_tiles * self._height_in_tiles) * [default_tile]
)
+ self._in_group = False
+ self._absolute_transform = None
+ self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
+ self._dirty_area = Area(0, 0, 0, 0)
+ self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
self._moved = False
+ self._full_change = True
+ self._partial_change = True
+ self._bitmap_width_in_tiles = bitmap_width // tile_width
+ self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
+ bitmap_height // tile_height
+ )
def _update_transform(self, absolute_transform):
"""Update the parent transform and child transforms"""
+ self._in_group = absolute_transform is not None
self._absolute_transform = absolute_transform
if self._absolute_transform is not None:
+ self._moved = True
self._update_current_x()
self._update_current_y()
width = self._pixel_height
else:
width = self._pixel_width
- if self._absolute_transform.transpose_xy:
+
+ absolute_transform = (
+ null_transform
+ if self._absolute_transform is None
+ else self._absolute_transform
+ )
+
+ if absolute_transform.transpose_xy:
self._current_area.y1 = (
- self._absolute_transform.y + self._absolute_transform.dy * self._x
+ absolute_transform.y + absolute_transform.dy * self._x
)
- self._current_area.y2 = (
- self._absolute_transform.y
- + self._absolute_transform.dy * (self._x + width)
+ self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+ self._x + width
)
if self._current_area.y2 < self._current_area.y1:
self._current_area.y1, self._current_area.y2 = (
)
else:
self._current_area.x1 = (
- self._absolute_transform.x + self._absolute_transform.dx * self._x
+ absolute_transform.x + absolute_transform.dx * self._x
)
- self._current_area.x2 = (
- self._absolute_transform.x
- + self._absolute_transform.dx * (self._x + width)
+ self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+ self._x + width
)
if self._current_area.x2 < self._current_area.x1:
self._current_area.x1, self._current_area.x2 = (
height = self._pixel_width
else:
height = self._pixel_height
- if self._absolute_transform.transpose_xy:
+
+ absolute_transform = (
+ null_transform
+ if self._absolute_transform is None
+ else self._absolute_transform
+ )
+
+ if absolute_transform.transpose_xy:
self._current_area.x1 = (
- self._absolute_transform.x + self._absolute_transform.dx * self._y
+ absolute_transform.x + absolute_transform.dx * self._y
)
- self._current_area.x2 = (
- self._absolute_transform.x
- + self._absolute_transform.dx * (self._y + height)
+ self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+ self._y + height
)
if self._current_area.x2 < self._current_area.x1:
self._current_area.x1, self._current_area.x2 = (
)
else:
self._current_area.y1 = (
- self._absolute_transform.y + self._absolute_transform.dy * self._y
+ absolute_transform.y + absolute_transform.dy * self._y
)
- self._current_area.y2 = (
- self._absolute_transform.y
- + self._absolute_transform.dy * (self._y + height)
+ self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+ self._y + height
)
if self._current_area.y2 < self._current_area.y1:
self._current_area.y1, self._current_area.y2 = (
)
image.putalpha(alpha.convert("L"))
- def _fill_area(self, buffer):
- # pylint: disable=too-many-locals,too-many-branches,too-many-statements
+ def _fill_area(
+ self,
+ colorspace: Colorspace,
+ area: Area,
+ mask: WriteableBuffer,
+ buffer: WriteableBuffer,
+ ) -> bool:
"""Draw onto the image"""
- if self._hidden_tilegrid:
- return
+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
+
+ # If no tiles are present we have no impact
+ tiles = self._tiles
+
+ if tiles is None or len(tiles) == 0:
+ return False
+
+ if self._hidden_tilegrid or self._hidden_by_parent:
+ return False
+ overlap = Area() # area, current_area, overlap
+ if not area.compute_overlap(self._current_area, overlap):
+ return False
+ # else:
+ # print("Checking", area.x1, area.y1, area.x2, area.y2)
+ # print("Overlap", overlap.x1, overlap.y1, overlap.x2, overlap.y2)
if self._bitmap.width <= 0 or self._bitmap.height <= 0:
- return
+ return False
+
+ x_stride = 1
+ y_stride = area.width()
+
+ flip_x = self._flip_x
+ flip_y = self._flip_y
+ if self._transpose_xy != self._absolute_transform.transpose_xy:
+ flip_x, flip_y = flip_y, flip_x
+
+ start = 0
+ if (self._absolute_transform.dx < 0) != flip_x:
+ start += (area.x2 - area.x1 - 1) * x_stride
+ x_stride *= -1
+ if (self._absolute_transform.dy < 0) != flip_y:
+ start += (area.y2 - area.y1 - 1) * y_stride
+ y_stride *= -1
+
+ # Track if this layer finishes filling in the given area. We can ignore any remaining
+ # layers at that point.
+ full_coverage = area == overlap
+
+ transformed = Area()
+ area.transform_within(
+ flip_x != (self._absolute_transform.dx < 0),
+ flip_y != (self._absolute_transform.dy < 0),
+ self.transpose_xy != self._absolute_transform.transpose_xy,
+ overlap,
+ self._current_area,
+ transformed,
+ )
- # Copy class variables to local variables in case something changes
- x = self._x
- y = self._y
- width = self._width
- height = self._height
- tile_width = self._tile_width
- tile_height = self._tile_height
- bitmap_width = self._bitmap.width
- pixel_width = self._pixel_width
- pixel_height = self._pixel_height
- tiles = self._tiles
- absolute_transform = self._absolute_transform
- pixel_shader = self._pixel_shader
- bitmap = self._bitmap
- tiles = self._tiles
+ start_x = transformed.x1 - self._current_area.x1
+ end_x = transformed.x2 - self._current_area.x1
+ start_y = transformed.y1 - self._current_area.y1
+ end_y = transformed.y2 - self._current_area.y1
+
+ if (self._absolute_transform.dx < 0) != flip_x:
+ x_shift = area.x2 - overlap.x2
+ else:
+ x_shift = overlap.x1 - area.x1
+ if (self._absolute_transform.dy < 0) != flip_y:
+ y_shift = area.y2 - overlap.y2
+ else:
+ y_shift = overlap.y1 - area.y1
+
+ # This untransposes x and y so it aligns with bitmap rows
+ if self._transpose_xy != self._absolute_transform.transpose_xy:
+ x_stride, y_stride = y_stride, x_stride
+ x_shift, y_shift = y_shift, x_shift
+
+ pixels_per_byte = 8 // colorspace.depth
+
+ input_pixel = InputPixelStruct()
+ output_pixel = OutputPixelStruct()
+ for input_pixel.y in range(start_y, end_y):
+ row_start = (
+ start + (input_pixel.y - start_y + y_shift) * y_stride
+ ) # In Pixels
+ local_y = input_pixel.y // self._absolute_transform.scale
+ for input_pixel.x in range(start_x, end_x):
+ # Compute the destination pixel in the buffer and mask based on the transformations
+ offset = (
+ row_start + (input_pixel.x - start_x + x_shift) * x_stride
+ ) # In Pixels
+
+ # Check the mask first to see if the pixel has already been set
+ if mask[offset // 8] & (1 << (offset % 8)):
+ continue
+ local_x = input_pixel.x // self._absolute_transform.scale
+ tile_location = (
+ (local_y // self._tile_height + self._top_left_y)
+ % self._height_in_tiles
+ ) * self._width_in_tiles + (
+ local_x // self._tile_width + self._top_left_x
+ ) % self._width_in_tiles
+ input_pixel.tile = tiles[tile_location]
+ input_pixel.tile_x = (
+ input_pixel.tile % self._bitmap_width_in_tiles
+ ) * self._tile_width + local_x % self._tile_width
+ input_pixel.tile_y = (
+ input_pixel.tile // self._bitmap_width_in_tiles
+ ) * self._tile_height + local_y % self._tile_height
+
+ output_pixel.pixel = 0
+ input_pixel.pixel = 0
+
+ # We always want to read bitmap pixels by row first and then transpose into
+ # the destination buffer because most bitmaps are row associated.
+ if isinstance(self._bitmap, (Bitmap, Shape, OnDiskBitmap)):
+ input_pixel.pixel = (
+ self._bitmap._get_pixel( # pylint: disable=protected-access
+ input_pixel.tile_x, input_pixel.tile_y
+ )
+ )
+
+ output_pixel.opaque = True
+ if self._pixel_shader is None:
+ output_pixel.pixel = input_pixel.pixel
+ elif isinstance(self._pixel_shader, Palette):
+ self._pixel_shader._get_color( # pylint: disable=protected-access
+ colorspace, input_pixel, output_pixel
+ )
+ elif isinstance(self._pixel_shader, ColorConverter):
+ self._pixel_shader._convert( # pylint: disable=protected-access
+ colorspace, input_pixel, output_pixel
+ )
+
+ if not output_pixel.opaque:
+ full_coverage = False
+ else:
+ mask[offset // 8] |= 1 << (offset % 8)
+ # print("Mask", mask)
+ if colorspace.depth == 16:
+ struct.pack_into(
+ "H",
+ buffer,
+ offset * 2,
+ output_pixel.pixel,
+ )
+ elif colorspace.depth == 32:
+ struct.pack_into(
+ "I",
+ buffer,
+ offset * 4,
+ output_pixel.pixel,
+ )
+ elif colorspace.depth == 8:
+ buffer[offset] = output_pixel.pixel & 0xFF
+ elif colorspace.depth < 8:
+ # Reorder the offsets to pack multiple rows into
+ # a byte (meaning they share a column).
+ if not colorspace.pixels_in_byte_share_row:
+ width = area.width()
+ row = offset // width
+ col = offset % width
+ # Dividing by pixels_per_byte does truncated division
+ # even if we multiply it back out
+ offset = (
+ col * pixels_per_byte
+ + (row // pixels_per_byte) * pixels_per_byte * width
+ + (row % pixels_per_byte)
+ )
+ shift = (offset % pixels_per_byte) * colorspace.depth
+ if colorspace.reverse_pixels_in_byte:
+ # Reverse the shift by subtracting it from the leftmost shift
+ shift = (pixels_per_byte - 1) * colorspace.depth - shift
+ buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
+
+ return full_coverage
+
+ def _finish_refresh(self):
+ first_draw = self._previous_area.x1 == self._previous_area.x2
+ hidden = self._hidden_tilegrid or self._hidden_by_parent
+ if not first_draw and hidden:
+ self._previous_area.x2 = self._previous_area.x1
+ elif self._moved or first_draw:
+ self._current_area.copy_into(self._previous_area)
- tile_count_x = bitmap_width // tile_width
+ self._moved = False
+ self._full_change = False
+ self._partial_change = False
+ if isinstance(self._pixel_shader, (Palette, ColorConverter)):
+ self._pixel_shader._finish_refresh() # pylint: disable=protected-access
+ if isinstance(self._bitmap, (Bitmap, Shape)):
+ self._bitmap._finish_refresh() # pylint: disable=protected-access
+
+ def _get_refresh_areas(self, areas: list[Area]) -> None:
+ # pylint: disable=invalid-name, too-many-branches, too-many-statements
+ first_draw = self._previous_area.x1 == self._previous_area.x2
+ hidden = self._hidden_tilegrid or self._hidden_by_parent
+
+ # Check hidden first because it trumps all other changes
+ if hidden:
+ self._rendered_hidden = True
+ if not first_draw:
+ areas.append(self._previous_area)
+ return
+ if self._moved and not first_draw:
+ self._previous_area.union(self._current_area, self._dirty_area)
+ if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
+ areas.append(self._dirty_area)
+ return
+ areas.append(self._current_area)
+ areas.append(self._previous_area)
+ return
- image = Image.new(
- "RGBA",
- (width * tile_width, height * tile_height),
- (0, 0, 0, 0),
+ tail = areas[-1] if areas else None
+ # If we have an in-memory bitmap, then check it for modifications
+ if isinstance(self._bitmap, Bitmap):
+ self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access
+ refresh_area = areas[-1] if areas else None
+ if refresh_area != tail:
+ # Special case a TileGrid that shows a full bitmap and use its
+ # dirty area. Copy it to ours so we can transform it.
+ if self._tiles_in_bitmap == 1:
+ refresh_area.copy_into(self._dirty_area)
+ self._partial_change = True
+ else:
+ self._full_change = True
+ elif isinstance(self._bitmap, Shape):
+ self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access
+ refresh_area = areas[-1] if areas else None
+ if refresh_area != tail:
+ refresh_area.copy_into(self._dirty_area)
+ self._partial_change = True
+
+ self._full_change = self._full_change or (
+ isinstance(self._pixel_shader, (Palette, ColorConverter))
+ and self._pixel_shader._needs_refresh # pylint: disable=protected-access
)
+ if self._full_change or first_draw:
+ areas.append(self._current_area)
+ return
- for tile_x in range(width):
- for tile_y in range(height):
- tile_index = tiles[tile_y * width + tile_x]
- tile_index_x = tile_index % tile_count_x
- tile_index_y = tile_index // tile_count_x
- tile_image = bitmap._image # pylint: disable=protected-access
- if isinstance(pixel_shader, Palette):
- tile_image = tile_image.copy().convert("P")
- self._apply_palette(tile_image)
- tile_image = tile_image.convert("RGBA")
- self._add_alpha(tile_image)
- elif isinstance(pixel_shader, ColorConverter):
- # This will be needed for eInks, grayscale, and monochrome displays
- pass
- image.alpha_composite(
- tile_image,
- dest=(tile_x * tile_width, tile_y * tile_height),
- source=(
- tile_index_x * tile_width,
- tile_index_y * tile_height,
- tile_index_x * tile_width + tile_width,
- tile_index_y * tile_height + tile_height,
- ),
+ if self._partial_change:
+ x = self._x
+ y = self._y
+ if self._absolute_transform.transpose_xy:
+ x, y = y, x
+ x1 = self._dirty_area.x1
+ x2 = self._dirty_area.x2
+ if self._flip_x:
+ x1 = self._pixel_width - x1
+ x2 = self._pixel_width - x2
+ y1 = self._dirty_area.y1
+ y2 = self._dirty_area.y2
+ if self._flip_y:
+ y1 = self._pixel_height - y1
+ y2 = self._pixel_height - y2
+ if self._transpose_xy != self._absolute_transform.transpose_xy:
+ x1, y1 = y1, x1
+ x2, y2 = y2, x2
+ self._dirty_area.x1 = (
+ self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
+ )
+ self._dirty_area.y1 = (
+ self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
+ )
+ self._dirty_area.x2 = (
+ self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
+ )
+ self._dirty_area.y2 = (
+ self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
+ )
+ if self._dirty_area.y2 < self._dirty_area.y1:
+ self._dirty_area.y1, self._dirty_area.y2 = (
+ self._dirty_area.y2,
+ self._dirty_area.y1,
)
-
- if absolute_transform is not None:
- if absolute_transform.scale > 1:
- image = image.resize(
- (
- int(pixel_width * absolute_transform.scale),
- int(
- pixel_height * absolute_transform.scale,
- ),
- ),
- resample=Image.NEAREST,
+ if self._dirty_area.x2 < self._dirty_area.x1:
+ self._dirty_area.x1, self._dirty_area.x2 = (
+ self._dirty_area.x2,
+ self._dirty_area.x1,
)
- if absolute_transform.mirror_x != self._flip_x:
- image = image.transpose(Image.FLIP_LEFT_RIGHT)
- if absolute_transform.mirror_y != self._flip_y:
- image = image.transpose(Image.FLIP_TOP_BOTTOM)
- if absolute_transform.transpose_xy != self._transpose_xy:
- image = image.transpose(Image.TRANSPOSE)
- x *= absolute_transform.dx
- y *= absolute_transform.dy
- x += absolute_transform.x
- y += absolute_transform.y
-
- source_x = source_y = 0
- if x < 0:
- source_x = round(0 - x)
- x = 0
- if y < 0:
- source_y = round(0 - y)
- y = 0
-
- x = round(x)
- y = round(y)
-
- if (
- x <= buffer.width
- and y <= buffer.height
- and source_x <= image.width
- and source_y <= image.height
- ):
- buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
+ areas.append(self._dirty_area)
+
+ def _set_hidden(self, hidden: bool) -> None:
+ self._hidden_tilegrid = hidden
+ self._rendered_hidden = False
+ if not hidden:
+ self._full_change = True
+
+ def _set_hidden_by_parent(self, hidden: bool) -> None:
+ self._hidden_by_parent = hidden
+ self._rendered_hidden = False
+ if not hidden:
+ self._full_change = True
+
+ def _get_rendered_hidden(self) -> bool:
+ return self._rendered_hidden
+
+ def _set_all_tiles(self, tile_index: int) -> None:
+ """Set all tiles to the given tile index"""
+ if tile_index >= self._tiles_in_bitmap:
+ raise ValueError("Tile index out of bounds")
+ self._tiles = bytearray(
+ (self._width_in_tiles * self._height_in_tiles) * [tile_index]
+ )
+ self._full_change = True
- def _finish_refresh(self):
- pass
+ def _set_tile(self, x: int, y: int, tile_index: int) -> None:
+ self._tiles[y * self._width_in_tiles + x] = tile_index
+ temp_area = Area()
+ if not self._partial_change:
+ tile_area = self._dirty_area
+ else:
+ tile_area = temp_area
+ top_x = (x - self._top_left_x) % self._width_in_tiles
+ if top_x < 0:
+ top_x += self._width_in_tiles
+ tile_area.x1 = top_x * self._tile_width
+ tile_area.x2 = tile_area.x1 + self._tile_width
+ top_y = (y - self._top_left_y) % self._height_in_tiles
+ if top_y < 0:
+ top_y += self._height_in_tiles
+ tile_area.y1 = top_y * self._tile_height
+ tile_area.y2 = tile_area.y1 + self._tile_height
+
+ if self._partial_change:
+ self._dirty_area.union(temp_area, self._dirty_area)
+
+ self._partial_change = True
+
+ def _set_top_left(self, x: int, y: int) -> None:
+ self._top_left_x = x
+ self._top_left_y = y
+ self._full_change = True
@property
def hidden(self) -> bool:
def hidden(self, value: bool):
if not isinstance(value, (bool, int)):
raise ValueError("Expecting a boolean or integer value")
- self._hidden_tilegrid = bool(value)
+ value = bool(value)
+ self._set_hidden(value)
@property
def x(self) -> int:
if not isinstance(value, int):
raise TypeError("X should be a integer type")
if self._x != value:
+ self._moved = True
self._x = value
- self._update_current_x()
+ if self._absolute_transform is not None:
+ self._update_current_x()
@property
def y(self) -> int:
if not isinstance(value, int):
raise TypeError("Y should be a integer type")
if self._y != value:
+ self._moved = True
self._y = value
- self._update_current_y()
+ if self._absolute_transform is not None:
+ self._update_current_y()
@property
def flip_x(self) -> bool:
raise TypeError("Flip X should be a boolean type")
if self._flip_x != value:
self._flip_x = value
+ self._full_change = True
@property
def flip_y(self) -> bool:
raise TypeError("Flip Y should be a boolean type")
if self._flip_y != value:
self._flip_y = value
+ self._full_change = True
@property
def transpose_xy(self) -> bool:
return self._transpose_xy
@transpose_xy.setter
- def transpose_xy(self, value: bool):
+ def transpose_xy(self, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("Transpose XY should be a boolean type")
if self._transpose_xy != value:
self._transpose_xy = value
+ if self._pixel_width == self._pixel_height:
+ self._full_change = True
+ return
self._update_current_x()
self._update_current_y()
+ self._moved = True
@property
def pixel_shader(self) -> Union[ColorConverter, Palette]:
)
self._pixel_shader = new_pixel_shader
+ self._full_change = True
@property
def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
raise ValueError("New bitmap must be same size as old bitmap")
self._bitmap = new_bitmap
+ self._full_change = True
def _extract_and_check_index(self, index):
if isinstance(index, (tuple, list)):
x = index[0]
y = index[1]
- index = y * self._width + x
+ index = y * self._width_in_tiles + x
elif isinstance(index, int):
- x = index % self._width
- y = index // self._width
- if x > self._width or y > self._height or index >= len(self._tiles):
+ x = index % self._width_in_tiles
+ y = index // self._width_in_tiles
+ if (
+ x > self._width_in_tiles
+ or y > self._height_in_tiles
+ or index >= len(self._tiles)
+ ):
raise ValueError("Tile index out of bounds")
- return index
+ return x, y
def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
"""Returns the tile index at the given index. The index can either be
an x,y tuple or an int equal to ``y * width + x``'.
"""
- index = self._extract_and_check_index(index)
- return self._tiles[index]
+ x, y = self._extract_and_check_index(index)
+ return self._tiles[y * self._width_in_tiles + x]
def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
"""Sets the tile index at the given index. The index can either be
an x,y tuple or an int equal to ``y * width + x``.
"""
- index = self._extract_and_check_index(index)
+ x, y = self._extract_and_check_index(index)
if not 0 <= value <= 255:
raise ValueError("Tile value out of bounds")
- self._tiles[index] = value
+ self._set_tile(x, y, value)
@property
def width(self) -> int:
"""Width in tiles"""
- return self._width
+ return self._width_in_tiles
@property
def height(self) -> int:
"""Height in tiles"""
- return self._height
+ return self._height_in_tiles
@property
def tile_width(self) -> int:
--- /dev/null
+# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
--- /dev/null
+SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams, written for Adafruit Industries
+
+SPDX-License-Identifier: MIT
--- /dev/null
+SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams, written for Adafruit Industries
+
+SPDX-License-Identifier: MIT
"""
+import os
from typing import Union, Tuple, Optional
from PIL import ImageFont
from displayio import Bitmap
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
+DEFAULT_FONT = "displayio/resources/ter-u12n.pil"
+
class FontProtocol(Protocol):
"""A protocol shared by `BuiltinFont` and classes in ``adafruit_bitmap_font``"""
"""Simulate a font built into CircuitPython"""
def __init__(self):
- self._font = ImageFont.load_default()
+ self._font = ImageFont.load(os.path.dirname(__file__) + "/" + DEFAULT_FONT)
self._generate_bitmap(0x20, 0x7E)
def _generate_bitmap(self, start_range, end_range):
"""
+from typing import Optional
import microcontroller
-import circuitpython_typing
+from circuitpython_typing import ReadableBuffer
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
command: microcontroller.Pin,
chip_select: microcontroller.Pin,
write: microcontroller.Pin,
- read: microcontroller.Pin,
- reset: microcontroller.Pin,
+ read: Optional[microcontroller.Pin],
+ reset: Optional[microcontroller.Pin] = None,
frequency: int = 30000000,
):
# pylint: disable=unnecessary-pass
"""
raise NotImplementedError("ParallelBus reset has not been implemented yet")
- def send(self, command: int, data: circuitpython_typing.ReadableBuffer) -> None:
+ def send(self, command: int, data: ReadableBuffer) -> None:
"""Sends the given command value followed by the full set of data. Display state,
such as vertical scroll, set via ``send`` may or may not be reset once the code is
done.
"""
raise NotImplementedError("ParallelBus send has not been implemented yet")
+
+ def _send(
+ self,
+ _data_type: int,
+ _chip_select: int,
+ _data: ReadableBuffer,
+ ) -> None:
+ pass
+
+ def _free(self) -> bool:
+ """Attempt to free the bus and return False if busy"""
+
+ def _begin_transaction(self) -> bool:
+ pass
+
+ def _end_transaction(self) -> None:
+ pass
Adafruit-Blinka>=7.0.0
adafruit-circuitpython-typing
pillow>=9.2.0
-numpy
"Adafruit-Blinka>=7.0.0",
"adafruit-circuitpython-typing",
"pillow",
- "numpy",
]
if sys.version_info > (3, 9):
"""
-import sys # pylint: disable=unused-import
import fontio
__version__ = "0.0.0+auto.0"
FONT = fontio.BuiltinFont()
# TODO: Tap into stdout to get the REPL
+# Look at how Adafruit_Python_Shell's run_command works as an option
+# Additionally, adding supervisor to Blinka may be helpful to keep track of REPL output
# sys.stdout = open('out.dat', 'w')
# sys.stdout.close()