diff --git a/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz b/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz index 3fc33f13a..85b10a468 100644 Binary files a/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz and b/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz differ diff --git a/src/base_circuitpython/board.py b/src/base_circuitpython/board.py index 62de38b1e..92f501408 100644 --- a/src/base_circuitpython/board.py +++ b/src/base_circuitpython/board.py @@ -15,20 +15,114 @@ def show(self, group=None): self.active_group = group if group == None: - self.terminal.draw() + self.terminal._Terminal__draw() return # if the group has no attribute called # "draw", then it is liable for updating itself # when it calls show - if hasattr(group, "draw"): - group.draw() + if hasattr(group, "_Group__draw"): + group._Group__draw() DISPLAY = Display() -# deafult pin, +# default pin for neopixel, # shows that this could # refer to the CPX # or CLUE neopixel pin NEOPIXEL = "D00" + +# ETC PINS WITHIN THE CLUE THAT MAY BE REFERENCED +# found by runing print(dir(board)) on clue +A0 = 0 +A1 = 0 +A2 = 0 +A3 = 0 +A4 = 0 +A5 = 0 +A6 = 0 +A7 = 0 + +ACCELEROMETER_GYRO_INTERRUPT = 0 + +BUTTON_A = 0 +BUTTON_B = 0 + +D0 = 0 +D1 = 0 +D2 = 0 +D3 = 0 +D4 = 0 +D5 = 0 +D6 = 0 +D7 = 0 +D8 = 0 +D9 = 0 +D10 = 0 +D11 = 0 +D12 = 0 +D13 = 0 +D14 = 0 +D15 = 0 +D16 = 0 +D17 = 0 +D18 = 0 +D19 = 0 +D20 = 0 + +I2C = 0 + +L = 0 + +MICROPHONE_CLOCK = 0 +MICROPHONE_DATA = 0 + +MISO = 0 +MOSI = 0 + +P0 = 0 +P1 = 0 +P2 = 0 +P3 = 0 +P4 = 0 +P5 = 0 +P6 = 0 +P7 = 0 +P8 = 0 +P9 = 0 +P10 = 0 +P11 = 0 +P12 = 0 +P13 = 0 +P14 = 0 +P15 = 0 +P16 = 0 +P17 = 0 +P18 = 0 +P19 = 0 +P20 = 0 + +PROXIMITY_LIGHT_INTERRUPT = 0 + +RX = 0 +SCK = 0 +SCL = 0 +SDA = 0 + +SPEAKER = 0 + +SPI = 0 + +TFT_BACKLIGHT = 0 +TFT_CS = 0 +TFT_DC = 0 +TFT_MOSI = 0 +TFT_RESET = 0 +TFT_SCK = 0 + +TX = 0 + +UART = 0 + +WHITE_LEDS = 0 diff --git a/src/base_circuitpython/displayio/__init__.py b/src/base_circuitpython/displayio/__init__.py index 8cfadeee0..a926bf965 100644 --- a/src/base_circuitpython/displayio/__init__.py +++ b/src/base_circuitpython/displayio/__init__.py @@ -4,7 +4,6 @@ # https://circuitpython.readthedocs.io/en/5.0.x/shared-bindings/displayio/__init__.html from .bitmap import Bitmap -from .color_type import ColorType from .group import Group from .palette import Palette diff --git a/src/base_circuitpython/displayio/bitmap.py b/src/base_circuitpython/displayio/bitmap.py index f995c8025..c913dff2b 100644 --- a/src/base_circuitpython/displayio/bitmap.py +++ b/src/base_circuitpython/displayio/bitmap.py @@ -12,6 +12,26 @@ class Bitmap: + """ + .. currentmodule:: displayio + + `Bitmap` -- Stores values in a 2D array + ========================================================================== + + Stores values of a certain size in a 2D array + + .. class:: Bitmap(width, height, value_count) + + Create a Bitmap object with the given fixed size. Each pixel stores a value that is used to + index into a corresponding palette. This enables differently colored sprites to share the + underlying Bitmap. value_count is used to minimize the memory used to store the Bitmap. + + :param int width: The number of values wide + :param int height: The number of values high + :param int value_count: The number of possible pixel values. + + """ + def __init__(self, width, height, value_count=255): self.__width = width self.__height = height @@ -19,13 +39,36 @@ def __init__(self, width, height, value_count=255): @property def width(self): + """ + .. attribute:: width + + Width of the bitmap. (read only) + + """ return self.__width @property def height(self): + """ + .. attribute:: height + + Height of the bitmap. (read only) + + """ return self.__height def __setitem__(self, index, value): + """ + .. method:: __setitem__(index, value) + + Sets the value at the given index. The index can either be an x,y tuple or an int equal + to ``y * width + x``. + + This allows you to:: + + bitmap[0,1] = 3 + + """ if isinstance(index, tuple): if index[0] >= self.width or index[1] >= self.height: raise IndexError(CONSTANTS.PIXEL_OUT_OF_BOUNDS) @@ -38,6 +81,17 @@ def __setitem__(self, index, value): raise IndexError(CONSTANTS.PIXEL_OUT_OF_BOUNDS) def __getitem__(self, index): + """ + .. method:: __getitem__(index) + + Returns the value at the given index. The index can either be an x,y tuple or an int equal + to ``y * width + x``. + + This allows you to:: + + print(bitmap[0,1]) + + """ if isinstance(index, tuple): if index[0] >= self.width or index[1] >= self.height: @@ -49,6 +103,3 @@ def __getitem__(self, index): return self.values[index] except IndexError: raise IndexError(CONSTANTS.PIXEL_OUT_OF_BOUNDS) - - def __len__(self): - return self.width * self.height diff --git a/src/base_circuitpython/displayio/color_type.py b/src/base_circuitpython/displayio/color_type.py index b4da02c41..c56347315 100644 --- a/src/base_circuitpython/displayio/color_type.py +++ b/src/base_circuitpython/displayio/color_type.py @@ -1,12 +1,12 @@ # Datatype for the items within a palette -class ColorType: +class _ColorType: def __init__(self, rgb888): self.rgb888 = rgb888 self.transparent = False def __eq__(self, other): return ( - isinstance(other, ColorType) + isinstance(other, _ColorType) and self.rgb888 == other.rgb888 and self.transparent == other.transparent ) diff --git a/src/base_circuitpython/displayio/group.py b/src/base_circuitpython/displayio/group.py index 17109b03a..1f66b291d 100644 --- a/src/base_circuitpython/displayio/group.py +++ b/src/base_circuitpython/displayio/group.py @@ -17,57 +17,221 @@ class Group: - def __init__(self, max_size, scale=1, check_active_group_ref=True, auto_write=True): + """ + `Group` -- Group together sprites and subgroups + ========================================================================== + + Manage a group of sprites and groups and how they are inter-related. + + .. class:: Group(*, max_size=4, scale=1, x=0, y=0) + Create a Group of a given size and scale. Scale is in one dimension. For example, scale=2 + leads to a layer's pixel being 2x2 pixels when in the group. + :param int max_size: The maximum group size. + :param int scale: Scale of layer pixels in one dimension. + :param int x: Initial x position within the parent. + :param int y: Initial y position within the parent. + """ + + def __init__( + self, max_size, scale=1, x=0, y=0, check_active_group_ref=True, auto_write=True + ): self.__check_active_group_ref = check_active_group_ref self.__auto_write = auto_write self.__contents = [] - self.max_size = max_size + self.__max_size = max_size self.scale = scale - self.parent = None + """ + .. attribute:: scale + + Scales each pixel within the Group in both directions. For example, when scale=2 each pixel + will be represented by 2x2 pixels. + + """ + self.x = x + """ + .. attribute:: x + + X position of the Group in the parent. + + """ + self.y = y + """ + .. attribute:: y + + Y position of the Group in the parent. + """ + self.__parent = None + self.__hidden = False @property - def in_group(self): - return self.parent != None + def hidden(self): + """ + .. attribute:: hidden + + True when the Group and all of it's layers are not visible. When False, the Group's layers + are visible if they haven't been hidden. + """ + return self.__hidden + + @hidden.setter + def hidden(self, val): + changed = val != self.__hidden + self.__hidden = val + for elem in self.__contents: + elem.hidden = val + + if changed: + self.__elem_changed() def append(self, item): - if len(self.__contents) == self.max_size: + """ + .. method:: append(layer) + + Append a layer to the group. It will be drawn above other layers. + """ + self.__prepare_for_add(item) + self.__contents.append(item) + self.__elem_changed() + + def insert(self, idx, item): + """ + .. method:: insert(index, layer) + + Insert a layer into the group. + """ + self.__prepare_for_add(item) + self.__contents.insert(idx, item) + self.__elem_changed() + + def index(self, layer): + """ + .. method:: index(layer) + + Returns the index of the first copy of layer. Raises ValueError if not found. + """ + for idx, elem in enumerate(self.__contents): + if elem == layer: + return idx + + return ValueError() + + def pop(self, i=-1): + """ + .. method:: pop(i=-1) + + Remove the ith item and return it. + """ + item = self.__contents.pop(i) + self.__set_parent(item, None) + self.__elem_changed() + return item + + def remove(self, layer): + """ + .. method:: remove(layer) + + Remove the first copy of layer. Raises ValueError if it is not present. + """ + idx = self.index(layer) + item = self.__contents[idx] + + self.__set_parent(item, None) + self.__contents.pop(idx) + self.__elem_changed() + + def __delitem__(self, index): + """ + .. method:: __delitem__(index) + + Deletes the value at the given index. + + This allows you to:: + + del group[0] + """ + item = self.__contents[index] + self.__set_parent(item, None) + del self.__contents[index] + self.__elem_changed() + + def __getitem__(self, index): + """ + .. method:: __getitem__(index) + + Returns the value at the given index. + + This allows you to:: + + print(group[0]) + + """ + return self.__contents[index] + + def __setitem__(self, index, val): + """ + .. method:: __setitem__(index, value) + + Sets the value at the given index. + + This allows you to:: + + group[0] = sprite + """ + old_val = self.__contents[index] + + self.__contents[index] = val + if old_val != val: + self.__elem_changed() + + def __len__(self): + """ + .. method:: __len__() + + Returns the number of layers in a Group + """ + if not self.__contents: + return 0 + else: + return len(self.__contents) + + @property + def __in_group(self): + return self.__parent != None + + def __prepare_for_add(self, item): + if len(self.__contents) == self.__max_size: raise RuntimeError(CONSTANTS.GROUP_FULL) elif not isinstance(item, TileGrid) and not isinstance(item, Group): raise ValueError(CONSTANTS.INCORR_SUBCLASS) - elif item.in_group: + elif (isinstance(item, Group) and item._Group__in_group) or ( + isinstance(item, TileGrid) and item._TileGrid__in_group + ): raise ValueError(CONSTANTS.LAYER_ALREADY_IN_GROUP) + self.__set_parent(item, self) - self.__contents.append(item) - item.parent = self - self.elem_changed() + def __set_parent(self, item, val): + if isinstance(item, TileGrid): + item._TileGrid__parent = val + else: + item._Group__parent = val - def elem_changed(self): + def __elem_changed(self): # Ensure that this group is what the board is currently showing. # Otherwise, don't bother to draw it. if self.__auto_write: - self.trigger_draw() + self.__trigger_draw() - def trigger_draw(self): + def __trigger_draw(self): # select the correct parent to draw from if necessary if self.__check_active_group_ref and board.DISPLAY.active_group == self: - self.draw() + self.__draw() - elif self.in_group: + elif self.__in_group: # If a sub-group is modified, propagate to top level to # see if one of the parents are the current active group. - self.parent.elem_changed() + self.__parent._Group__elem_changed() - def __getitem__(self, index): - return self.__contents[index] - - def __setitem__(self, index, val): - old_val = self.__contents[index] - - self.__contents[index] = val - if old_val != val: - self.elem_changed() - - def draw(self, img=None, x=0, y=0, scale=None, show=True): + def __draw(self, img=None, x=0, y=0, scale=None, show=True): # this function is not a part of the orignal implementation # it is what draws itself and its children and potentially shows it to the # frontend @@ -103,19 +267,20 @@ def draw(self, img=None, x=0, y=0, scale=None, show=True): pass for elem in self.__contents: - if isinstance(elem, Group): - img = elem.draw(img=img, x=x, y=y, scale=scale, show=False,) - else: - img = elem.draw(img=img, x=x, y=y, scale=scale) + if not elem.hidden: + if isinstance(elem, Group): + img = elem._Group__draw(img=img, x=x, y=y, scale=scale, show=False,) + else: + img = elem._TileGrid__draw(img=img, x=x, y=y, scale=scale) # show should only be true to the highest parent group if show: - self.show(img) + self.__show(img) # return value only used if this is within another group return img - def show(self, img): + def __show(self, img): # sends current img to the frontend buffered = BytesIO() img.save(buffered, format="BMP") @@ -124,15 +289,3 @@ def show(self, img): sendable_json = {CONSTANTS.BASE_64: img_str} common.utils.send_to_simulator(sendable_json, CONSTANTS.CLUE) - - def __len__(self): - if not self.__contents: - return 0 - else: - return len(self.__contents) - - def pop(self, i=-1): - item = self.__contents.pop(i) - item.parent = None - self.elem_changed() - return item diff --git a/src/base_circuitpython/displayio/palette.py b/src/base_circuitpython/displayio/palette.py index 8a6588cf7..b954d982b 100644 --- a/src/base_circuitpython/displayio/palette.py +++ b/src/base_circuitpython/displayio/palette.py @@ -1,4 +1,4 @@ -from .color_type import ColorType +from .color_type import _ColorType from . import constants as CONSTANTS # Palette implementation loosely based on the @@ -9,30 +9,71 @@ class Palette: + """ + `Palette` -- Stores a mapping from bitmap pixel palette_indexes to display colors + ========================================================================================= + + Map a pixel palette_index to a full color. Colors are transformed to the display's format internally to + save memory. + + .. class:: Palette(color_count) + + Create a Palette object to store a set number of colors. + + :param int color_count: The number of colors in the Palette + """ + def __init__(self, color_count): - self.color_count = color_count + self.__color_count = color_count self.__contents = [] # set all colours to black by default - for i in range(self.color_count): - self.__contents.append(ColorType((0, 0, 0))) - - def __getitem__(self, index): - if index >= self.color_count: - raise IndexError(CONSTANTS.PALETTE_OUT_OF_RANGE) - - return self.__contents[index].rgb888 + for i in range(self.__color_count): + self.__contents.append(_ColorType((0, 0, 0))) def __setitem__(self, index, value): - if index >= self.color_count: + """ + .. method:: __setitem__(index, value) + + Sets the pixel color at the given index. The index should be an integer in the range 0 to color_count-1. + + The value argument represents a color, and can be from 0x000000 to 0xFFFFFF (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. + + This allows you to:: + + palette[0] = 0xFFFFFF # set using an integer + palette[1] = b'\xff\xff\x00' # set using 3 bytes + palette[2] = b'\xff\xff\x00\x00' # set using 4 bytes + palette[3] = bytearray(b'\x00\x00\xFF') # set using a bytearay of 3 or 4 bytes + palette[4] = (10, 20, 30) # set using a tuple of 3 integers + + """ + if index >= self.__color_count: raise IndexError(CONSTANTS.PALETTE_OUT_OF_RANGE) self.__contents[index].rgb888 = value + def __len__(self): + """ + .. method:: __len__() + + Returns the number of colors in a Palette + + """ + return self.__color_count + def make_transparent(self, index): + """ + .. method:: make_transparent(palette_index) + """ self.__toggle_transparency(index, True) def make_opaque(self, index): + """ + .. method:: make_opaque(palette_index) + """ self.__toggle_transparency(index, False) def __toggle_transparency(self, index, transparency): diff --git a/src/base_circuitpython/displayio/test/test_bitmap.py b/src/base_circuitpython/displayio/test/test_bitmap.py index 4691d4886..2967aa632 100644 --- a/src/base_circuitpython/displayio/test/test_bitmap.py +++ b/src/base_circuitpython/displayio/test/test_bitmap.py @@ -42,8 +42,3 @@ def test_get_set_index_err_singular_index(self, x_size, y_size, i): with pytest.raises(IndexError, match=CONSTANTS.PIXEL_OUT_OF_BOUNDS): val = bitmap[i] - - @pytest.mark.parametrize("x, y", [(1, 231), (342, 36), (0, 0)]) - def test_get_len(self, x, y): - bitmap = Bitmap(x, y) - assert len(bitmap) == x * y diff --git a/src/base_circuitpython/displayio/test/test_group.py b/src/base_circuitpython/displayio/test/test_group.py index 48325663d..7eee71159 100644 --- a/src/base_circuitpython/displayio/test/test_group.py +++ b/src/base_circuitpython/displayio/test/test_group.py @@ -155,8 +155,7 @@ def test_draw_group( group_sub.append(tg) group_main.append(group_sub) group_main.append(tg2) - # img = Image.new("RGBA", (240, 240)) - img = group_main.draw() + img = group_main._Group__draw() img.putalpha(255) expected = Image.open( diff --git a/src/base_circuitpython/displayio/test/test_palette.py b/src/base_circuitpython/displayio/test/test_palette.py index bcf6b4284..8b0d1aa38 100644 --- a/src/base_circuitpython/displayio/test/test_palette.py +++ b/src/base_circuitpython/displayio/test/test_palette.py @@ -1,7 +1,7 @@ import pytest from ..palette import Palette from .. import constants as CONSTANTS -from ..color_type import ColorType +from ..color_type import _ColorType class TestPalette(object): @@ -12,7 +12,7 @@ class TestPalette(object): def test_get_and_set_palette(self, color_count, palette_num, val): palette = Palette(color_count) palette[palette_num] = val - assert palette._Palette__contents[palette_num] == ColorType(val) + assert palette._Palette__contents[palette_num] == _ColorType(val) @pytest.mark.parametrize("palette_size, palette_index", [(3, 7), (0, 0), (3, 3)]) def test_get_and_set_palette_err(self, palette_size, palette_index): @@ -20,9 +20,6 @@ def test_get_and_set_palette_err(self, palette_size, palette_index): with pytest.raises(IndexError, match=CONSTANTS.PALETTE_OUT_OF_RANGE): palette[palette_index] = 0 - with pytest.raises(IndexError, match=CONSTANTS.PALETTE_OUT_OF_RANGE): - pal = palette[palette_index] - def test_set_transparency(self): palette = Palette(5) assert palette._Palette__contents[2].transparent == False diff --git a/src/base_circuitpython/displayio/test/test_tile_grid.py b/src/base_circuitpython/displayio/test/test_tile_grid.py index 8c4a591b2..1f2d9d5fa 100644 --- a/src/base_circuitpython/displayio/test/test_tile_grid.py +++ b/src/base_circuitpython/displayio/test/test_tile_grid.py @@ -42,11 +42,11 @@ def test_basic_constructor( tile_grids = [tg1, tg2] for tg in tile_grids: - assert tg.bitmap.width == bitmap_w - assert tg.bitmap.height == bitmap_h + assert tg._TileGrid__bitmap.width == bitmap_w + assert tg._TileGrid__bitmap.height == bitmap_h assert len(tg.pixel_shader._Palette__contents) == palette_num - assert tg.tile_width == tile_width - assert tg.tile_height == tile_height + assert tg._TileGrid__tile_width == tile_width + assert tg._TileGrid__tile_height == tile_height assert tg.x == position[0] assert tg.y == position[1] @@ -58,11 +58,11 @@ def test_basic_constructor( y=position[1], ) - assert tg3.bitmap.width == bitmap_w - assert tg3.bitmap.height == bitmap_h + assert tg3._TileGrid__bitmap.width == bitmap_w + assert tg3._TileGrid__bitmap.height == bitmap_h assert len(tg3.pixel_shader._Palette__contents) == palette_num - assert tg3.tile_width == bitmap_w - assert tg3.tile_height == bitmap_h + assert tg3._TileGrid__tile_width == bitmap_w + assert tg3._TileGrid__tile_height == bitmap_h assert tg3.x == position[0] assert tg3.y == position[1] @@ -79,7 +79,7 @@ def test_tile_set_get(self, w, h, x, y): ) tg_x_y = tg[x, y] - assert tg_x_y == tg.bitmap[x, y] + assert tg_x_y == tg._TileGrid__bitmap[x, y] @pytest.mark.parametrize( "w, h, x, y", [(55, 56, 100, 1), (55, 56, 0, 56), (66, 88, 66, 88)] @@ -140,7 +140,7 @@ def test_draw( "RGBA", (CONSTANTS.SCREEN_HEIGHT_WIDTH, CONSTANTS.SCREEN_HEIGHT_WIDTH) ) # without scaling, test output - img = tg.draw(img, x_offset, y_offset, 1) + img = tg._TileGrid__draw(img, x_offset, y_offset, 1) bmp_img = img.load() for i in range(CONSTANTS.SCREEN_HEIGHT_WIDTH): for j in range(CONSTANTS.SCREEN_HEIGHT_WIDTH): @@ -157,7 +157,7 @@ def test_draw( "RGBA", (CONSTANTS.SCREEN_HEIGHT_WIDTH, CONSTANTS.SCREEN_HEIGHT_WIDTH) ) # with scaling, test output - img = tg.draw(img, x_offset, y_offset, scale) + img = tg._TileGrid__draw(img, x_offset, y_offset, scale) bmp_img = img.load() for i in range(CONSTANTS.SCREEN_HEIGHT_WIDTH): for j in range(CONSTANTS.SCREEN_HEIGHT_WIDTH): diff --git a/src/base_circuitpython/displayio/tile_grid.py b/src/base_circuitpython/displayio/tile_grid.py index 08610085b..730eb75e1 100644 --- a/src/base_circuitpython/displayio/tile_grid.py +++ b/src/base_circuitpython/displayio/tile_grid.py @@ -2,6 +2,7 @@ from . import constants as CONSTANTS import threading import queue +from common import utils # TileGrid implementation loosely based on the # displayio.TileGrid class in Adafruit CircuitPython @@ -13,6 +14,34 @@ class TileGrid: + """ + `TileGrid` -- A grid of tiles sourced out of one bitmap + ========================================================================== + + Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple grids + can share bitmaps and pixel shaders. + + A single tile grid is also known as a Sprite. + + .. class:: TileGrid(bitmap, *, pixel_shader, width=1, height=1, tile_width=None, tile_height=None, default_tile=0, x=0, y=0) + + Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is used to + convert the value and its location to a display native pixel color. This may be a simple color + palette lookup, a gradient, a pattern or a color transformer. + + tile_width and tile_height match the height of the bitmap by default. + + :param displayio.Bitmap bitmap: The bitmap storing one or more tiles. + :param displayio.Palette pixel_shader: The pixel shader that produces colors from values + :param int width: Width of the grid in tiles. + :param int height: Height of the grid in tiles. + :param int tile_width: Width of a single tile in pixels. Defaults to the full Bitmap and must evenly divide into the Bitmap's dimensions. + :param int tile_height: Height of a single tile in pixels. Defaults to the full Bitmap and must evenly divide into the Bitmap's dimensions. + :param int default_tile: Default tile index to show. + :param int x: Initial x position of the left edge within the parent. + :param int y: Initial y position of the top edge within the parent. + """ + def __init__( self, bitmap, @@ -24,16 +53,44 @@ def __init__( y=0, position=None, ): + self.x = None + """ + .. attribute:: x + + X position of the left edge in the parent. + """ + self.y = None + """ + .. attribute:: y + + Y position of the top edge in the parent. + """ + self.pixel_shader = pixel_shader + """ + .. attribute:: pixel_shader + + The pixel shader of the tilegrid. + """ + self.hidden = False + """ + .. attribute:: hidden + + True when the TileGrid is hidden. This may be False even when a part of a hidden Group. + """ + + self.__bitmap = bitmap + self.__tile_width = None + self.__tile_height = None if tile_width is None: - self.tile_width = bitmap.width + self.__tile_width = bitmap.width else: - self.tile_width = tile_width + self.__tile_width = tile_width if tile_height is None: - self.tile_height = bitmap.height + self.__tile_height = bitmap.height else: - self.tile_height = tile_height + self.__tile_height = tile_height if position and isinstance(position, tuple): self.x = position[0] @@ -42,49 +99,118 @@ def __init__( self.x = x self.y = y - self.bitmap = bitmap - self.pixel_shader = pixel_shader - self.default_tile = default_tile - self.parent = None + self.__parent = None + + # unimplemented features + self.__flip_x = False + self.__flip_y = False + self.__transpose_xy = False + + @property + def flip_x(self): + """ + .. attribute:: flip_x + + If true, the left edge rendered will be the right edge of the right-most tile. + """ + return self.__flip_x + + @flip_x.setter + def flip_x(self, val): + utils.print_for_unimplemented_functions(TileGrid.flip_x.__name_) + self.__flip_x = val @property - def in_group(self): - return self.parent != None + def flip_y(self): + + """ + .. attribute:: flip_y + + If true, the top edge rendered will be the bottom edge of the bottom-most tile. + """ + return self.__flip_y + + @flip_y.setter + def flip_y(self, val): + utils.print_for_unimplemented_functions(TileGrid.flip_y.__name_) + self.__flip_y = val + + @property + def transpose_xy(self): + + """ + .. attribute:: transpose_xy + + If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90 degree + rotation can be achieved along with the corresponding mirrored version. + """ + return self.__transpose_xy + + @transpose_xy.setter + def transpose_xy(self, val): + utils.print_for_unimplemented_functions(TileGrid.transpose_xy.__name_) + self.__transpose_xy = val # setitem for an index simply gets the index of the bitmap # rather than the tile index def __setitem__(self, index, value): + """ + .. method:: __setitem__(index, tile_index) + 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``. + + This allows you to:: + + grid[0] = 10 + + or:: + + grid[0,0] = 10 + """ if isinstance(index, tuple): - if index[0] >= self.tile_width or index[1] >= self.tile_height: + if index[0] >= self.__tile_width or index[1] >= self.__tile_height: raise IndexError(CONSTANTS.TILE_OUT_OF_BOUNDS) - self.bitmap[index] = value + self.__bitmap[index] = value # getitem for an index simply gets the index of the bitmap # rather than the tile index def __getitem__(self, index): + """ + .. method:: __getitem__(index) + 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``. + + This allows you to:: + + print(grid[0]) + """ if isinstance(index, tuple): - if index[0] >= self.tile_width or index[1] >= self.tile_height: + if index[0] >= self.__tile_width or index[1] >= self.__tile_height: raise IndexError(CONSTANTS.TILE_OUT_OF_BOUNDS) - return self.bitmap[index] + return self.__bitmap[index] + + @property + def __in_group(self): + return self.__parent != None # methods that are not in the origin class: - def draw(self, img, x, y, scale): + def __draw(self, img, x, y, scale): # draw the current bitmap with # appropriate scale on the global bmp_img x = self.x * scale + x y = self.y * scale + y - new_shape = self.draw_group( - x, y, 0, self.tile_height, 0, self.tile_width, scale + new_shape = self.__draw_group( + x, y, 0, self.__tile_height, 0, self.__tile_width, scale ) img.paste(new_shape, (x, y), new_shape) return img - def draw_group(self, x, y, y_start, y_end, x_start, x_end, scale): + def __draw_group(self, x, y, y_start, y_end, x_start, x_end, scale): height = y_end - y_start width = x_end - x_start @@ -100,13 +226,15 @@ def draw_group(self, x, y, y_start, y_end, x_start, x_end, scale): x_max = min(x_offset + scale, width * scale) y_max = min(y_offset + scale, height * scale) - curr_val = self.bitmap[j, i] - transparent = self.pixel_shader._Palette__contents[curr_val].transparent + curr_val = self.__bitmap[j, i] + palette_obj = self.pixel_shader._Palette__contents[curr_val] + + transparent = palette_obj.transparent if not transparent and x_offset >= 0 and y_offset >= 0: - curr_colour = self.pixel_shader[curr_val] - self.fill_pixel( + curr_colour = palette_obj.rgb888 + self.__fill_pixel( curr_val, curr_colour, x_offset, @@ -121,7 +249,7 @@ def draw_group(self, x, y, y_start, y_end, x_start, x_end, scale): # helper method for drawing pixels on bmp_img # given the src, offset, and scale - def fill_pixel( + def __fill_pixel( self, curr_val, curr_colour, diff --git a/src/base_circuitpython/pulseio.py b/src/base_circuitpython/pulseio.py new file mode 100644 index 000000000..f69a45836 --- /dev/null +++ b/src/base_circuitpython/pulseio.py @@ -0,0 +1,16 @@ +from common import utils + + +class PulseIn: + def __init__(self, pin, maxlen=2, *, idle_state=False): + utils.print_for_unimplemented_functions(PulseIn.__init__.__qualname__) + + +class PulseOut: + def __init__(self, carrier): + utils.print_for_unimplemented_functions(PulseOut.__init__.__qualname__) + + +class PWMOut: + def __init__(self, pin, *, duty_cycle=0, frequency=500, variable_frequency=False): + utils.print_for_unimplemented_functions(PWMOut.__init__.__qualname__) diff --git a/src/base_circuitpython/terminal_handler.py b/src/base_circuitpython/terminal_handler.py index 2fff004e5..83130f0b0 100644 --- a/src/base_circuitpython/terminal_handler.py +++ b/src/base_circuitpython/terminal_handler.py @@ -37,7 +37,7 @@ def __create_newline(self, str_list): self.__lock.release() - def draw(self, no_verif=False): + def __draw(self, no_verif=False): import adafruit_display_text.label @@ -74,7 +74,7 @@ def draw(self, no_verif=False): self.__lock.release() - splash.draw(img=self.__base_img.copy()) + splash._Group__draw(img=self.__base_img.copy()) def add_str_to_terminal(self, curr_display_string=""): @@ -105,4 +105,4 @@ def add_str_to_terminal(self, curr_display_string=""): # only go ahead to draw the screen # if the terminal is actively on the screen if board.DISPLAY.active_group == None: - self.draw() + self.__draw() diff --git a/src/clue/adafruit_clue.py b/src/clue/adafruit_clue.py index 189ab988b..54c652339 100644 --- a/src/clue/adafruit_clue.py +++ b/src/clue/adafruit_clue.py @@ -162,14 +162,10 @@ def add_text_line(self, color=0xFFFFFF): def show(self): """Call show() to display the data list.""" self._display.show(self.text_group) - # https://stackoverflow.com/questions/31826335/how-to-convert-pil-image-image-object-to-base64-string def show_terminal(self): """Revert to terminalio screen.""" - self._display.show(None) - # TODO: implement terminal for clue screen - return class Clue: # pylint: disable=too-many-instance-attributes, too-many-public-methods diff --git a/src/clue/adafruit_slideshow.py b/src/clue/adafruit_slideshow.py index cef98428b..f38e7b0cf 100644 --- a/src/clue/adafruit_slideshow.py +++ b/src/clue/adafruit_slideshow.py @@ -40,6 +40,62 @@ class PlayBackDirection: # custom class SlideShow: + """ + Class for displaying a slideshow of .bmp images on displays. + :param str folder: Specify the folder containing the image files, in quotes. Default is + the root directory, ``"/"``. + :param PlayBackOrder order: The order in which the images display. You can choose random + (``RANDOM``) or alphabetical (``ALPHABETICAL``). Default is + ``ALPHABETICAL``. + :param bool loop: Specify whether to loop the images or play through the list once. `True` + if slideshow will continue to loop, ``False`` if it will play only once. + Default is ``True``. + :param int dwell: The number of seconds each image displays, in seconds. Default is 3. + :param bool fade_effect: Specify whether to include the fade effect between images. ``True`` + tells the code to fade the backlight up and down between image display + transitions. ``False`` maintains max brightness on the backlight between + image transitions. Default is ``True``. + :param bool auto_advance: Specify whether to automatically advance after dwell seconds. ``True`` + if slideshow should auto play, ``False`` if you want to control advancement + manually. Default is ``True``. + :param PlayBackDirection direction: The playback direction. + Example code for Hallowing Express. With this example, the slideshow will play through once + in alphabetical order: + .. code-block:: python + from adafruit_slideshow import PlayBackOrder, SlideShow + import board + import pulseio + slideshow = SlideShow(board.DISPLAY, pulseio.PWMOut(board.TFT_BACKLIGHT), folder="/", + loop=False, order=PlayBackOrder.ALPHABETICAL) + while slideshow.update(): + pass + Example code for Hallowing Express. Sets ``dwell`` to 0 seconds, turns ``auto_advance`` off, + and uses capacitive touch to advance backwards and forwards through the images and to control + the brightness level of the backlight: + .. code-block:: python + from adafruit_slideshow import PlayBackOrder, SlideShow, PlayBackDirection + import touchio + import board + import pulseio + forward_button = touchio.TouchIn(board.TOUCH4) + back_button = touchio.TouchIn(board.TOUCH1) + brightness_up = touchio.TouchIn(board.TOUCH3) + brightness_down = touchio.TouchIn(board.TOUCH2) + slideshow = SlideShow(board.DISPLAY, pulseio.PWMOut(board.TFT_BACKLIGHT), folder="/", + auto_advance=False, dwell=0) + while True: + if forward_button.value: + slideshow.direction = PlayBackDirection.FORWARD + slideshow.advance() + if back_button.value: + slideshow.direction = PlayBackDirection.BACKWARD + slideshow.advance() + if brightness_up.value: + slideshow.brightness += 0.001 + elif brightness_down.value: + slideshow.brightness -= 0.001 + """ + def __init__( self, display, @@ -76,7 +132,7 @@ def __init__( """Specify the playback direction. Default is ``PlayBackDirection.FORWARD``. Can also be ``PlayBackDirection.BACKWARD``.""" - self.advance = self._advance_with_fade + self.advance = self.__advance_with_fade """Displays the next image. Returns True when a new image was displayed, False otherwise. """ @@ -84,7 +140,7 @@ def __init__( # assign new advance method if fade is disabled if not fade_effect: - self.advance = self._advance_no_fade + self.advance = self.__advance_no_fade self._img_start = None @@ -111,7 +167,7 @@ def __init__( self._curr_img = "" # load images into main queue - self._load_images() + self.__load_images() display.show(self) # show the first working image @@ -134,7 +190,7 @@ def order(self, order): raise ValueError("Order must be either 'RANDOM' or 'ALPHABETICAL'") self._order = order - self._load_images() + self.__load_images() @property def brightness(self): @@ -157,12 +213,12 @@ def update(self): return self.advance() - def _get_next_img(self): + def __get_next_img(self): # handle empty queue if not len(self.pic_queue): if self.loop: - self._load_images() + self.__load_images() else: return "" @@ -171,7 +227,7 @@ def _get_next_img(self): else: return self.pic_queue.pop() - def _load_images(self): + def __load_images(self): dir_imgs = [] for d in self.dirs: try: @@ -195,7 +251,7 @@ def _load_images(self): # (must be list beforehand for potential randomization) self.pic_queue = collections.deque(dir_imgs) - def _advance_with_fade(self): + def __advance_with_fade(self): if board.DISPLAY.active_group != self: return @@ -203,7 +259,7 @@ def _advance_with_fade(self): advance_sucessful = False while not advance_sucessful: - new_path = self._get_next_img() + new_path = self.__get_next_img() if new_path == "": return False @@ -236,7 +292,7 @@ def _advance_with_fade(self): sendable_img = Image.blend( black_overlay, old_img, i * self.brightness / self.fade_frames ) - self._send(sendable_img) + self.__send(sendable_img) time.sleep(self._BASE_DWELL_DARK) @@ -245,14 +301,14 @@ def _advance_with_fade(self): sendable_img = Image.blend( black_overlay, new_img, i * self.brightness / self.fade_frames ) - self._send(sendable_img) + self.__send(sendable_img) self._curr_img_handle = new_img self._curr_img = new_path self._img_start = time.monotonic() return True - def _advance_no_fade(self): + def __advance_no_fade(self): if board.DISPLAY.active_group != self: return @@ -261,7 +317,7 @@ def _advance_no_fade(self): advance_sucessful = False while not advance_sucessful: - new_path = self._get_next_img() + new_path = self.__get_next_img() if new_path == "": return False @@ -303,14 +359,14 @@ def _advance_no_fade(self): ) img_piece = new_img.crop((0, 0, CONSTANTS.SCREEN_HEIGHT_WIDTH, curr_y)) old_img.paste(img_piece) - self._send(old_img) + self.__send(old_img) self._curr_img_handle = new_img self._curr_img = new_path self._img_start = time.monotonic() return True - def _send(self, img): + def __send(self, img): # sends current bmp_img to the frontend buffered = BytesIO() img.save(buffered, format=CONSTANTS.BMP_IMG) diff --git a/src/clue/test/test_adafruit_clue.py b/src/clue/test/test_adafruit_clue.py index fcd3f7e3e..4ce4fcf50 100644 --- a/src/clue/test/test_adafruit_clue.py +++ b/src/clue/test/test_adafruit_clue.py @@ -38,7 +38,7 @@ def test_clue_display_text(self): expected = img.load() clue_data = clue.simple_text_display(title="LET'S TEST!", title_scale=2) - clue_data.text_group.show = self._send_helper + clue_data.text_group._Group__show = self.__send_helper clue_data.text_group._Group__check_active_group_ref = False clue_data[0].text = "Lorem ipsum" @@ -58,7 +58,7 @@ def test_clue_display_text(self): clue_data.show() helper._Helper__test_image_equality(self.main_img.load(), expected) - def _send_helper(self, image): + def __send_helper(self, image): self.main_img = image def test_buttons(self): diff --git a/src/clue/test/test_adafruit_display_shapes.py b/src/clue/test/test_adafruit_display_shapes.py index 0036b2e73..8c1c3c303 100644 --- a/src/clue/test/test_adafruit_display_shapes.py +++ b/src/clue/test/test_adafruit_display_shapes.py @@ -48,7 +48,7 @@ def test_shapes(self): # TAKEN FROM ADAFRUIT'S DISPLAY SHAPES LIBRARY # https://github.com/ladyada/Adafruit_CircuitPython_Display_Shapes/blob/master/examples/display_shapes_simpletest.py splash = displayio.Group(max_size=10) - splash.show = self._send_helper + splash._Group__show = self.__send_helper board.DISPLAY.show(splash) color_bitmap = displayio.Bitmap(320, 240, 1) color_palette = displayio.Palette(1) @@ -77,5 +77,5 @@ def test_shapes(self): helper._Helper__test_image_equality(self.main_img.load(), expected_images[4]) - def _send_helper(self, image): + def __send_helper(self, image): self.main_img = image diff --git a/src/clue/test/test_adafruit_display_text.py b/src/clue/test/test_adafruit_display_text.py index 5d6078704..922230275 100644 --- a/src/clue/test/test_adafruit_display_text.py +++ b/src/clue/test/test_adafruit_display_text.py @@ -59,7 +59,7 @@ def test_display_text(self, text, x, y, scale, color): text_area.x = x text_area.y = y - main_img = text_area.draw() + main_img = text_area._Group__draw() helper._Helper__test_image_equality(main_img.load(), loaded_img) test_count += 1 diff --git a/src/clue/test/test_adafruit_slideshow.py b/src/clue/test/test_adafruit_slideshow.py index 649e3ccb4..1f966b15a 100644 --- a/src/clue/test/test_adafruit_slideshow.py +++ b/src/clue/test/test_adafruit_slideshow.py @@ -61,7 +61,7 @@ def test_slideshow(self): direction=PlayBackDirection.FORWARD, ) - slideshow._send = self._send_helper + slideshow._SlideShow__send = self.__send_helper # first image's appear time is unstable,since it fades/scrolls in # can only predict following ones... @@ -84,7 +84,7 @@ def test_slideshow(self): direction=PlayBackDirection.BACKWARD, ) - slideshow2._send = self._send_helper + slideshow2._SlideShow__send = self.__send_helper helper._Helper__test_image_equality( self.main_img.load(), slideshow_images[7].load() @@ -96,5 +96,5 @@ def test_slideshow(self): self.main_img.load(), slideshow_images[i].load() ) - def _send_helper(self, image): + def __send_helper(self, image): self.main_img = image diff --git a/src/constants.ts b/src/constants.ts index 792735372..9ca39ef45 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -132,9 +132,11 @@ export const CONSTANTS = { ), }, FILESYSTEM: { - OUTPUT_DIRECTORY: "out", + BASE_CIRCUITPYTHON: "base_circuitpython", + CLUE: "clue", PYTHON_VENV_DIR: "venv", MICROPYTHON_DIRECTORY: "micropython", + OUTPUT_DIRECTORY: "out", }, INFO: { ALREADY_SUCCESSFUL_INSTALL: localize( diff --git a/src/extension.ts b/src/extension.ts index 68a069d18..13f0ce1d5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1011,6 +1011,8 @@ const updatePythonExtraPaths = () => { [ __dirname, path.join(__dirname, CONSTANTS.FILESYSTEM.MICROPYTHON_DIRECTORY), + path.join(__dirname, CONSTANTS.FILESYSTEM.CLUE), + path.join(__dirname, CONSTANTS.FILESYSTEM.BASE_CIRCUITPYTHON), ], vscode.ConfigurationTarget.Global ); @@ -1027,13 +1029,24 @@ const updatePylintArgs = (context: vscode.ExtensionContext) => { CONSTANTS.FILESYSTEM.MICROPYTHON_DIRECTORY ); + const cluePath: string = utils.createEscapedPath( + context.extensionPath, + CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, + CONSTANTS.FILESYSTEM.CLUE + ); + + const baseCircuitPythonPath: string = utils.createEscapedPath( + context.extensionPath, + CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, + CONSTANTS.FILESYSTEM.BASE_CIRCUITPYTHON + ); // update pylint args to extend system path // to include python libs local to extention updateConfigLists( "python.linting.pylintArgs", [ "--init-hook", - `import sys; sys.path.extend([\"${outPath}\",\"${micropythonPath}\"])`, + `import sys; sys.path.extend([\"${outPath}\",\"${micropythonPath}\",\"${cluePath}\",\"${baseCircuitPythonPath}\"])`, ], vscode.ConfigurationTarget.Workspace ); diff --git a/src/process_user_code.py b/src/process_user_code.py index 1287fc241..b1d55554a 100644 --- a/src/process_user_code.py +++ b/src/process_user_code.py @@ -24,7 +24,7 @@ abs_path_to_parent_dir = os.path.dirname(os.path.abspath(__file__)) # Insert absolute path to library for CLUE into sys.path -sys.path.insert(0, os.path.join(abs_path_to_parent_dir, CONSTANTS.CLUE)) +sys.path.insert(0, os.path.join(abs_path_to_parent_dir, CONSTANTS.CLUE_DIR)) # Insert absolute path to Circuitpython libraries for CLUE into sys.path sys.path.insert(0, os.path.join(abs_path_to_parent_dir, CONSTANTS.CIRCUITPYTHON)) diff --git a/src/python_constants.py b/src/python_constants.py index 098e854f7..f469ec149 100644 --- a/src/python_constants.py +++ b/src/python_constants.py @@ -47,6 +47,7 @@ MICROBIT = "micro:bit" CLUE = "CLUE" +CLUE_DIR = "clue" CIRCUITPYTHON = "base_circuitpython"