From 4fa256c11cde69c408ef6003c77c1f51e6dce94c Mon Sep 17 00:00:00 2001 From: Nick Trimborn Date: Sun, 3 Nov 2024 19:41:03 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 2 - E_Ink_Test/code.py | 88 +++ .../lib/adafruit_display_text/__init__.py | 476 +++++++++++++ .../lib/adafruit_display_text/bitmap_label.py | 594 ++++++++++++++++ E_Ink_Test/lib/adafruit_display_text/label.py | 447 ++++++++++++ .../adafruit_display_text/outlined_label.py | 188 ++++++ .../adafruit_display_text/scrolling_label.py | 158 +++++ E_Ink_Test/lib/adafruit_framebuf.py | 639 ++++++++++++++++++ E_Ink_Test/lib/adafruit_ssd1680.py | 110 +++ E_Ink_Test/lib/display_ruler.bmp | Bin 0 -> 360122 bytes E_Ink_Test/lib/neopixel.py | 180 +++++ 11 files changed, 2880 insertions(+), 2 deletions(-) create mode 100644 E_Ink_Test/code.py create mode 100644 E_Ink_Test/lib/adafruit_display_text/__init__.py create mode 100644 E_Ink_Test/lib/adafruit_display_text/bitmap_label.py create mode 100644 E_Ink_Test/lib/adafruit_display_text/label.py create mode 100644 E_Ink_Test/lib/adafruit_display_text/outlined_label.py create mode 100644 E_Ink_Test/lib/adafruit_display_text/scrolling_label.py create mode 100644 E_Ink_Test/lib/adafruit_framebuf.py create mode 100644 E_Ink_Test/lib/adafruit_ssd1680.py create mode 100644 E_Ink_Test/lib/display_ruler.bmp create mode 100644 E_Ink_Test/lib/neopixel.py diff --git a/.gitignore b/.gitignore index 5d381cc..466070a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ diff --git a/E_Ink_Test/code.py b/E_Ink_Test/code.py new file mode 100644 index 0000000..7653a96 --- /dev/null +++ b/E_Ink_Test/code.py @@ -0,0 +1,88 @@ +import time +import board +import displayio +#import fourwire +import adafruit_ssd1680 +import busio +import terminalio +from adafruit_display_text import label + +BLACK = 0x000000 +WHITE = 0xFFFFFF +RED = 0xFF0000 + +FOREGROUND_COLOR = BLACK +BACKGROUND_COLOR = WHITE + +# Create the display object - the third color is red (0xff0000) +DISPLAY_WIDTH = 296 +DISPLAY_HEIGHT = 127 + +# For 8.x.x and 9.x.x. When 8.x.x is discontinued as a stable release, change this. +try: + from fourwire import FourWire +except ImportError: + from displayio import FourWire + +displayio.release_displays() + +# Pin order defined based on WeAct Header order +edp_busy = board.IO0 +epd_reset = board.IO1 +epd_dc = board.IO2 +epd_cs = board.IO3 +edp_clk = board.IO4 +edp_mosi = board.IO5 +#edp_miso = +spi = busio.SPI(clock=edp_clk, MOSI=edp_mosi) + +display_bus = FourWire( + spi, command=epd_dc, chip_select=epd_cs, reset=epd_reset, baudrate=1000000 +) + +time.sleep(1) + +display = adafruit_ssd1680.SSD1680( + display_bus, + colstart=1, + width=DISPLAY_WIDTH, + height=DISPLAY_HEIGHT, + busy_pin=edp_busy, + highlight_color=FOREGROUND_COLOR, + rotation=270, +) + +# Create a display group for our screen objects +g = displayio.Group() + +# Set a background +background_bitmap = displayio.Bitmap(DISPLAY_WIDTH, DISPLAY_HEIGHT, 1) +# Map colors in a palette +palette = displayio.Palette(1) +palette[0] = BACKGROUND_COLOR + +# Create a Tilegrid with the background and put in the displayio group +t = displayio.TileGrid(background_bitmap, pixel_shader=palette) +g.append(t) + +# Draw simple text using the built-in font into a displayio group +text_group = displayio.Group(scale=3, x=20, y=80) +text = "Hello World2!" +text_area = label.Label(terminalio.FONT, text=text, color=FOREGROUND_COLOR) +text_group.append(text_area) # Add this text to the text group +g.append(text_group) + +# Place the display group on the screen +display.root_group = g + +# Refresh the display to have everything show on the display +# NOTE: Do not refresh eInk displays more often than 180 seconds! +display.refresh() + +time.sleep(120) + +while True: + pass + + + diff --git a/E_Ink_Test/lib/adafruit_display_text/__init__.py b/E_Ink_Test/lib/adafruit_display_text/__init__.py new file mode 100644 index 0000000..270e25f --- /dev/null +++ b/E_Ink_Test/lib/adafruit_display_text/__init__.py @@ -0,0 +1,476 @@ +# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text` +======================= +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + +from displayio import Group, Palette + +try: + from typing import Optional, List, Tuple + from fontio import FontProtocol +except ImportError: + pass + + +def wrap_text_to_pixels( + string: str, + max_width: int, + font: Optional[FontProtocol] = None, + indent0: str = "", + indent1: str = "", +) -> List[str]: + # pylint: disable=too-many-branches, too-many-locals, too-many-nested-blocks, too-many-statements + + """wrap_text_to_pixels function + A helper that will return a list of lines with word-break wrapping. + Leading and trailing whitespace in your string will be removed. If + you wish to use leading whitespace see ``indent0`` and ``indent1`` + parameters. + + :param str string: The text to be wrapped. + :param int max_width: The maximum number of pixels on a line before wrapping. + :param font: The font to use for measuring the text. + :type font: ~fontio.FontProtocol + :param str indent0: Additional character(s) to add to the first line. + :param str indent1: Additional character(s) to add to all other lines. + + :return: A list of the lines resulting from wrapping the + input text at ``max_width`` pixels size + :rtype: List[str] + + """ + if font is None: + + def measure(text): + return len(text) + + else: + if hasattr(font, "load_glyphs"): + font.load_glyphs(string) + + def measure(text): + total_len = 0 + for char in text: + this_glyph = font.get_glyph(ord(char)) + if this_glyph: + total_len += this_glyph.shift_x + return total_len + + lines = [] + partial = [indent0] + width = measure(indent0) + swidth = measure(" ") + firstword = True + for line_in_input in string.split("\n"): + newline = True + for index, word in enumerate(line_in_input.split(" ")): + wwidth = measure(word) + word_parts = [] + cur_part = "" + + if wwidth > max_width: + for char in word: + if newline: + extraspace = 0 + leadchar = "" + else: + extraspace = swidth + leadchar = " " + if ( + measure("".join(partial)) + + measure(cur_part) + + measure(char) + + measure("-") + + extraspace + > max_width + ): + if cur_part: + word_parts.append( + "".join(partial) + leadchar + cur_part + "-" + ) + + else: + word_parts.append("".join(partial)) + cur_part = char + partial = [indent1] + newline = True + else: + cur_part += char + if cur_part: + word_parts.append(cur_part) + for line in word_parts[:-1]: + lines.append(line) + partial.append(word_parts[-1]) + width = measure(word_parts[-1]) + if firstword: + firstword = False + else: + if firstword: + partial.append(word) + firstword = False + width += wwidth + elif width + swidth + wwidth < max_width: + if index > 0: + partial.append(" ") + partial.append(word) + width += wwidth + swidth + else: + lines.append("".join(partial)) + partial = [indent1, word] + width = measure(indent1) + wwidth + if newline: + newline = False + + lines.append("".join(partial)) + partial = [indent1] + width = measure(indent1) + + return lines + + +def wrap_text_to_lines(string: str, max_chars: int) -> List[str]: + """wrap_text_to_lines function + A helper that will return a list of lines with word-break wrapping + + :param str string: The text to be wrapped + :param int max_chars: The maximum number of characters on a line before wrapping + + :return: A list of lines where each line is separated based on the amount + of ``max_chars`` provided + :rtype: List[str] + """ + + def chunks(lst, n): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i : i + n] + + string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines + words = string.split(" ") + the_lines = [] + the_line = "" + for w in words: + if len(w) > max_chars: + if the_line: # add what we had stored + the_lines.append(the_line) + parts = [] + for part in chunks(w, max_chars - 1): + parts.append("{}-".format(part)) + the_lines.extend(parts[:-1]) + the_line = parts[-1][:-1] + continue + + if len(the_line + " " + w) <= max_chars: + the_line += " " + w + elif not the_line and len(w) == max_chars: + the_lines.append(w) + else: + the_lines.append(the_line) + the_line = "" + w + if the_line: # Last line remaining + the_lines.append(the_line) + # Remove any blank lines + while not the_lines[0]: + del the_lines[0] + # Remove first space from first line: + if the_lines[0][0] == " ": + the_lines[0] = the_lines[0][1:] + return the_lines + + +class LabelBase(Group): + # pylint: disable=too-many-instance-attributes + + """Superclass that all other types of labels will extend. This contains + all of the properties and functions that work the same way in all labels. + + **Note:** This should be treated as an abstract base class. + + Subclasses should implement ``_set_text``, ``_set_font``, and ``_set_line_spacing`` to + have the correct behavior for that type of label. + + :param font: A font class that has ``get_bounding_box`` and ``get_glyph``. + Must include a capital M for measuring character size. + :type font: ~fontio.FontProtocol + :param str text: Text to display + :param int color: Color of all text in RGB hex + :param int background_color: Color of the background, use `None` for transparent + :param float line_spacing: Line spacing of text to display + :param bool background_tight: Set `True` only if you want background box to tightly + surround text. When set to 'True' Padding parameters will be ignored. + :param int padding_top: Additional pixels added to background bounding box at top + :param int padding_bottom: Additional pixels added to background bounding box at bottom + :param int padding_left: Additional pixels added to background bounding box at left + :param int padding_right: Additional pixels added to background bounding box at right + :param (float,float) anchor_point: Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.) + :param (int,int) anchored_position: Position relative to the anchor_point. Tuple + containing x,y pixel coordinates. + :param int scale: Integer value of the pixel scaling + :param bool base_alignment: when True allows to align text label to the baseline. + This is helpful when two or more labels need to be aligned to the same baseline + :param (int,str) tab_replacement: tuple with tab character replace information. When + (4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by + tab character + :param str label_direction: string defining the label text orientation. See the + subclass documentation for the possible values. + :param bool verbose: print debugging information in some internal functions. Default to False + """ + + def __init__( + self, + font: FontProtocol, + x: int = 0, + y: int = 0, + text: str = "", + color: int = 0xFFFFFF, + background_color: int = None, + line_spacing: float = 1.25, + background_tight: bool = False, + padding_top: int = 0, + padding_bottom: int = 0, + padding_left: int = 0, + padding_right: int = 0, + anchor_point: Tuple[float, float] = None, + anchored_position: Tuple[int, int] = None, + scale: int = 1, + base_alignment: bool = False, + tab_replacement: Tuple[int, str] = (4, " "), + label_direction: str = "LTR", + verbose: bool = False, + **kwargs, # pylint: disable=unused-argument + ) -> None: + # pylint: disable=too-many-arguments, too-many-locals + + super().__init__(x=x, y=y, scale=1) + + self._font = font + self._text = text + self._palette = Palette(2) + self._color = 0xFFFFFF + self._background_color = None + self._line_spacing = line_spacing + self._background_tight = background_tight + self._padding_top = padding_top + self._padding_bottom = padding_bottom + self._padding_left = padding_left + self._padding_right = padding_right + self._anchor_point = anchor_point + self._anchored_position = anchored_position + self._base_alignment = base_alignment + self._label_direction = label_direction + self._tab_replacement = tab_replacement + self._tab_text = self._tab_replacement[1] * self._tab_replacement[0] + self._verbose = verbose + + if "max_glyphs" in kwargs: + print("Please update your code: 'max_glyphs' is not needed anymore.") + + self._ascent, self._descent = self._get_ascent_descent() + self._bounding_box = None + + self.color = color + self.background_color = background_color + + # local group will hold background and text + # the self group scale should always remain at 1, the self._local_group will + # be used to set the scale of the label + self._local_group = Group(scale=scale) + self.append(self._local_group) + + self._baseline = -1.0 + + if self._base_alignment: + self._y_offset = 0 + else: + self._y_offset = self._ascent // 2 + + def _get_ascent_descent(self) -> Tuple[int, int]: + """Private function to calculate ascent and descent font values""" + if hasattr(self.font, "ascent") and hasattr(self.font, "descent"): + return self.font.ascent, self.font.descent + + # check a few glyphs for maximum ascender and descender height + glyphs = "M j'" # choose glyphs with highest ascender and lowest + try: + self._font.load_glyphs(glyphs) + except AttributeError: + # Builtin font doesn't have or need load_glyphs + pass + # descender, will depend upon font used + ascender_max = descender_max = 0 + for char in glyphs: + this_glyph = self._font.get_glyph(ord(char)) + if this_glyph: + ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy) + descender_max = max(descender_max, -this_glyph.dy) + return ascender_max, descender_max + + @property + def font(self) -> FontProtocol: + """Font to use for text display.""" + return self._font + + def _set_font(self, new_font: FontProtocol) -> None: + raise NotImplementedError("{} MUST override '_set_font'".format(type(self))) + + @font.setter + def font(self, new_font: FontProtocol) -> None: + self._set_font(new_font) + + @property + def color(self) -> int: + """Color of the text as an RGB hex number.""" + return self._color + + @color.setter + def color(self, new_color: int): + self._color = new_color + if new_color is not None: + self._palette[1] = new_color + self._palette.make_opaque(1) + else: + self._palette[1] = 0 + self._palette.make_transparent(1) + + @property + def background_color(self) -> int: + """Color of the background as an RGB hex number.""" + return self._background_color + + def _set_background_color(self, new_color): + raise NotImplementedError( + "{} MUST override '_set_background_color'".format(type(self)) + ) + + @background_color.setter + def background_color(self, new_color: int) -> None: + self._set_background_color(new_color) + + @property + def anchor_point(self) -> Tuple[float, float]: + """Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)""" + return self._anchor_point + + @anchor_point.setter + def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None: + if new_anchor_point[1] == self._baseline: + self._anchor_point = (new_anchor_point[0], -1.0) + else: + self._anchor_point = new_anchor_point + + # update the anchored_position using setter + self.anchored_position = self._anchored_position + + @property + def anchored_position(self) -> Tuple[int, int]: + """Position relative to the anchor_point. Tuple containing x,y + pixel coordinates.""" + return self._anchored_position + + @anchored_position.setter + def anchored_position(self, new_position: Tuple[int, int]) -> None: + self._anchored_position = new_position + # Calculate (x,y) position + if (self._anchor_point is not None) and (self._anchored_position is not None): + self.x = int( + new_position[0] + - (self._bounding_box[0] * self.scale) + - round(self._anchor_point[0] * (self._bounding_box[2] * self.scale)) + ) + if self._anchor_point[1] == self._baseline: + self.y = int(new_position[1] - (self._y_offset * self.scale)) + else: + self.y = int( + new_position[1] + - (self._bounding_box[1] * self.scale) + - round(self._anchor_point[1] * self._bounding_box[3] * self.scale) + ) + + @property + def scale(self) -> int: + """Set the scaling of the label, in integer values""" + return self._local_group.scale + + @scale.setter + def scale(self, new_scale: int) -> None: + self._local_group.scale = new_scale + self.anchored_position = self._anchored_position # update the anchored_position + + def _set_text(self, new_text: str, scale: int) -> None: + raise NotImplementedError("{} MUST override '_set_text'".format(type(self))) + + @property + def text(self) -> str: + """Text to be displayed.""" + return self._text + + @text.setter # Cannot set color or background color with text setter, use separate setter + def text(self, new_text: str) -> None: + self._set_text(new_text, self.scale) + + @property + def bounding_box(self) -> Tuple[int, int]: + """An (x, y, w, h) tuple that completely covers all glyphs. The + first two numbers are offset from the x, y origin of this group""" + return tuple(self._bounding_box) + + @property + def height(self) -> int: + """The height of the label determined from the bounding box.""" + return self._bounding_box[3] + + @property + def width(self) -> int: + """The width of the label determined from the bounding box.""" + return self._bounding_box[2] + + @property + def line_spacing(self) -> float: + """The amount of space between lines of text, in multiples of the font's + bounding-box height. (E.g. 1.0 is the bounding-box height)""" + return self._line_spacing + + def _set_line_spacing(self, new_line_spacing: float) -> None: + raise NotImplementedError( + "{} MUST override '_set_line_spacing'".format(type(self)) + ) + + @line_spacing.setter + def line_spacing(self, new_line_spacing: float) -> None: + self._set_line_spacing(new_line_spacing) + + @property + def label_direction(self) -> str: + """Set the text direction of the label""" + return self._label_direction + + def _set_label_direction(self, new_label_direction: str) -> None: + raise NotImplementedError( + "{} MUST override '_set_label_direction'".format(type(self)) + ) + + def _get_valid_label_directions(self) -> Tuple[str, ...]: + raise NotImplementedError( + "{} MUST override '_get_valid_label_direction'".format(type(self)) + ) + + @label_direction.setter + def label_direction(self, new_label_direction: str) -> None: + """Set the text direction of the label""" + if new_label_direction not in self._get_valid_label_directions(): + raise RuntimeError("Please provide a valid text direction") + self._set_label_direction(new_label_direction) + + def _replace_tabs(self, text: str) -> str: + return text if text.find("\t") < 0 else self._tab_text.join(text.split("\t")) diff --git a/E_Ink_Test/lib/adafruit_display_text/bitmap_label.py b/E_Ink_Test/lib/adafruit_display_text/bitmap_label.py new file mode 100644 index 0000000..ad6ea5f --- /dev/null +++ b/E_Ink_Test/lib/adafruit_display_text/bitmap_label.py @@ -0,0 +1,594 @@ +# SPDX-FileCopyrightText: 2020 Kevin Matocha +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text.bitmap_label` +================================================================================ + +Text graphics handling for CircuitPython, including text boxes + + +* Author(s): Kevin Matocha + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + +import displayio +from adafruit_display_text import LabelBase + +try: + import bitmaptools +except ImportError: + # We have a slower fallback for bitmaptools + pass + +try: + from typing import Optional, Tuple + from fontio import FontProtocol +except ImportError: + pass + + +# pylint: disable=too-many-instance-attributes +class Label(LabelBase): + """A label displaying a string of text that is stored in a bitmap. + Note: This ``bitmap_label.py`` library utilizes a :py:class:`~displayio.Bitmap` + to display the text. This method is memory-conserving relative to ``label.py``. + + For further reduction in memory usage, set ``save_text=False`` (text string will not + be stored and ``line_spacing`` and ``font`` are immutable with ``save_text`` + set to ``False``). + + The origin point set by ``x`` and ``y`` + properties will be the left edge of the bounding box, and in the center of a M + glyph (if its one line), or the (number of lines * linespacing + M)/2. That is, + it will try to have it be center-left as close as possible. + + :param font: A font class that has ``get_bounding_box`` and ``get_glyph``. + Must include a capital M for measuring character size. + :type font: ~fontio.FontProtocol + :param str text: Text to display + :param int|Tuple(int, int, int) color: Color of all text in HEX or RGB + :param int|Tuple(int, int, int)|None background_color: Color of the background, use `None` + for transparent + :param float line_spacing: Line spacing of text to display + :param bool background_tight: Set `True` only if you want background box to tightly + surround text. When set to 'True' Padding parameters will be ignored. + :param int padding_top: Additional pixels added to background bounding box at top + :param int padding_bottom: Additional pixels added to background bounding box at bottom + :param int padding_left: Additional pixels added to background bounding box at left + :param int padding_right: Additional pixels added to background bounding box at right + :param Tuple(float, float) anchor_point: Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.) + :param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple + containing x,y pixel coordinates. + :param int scale: Integer value of the pixel scaling + :param bool save_text: Set True to save the text string as a constant in the + label structure. Set False to reduce memory use. + :param bool base_alignment: when True allows to align text label to the baseline. + This is helpful when two or more labels need to be aligned to the same baseline + :param Tuple(int, str) tab_replacement: tuple with tab character replace information. When + (4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by + tab character + :param str label_direction: string defining the label text orientation. There are 5 + configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left + ``UPD``-Upside Down ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR`` + :param bool verbose: print debugging information in some internal functions. Default to False + + """ + + # This maps label_direction to TileGrid's transpose_xy, flip_x, flip_y + _DIR_MAP = { + "UPR": (True, True, False), + "DWR": (True, False, True), + "UPD": (False, True, True), + "LTR": (False, False, False), + "RTL": (False, False, False), + } + + def __init__(self, font: FontProtocol, save_text: bool = True, **kwargs) -> None: + self._bitmap = None + self._tilegrid = None + self._prev_label_direction = None + + super().__init__(font, **kwargs) + + self._save_text = save_text + self._text = self._replace_tabs(self._text) + + # call the text updater with all the arguments. + self._reset_text( + font=font, + text=self._text, + line_spacing=self._line_spacing, + scale=self.scale, + ) + + def _reset_text( + self, + font: Optional[FontProtocol] = None, + text: Optional[str] = None, + line_spacing: Optional[float] = None, + scale: Optional[int] = None, + ) -> None: + # pylint: disable=too-many-branches, too-many-statements, too-many-locals + + # Store all the instance variables + if font is not None: + self._font = font + if line_spacing is not None: + self._line_spacing = line_spacing + + # if text is not provided as a parameter (text is None), use the previous value. + if (text is None) and self._save_text: + text = self._text + + if self._save_text: # text string will be saved + self._text = self._replace_tabs(text) + else: + self._text = None # save a None value since text string is not saved + + # Check for empty string + if (text == "") or ( + text is None + ): # If empty string, just create a zero-sized bounding box and that's it. + self._bounding_box = ( + 0, + 0, + 0, # zero width with text == "" + 0, # zero height with text == "" + ) + # Clear out any items in the self._local_group Group, in case this is an + # update to the bitmap_label + for _ in self._local_group: + self._local_group.pop(0) + + # Free the bitmap and tilegrid since they are removed + self._bitmap = None + self._tilegrid = None + + else: # The text string is not empty, so create the Bitmap and TileGrid and + # append to the self Group + + # Calculate the text bounding box + + # Calculate both "tight" and "loose" bounding box dimensions to match label for + # anchor_position calculations + ( + box_x, + tight_box_y, + x_offset, + tight_y_offset, + loose_box_y, + loose_y_offset, + ) = self._text_bounding_box( + text, + self._font, + ) # calculate the box size for a tight and loose backgrounds + + if self._background_tight: + box_y = tight_box_y + y_offset = tight_y_offset + self._padding_left = 0 + self._padding_right = 0 + self._padding_top = 0 + self._padding_bottom = 0 + + else: # calculate the box size for a loose background + box_y = loose_box_y + y_offset = loose_y_offset + + # Calculate the background size including padding + tight_box_x = box_x + box_x = box_x + self._padding_left + self._padding_right + box_y = box_y + self._padding_top + self._padding_bottom + + # Create the Bitmap unless it can be reused + new_bitmap = None + if ( + self._bitmap is None + or self._bitmap.width != box_x + or self._bitmap.height != box_y + ): + new_bitmap = displayio.Bitmap(box_x, box_y, len(self._palette)) + self._bitmap = new_bitmap + else: + self._bitmap.fill(0) + + # Place the text into the Bitmap + self._place_text( + self._bitmap, + text if self._label_direction != "RTL" else "".join(reversed(text)), + self._font, + self._padding_left - x_offset, + self._padding_top + y_offset, + ) + + if self._base_alignment: + label_position_yoffset = 0 + else: + label_position_yoffset = self._ascent // 2 + + # Create the TileGrid if not created bitmap unchanged + if self._tilegrid is None or new_bitmap: + self._tilegrid = displayio.TileGrid( + self._bitmap, + pixel_shader=self._palette, + width=1, + height=1, + tile_width=box_x, + tile_height=box_y, + default_tile=0, + x=-self._padding_left + x_offset, + y=label_position_yoffset - y_offset - self._padding_top, + ) + # Clear out any items in the local_group Group, in case this is an update to + # the bitmap_label + for _ in self._local_group: + self._local_group.pop(0) + self._local_group.append( + self._tilegrid + ) # add the bitmap's tilegrid to the group + + # Set TileGrid properties based on label_direction + if self._label_direction != self._prev_label_direction: + tg1 = self._tilegrid + tg1.transpose_xy, tg1.flip_x, tg1.flip_y = self._DIR_MAP[ + self._label_direction + ] + + # Update bounding_box values. Note: To be consistent with label.py, + # this is the bounding box for the text only, not including the background. + if self._label_direction in ("UPR", "DWR"): + if self._label_direction == "UPR": + top = self._padding_right + left = self._padding_top + if self._label_direction == "DWR": + top = self._padding_left + left = self._padding_bottom + self._bounding_box = ( + self._tilegrid.x + left, + self._tilegrid.y + top, + tight_box_y, + tight_box_x, + ) + else: + self._bounding_box = ( + self._tilegrid.x + self._padding_left, + self._tilegrid.y + self._padding_top, + tight_box_x, + tight_box_y, + ) + + if ( + scale is not None + ): # Scale will be defined in local_group (Note: self should have scale=1) + self.scale = scale # call the setter + + # set the anchored_position with setter after bitmap is created, sets the + # x,y positions of the label + self.anchored_position = self._anchored_position + + @staticmethod + def _line_spacing_ypixels(font: FontProtocol, line_spacing: float) -> int: + # Note: Scaling is provided at the Group level + return_value = int(line_spacing * font.get_bounding_box()[1]) + return return_value + + def _text_bounding_box( + self, text: str, font: FontProtocol + ) -> Tuple[int, int, int, int, int, int]: + # pylint: disable=too-many-locals + + ascender_max, descender_max = self._ascent, self._descent + + lines = 1 + + xposition = ( + x_start + ) = yposition = y_start = 0 # starting x and y position (left margin) + + left = None + right = x_start + top = bottom = y_start + + y_offset_tight = self._ascent // 2 + + newlines = 0 + line_spacing = self._line_spacing + + for char in text: + if char == "\n": # newline + newlines += 1 + + else: + my_glyph = font.get_glyph(ord(char)) + + if my_glyph is None: # Error checking: no glyph found + print("Glyph not found: {}".format(repr(char))) + else: + if newlines: + xposition = x_start # reset to left column + yposition += ( + self._line_spacing_ypixels(font, line_spacing) * newlines + ) # Add the newline(s) + lines += newlines + newlines = 0 + if xposition == x_start: + if left is None: + left = 0 + else: + left = min(left, my_glyph.dx) + xright = xposition + my_glyph.width + my_glyph.dx + xposition += my_glyph.shift_x + + right = max(right, xposition, xright) + + if yposition == y_start: # first line, find the Ascender height + top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight) + bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight) + + if left is None: + left = 0 + + final_box_width = right - left + + final_box_height_tight = bottom - top + final_y_offset_tight = -top + y_offset_tight + + final_box_height_loose = (lines - 1) * self._line_spacing_ypixels( + font, line_spacing + ) + (ascender_max + descender_max) + final_y_offset_loose = ascender_max + + # return (final_box_width, final_box_height, left, final_y_offset) + + return ( + final_box_width, + final_box_height_tight, + left, + final_y_offset_tight, + final_box_height_loose, + final_y_offset_loose, + ) + + # pylint: disable = too-many-branches + def _place_text( + self, + bitmap: displayio.Bitmap, + text: str, + font: FontProtocol, + xposition: int, + yposition: int, + skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index + # when copying glyph bitmaps (this is important for slanted text + # where rectangular glyph boxes overlap) + ) -> Tuple[int, int, int, int]: + # pylint: disable=too-many-arguments, too-many-locals + + # placeText - Writes text into a bitmap at the specified location. + # + # Note: scale is pushed up to Group level + + x_start = xposition # starting x position (left margin) + y_start = yposition + + left = None + right = x_start + top = bottom = y_start + line_spacing = self._line_spacing + + for char in text: + if char == "\n": # newline + xposition = x_start # reset to left column + yposition = yposition + self._line_spacing_ypixels( + font, line_spacing + ) # Add a newline + + else: + my_glyph = font.get_glyph(ord(char)) + + if my_glyph is None: # Error checking: no glyph found + print("Glyph not found: {}".format(repr(char))) + else: + if xposition == x_start: + if left is None: + left = 0 + else: + left = min(left, my_glyph.dx) + + right = max( + right, + xposition + my_glyph.shift_x, + xposition + my_glyph.width + my_glyph.dx, + ) + if yposition == y_start: # first line, find the Ascender height + top = min(top, -my_glyph.height - my_glyph.dy) + bottom = max(bottom, yposition - my_glyph.dy) + + glyph_offset_x = ( + my_glyph.tile_index * my_glyph.width + ) # for type BuiltinFont, this creates the x-offset in the glyph bitmap. + # for BDF loaded fonts, this should equal 0 + + y_blit_target = yposition - my_glyph.height - my_glyph.dy + + # Clip glyph y-direction if outside the font ascent/descent metrics. + # Note: bitmap.blit will automatically clip the bottom of the glyph. + y_clip = 0 + if y_blit_target < 0: + y_clip = -y_blit_target # clip this amount from top of bitmap + y_blit_target = 0 # draw the clipped bitmap at y=0 + if self._verbose: + print( + 'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format( + char + ) + ) + + if (y_blit_target + my_glyph.height) > bitmap.height: + if self._verbose: + print( + 'Warning: Glyph clipped, exceeds descent property: "{}"'.format( + char + ) + ) + + self._blit( + bitmap, + max(xposition + my_glyph.dx, 0), + y_blit_target, + my_glyph.bitmap, + x_1=glyph_offset_x, + y_1=y_clip, + x_2=glyph_offset_x + my_glyph.width, + y_2=my_glyph.height, + skip_index=skip_index, # do not copy over any 0 background pixels + ) + + xposition = xposition + my_glyph.shift_x + + # bounding_box + return left, top, right - left, bottom - top + + def _blit( + self, + bitmap: displayio.Bitmap, # target bitmap + x: int, # target x upper left corner + y: int, # target y upper left corner + source_bitmap: displayio.Bitmap, # source bitmap + x_1: int = 0, # source x start + y_1: int = 0, # source y start + x_2: int = None, # source x end + y_2: int = None, # source y end + skip_index: int = None, # palette index that will not be copied + # (for example: the background color of a glyph) + ) -> None: + # pylint: disable=no-self-use, too-many-arguments + + if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it + # this function should perform its own input checks + bitmap.blit( + x, + y, + source_bitmap, + x1=x_1, + y1=y_1, + x2=x_2, + y2=y_2, + skip_index=skip_index, + ) + elif hasattr(bitmaptools, "blit"): + bitmaptools.blit( + bitmap, + source_bitmap, + x, + y, + x1=x_1, + y1=y_1, + x2=x_2, + y2=y_2, + skip_source_index=skip_index, + ) + + else: # perform pixel by pixel copy of the bitmap + # Perform input checks + + if x_2 is None: + x_2 = source_bitmap.width + if y_2 is None: + y_2 = source_bitmap.height + + # Rearrange so that x_1 < x_2 and y1 < y2 + if x_1 > x_2: + x_1, x_2 = x_2, x_1 + if y_1 > y_2: + y_1, y_2 = y_2, y_1 + + # Ensure that x2 and y2 are within source bitmap size + x_2 = min(x_2, source_bitmap.width) + y_2 = min(y_2, source_bitmap.height) + + for y_count in range(y_2 - y_1): + for x_count in range(x_2 - x_1): + x_placement = x + x_count + y_placement = y + y_count + + if (bitmap.width > x_placement >= 0) and ( + bitmap.height > y_placement >= 0 + ): # ensure placement is within target bitmap + # get the palette index from the source bitmap + this_pixel_color = source_bitmap[ + y_1 + + ( + y_count * source_bitmap.width + ) # Direct index into a bitmap array is speedier than [x,y] tuple + + x_1 + + x_count + ] + + if (skip_index is None) or (this_pixel_color != skip_index): + bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple + y_placement * bitmap.width + x_placement + ] = this_pixel_color + elif y_placement > bitmap.height: + break + + def _set_line_spacing(self, new_line_spacing: float) -> None: + if self._save_text: + self._reset_text(line_spacing=new_line_spacing, scale=self.scale) + else: + raise RuntimeError("line_spacing is immutable when save_text is False") + + def _set_font(self, new_font: FontProtocol) -> None: + self._font = new_font + if self._save_text: + self._reset_text(font=new_font, scale=self.scale) + else: + raise RuntimeError("font is immutable when save_text is False") + + def _set_text(self, new_text: str, scale: int) -> None: + self._reset_text(text=self._replace_tabs(new_text), scale=self.scale) + + def _set_background_color(self, new_color: Optional[int]): + self._background_color = new_color + if new_color is not None: + self._palette[0] = new_color + self._palette.make_opaque(0) + else: + self._palette[0] = 0 + self._palette.make_transparent(0) + + def _set_label_direction(self, new_label_direction: str) -> None: + # Only make changes if new direction is different + # to prevent errors in the _reset_text() direction checks + if self._label_direction != new_label_direction: + self._prev_label_direction = self._label_direction + self._label_direction = new_label_direction + self._reset_text(text=str(self._text)) # Force a recalculation + + def _get_valid_label_directions(self) -> Tuple[str, ...]: + return "LTR", "RTL", "UPD", "UPR", "DWR" + + @property + def bitmap(self) -> displayio.Bitmap: + """ + The Bitmap object that the text and background are drawn into. + + :rtype: displayio.Bitmap + """ + return self._bitmap diff --git a/E_Ink_Test/lib/adafruit_display_text/label.py b/E_Ink_Test/lib/adafruit_display_text/label.py new file mode 100644 index 0000000..775845c --- /dev/null +++ b/E_Ink_Test/lib/adafruit_display_text/label.py @@ -0,0 +1,447 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text.label` +==================================================== + +Displays text labels using CircuitPython's displayio. + +* Author(s): Scott Shawcroft + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + + +from displayio import Bitmap, Palette, TileGrid +from adafruit_display_text import LabelBase + +try: + from typing import Optional, Tuple + from fontio import FontProtocol +except ImportError: + pass + + +class Label(LabelBase): + # pylint: disable=too-many-instance-attributes + + """A label displaying a string of text. The origin point set by ``x`` and ``y`` + properties will be the left edge of the bounding box, and in the center of a M + glyph (if its one line), or the (number of lines * linespacing + M)/2. That is, + it will try to have it be center-left as close as possible. + + :param font: A font class that has ``get_bounding_box`` and ``get_glyph``. + Must include a capital M for measuring character size. + :type font: ~fontio.FontProtocol + :param str text: Text to display + :param int|Tuple(int, int, int) color: Color of all text in HEX or RGB + :param int|Tuple(int, int, int)|None background_color: Color of the background, use `None` + for transparent + :param float line_spacing: Line spacing of text to display + :param bool background_tight: Set `True` only if you want background box to tightly + surround text. When set to 'True' Padding parameters will be ignored. + :param int padding_top: Additional pixels added to background bounding box at top. + This parameter could be negative indicating additional pixels subtracted from the + background bounding box. + :param int padding_bottom: Additional pixels added to background bounding box at bottom. + This parameter could be negative indicating additional pixels subtracted from the + background bounding box. + :param int padding_left: Additional pixels added to background bounding box at left. + This parameter could be negative indicating additional pixels subtracted from the + background bounding box. + :param int padding_right: Additional pixels added to background bounding box at right. + This parameter could be negative indicating additional pixels subtracted from the + background bounding box. + :param Tuple(float, float) anchor_point: Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.) + :param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple + containing x,y pixel coordinates. + :param int scale: Integer value of the pixel scaling + :param bool base_alignment: when True allows to align text label to the baseline. + This is helpful when two or more labels need to be aligned to the same baseline + :param Tuple(int, str) tab_replacement: tuple with tab character replace information. When + (4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by + tab character + :param str label_direction: string defining the label text orientation. There are 5 + configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left + ``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``""" + + def __init__(self, font: FontProtocol, **kwargs) -> None: + self._background_palette = Palette(1) + self._added_background_tilegrid = False + + super().__init__(font, **kwargs) + + text = self._replace_tabs(self._text) + + self._width = len(text) + self._height = self._font.get_bounding_box()[1] + + # Create the two-color text palette + self._palette[0] = 0 + self._palette.make_transparent(0) + + if text is not None: + self._reset_text(str(text)) + + # pylint: disable=too-many-branches + def _create_background_box(self, lines: int, y_offset: int) -> TileGrid: + """Private Class function to create a background_box + :param lines: int number of lines + :param y_offset: int y pixel bottom coordinate for the background_box""" + + left = self._bounding_box[0] + if self._background_tight: # draw a tight bounding box + box_width = self._bounding_box[2] + box_height = self._bounding_box[3] + x_box_offset = 0 + y_box_offset = self._bounding_box[1] + + else: # draw a "loose" bounding box to include any ascenders/descenders. + ascent, descent = self._ascent, self._descent + + if self._label_direction in ("DWR", "UPR"): + box_height = ( + self._bounding_box[3] + self._padding_right + self._padding_left + ) + x_box_offset = -self._padding_left + box_width = ( + (ascent + descent) + + int((lines - 1) * self._width * self._line_spacing) + + self._padding_top + + self._padding_bottom + ) + elif self._label_direction == "TTB": + box_height = ( + self._bounding_box[3] + self._padding_top + self._padding_bottom + ) + x_box_offset = -self._padding_left + box_width = ( + (ascent + descent) + + int((lines - 1) * self._height * self._line_spacing) + + self._padding_right + + self._padding_left + ) + else: + box_width = ( + self._bounding_box[2] + self._padding_left + self._padding_right + ) + x_box_offset = -self._padding_left + box_height = ( + (ascent + descent) + + int((lines - 1) * self._height * self._line_spacing) + + self._padding_top + + self._padding_bottom + ) + + if self._label_direction == "DWR": + padding_to_use = self._padding_bottom + elif self._label_direction == "TTB": + padding_to_use = self._padding_top + y_offset = 0 + ascent = 0 + else: + padding_to_use = self._padding_top + + if self._base_alignment: + y_box_offset = -ascent - padding_to_use + else: + y_box_offset = -ascent + y_offset - padding_to_use + + box_width = max(0, box_width) # remove any negative values + box_height = max(0, box_height) # remove any negative values + + if self._label_direction == "UPR": + movx = y_box_offset + movy = -box_height - x_box_offset + elif self._label_direction == "DWR": + movx = y_box_offset + movy = x_box_offset + elif self._label_direction == "TTB": + movx = x_box_offset + movy = y_box_offset + else: + movx = left + x_box_offset + movy = y_box_offset + + background_bitmap = Bitmap(box_width, box_height, 1) + tile_grid = TileGrid( + background_bitmap, + pixel_shader=self._background_palette, + x=movx, + y=movy, + ) + + return tile_grid + + # pylint: enable=too-many-branches + def _set_background_color(self, new_color: Optional[int]) -> None: + """Private class function that allows updating the font box background color + + :param int new_color: Color as an RGB hex number, setting to None makes it transparent + """ + + if new_color is None: + self._background_palette.make_transparent(0) + if self._added_background_tilegrid: + self._local_group.pop(0) + self._added_background_tilegrid = False + else: + self._background_palette.make_opaque(0) + self._background_palette[0] = new_color + self._background_color = new_color + + lines = self._text.rstrip("\n").count("\n") + 1 + y_offset = self._ascent // 2 + + if self._bounding_box is None: + # Still in initialization + return + + if not self._added_background_tilegrid: # no bitmap is in the self Group + # add bitmap if text is present and bitmap sizes > 0 pixels + if ( + (len(self._text) > 0) + and ( + self._bounding_box[2] + self._padding_left + self._padding_right > 0 + ) + and ( + self._bounding_box[3] + self._padding_top + self._padding_bottom > 0 + ) + ): + self._local_group.insert( + 0, self._create_background_box(lines, y_offset) + ) + self._added_background_tilegrid = True + + else: # a bitmap is present in the self Group + # update bitmap if text is present and bitmap sizes > 0 pixels + if ( + (len(self._text) > 0) + and ( + self._bounding_box[2] + self._padding_left + self._padding_right > 0 + ) + and ( + self._bounding_box[3] + self._padding_top + self._padding_bottom > 0 + ) + ): + self._local_group[0] = self._create_background_box( + lines, self._y_offset + ) + else: # delete the existing bitmap + self._local_group.pop(0) + self._added_background_tilegrid = False + + def _update_text(self, new_text: str) -> None: + # pylint: disable=too-many-branches,too-many-statements + + x = 0 + y = 0 + if self._added_background_tilegrid: + i = 1 + else: + i = 0 + tilegrid_count = i + if self._base_alignment: + self._y_offset = 0 + else: + self._y_offset = self._ascent // 2 + + if self._label_direction == "RTL": + left = top = bottom = 0 + right = None + elif self._label_direction == "LTR": + right = top = bottom = 0 + left = None + else: + top = right = left = 0 + bottom = 0 + + for character in new_text: + if character == "\n": + y += int(self._height * self._line_spacing) + x = 0 + continue + glyph = self._font.get_glyph(ord(character)) + if not glyph: + continue + + position_x, position_y = 0, 0 + + if self._label_direction in ("LTR", "RTL"): + bottom = max(bottom, y - glyph.dy + self._y_offset) + if y == 0: # first line, find the Ascender height + top = min(top, -glyph.height - glyph.dy + self._y_offset) + position_y = y - glyph.height - glyph.dy + self._y_offset + + if self._label_direction == "LTR": + right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx) + if x == 0: + if left is None: + left = 0 + else: + left = min(left, glyph.dx) + position_x = x + glyph.dx + else: + left = max( + left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx + ) + if x == 0: + if right is None: + right = 0 + else: + right = max(right, glyph.dx) + position_x = x - glyph.width + + elif self._label_direction == "TTB": + if x == 0: + if left is None: + left = 0 + else: + left = min(left, glyph.dx) + if y == 0: + top = min(top, -glyph.dy) + + bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy) + right = max( + right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx + ) + position_y = y + glyph.dy + position_x = x - glyph.width // 2 + self._y_offset + + elif self._label_direction == "UPR": + if x == 0: + if bottom is None: + bottom = -glyph.dx + + if y == 0: # first line, find the Ascender height + bottom = min(bottom, -glyph.dy) + left = min(left, x - glyph.height + self._y_offset) + top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x) + right = max(right, x + glyph.height, x + glyph.height - glyph.dy) + position_y = y - glyph.width - glyph.dx + position_x = x - glyph.height - glyph.dy + self._y_offset + + elif self._label_direction == "DWR": + if y == 0: + if top is None: + top = -glyph.dx + top = min(top, -glyph.dx) + if x == 0: + left = min(left, -glyph.dy) + left = min(left, x, x - glyph.dy - self._y_offset) + bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x) + right = max(right, x + glyph.height) + position_y = y + glyph.dx + position_x = x + glyph.dy - self._y_offset + + if glyph.width > 0 and glyph.height > 0: + face = TileGrid( + glyph.bitmap, + pixel_shader=self._palette, + default_tile=glyph.tile_index, + tile_width=glyph.width, + tile_height=glyph.height, + x=position_x, + y=position_y, + ) + + if self._label_direction == "UPR": + face.transpose_xy = True + face.flip_x = True + if self._label_direction == "DWR": + face.transpose_xy = True + face.flip_y = True + + if tilegrid_count < len(self._local_group): + self._local_group[tilegrid_count] = face + else: + self._local_group.append(face) + tilegrid_count += 1 + + if self._label_direction == "RTL": + x = x - glyph.shift_x + if self._label_direction == "TTB": + if glyph.height < 2: + y = y + glyph.shift_x + else: + y = y + glyph.height + 1 + if self._label_direction == "UPR": + y = y - glyph.shift_x + if self._label_direction == "DWR": + y = y + glyph.shift_x + if self._label_direction == "LTR": + x = x + glyph.shift_x + + i += 1 + + if self._label_direction == "LTR" and left is None: + left = 0 + if self._label_direction == "RTL" and right is None: + right = 0 + if self._label_direction == "TTB" and top is None: + top = 0 + + while len(self._local_group) > tilegrid_count: # i: + self._local_group.pop() + + if self._label_direction == "RTL": + # pylint: disable=invalid-unary-operand-type + # type-checkers think left can be None + self._bounding_box = (-left, top, left - right, bottom - top) + if self._label_direction == "TTB": + self._bounding_box = (left, top, right - left, bottom - top) + if self._label_direction == "UPR": + self._bounding_box = (left, top, right, bottom - top) + if self._label_direction == "DWR": + self._bounding_box = (left, top, right, bottom - top) + if self._label_direction == "LTR": + self._bounding_box = (left, top, right - left, bottom - top) + + self._text = new_text + + if self._background_color is not None: + self._set_background_color(self._background_color) + + def _reset_text(self, new_text: str) -> None: + current_anchored_position = self.anchored_position + self._update_text(str(self._replace_tabs(new_text))) + self.anchored_position = current_anchored_position + + def _set_font(self, new_font: FontProtocol) -> None: + old_text = self._text + current_anchored_position = self.anchored_position + self._text = "" + self._font = new_font + self._height = self._font.get_bounding_box()[1] + self._update_text(str(old_text)) + self.anchored_position = current_anchored_position + + def _set_line_spacing(self, new_line_spacing: float) -> None: + self._line_spacing = new_line_spacing + self.text = self._text # redraw the box + + def _set_text(self, new_text: str, scale: int) -> None: + self._reset_text(new_text) + + def _set_label_direction(self, new_label_direction: str) -> None: + self._label_direction = new_label_direction + self._update_text(str(self._text)) + + def _get_valid_label_directions(self) -> Tuple[str, ...]: + return "LTR", "RTL", "UPR", "DWR", "TTB" diff --git a/E_Ink_Test/lib/adafruit_display_text/outlined_label.py b/E_Ink_Test/lib/adafruit_display_text/outlined_label.py new file mode 100644 index 0000000..1c66a99 --- /dev/null +++ b/E_Ink_Test/lib/adafruit_display_text/outlined_label.py @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: 2023 Tim C +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text.outlined_label` +==================================================== + +Subclass of BitmapLabel that adds outline color and stroke size +functionalities. + +* Author(s): Tim Cocks + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + +import bitmaptools +from displayio import Palette, Bitmap +from adafruit_display_text import bitmap_label + +try: + from typing import Optional, Tuple, Union + from fontio import FontProtocol +except ImportError: + pass + + +class OutlinedLabel(bitmap_label.Label): + """ + OutlinedLabel - A BitmapLabel subclass that includes arguments and properties for specifying + outline_size and outline_color to get drawn as a stroke around the text. + + :param Union[Tuple, int] outline_color: The color of the outline stroke as RGB tuple, or hex. + :param int outline_size: The size in pixels of the outline stroke. + + """ + + # pylint: disable=too-many-arguments + def __init__( + self, + font, + outline_color: Union[int, Tuple] = 0x999999, + outline_size: int = 1, + padding_top: Optional[int] = None, + padding_bottom: Optional[int] = None, + padding_left: Optional[int] = None, + padding_right: Optional[int] = None, + **kwargs + ): + if padding_top is None: + padding_top = outline_size + 0 + if padding_bottom is None: + padding_bottom = outline_size + 2 + if padding_left is None: + padding_left = outline_size + 0 + if padding_right is None: + padding_right = outline_size + 0 + + super().__init__( + font, + padding_top=padding_top, + padding_bottom=padding_bottom, + padding_left=padding_left, + padding_right=padding_right, + **kwargs + ) + + _background_color = self._palette[0] + _foreground_color = self._palette[1] + _background_is_transparent = self._palette.is_transparent(0) + self._palette = Palette(3) + self._palette[0] = _background_color + self._palette[1] = _foreground_color + self._palette[2] = outline_color + if _background_is_transparent: + self._palette.make_transparent(0) + + self._outline_size = outline_size + self._stamp_source = Bitmap((outline_size * 2) + 1, (outline_size * 2) + 1, 3) + self._stamp_source.fill(2) + + self._bitmap = None + + self._reset_text( + font=font, + text=self._text, + line_spacing=self._line_spacing, + scale=self.scale, + ) + + def _add_outline(self): + """ + Blit the outline into the labels Bitmap. We will stamp self._stamp_source for each + pixel of the foreground color but skip the foreground color when we blit. + :return: None + """ + if hasattr(self, "_stamp_source"): + for y in range(self.bitmap.height): + for x in range(self.bitmap.width): + if self.bitmap[x, y] == 1: + try: + bitmaptools.blit( + self.bitmap, + self._stamp_source, + x - self._outline_size, + y - self._outline_size, + skip_dest_index=1, + ) + except ValueError as value_error: + raise ValueError( + "Padding must be big enough to fit outline_size " + "all the way around the text. " + "Try using either larger padding sizes, or smaller outline_size." + ) from value_error + + def _place_text( + self, + bitmap: Bitmap, + text: str, + font: FontProtocol, + xposition: int, + yposition: int, + skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index + # when copying glyph bitmaps (this is important for slanted text + # where rectangular glyph boxes overlap) + ) -> Tuple[int, int, int, int]: + """ + Copy the glpyphs that represent the value of the string into the labels Bitmap. + :param bitmap: The bitmap to place text into + :param text: The text to render + :param font: The font to render the text in + :param xposition: x location of the starting point within the bitmap + :param yposition: y location of the starting point within the bitmap + :param skip_index: Color index to skip during rendering instead of covering up + :return Tuple bounding_box: tuple with x, y, width, height values of the bitmap + """ + parent_result = super()._place_text( + bitmap, text, font, xposition, yposition, skip_index=skip_index + ) + + self._add_outline() + + return parent_result + + @property + def outline_color(self): + """Color of the outline to draw around the text.""" + return self._palette[2] + + @outline_color.setter + def outline_color(self, new_outline_color): + self._palette[2] = new_outline_color + + @property + def outline_size(self): + """Stroke size of the outline to draw around the text.""" + return self._outline_size + + @outline_size.setter + def outline_size(self, new_outline_size): + self._outline_size = new_outline_size + + self._padding_top = new_outline_size + 0 + self._padding_bottom = new_outline_size + 2 + self._padding_left = new_outline_size + 0 + self._padding_right = new_outline_size + 0 + + self._stamp_source = Bitmap( + (new_outline_size * 2) + 1, (new_outline_size * 2) + 1, 3 + ) + self._stamp_source.fill(2) + self._reset_text( + font=self._font, + text=self._text, + line_spacing=self._line_spacing, + scale=self.scale, + ) diff --git a/E_Ink_Test/lib/adafruit_display_text/scrolling_label.py b/E_Ink_Test/lib/adafruit_display_text/scrolling_label.py new file mode 100644 index 0000000..2031c5d --- /dev/null +++ b/E_Ink_Test/lib/adafruit_display_text/scrolling_label.py @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text.scrolling_label` +==================================================== + +Displays text into a fixed-width label that scrolls leftward +if the full_text is large enough to need it. + +* Author(s): Tim Cocks + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + +import adafruit_ticks +from adafruit_display_text import bitmap_label + +try: + from typing import Optional + from fontio import FontProtocol +except ImportError: + pass + + +class ScrollingLabel(bitmap_label.Label): + """ScrollingLabel - A fixed-width label that will scroll to the left + in order to show the full text if it's larger than the fixed-width. + + :param font: The font to use for the label. + :type: ~fontio.FontProtocol + :param int max_characters: The number of characters that sets the fixed-width. Default is 10. + :param str text: The full text to show in the label. If this is longer than + ``max_characters`` then the label will scroll to show everything. + :param float animate_time: The number of seconds in between scrolling animation + frames. Default is 0.3 seconds. + :param int current_index: The index of the first visible character in the label. + Default is 0, the first character. Will increase while scrolling.""" + + # pylint: disable=too-many-arguments + def __init__( + self, + font: FontProtocol, + max_characters: int = 10, + text: Optional[str] = "", + animate_time: Optional[float] = 0.3, + current_index: Optional[int] = 0, + **kwargs + ) -> None: + super().__init__(font, **kwargs) + self.animate_time = animate_time + self._current_index = current_index + self._last_animate_time = -1 + self.max_characters = max_characters + + if text[-1] != " ": + text = "{} ".format(text) + self._full_text = text + + self.update() + + def update(self, force: bool = False) -> None: + """Attempt to update the display. If ``animate_time`` has elapsed since + previews animation frame then move the characters over by 1 index. + Must be called in the main loop of user code. + + :param bool force: whether to ignore ``animation_time`` and force the update. + Default is False. + :return: None + """ + _now = adafruit_ticks.ticks_ms() + if force or adafruit_ticks.ticks_less( + self._last_animate_time + int(self.animate_time * 1000), _now + ): + if len(self.full_text) <= self.max_characters: + super()._set_text(self.full_text, self.scale) + self._last_animate_time = _now + return + + if self.current_index + self.max_characters <= len(self.full_text): + _showing_string = self.full_text[ + self.current_index : self.current_index + self.max_characters + ] + else: + _showing_string_start = self.full_text[self.current_index :] + _showing_string_end = "{}".format( + self.full_text[ + : (self.current_index + self.max_characters) + % len(self.full_text) + ] + ) + + _showing_string = "{}{}".format( + _showing_string_start, _showing_string_end + ) + super()._set_text(_showing_string, self.scale) + self.current_index += 1 + self._last_animate_time = _now + + return + + @property + def current_index(self) -> int: + """Index of the first visible character. + + :return int: The current index + """ + return self._current_index + + @current_index.setter + def current_index(self, new_index: int) -> None: + if new_index < len(self.full_text): + self._current_index = new_index + else: + self._current_index = new_index % len(self.full_text) + + @property + def full_text(self) -> str: + """The full text to be shown. If it's longer than ``max_characters`` then + scrolling will occur as needed. + + :return str: The full text of this label. + """ + return self._full_text + + @full_text.setter + def full_text(self, new_text: str) -> None: + if new_text[-1] != " ": + new_text = "{} ".format(new_text) + self._full_text = new_text + self.current_index = 0 + self.update() + + @property + def text(self): + """The full text to be shown. If it's longer than ``max_characters`` then + scrolling will occur as needed. + + :return str: The full text of this label. + """ + return self.full_text + + @text.setter + def text(self, new_text): + self.full_text = new_text diff --git a/E_Ink_Test/lib/adafruit_framebuf.py b/E_Ink_Test/lib/adafruit_framebuf.py new file mode 100644 index 0000000..b86c54a --- /dev/null +++ b/E_Ink_Test/lib/adafruit_framebuf.py @@ -0,0 +1,639 @@ +# SPDX-FileCopyrightText: 2018 Kattni Rembor, Melissa LeBlanc-Williams +# and Tony DiCola, for Adafruit Industries. +# Original file created by Damien P. George +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_framebuf` +==================================================== + +CircuitPython pure-python framebuf module, based on the micropython framebuf module. + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit SSD1306 OLED displays `_ +* `Adafruit HT16K33 Matrix displays `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_framebuf.git" + +import os +import struct + +# Framebuf format constants: +MVLSB = 0 # Single bit displays (like SSD1306 OLED) +RGB565 = 1 # 16-bit color displays +GS4_HMSB = 2 # Unimplemented! +MHMSB = 3 # Single bit displays like the Sharp Memory +RGB888 = 4 # Neopixels and Dotstars +GS2_HMSB = 5 # 2-bit color displays like the HT16K33 8x8 Matrix + + +class GS2HMSBFormat: + """GS2HMSBFormat""" + + @staticmethod + def set_pixel(framebuf, x, y, color): + """Set a given pixel to a color.""" + index = (y * framebuf.stride + x) >> 2 + pixel = framebuf.buf[index] + + shift = (x & 0b11) << 1 + mask = 0b11 << shift + color = (color & 0b11) << shift + + framebuf.buf[index] = color | (pixel & (~mask)) + + @staticmethod + def get_pixel(framebuf, x, y): + """Get the color of a given pixel""" + index = (y * framebuf.stride + x) >> 2 + pixel = framebuf.buf[index] + + shift = (x & 0b11) << 1 + return (pixel >> shift) & 0b11 + + @staticmethod + def fill(framebuf, color): + """completely fill/clear the buffer with a color""" + if color: + bits = color & 0b11 + fill = (bits << 6) | (bits << 4) | (bits << 2) | (bits << 0) + else: + fill = 0x00 + + framebuf.buf = [fill for i in range(len(framebuf.buf))] + + @staticmethod + def rect(framebuf, x, y, width, height, color): + """Draw the outline of a rectangle at the given location, size and color.""" + # pylint: disable=too-many-arguments + for _x in range(x, x + width): + for _y in range(y, y + height): + if _x in [x, x + width] or _y in [y, y + height]: + GS2HMSBFormat.set_pixel(framebuf, _x, _y, color) + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + """Draw the outline and interior of a rectangle at the given location, size and color.""" + # pylint: disable=too-many-arguments + for _x in range(x, x + width): + for _y in range(y, y + height): + GS2HMSBFormat.set_pixel(framebuf, _x, _y, color) + + +class MHMSBFormat: + """MHMSBFormat""" + + @staticmethod + def set_pixel(framebuf, x, y, color): + """Set a given pixel to a color.""" + index = (y * framebuf.stride + x) // 8 + offset = 7 - x & 0x07 + framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def get_pixel(framebuf, x, y): + """Get the color of a given pixel""" + index = (y * framebuf.stride + x) // 8 + offset = 7 - x & 0x07 + return (framebuf.buf[index] >> offset) & 0x01 + + @staticmethod + def fill(framebuf, color): + """completely fill/clear the buffer with a color""" + if color: + fill = 0xFF + else: + fill = 0x00 + for i in range(len(framebuf.buf)): # pylint: disable=consider-using-enumerate + framebuf.buf[i] = fill + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws + both the outline and interior.""" + # pylint: disable=too-many-arguments + for _x in range(x, x + width): + offset = 7 - _x & 0x07 + for _y in range(y, y + height): + index = (_y * framebuf.stride + _x) // 8 + framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + +class MVLSBFormat: + """MVLSBFormat""" + + @staticmethod + def set_pixel(framebuf, x, y, color): + """Set a given pixel to a color.""" + index = (y >> 3) * framebuf.stride + x + offset = y & 0x07 + framebuf.buf[index] = (framebuf.buf[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def get_pixel(framebuf, x, y): + """Get the color of a given pixel""" + index = (y >> 3) * framebuf.stride + x + offset = y & 0x07 + return (framebuf.buf[index] >> offset) & 0x01 + + @staticmethod + def fill(framebuf, color): + """completely fill/clear the buffer with a color""" + if color: + fill = 0xFF + else: + fill = 0x00 + for i in range(len(framebuf.buf)): # pylint: disable=consider-using-enumerate + framebuf.buf[i] = fill + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws + both the outline and interior.""" + # pylint: disable=too-many-arguments + while height > 0: + index = (y >> 3) * framebuf.stride + x + offset = y & 0x07 + for w_w in range(width): + framebuf.buf[index + w_w] = ( + framebuf.buf[index + w_w] & ~(0x01 << offset) + ) | ((color != 0) << offset) + y += 1 + height -= 1 + + +class RGB565Format: + """ + This class implements the RGB565 format + It assumes a little-endian byte order in the frame buffer + """ + + @staticmethod + def color_to_rgb565(color): + """Convert a color in either tuple or 24 bit integer form to RGB565, + and return as two bytes""" + if isinstance(color, tuple): + hibyte = (color[0] & 0xF8) | (color[1] >> 5) + lobyte = ((color[1] << 5) & 0xE0) | (color[2] >> 3) + else: + hibyte = ((color >> 16) & 0xF8) | ((color >> 13) & 0x07) + lobyte = ((color >> 5) & 0xE0) | ((color >> 3) & 0x1F) + return bytes([lobyte, hibyte]) + + def set_pixel(self, framebuf, x, y, color): + """Set a given pixel to a color.""" + index = (y * framebuf.stride + x) * 2 + framebuf.buf[index : index + 2] = self.color_to_rgb565(color) + + @staticmethod + def get_pixel(framebuf, x, y): + """Get the color of a given pixel""" + index = (y * framebuf.stride + x) * 2 + lobyte, hibyte = framebuf.buf[index : index + 2] + r = hibyte & 0xF8 + g = ((hibyte & 0x07) << 5) | ((lobyte & 0xE0) >> 5) + b = (lobyte & 0x1F) << 3 + return (r << 16) | (g << 8) | b + + def fill(self, framebuf, color): + """completely fill/clear the buffer with a color""" + rgb565_color = self.color_to_rgb565(color) + for i in range(0, len(framebuf.buf), 2): + framebuf.buf[i : i + 2] = rgb565_color + + def fill_rect(self, framebuf, x, y, width, height, color): + """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws + both the outline and interior.""" + # pylint: disable=too-many-arguments + rgb565_color = self.color_to_rgb565(color) + for _y in range(2 * y, 2 * (y + height), 2): + offset2 = _y * framebuf.stride + for _x in range(2 * x, 2 * (x + width), 2): + index = offset2 + _x + framebuf.buf[index : index + 2] = rgb565_color + + +class RGB888Format: + """RGB888Format""" + + @staticmethod + def set_pixel(framebuf, x, y, color): + """Set a given pixel to a color.""" + index = (y * framebuf.stride + x) * 3 + if isinstance(color, tuple): + framebuf.buf[index : index + 3] = bytes(color) + else: + framebuf.buf[index : index + 3] = bytes( + ((color >> 16) & 255, (color >> 8) & 255, color & 255) + ) + + @staticmethod + def get_pixel(framebuf, x, y): + """Get the color of a given pixel""" + index = (y * framebuf.stride + x) * 3 + return ( + (framebuf.buf[index] << 16) + | (framebuf.buf[index + 1] << 8) + | framebuf.buf[index + 2] + ) + + @staticmethod + def fill(framebuf, color): + """completely fill/clear the buffer with a color""" + fill = (color >> 16) & 255, (color >> 8) & 255, color & 255 + for i in range(0, len(framebuf.buf), 3): + framebuf.buf[i : i + 3] = bytes(fill) + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws + both the outline and interior.""" + # pylint: disable=too-many-arguments + fill = (color >> 16) & 255, (color >> 8) & 255, color & 255 + for _x in range(x, x + width): + for _y in range(y, y + height): + index = (_y * framebuf.stride + _x) * 3 + framebuf.buf[index : index + 3] = bytes(fill) + + +class FrameBuffer: + """FrameBuffer object. + + :param buf: An object with a buffer protocol which must be large enough to contain every + pixel defined by the width, height and format of the FrameBuffer. + :param width: The width of the FrameBuffer in pixel + :param height: The height of the FrameBuffer in pixel + :param buf_format: Specifies the type of pixel used in the FrameBuffer; permissible values + are listed under Constants below. These set the number of bits used to + encode a color value and the layout of these bits in ``buf``. Where a + color value c is passed to a method, c is a small integer with an encoding + that is dependent on the format of the FrameBuffer. + :param stride: The number of pixels between each horizontal line of pixels in the + FrameBuffer. This defaults to ``width`` but may need adjustments when + implementing a FrameBuffer within another larger FrameBuffer or screen. The + ``buf`` size must accommodate an increased step size. + + """ + + def __init__(self, buf, width, height, buf_format=MVLSB, stride=None): + # pylint: disable=too-many-arguments + self.buf = buf + self.width = width + self.height = height + self.stride = stride + self._font = None + if self.stride is None: + self.stride = width + if buf_format == MVLSB: + self.format = MVLSBFormat() + elif buf_format == MHMSB: + self.format = MHMSBFormat() + elif buf_format == RGB888: + self.format = RGB888Format() + elif buf_format == RGB565: + self.format = RGB565Format() + elif buf_format == GS2_HMSB: + self.format = GS2HMSBFormat() + else: + raise ValueError("invalid format") + self._rotation = 0 + + @property + def rotation(self): + """The rotation setting of the display, can be one of (0, 1, 2, 3)""" + return self._rotation + + @rotation.setter + def rotation(self, val): + if not val in (0, 1, 2, 3): + raise RuntimeError("Bad rotation setting") + self._rotation = val + + def fill(self, color): + """Fill the entire FrameBuffer with the specified color.""" + self.format.fill(self, color) + + def fill_rect(self, x, y, width, height, color): + """Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws + both the outline and interior.""" + # pylint: disable=too-many-arguments, too-many-boolean-expressions + self.rect(x, y, width, height, color, fill=True) + + def pixel(self, x, y, color=None): + """If ``color`` is not given, get the color value of the specified pixel. If ``color`` is + given, set the specified pixel to the given color.""" + if self.rotation == 1: + x, y = y, x + x = self.width - x - 1 + if self.rotation == 2: + x = self.width - x - 1 + y = self.height - y - 1 + if self.rotation == 3: + x, y = y, x + y = self.height - y - 1 + + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return None + if color is None: + return self.format.get_pixel(self, x, y) + self.format.set_pixel(self, x, y, color) + return None + + def hline(self, x, y, width, color): + """Draw a horizontal line up to a given length.""" + self.rect(x, y, width, 1, color, fill=True) + + def vline(self, x, y, height, color): + """Draw a vertical line up to a given length.""" + self.rect(x, y, 1, height, color, fill=True) + + def circle(self, center_x, center_y, radius, color): + """Draw a circle at the given midpoint location, radius and color. + The ```circle``` method draws only a 1 pixel outline.""" + x = radius - 1 + y = 0 + d_x = 1 + d_y = 1 + err = d_x - (radius << 1) + while x >= y: + self.pixel(center_x + x, center_y + y, color) + self.pixel(center_x + y, center_y + x, color) + self.pixel(center_x - y, center_y + x, color) + self.pixel(center_x - x, center_y + y, color) + self.pixel(center_x - x, center_y - y, color) + self.pixel(center_x - y, center_y - x, color) + self.pixel(center_x + y, center_y - x, color) + self.pixel(center_x + x, center_y - y, color) + if err <= 0: + y += 1 + err += d_y + d_y += 2 + if err > 0: + x -= 1 + d_x += 2 + err += d_x - (radius << 1) + + def rect(self, x, y, width, height, color, *, fill=False): + """Draw a rectangle at the given location, size and color. The ```rect``` method draws only + a 1 pixel outline.""" + # pylint: disable=too-many-arguments + if self.rotation == 1: + x, y = y, x + width, height = height, width + x = self.width - x - width + if self.rotation == 2: + x = self.width - x - width + y = self.height - y - height + if self.rotation == 3: + x, y = y, x + width, height = height, width + y = self.height - y - height + + # pylint: disable=too-many-boolean-expressions + if ( + width < 1 + or height < 1 + or (x + width) <= 0 + or (y + height) <= 0 + or y >= self.height + or x >= self.width + ): + return + x_end = min(self.width - 1, x + width - 1) + y_end = min(self.height - 1, y + height - 1) + x = max(x, 0) + y = max(y, 0) + if fill: + self.format.fill_rect(self, x, y, x_end - x + 1, y_end - y + 1, color) + else: + self.format.fill_rect(self, x, y, x_end - x + 1, 1, color) + self.format.fill_rect(self, x, y, 1, y_end - y + 1, color) + self.format.fill_rect(self, x, y_end, x_end - x + 1, 1, color) + self.format.fill_rect(self, x_end, y, 1, y_end - y + 1, color) + + def line(self, x_0, y_0, x_1, y_1, color): + # pylint: disable=too-many-arguments + """Bresenham's line algorithm""" + d_x = abs(x_1 - x_0) + d_y = abs(y_1 - y_0) + x, y = x_0, y_0 + s_x = -1 if x_0 > x_1 else 1 + s_y = -1 if y_0 > y_1 else 1 + if d_x > d_y: + err = d_x / 2.0 + while x != x_1: + self.pixel(x, y, color) + err -= d_y + if err < 0: + y += s_y + err += d_x + x += s_x + else: + err = d_y / 2.0 + while y != y_1: + self.pixel(x, y, color) + err -= d_x + if err < 0: + x += s_x + err += d_y + y += s_y + self.pixel(x, y, color) + + def blit(self): + """blit is not yet implemented""" + raise NotImplementedError() + + def scroll(self, delta_x, delta_y): + """shifts framebuf in x and y direction""" + if delta_x < 0: + shift_x = 0 + xend = self.width + delta_x + dt_x = 1 + else: + shift_x = self.width - 1 + xend = delta_x - 1 + dt_x = -1 + if delta_y < 0: + y = 0 + yend = self.height + delta_y + dt_y = 1 + else: + y = self.height - 1 + yend = delta_y - 1 + dt_y = -1 + while y != yend: + x = shift_x + while x != xend: + self.format.set_pixel( + self, x, y, self.format.get_pixel(self, x - delta_x, y - delta_y) + ) + x += dt_x + y += dt_y + + # pylint: disable=too-many-arguments + def text(self, string, x, y, color, *, font_name="font5x8.bin", size=1): + """Place text on the screen in variables sizes. Breaks on \n to next line. + + Does not break on line going off screen. + """ + # determine our effective width/height, taking rotation into account + frame_width = self.width + frame_height = self.height + if self.rotation in (1, 3): + frame_width, frame_height = frame_height, frame_width + + for chunk in string.split("\n"): + if not self._font or self._font.font_name != font_name: + # load the font! + self._font = BitmapFont(font_name) + width = self._font.font_width + height = self._font.font_height + for i, char in enumerate(chunk): + char_x = x + (i * (width + 1)) * size + if ( + char_x + (width * size) > 0 + and char_x < frame_width + and y + (height * size) > 0 + and y < frame_height + ): + self._font.draw_char(char, char_x, y, self, color, size=size) + y += height * size + + # pylint: enable=too-many-arguments + + def image(self, img): + """Set buffer to value of Python Imaging Library image. The image should + be in 1 bit mode and a size equal to the display size.""" + # determine our effective width/height, taking rotation into account + width = self.width + height = self.height + if self.rotation in (1, 3): + width, height = height, width + + if isinstance(self.format, (RGB565Format, RGB888Format)) and img.mode != "RGB": + raise ValueError("Image must be in mode RGB.") + if isinstance(self.format, (MHMSBFormat, MVLSBFormat)) and img.mode != "1": + raise ValueError("Image must be in mode 1.") + + imwidth, imheight = img.size + if imwidth != width or imheight != height: + raise ValueError( + f"Image must be same dimensions as display ({width}x{height})." + ) + # Grab all the pixels from the image, faster than getpixel. + pixels = img.load() + # Clear buffer + for i in range(len(self.buf)): # pylint: disable=consider-using-enumerate + self.buf[i] = 0 + # Iterate through the pixels + for x in range(width): # yes this double loop is slow, + for y in range(height): # but these displays are small! + if img.mode == "RGB": + self.pixel(x, y, pixels[(x, y)]) + elif pixels[(x, y)]: + self.pixel(x, y, 1) # only write if pixel is true + + +# MicroPython basic bitmap font renderer. +# Author: Tony DiCola +# License: MIT License (https://opensource.org/licenses/MIT) +class BitmapFont: + """A helper class to read binary font tiles and 'seek' through them as a + file to display in a framebuffer. We use file access so we dont waste 1KB + of RAM on a font!""" + + def __init__(self, font_name="font5x8.bin"): + # Specify the drawing area width and height, and the pixel function to + # call when drawing pixels (should take an x and y param at least). + # Optionally specify font_name to override the font file to use (default + # is font5x8.bin). The font format is a binary file with the following + # format: + # - 1 unsigned byte: font character width in pixels + # - 1 unsigned byte: font character height in pixels + # - x bytes: font data, in ASCII order covering all 255 characters. + # Each character should have a byte for each pixel column of + # data (i.e. a 5x8 font has 5 bytes per character). + self.font_name = font_name + + # Open the font file and grab the character width and height values. + # Note that only fonts up to 8 pixels tall are currently supported. + try: + self._font = open( # pylint: disable=consider-using-with + self.font_name, "rb" + ) + self.font_width, self.font_height = struct.unpack("BB", self._font.read(2)) + # simple font file validation check based on expected file size + if 2 + 256 * self.font_width != os.stat(font_name)[6]: + raise RuntimeError("Invalid font file: " + font_name) + except OSError: + print("Could not find font file", font_name) + raise + except OverflowError: + # os.stat can throw this on boards without long int support + # just hope the font file is valid and press on + pass + + def deinit(self): + """Close the font file as cleanup.""" + self._font.close() + + def __enter__(self): + """Initialize/open the font file""" + self.__init__() + return self + + def __exit__(self, exception_type, exception_value, traceback): + """cleanup on exit""" + self.deinit() + + def draw_char( + self, char, x, y, framebuffer, color, size=1 + ): # pylint: disable=too-many-arguments + """Draw one character at position (x,y) to a framebuffer in a given color""" + size = max(size, 1) + # Don't draw the character if it will be clipped off the visible area. + # if x < -self.font_width or x >= framebuffer.width or \ + # y < -self.font_height or y >= framebuffer.height: + # return + # Go through each column of the character. + for char_x in range(self.font_width): + # Grab the byte for the current column of font data. + self._font.seek(2 + (ord(char) * self.font_width) + char_x) + try: + line = struct.unpack("B", self._font.read(1))[0] + except RuntimeError: + continue # maybe character isnt there? go to next + # Go through each row in the column byte. + for char_y in range(self.font_height): + # Draw a pixel for each bit that's flipped on. + if (line >> char_y) & 0x1: + framebuffer.fill_rect( + x + char_x * size, y + char_y * size, size, size, color + ) + + def width(self, text): + """Return the pixel width of the specified text message.""" + return len(text) * (self.font_width + 1) + + +class FrameBuffer1(FrameBuffer): # pylint: disable=abstract-method + """FrameBuffer1 object. Inherits from FrameBuffer.""" diff --git a/E_Ink_Test/lib/adafruit_ssd1680.py b/E_Ink_Test/lib/adafruit_ssd1680.py new file mode 100644 index 0000000..a534e36 --- /dev/null +++ b/E_Ink_Test/lib/adafruit_ssd1680.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_ssd1680` +================================================================================ + +CircuitPython `displayio` driver for SSD1680-based ePaper displays + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit 2.13" Tri-Color eInk Display Breakout `_ +* `Adafruit 2.13" Tri-Color eInk Display FeatherWing `_ +* `Adafruit 2.13" Mono eInk Display FeatherWing `_ + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from epaperdisplay import EPaperDisplay + from fourwire import FourWire +except ImportError: + from displayio import EPaperDisplay + from displayio import FourWire + + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SSD1680.git" + +_START_SEQUENCE = ( + b"\x12\x80\x14" # soft reset and wait 20ms + b"\x11\x01\x03" # Ram data entry mode + b"\x3C\x01\x05" # border color + b"\x2c\x01\x36" # Set vcom voltage + b"\x03\x01\x17" # Set gate voltage + b"\x04\x03\x41\x00\x32" # Set source voltage + b"\x4e\x01\x01" # ram x count + b"\x4f\x02\x00\x00" # ram y count + b"\x01\x03\x00\x00\x00" # set display size + b"\x22\x01\xf4" # display update mode +) + +_STOP_SEQUENCE = b"\x10\x81\x01\x64" # Deep Sleep + + +# pylint: disable=too-few-public-methods +class SSD1680(EPaperDisplay): + r"""SSD1680 driver + + :param bus: The data bus the display is on + :param \**kwargs: + See below + + :Keyword Arguments: + * *width* (``int``) -- + Display width + * *height* (``int``) -- + Display height + * *rotation* (``int``) -- + Display rotation + """ + + def __init__(self, bus: FourWire, column_correction=1,**kwargs) -> None: + if "colstart" not in kwargs: + kwargs["colstart"] = 8 + stop_sequence = bytearray(_STOP_SEQUENCE) + try: + bus.reset() + except RuntimeError: + # No reset pin defined, so no deep sleeping + stop_sequence = b"" + + start_sequence = bytearray(_START_SEQUENCE) + width = kwargs["width"] + height = kwargs["height"] + if "rotation" in kwargs and kwargs["rotation"] % 180 != 90: + width, height = height, width + start_sequence[29] = (width - 1) & 0xFF + start_sequence[30] = ((width - 1) >> 8) & 0xFF + + super().__init__( + bus, + start_sequence, + stop_sequence, + **kwargs, + ram_width=250, + ram_height=296, + busy_state=True, + write_black_ram_command=0x24, + write_color_ram_command=0x26, + set_column_window_command=0x44, + set_row_window_command=0x45, + set_current_column_command=0x4E, + set_current_row_command=0x4F, + refresh_display_command=0x20, + always_toggle_chip_select=False, + address_little_endian=True + ) diff --git a/E_Ink_Test/lib/display_ruler.bmp b/E_Ink_Test/lib/display_ruler.bmp new file mode 100644 index 0000000000000000000000000000000000000000..726b5e026fd30b7e5baf5c23323d159b0eb5f65d GIT binary patch literal 360122 zcmeI5J<>PHa@9vfU}y)p3NQ(_AVLn79IXQnhrqzWkgEj{fdQ^IAh~k216($MfjgY% z$@}EwoAuGByQ``{{-@)Ad$O}nW@Y`VDsR7YN29;`>woyK|MIu5>%ZVX{}KQByFdHw zw||TO{Mm2+>dXDxzsLK3{qKJJ{?E4qx4-%ifAgPyZ1%7JA_;#dE)Q`n<8kq849xm9 zbD2F6vEm7f70+r@8DW~ucsA2MQ4lBo#QIm(ELJ>ivEo_nQX1HZx8>QD_9@~x@u$>$ zRh?qRlNT$V)h?xXCnwzU>`MC-vCsM`1z%XFSn=@1if2VCfz+v*(Z7>vpCxkw&no+p zYQ>7jE>=9NUCP*_5QxW?XII*%h;6b@srK4B#fs-IRy?asWrS%qIo2tat)a&LE&O z$Ws+7vEm6xIfH=CAWv1S#EK^%vyzf0pnWA)JON&*eZ{kqk|&^jC00BEUa5V>vyzf0pnWA)JON&* zeZ{kqk|&^j^-xxS^S}QyepWn;KmUJ!%)|YExRzVmSId!q#DLmY{4!t8%KFm({O|v! z^Ml5I{Jdr;FK0DlQp$Nb0qrY(2`*t}A4|A*dQ|z!qVUmW20zD9%1c;{DqeYROF;XI zUz$r;9qU)#o$(0R%)DD(XOfBzQ0)?4+fauILnv445H z{=sc#X|6hhOl^15T*9hsRs=OWdffS$o>wkrb-rL_cmx6MD}I?)va%-Q%?ka;`jWLK`0dfUmUX9Oe>JF| zZ!^or&-A=RTXu|vhM(+7q0tE4ro&{gbL?N@Nx&wG&@{a5<);I6fn z_?`sToWXm}eQ#B3SXs0BYA?n26$<_z|LuSM{a=3nD`9_q`_-&|{~fO&fZ{%%P-3Vx3@hh^16*>$`Lcc<9iEYO_5}H!{8}Htc;KZwHH?tmScYeNS zlxtX>sqj9rU}e{RO1f<&0Cn5i?pD|uR`v;meuZD`SJua@BYpL&Uq7Po82eQU{Q6q^ zzAX8l9%lY!4Xc?mQ0_Yt(7xhVWeqF)vO;62_AC5x&pxic%KrHyp?!Mcj#t$`4_Zz% zG4(5cxuv`zfi-9Fh7&(f(i&E=U!lGDmy$Q7*rx1l^tj9et@Yt<#({|+2wB7GKw%1b zGy&}^e)*QMvfg6%KXs(aUoHEra+r%*`M5IPO+fpKU*E;7ykFr@cFd<#sX-tALzq}(^vu1@1c*Re1h;lKjAvIr>#Q**6Zd_H#{m0e5;@4;~EA$!E=3OhbBY%V-_NC~z zSR_%u;#XP9I}=!Z2Jehwv$n;otY4uj>qy>+lHtt?Mto&{yKhcyxL`4>;q@!wJqc)E z@$0gfmG!IGc49j++rFUeZc5kS5tsTEzrIr5pTOcXcz+A8%7;z>gnn?@MODV<8AP4L`?G%Ehck zm90ECC!l@BFVI!2tmk+y@~)NIl*>$jx46`=`1O_Y{sgW%gZC#>R(KUF>p6D!W`)PH zU-_4n8JGfOEu41)dY8v7~0@_zR{0mumck+Ja&B|X{Yk~l;@PolmZMgD6R>S9_ zg!dt!eZ{ZEg{-V8`9H*NkKVE{1$fM9UsmxIKf!^@3t0`Eg;L#(fc6!?A{Vlhuq% z)UV+8Hf4H#Qj?UIu$nX%rM(XU?JIsQE@$Qa3N>4U!X38tE1O8P`Kb+8Ue0RxjFj*W z1hlXCRk#nU`1h}@ersH?V^8C!He7ihR>P;JgclOfzT)BEjg@~&xxvq6w(@SQX3tmo z*AURY;yK@qm48av-?Q>hEPh7wly_q_Z>Gw;l7RLVPy24Hyg~W@I0%EE&Q#^ySWTU* zk}o5meZ`}`Eh{uG`_@vw;#c!NO8slw`L($18RVz1OnF;Y{-L!D#pXb_Wwlwk1a~H& zeZ{ZNJz3qbSpn%iS@CNv7^Zij`RL1f-lnKxdGrDpq2}6OeKS0i8jfs#u8?Pe95U1at;@s$wNpJOL?Z z5YQRqsfv|Y@dS7!_LW_H$j2%H>SO1=cj98j6B8?*m6SXI?JKe33GhnoE1s2DnZqeYJ_m1!CnWHVv`bL?poh0@_zRG_evZo`94y2Io2tat)a&LE&O z$Ws+7vEm6xIfH=CAWv1S#EK^%e)zyyADgmR9pl!DN<8DE*`=qIO8yTa5VypKO!@FM#p*D+R$xS0+NGzJ zNB$2X5VypKO!@FM#p*D+PM}kT5p9H&o=zUU`UC>LLq1`$C+8?uCjqv?4J>nSWsv_v z2wx|6>xH3JJ!swWbN zTjGiJudG?D#?l5)yFBfWe)zyyADgmR9pk1b5g0{4XOJf;R(n`| z{ma|+Uxqv}DH#GfgA66ph!xLDN)ebrKxdGrDpouzDMerg0i8jfs#x)?q!fV}1at;@ zs$#{nl2Qa_5YQRqsfrcPN=gx!K|p7arz%!FD=9@_1_7Nxo~l^!tfUly83Z1522tv@ z)m}ULjMZ085qK7X2c5yMZguHDfOzfb&)Qe2Rs?1cc+eUA>Q?GkGd975oVHlkuGfTV zDdoik9<-zE2wcePL98yMq@hJ%9)ZixAm4>}Vz--USPB>0!Jnlvw^6@jM^xcm(AU5Hmsf`2)yN%K-#5qJuL%g-R+ zg?Qy8_?NSqG%uwUfu|6-{0#D4h*wU6e>tm3^HN$7cnX2b&miB0c;zJcm$RBQFQpZM zrx3XO4DwxwS5AU|Ijc$YQd$vs3W3YdAm4>}Sr?ZN+#O@&E>2P)T|JOfNibn-NUPIJ5CGi2wJa>1y-ZHcsJ6Z?oURNZ9ZCfl2x5i zlLloT5zfd?YGRR7?UQx(!`HCt*Pyp!32g30A8u2b1QwW_%;T}ZYLpl6Mk>Jl$w;!z zM+;A~sxxZRpv)t}8QEb?EU$0><3Ij>@@M%Em%14^o7H9k5{xC#-;ilLmEu@T>ut;L zu`NN%Z-K41$?v1Mu_@^1@4u}kuR6V+y#})F4cVnn)!J~S6P}aJ3gMO(#-z)NpOjo_ zc>?AB;bUD{CaHR&{xAOGYmeF8{cgm!ug>u9Ne{A- z$^$YaPnRc2{`1YsG9az`l|z23kv30!B&Qrh`nD%bGWWYt6;z!bVn$U{b;iJ|pz7R= zih@o_QC1y@cg9q^Yz(nWze}9zx3PT0_*VUD7uXLM$DPx%JBV5#Rln!K+x}Na^=*ps zY|*G{QZ{H}2y7-Qg|wndSsJ|*@S|6iWkS+Zav%d48Is2fA^vFL{y3yxSvGxNF-!Ak zjc=PxqnxINr1J=8%i|eZf|lO`(_7CLjjE2qg-JgtOd8$$5h^c_qqtCP@IO#`zyO8I{Y-XbGNOmHQ~jPWg+o?);7t0I%^o|Nl+%^1%t>Dk=NU{{3e0mX zPc@1hu~kX(V~FG#{)i!L6f@%8T9A1*$1MX>2ZKQUY7^r-1aUKvHYQFgqm0K@J(x%p z{nc!YrVOk`#j#+QS2e1X9m`XVB1bH1sCtmC>L2LD5b3f$b8-meEc?~Toc(Z&*ca&S zG6GnObUJ2uc18-zRiiM3 zU&FtDklDJH4N)y%loLaYN~JvK3HYX(XT&0L?&2@LGCRJ7U3I$O!&8(ClHc`iR0UP1 zhrmbzdyE3JlZ+HpqpHp_A?aCg27NmXZ&o%(+`NZJd|Acn9@{fb-^$ru7Z+v?O`F;FW$ z)qr!i>u2G+LQH%ta5HLC&>7ug<2}u*n!M_afiWRzW3fo(VOeWG6n^ZSgFk&=9bTPB z2Vx6h{~x~F+*=t+GV!eS?HIeMOahNh{>~#4Vw#i$&eGFz`Y0^{lPYRrLej=slgh)g zqA%4Q=s72U$UEs1&gQ;=)nUk=8Cc&*KVC5TF)M#{VwHF8LlKW#%l6D}^p)wYN5M$1 zs*j}JWGO@GEIkM*ugyFjm6Y+ZOuOm9Vr^>92&$W*>O9%35RSC!S7#RMiLu!KSoiAP zr_*^`jrG7aMm)l>&s+mjohc*6jx7o66_XFQk)OPV^ec=tuVo?ma6U?Sl0biV#pF=p z?AYD??ul>RcG*;8;E5iXYSfg+mV~7lRalt^$;pdvR;ai7Lk1g5{i0t>yHtVg{Vi6CcTD1zGA>RzIAU0go^yT{hOFrwq`#^QcXPMj2Mtpj&MbSCz5Am3r8SgCns#O`DRMGwaH zNA;_DtMKm3*g^-|l{PXL>z+Jf94!x?2?(|H>Ql0B6G z7I$8LZ%1a71Gzx`s)Qp?hX8g?r-9&Yj7>_sN*+9(`NgqhPbH96cI;T*j?5^NyFmS_ zgdto4SKmZFmZxketS2>WsRlh3XJlf+cJA4+%Og0n9 z%AeO$zS`^YR5F$k!0OK%%QvkV6Bc8O8{A31&pCqvN6R6b30?N0Wo)|8BJ%Oob(A&|b zvA17YkBWEotM!{_{fO8|XelS}UN)J>^)fCefF+w(1Co;$->>>g3N&(s`*W6&wp8Y24Ha;%j))KUvyE6lg_Z#E)QfXwPw z*Y2oEqhn?LD~PnMl3h`gidWiE1hDj@Qk8j-JYB9yezF~H%8*w5$|1kWIQ*G(IPgvM zUNaXQ@Og7dP07YGCkBmgHyaTtKu-0mc?)`N=FKmqYI&WgvXxac$axIZOSawyy;BJ{JymKuR;6WVQOVxYI*KZt!n5Hzya^PJ^3njBukY%{M>a})^l{BewxdlMvZ-I4-xcr6P`0M3 z=Y11@*^Inz%$S$;A{)Z(Xkbu#+-yXo0NK>9W-R5?b5^@kaz1^IpHM#z_`GR)Yr+t2 zC&ONm?PybmQV90M&E2iqyU-KujVc2qcfKc zDtR6~yALVeTgr_2TJqSPsy2ou+tH>B`PHvxEaf|LrUn%s-!Z}0&%#>Dq-k36YL80J z%|=8DkXHR_(rUgVZR}Cee#h)yKMQLqlct%PQs-m-36 zp$aPr%szw2j|SqqceXLFSpnT_R^{r*GnIh;FgO+OjO03lJXNvcSxG4ZGYIGm@>Io& zXCIo&XCHklHIv&V)N@*Z}iVZ=w{0M9NUK7<>)!Vie|R(N2D*Mx}G@TH7MZO_{f<}W?N3wU2bgI4`s!mHG; zj580xlKb7W?ajlFn3T%~O5;B9QLKiqT10Ag|318cHzfo*T;4-qR3Gr>Bqqb>XvFGs*7^}Mwf^8;0xiOAvVQ38YU%N|^~yVv8z#0gU?+bo&n|zbo?Ur9 zK`d7MDr1}5j}MV@F9GWbsWYHUFc+fi>We#?p9J}@NC=muwU*_N{jr$015+`>m6{SS z<0M6mM2lP2O^r`CH0KeoQA_ zj3c`V$V7Samz3ocC z#3=FQtVT^ldEP_-+tMzZl-awE%uj;mUrSt&#!h86oKcw`A-$E4vnqr6G*K5AtpDNr^{qIA+8<-j;;R4CaZ-YG4!`uxI4lRR+_YO3khz zMu{(HHEJTt^Hu_7(?~Nl0q!_cc2BJ-KAH_#N@9;mX_;NwigLj*V^Uha7|c3dj3us(zF+RX8dwp9gb{mvczY_;{6l zIjgHCu5q75AoUr0^pj-mE2?8{kQHSrna z+TM;kobjNNxC+tsB0G6pV}>c+nX-AFD2O4Zk9SvIZg}x#*s+40j@xFPHoGLVJbqMR zxXg_dGU{=5{>?fQK!*?mlP11eX4|!?Tb}Z5 ztmh6kDzsx2r@#~jeKo}V)jN~+jHX)8nS^^|Q!i4#+6(ZB`nc=NW;{02QW*)f-bR0! zCuV59jTy`??`57lc&yHJ&{~B-9IvX(52+cZuPTQ-G1l;$yAmdhjv^*Wn)qs^eL1U@ zlbt$Ch2@EJ2mAEsy!*+cMi7JgBf* zr1@K~%F1K2vO`^J=6R}h&KOSpY6jKY=EP^a{aO{4$HYCpC*9*olE;+MX&%$NZ30gM zBZfqOv`bC$_#=!Mcw0Lb>~^c#)}&&;vLvkxGyJWphL5FwH5}<%6ZZFH|1HmCR33iB zXDO?E;7t8$@uaF%&8R{BY6jI;=G@$q5Nv7^nNnYRQpkrtR{6l0uEmrv`kE2duhwwZ zC{+T>_IY*m$2qMjRm=A8tB$4`IK;VZ9o4Ufte29AzyboTUgXWnj`M;_(h%xb2}tL0 z8LLhWdL;r22(-4k_p4Szn_XM0u;Js9-&a1a0olAG?Xqhbt7S9NKq62jP~GZq#(q@| zob9X@He=AuepMY5<@GQ1t134+Mc^<2oVaRiI*C=yY-g*s;p6uERcE+FDLtxRbIpb&_G<|ajI}pp+0dPSwOMoOQSnjzYLlJ>BCv}8I^`Y6)(OF6eGd;b)UP_s^-2Wx64-1An}WR~?$R$C zUHxj;x=1Vn>j^O3fYHpWeA(#gSMwH8nMGg-fz58PDHu{lC&{t`)UP_LpjRSr2LhYj zU{f%A7Rw4?`c;}NYTLY!cG-numW`!>S_FJcv;^`WCa}4~Hw74a>XBp$I6VCrxY@6w z*lpjOEn_MiFT;#wtkSr8tP;?2RI!|KZX(?jV00YGd8d*o;0yte?e{B};SJWw!k8@!{NKQFeE+%V3x?dSrJ9M3ODpre^ke+$^9~`Cfmg zUwJBTR(#8v&Btqlv5Zw5^sRP;X?FC)Sx0|qmmZ6a{+La6ivUTiFPnWH`=hs}cEGQG zbpYCnL*m9WJ9^^8qrbFEkHtoR%qF`?dHfNEYiv2vqbc1P17&vz7|f1gog=K|5O2|{SK!c8+cVcWKSSvqnRxFVpqQ!yKu^OhCsDndBXl% zmn}2a*MnZvX-ECHQ?Yca8auA~)mWY{$X0$oBv196CJi{a$Mx<>l+0fZX*u0*2JUvX z#&SPoE!$psF-d9pqbBC@w+jsGOV#^=(W^tKUsbVGoy%BNM}L_!o6gmVQz}bO z8C80YMV-&nAJ^V~?0wl8)c4iL?y>5`dfqOTrN>5(_fiu*{c2;L544w^ zL499+;O^>7IiApt9bX|^d^EfC*r3s0dg5C?4--{tN(iL}p6IO3w9NB?9`!4J(U-B} zOM0}@4rx56>Dbz0#)_sAw~> z(hMiC>R0^QEn~%(B&7&^6oF-DP~TS{wXZZ;5tu<> zS-+Z5{NkKqwHUaD6M;lPXD|WjJc?BZj$Vnt0s=aNJY=!rSxG4ZGYIGm@>Io&XSJ!s z|9%Ia?fdu3pW9|O2|k3t*N)M}5fCem6Q%lTMa{p)EFvFie6p>bz^}eo?P6t}s1;&w zz42NKbP28Q%A$nlD|I>`fL9EEt_YK3)<8!8L>9Li^iivk$M-Q>L$NZjT zuhAd0^>*(lFVII6m`;2&J9*;c3#QLcAx^wXrlc1`_<>%gBw3xAHHP@UsE;dOCWQE) zqij#O*XS3}db@X&cj#L*%%9%I$9GKsz$~g{kSD@{9Ul*j^4t)cyWdMPRnM{c`X~%- z)5NFf;VYc1lPTRE*-)ZdjM_lMee>l%rUL=yV>@)c-Hcg`(sPvp4^obct z4x64Rc;b`~*BmEq_NFHa4v+ijKoBqpcDK1yJ!m!d^Z9;7eqZj1#?Q-N{u%|lY-FkG zqrYWW1NS;(a>U}%F3`uwQ?hcszjBXf za1V@_E!t7T+tnfX@Wsk!Rl68Y8cwVdkj|r6b>Q@0MRd{jC^+ph4{U$+dgG%edvZ0! ziU%lGJgcMxI;ZDGRr4+Upk@UKEg*@H9>r?c9?-tpg)52;UA)J)%j|o8wP|1R8$hg{ z&g!0LFj4hw2E*;7S@3vIRu=;4Fc+&1ob*bp5|Ga0Jz1rK;fsEF-`VbjzY{`uF<8>goHcUw?mn`{!4&k*!$$ zN+UjHTdX1xouglAV0Sb>8`^Kaa&!W}-^OvWt=LFJoPVX!+>nro)rYg{v<~f!58oSc T`pMSbh(J1r2r{u6#p?e9C4JK7 literal 0 HcmV?d00001 diff --git a/E_Ink_Test/lib/neopixel.py b/E_Ink_Test/lib/neopixel.py new file mode 100644 index 0000000..8cd2a36 --- /dev/null +++ b/E_Ink_Test/lib/neopixel.py @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: 2016 Damien P. George +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# SPDX-FileCopyrightText: 2019 Carter Nelson +# SPDX-FileCopyrightText: 2019 Roy Hooper +# +# SPDX-License-Identifier: MIT + +""" +`neopixel` - NeoPixel strip driver +==================================================== + +* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper +""" + +import sys +import board +import digitalio +from neopixel_write import neopixel_write + +import adafruit_pixelbuf + +try: + # Used only for typing + from typing import Optional, Type + from types import TracebackType + import microcontroller +except ImportError: + pass + + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git" + + +# Pixel color order constants +RGB = "RGB" +"""Red Green Blue""" +GRB = "GRB" +"""Green Red Blue""" +RGBW = "RGBW" +"""Red Green Blue White""" +GRBW = "GRBW" +"""Green Red Blue White""" + + +class NeoPixel(adafruit_pixelbuf.PixelBuf): + """ + A sequence of neopixels. + + :param ~microcontroller.Pin pin: The pin to output neopixel data on. + :param int n: The number of neopixels in the chain + :param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels. + :param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full + brightness + :param bool auto_write: True if the neopixels should immediately change when set. If False, + `show` must be called explicitly. + :param str pixel_order: Set the pixel color channel order. GRBW is set by default. + + Example for Circuit Playground Express: + + .. code-block:: python + + import neopixel + from board import * + + RED = 0x100000 # (0x10, 0, 0) also works + + pixels = neopixel.NeoPixel(NEOPIXEL, 10) + for i in range(len(pixels)): + pixels[i] = RED + + Example for Circuit Playground Express setting every other pixel red using a slice: + + .. code-block:: python + + import neopixel + from board import * + import time + + RED = 0x100000 # (0x10, 0, 0) also works + + # Using ``with`` ensures pixels are cleared after we're done. + with neopixel.NeoPixel(NEOPIXEL, 10) as pixels: + pixels[::2] = [RED] * (len(pixels) // 2) + time.sleep(2) + + .. py:method:: NeoPixel.show() + + Shows the new colors on the pixels themselves if they haven't already + been autowritten. + + The colors may or may not be showing after this function returns because + it may be done asynchronously. + + .. py:method:: NeoPixel.fill(color) + + Colors all pixels the given ***color***. + + .. py:attribute:: brightness + + Overall brightness of the pixel (0 to 1.0) + + """ + + def __init__( + self, + pin: microcontroller.Pin, + n: int, + *, + bpp: int = 3, + brightness: float = 1.0, + auto_write: bool = True, + pixel_order: str = None + ): + if not pixel_order: + pixel_order = GRB if bpp == 3 else GRBW + elif isinstance(pixel_order, tuple): + order_list = [RGBW[order] for order in pixel_order] + pixel_order = "".join(order_list) + + self._power = None + if ( + sys.implementation.version[0] >= 7 + and getattr(board, "NEOPIXEL", None) == pin + ): + power = getattr(board, "NEOPIXEL_POWER_INVERTED", None) + polarity = power is None + if not power: + power = getattr(board, "NEOPIXEL_POWER", None) + if power: + try: + self._power = digitalio.DigitalInOut(power) + self._power.switch_to_output(value=polarity) + except ValueError: + pass + + super().__init__( + n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write + ) + + self.pin = digitalio.DigitalInOut(pin) + self.pin.direction = digitalio.Direction.OUTPUT + + def deinit(self) -> None: + """Blank out the NeoPixels and release the pin.""" + self.fill(0) + self.show() + self.pin.deinit() + if self._power: + self._power.deinit() + + def __enter__(self): + return self + + def __exit__( + self, + exception_type: Optional[Type[BaseException]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], + ): + self.deinit() + + def __repr__(self): + return "[" + ", ".join([str(x) for x in self]) + "]" + + @property + def n(self) -> int: + """ + The number of neopixels in the chain (read-only) + """ + return len(self) + + def write(self) -> None: + """.. deprecated: 1.0.0 + + Use ``show`` instead. It matches Micro:Bit and Arduino APIs.""" + self.show() + + def _transmit(self, buffer: bytearray) -> None: + neopixel_write(self.pin, buffer)