* 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"""
+ 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._release() # pylint: disable=protected-access
+ 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()
BACKLIGHT_IN_OUT = 1
BACKLIGHT_PWM = 2
+
+NO_COMMAND = 0x100
+CIRCUITPY_DISPLAY_LIMIT = 1
import time
import struct
-import threading
from typing import Optional
from dataclasses import astuple
import digitalio
DISPLAY_DATA,
BACKLIGHT_IN_OUT,
BACKLIGHT_PWM,
+ NO_COMMAND,
)
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
-displays = []
-
class Display:
# pylint: disable=too-many-instance-attributes
The initialization sequence should always leave the display memory access inline with
the scan of the display to minimize tearing artifacts.
"""
+ # 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._first_manual_refresh = not auto_refresh
+ self._backlight_on_high = backlight_on_high
+
+ self._native_frames_per_second = native_frames_per_second
+ self._native_ms_per_frame = 1000 // native_frames_per_second
+
self._auto_brightness = auto_brightness
- self._brightness = 1.0
+ self._brightness = brightness
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._last_refresh_call = 0
self._refresh_thread = None
if self._auto_refresh:
self.auto_refresh = True
self._backlight.switch_to_output()
self.brightness = brightness
+ def __new__(cls, *args, **kwargs):
+ from . import ( # pylint: disable=import-outside-toplevel, cyclic-import
+ allocate_display,
+ )
+
+ allocate_display(cls)
+ return super().__new__(cls)
+
def _initialize(self, init_sequence):
i = 0
while i < len(init_sequence):
delay = (data_size & 0x80) > 0
data_size &= ~0x80
- if self._data_as_commands:
+ if self._core.data_as_commands:
self._core.send(
DISPLAY_COMMAND,
CHIP_SELECT_TOGGLE_EVERY_BYTE,
def _send(self, command, data):
self._core.begin_transaction()
- if self._data_as_commands:
+ if self._core.data_as_commands:
self._core.send(
DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
)
self._core.end_transaction()
def _send_pixels(self, data):
- if not self._data_as_commands:
+ if not self._core.data_as_commands:
self._core.send(
DISPLAY_COMMAND,
CHIP_SELECT_TOGGLE_EVERY_BYTE,
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.)
"""
+ maximum_ms_per_real_frame = 0xFFFFFFFF
+ if minimum_frames_per_second > 0:
+ maximum_ms_per_real_frame = 1000 // minimum_frames_per_second
+
+ 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
+
+ def _refresh_display(self):
+ # pylint: disable=protected-access
if not self._core.start_refresh():
return False
# 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))
+ 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(
+ self._core.current_group._fill_area(
buffer
) # pylint: disable=protected-access
# save image to buffer (or probably refresh buffer so we can compare)
return True
- def _refresh_loop(self):
- while self._auto_refresh:
+ def _background(self):
+ 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)
+ img = img.rotate(360 - self._core.rotation, expand=True)
display_rectangle = self._apply_rotation(rectangle)
img = img.crop(astuple(self._clip(display_rectangle)))
)
self._send(
- self._set_column_command,
+ self._core.column_command,
self._encode_pos(
- display_rectangle.x1 + self._colstart,
- display_rectangle.x2 + self._colstart - 1,
+ display_rectangle.x1 + self._core.colstart,
+ display_rectangle.x2 + self._core.colstart - 1,
),
)
self._send(
- self._set_row_command,
+ self._core.row_command,
self._encode_pos(
- display_rectangle.y1 + self._rowstart,
- display_rectangle.y2 + self._rowstart - 1,
+ display_rectangle.y1 + self._core.rowstart,
+ display_rectangle.y2 + self._core.rowstart - 1,
),
)
self._core.end_transaction()
def _clip(self, rectangle):
- if self._rotation in (90, 270):
- width, height = self._height, self._width
+ if self._core.rotation in (90, 270):
+ width, height = self._core.height, self._core.width
else:
- width, height = self._width, self._height
+ width, height = self._core.width, self._core.height
rectangle.x1 = max(rectangle.x1, 0)
rectangle.y1 = max(rectangle.y1, 0)
def _apply_rotation(self, rectangle):
"""Adjust the rectangle coordinates based on rotation"""
- if self._rotation == 90:
+ if self._core.rotation == 90:
return RectangleStruct(
- self._height - rectangle.y2,
+ self._core.height - rectangle.y2,
rectangle.x1,
- self._height - rectangle.y1,
+ self._core.height - rectangle.y1,
rectangle.x2,
)
- if self._rotation == 180:
+ if self._core.rotation == 180:
return RectangleStruct(
- self._width - rectangle.x2,
- self._height - rectangle.y2,
- self._width - rectangle.x1,
- self._height - rectangle.y1,
+ self._core.width - rectangle.x2,
+ self._core.height - rectangle.y2,
+ self._core.width - rectangle.x1,
+ self._core.height - rectangle.y1,
)
- if self._rotation == 270:
+ if self._core.rotation == 270:
return RectangleStruct(
rectangle.y1,
- self._width - rectangle.x2,
+ self._core.width - rectangle.x2,
rectangle.y2,
- self._width - rectangle.x1,
+ self._core.width - rectangle.x1,
)
return rectangle
self, y: int, buffer: circuitpython_typing.WriteableBuffer
) -> circuitpython_typing.WriteableBuffer:
"""Extract the pixels from a single row"""
- for x in range(0, self._width):
+ for x in range(0, self._core.width):
_rgb_565 = self._colorconverter.convert(self._buffer.getpixel((x, y)))
buffer[x * 2] = (_rgb_565 >> 8) & 0xFF
buffer[x * 2 + 1] = _rgb_565 & 0xFF
@auto_refresh.setter
def auto_refresh(self, value: bool):
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:
__repo__ = "https://github.com/adafruit/Adafruit_Blinka_Displayio.git"
-from typing import Union
+import time
import circuitpython_typing
from paralleldisplay import ParallelBus
from ._fourwire import FourWire
from ._i2cdisplay import I2CDisplay
from ._structs import ColorspaceStruct, TransformStruct, RectangleStruct
from ._area import Area
-
-displays = []
+from ._displaybus import _DisplayBus
class _DisplayCore:
- # pylint: disable=too-many-arguments, too-many-instance-attributes
+ # pylint: disable=too-many-arguments, too-many-instance-attributes, too-many-locals
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)):
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)
+ 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
circuitpython_splash = _Supervisor().circuitpython_splash
if not circuitpython_splash._in_group:
root_group = circuitpython_splash
- elif self._current_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:
+ if self.current_group is not None:
# Eventually calculate dirty rectangles here
- subrectangles.append(RectangleStruct(0, 0, self._width, self._height))
+ subrectangles.append(RectangleStruct(0, 0, self.width, self.height))
return subrectangles
def release(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,
# pylint: disable=protected-access
"""Call the current group's fill area function"""
- return self._current_group._fill_area(self._colorspace, area, mask, buffer)
+ return self.current_group._fill_area(self.colorspace, area, mask, buffer)
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:
+ 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:
"""
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]
"""
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,
+ )
+
+ allocate_display_bus(cls)
+ return super().__new__(cls)
+
def _release(self):
self.reset()
self._i2c.deinit()
"""
-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()