diff --git a/.gitignore b/.gitignore index b1203f1..0a3fecd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .vscode *.tmp.* node_modules -cardiograph.code-workspace \ No newline at end of file +cardiograph.code-workspace +*venv* +*__pycache__ diff --git a/.gitmodules b/.gitmodules index 9395a8d..5461013 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/argparser"] path = src/opter url = https://git.nloewen.com/n/argv-parser.git +[submodule "src/python/opter-py"] + path = src/python/opter-py + url = https://git.nloewen.com/n/opter-py.git diff --git a/src/assembler.js b/javascript-old/assembler.js similarity index 100% rename from src/assembler.js rename to javascript-old/assembler.js diff --git a/src/cardiograph.js b/javascript-old/cardiograph.js similarity index 100% rename from src/cardiograph.js rename to javascript-old/cardiograph.js diff --git a/src/conversions.js b/javascript-old/conversions.js similarity index 100% rename from src/conversions.js rename to javascript-old/conversions.js diff --git a/src/cpu.js b/javascript-old/cpu.js similarity index 100% rename from src/cpu.js rename to javascript-old/cpu.js diff --git a/src/dbg.js b/javascript-old/dbg.js similarity index 100% rename from src/dbg.js rename to javascript-old/dbg.js diff --git a/src/io.js b/javascript-old/io.js similarity index 100% rename from src/io.js rename to javascript-old/io.js diff --git a/src/jsconfig.json b/javascript-old/jsconfig.json similarity index 100% rename from src/jsconfig.json rename to javascript-old/jsconfig.json diff --git a/src/logging.js b/javascript-old/logging.js similarity index 100% rename from src/logging.js rename to javascript-old/logging.js diff --git a/src/machine.config.js b/javascript-old/machine.config.js similarity index 100% rename from src/machine.config.js rename to javascript-old/machine.config.js diff --git a/src/package-lock.json b/javascript-old/package-lock.json similarity index 100% rename from src/package-lock.json rename to javascript-old/package-lock.json diff --git a/src/package.json b/javascript-old/package.json similarity index 100% rename from src/package.json rename to javascript-old/package.json diff --git a/readme.md b/javascript-old/readme.md similarity index 100% rename from readme.md rename to javascript-old/readme.md diff --git a/test-programs/add-sub.asm b/javascript-old/test-programs/add-sub.asm similarity index 100% rename from test-programs/add-sub.asm rename to javascript-old/test-programs/add-sub.asm diff --git a/test-programs/constants.asm b/javascript-old/test-programs/constants.asm similarity index 100% rename from test-programs/constants.asm rename to javascript-old/test-programs/constants.asm diff --git a/test-programs/draw-xy.asm b/javascript-old/test-programs/draw-xy.asm similarity index 100% rename from test-programs/draw-xy.asm rename to javascript-old/test-programs/draw-xy.asm diff --git a/test-programs/fill-display.asm b/javascript-old/test-programs/fill-display.asm similarity index 100% rename from test-programs/fill-display.asm rename to javascript-old/test-programs/fill-display.asm diff --git a/test-programs/flag-carry--fhp-ftg.asm b/javascript-old/test-programs/flag-carry--fhp-ftg.asm similarity index 100% rename from test-programs/flag-carry--fhp-ftg.asm rename to javascript-old/test-programs/flag-carry--fhp-ftg.asm diff --git a/test-programs/flag-overflow-2.asm b/javascript-old/test-programs/flag-overflow-2.asm similarity index 100% rename from test-programs/flag-overflow-2.asm rename to javascript-old/test-programs/flag-overflow-2.asm diff --git a/test-programs/flag-overflow.asm b/javascript-old/test-programs/flag-overflow.asm similarity index 100% rename from test-programs/flag-overflow.asm rename to javascript-old/test-programs/flag-overflow.asm diff --git a/test-programs/jmp-to-label.asm b/javascript-old/test-programs/jmp-to-label.asm similarity index 100% rename from test-programs/jmp-to-label.asm rename to javascript-old/test-programs/jmp-to-label.asm diff --git a/test-programs/keypad.asm b/javascript-old/test-programs/keypad.asm similarity index 100% rename from test-programs/keypad.asm rename to javascript-old/test-programs/keypad.asm diff --git a/test-programs/life.asm b/javascript-old/test-programs/life.asm similarity index 100% rename from test-programs/life.asm rename to javascript-old/test-programs/life.asm diff --git a/test-programs/move-pixel-with-keypad.asm b/javascript-old/test-programs/move-pixel-with-keypad.asm similarity index 100% rename from test-programs/move-pixel-with-keypad.asm rename to javascript-old/test-programs/move-pixel-with-keypad.asm diff --git a/test-programs/nop.asm b/javascript-old/test-programs/nop.asm similarity index 100% rename from test-programs/nop.asm rename to javascript-old/test-programs/nop.asm diff --git a/test-programs/referencing-program-counter-during-assembly.asm b/javascript-old/test-programs/referencing-program-counter-during-assembly.asm similarity index 100% rename from test-programs/referencing-program-counter-during-assembly.asm rename to javascript-old/test-programs/referencing-program-counter-during-assembly.asm diff --git a/test-programs/relocate-display-mem.asm b/javascript-old/test-programs/relocate-display-mem.asm similarity index 100% rename from test-programs/relocate-display-mem.asm rename to javascript-old/test-programs/relocate-display-mem.asm diff --git a/test-programs/subroutines.asm b/javascript-old/test-programs/subroutines.asm similarity index 100% rename from test-programs/subroutines.asm rename to javascript-old/test-programs/subroutines.asm diff --git a/python-microcontrollers/meowbit/adafruit_display_text/__init__.py b/python-microcontrollers/meowbit/adafruit_display_text/__init__.py new file mode 100644 index 0000000..8ebfea8 --- /dev/null +++ b/python-microcontrollers/meowbit/adafruit_display_text/__init__.py @@ -0,0 +1,474 @@ +# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text` +======================= +""" + +__version__ = "3.2.2" +__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, + ) -> 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 + + 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: + if new_text == self._text: + return + 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/python-microcontrollers/meowbit/adafruit_display_text/bitmap_label.py b/python-microcontrollers/meowbit/adafruit_display_text/bitmap_label.py new file mode 100644 index 0000000..77a56fa --- /dev/null +++ b/python-microcontrollers/meowbit/adafruit_display_text/bitmap_label.py @@ -0,0 +1,597 @@ +# 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__ = "3.2.2" +__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,too-many-branches + + bbox = font.get_bounding_box() + if len(bbox) == 4: + ascender_max, descender_max = bbox[1], -bbox[3] + else: + ascender_max, descender_max = self._ascent, self._descent + + lines = 1 + + # starting x and y position (left margin) + xposition = x_start = yposition = y_start = 0 + + 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/python-microcontrollers/meowbit/adafruit_display_text/label.py b/python-microcontrollers/meowbit/adafruit_display_text/label.py new file mode 100644 index 0000000..e27bdca --- /dev/null +++ b/python-microcontrollers/meowbit/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__ = "3.2.2" +__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/python-microcontrollers/meowbit/adafruit_display_text/outlined_label.py b/python-microcontrollers/meowbit/adafruit_display_text/outlined_label.py new file mode 100644 index 0000000..e9e60bd --- /dev/null +++ b/python-microcontrollers/meowbit/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__ = "3.2.2" +__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/python-microcontrollers/meowbit/adafruit_display_text/scrolling_label.py b/python-microcontrollers/meowbit/adafruit_display_text/scrolling_label.py new file mode 100644 index 0000000..e068b0b --- /dev/null +++ b/python-microcontrollers/meowbit/adafruit_display_text/scrolling_label.py @@ -0,0 +1,160 @@ +# 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__ = "3.2.2" +__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 and 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: + if self._text != self.full_text: + 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 self.full_text: + self._current_index = new_index % len(self.full_text) + else: + self._current_index = 0 + + @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 and new_text[-1] != " ": + new_text = "{} ".format(new_text) + if new_text != self._full_text: + self._full_text = new_text + self.current_index = 0 + self.update(True) + + @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/python-microcontrollers/meowbit/adafruit_display_text/text_box.py b/python-microcontrollers/meowbit/adafruit_display_text/text_box.py new file mode 100644 index 0000000..a29e016 --- /dev/null +++ b/python-microcontrollers/meowbit/adafruit_display_text/text_box.py @@ -0,0 +1,435 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text.text_box` +================================================================================ + +Text graphics handling for CircuitPython, including text boxes + + +* Author(s): Tim Cocks + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +__version__ = "3.2.2" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + +import displayio +from micropython import const + +from adafruit_display_text import wrap_text_to_pixels +from adafruit_display_text import bitmap_label + +try: + from typing import Optional, Tuple + from fontio import FontProtocol +except ImportError: + pass + + +# pylint: disable=too-many-instance-attributes, duplicate-code +class TextBox(bitmap_label.Label): + """ + TextBox has a constrained width and optionally height. + You set the desired size when it's initialized it + will automatically wrap text to fit it within the allotted + size. + + Left, Right, and Center alignment of the text within the + box are supported. + + :param font: The font to use for the TextBox. + :param width: The width of the TextBox in pixels. + :param height: The height of the TextBox in pixels. + :param align: How to align the text within the box, + valid values are ``ALIGN_LEFT``, ``ALIGN_CENTER``, ``ALIGN_RIGHT``. + """ + + ALIGN_LEFT = const(0) + ALIGN_CENTER = const(1) + ALIGN_RIGHT = const(2) + + DYNAMIC_HEIGHT = const(-1) + + def __init__( + self, font: FontProtocol, width: int, height: int, align=ALIGN_LEFT, **kwargs + ) -> None: + self._bitmap = None + self._tilegrid = None + self._prev_label_direction = None + self._width = width + + if height != TextBox.DYNAMIC_HEIGHT: + self._height = height + self.dynamic_height = False + else: + self.dynamic_height = True + + if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT): + raise ValueError( + "Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT" + ) + self._align = align + + self._padding_left = kwargs.get("padding_left", 0) + self._padding_right = kwargs.get("padding_right", 0) + + self.lines = wrap_text_to_pixels( + kwargs.get("text", ""), + self._width - self._padding_left - self._padding_right, + font, + ) + + super(bitmap_label.Label, self).__init__(font, **kwargs) + + print(f"before reset: {self._text}") + + self._text = "\n".join(self.lines) + self._text = self._replace_tabs(self._text) + self._original_text = 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, + ) + print(f"after reset: {self._text}") + + 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, too-many-statements, too-many-branches + + # placeText - Writes text into a bitmap at the specified location. + # + # Note: scale is pushed up to Group level + original_xposition = xposition + cur_line_index = 0 + cur_line_width = self._text_bounding_box(self.lines[0], self.font)[0] + + if self.align == self.ALIGN_LEFT: + x_start = original_xposition # starting x position (left margin) + if self.align == self.ALIGN_CENTER: + unused_space = self._width - cur_line_width + x_start = original_xposition + unused_space // 2 + if self.align == self.ALIGN_RIGHT: + unused_space = self._width - cur_line_width + x_start = original_xposition + unused_space - self._padding_right + + xposition = x_start # pylint: disable=used-before-assignment + + y_start = yposition + # print(f"start loc {x_start}, {y_start}") + + left = None + right = x_start + top = bottom = y_start + line_spacing = self._line_spacing + + # print(f"cur_line width: {cur_line_width}") + for char in text: + if char == "\n": # newline + cur_line_index += 1 + cur_line_width = self._text_bounding_box( + self.lines[cur_line_index], self.font + )[0] + # print(f"cur_line width: {cur_line_width}") + if self.align == self.ALIGN_LEFT: + x_start = original_xposition # starting x position (left margin) + if self.align == self.ALIGN_CENTER: + unused_space = self._width - cur_line_width + x_start = original_xposition + unused_space // 2 + if self.align == self.ALIGN_RIGHT: + unused_space = self._width - cur_line_width + x_start = original_xposition + unused_space - self._padding_right + xposition = x_start + + 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 + ) + ) + try: + 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 + ) + except ValueError: + # ignore index out of bounds error + break + + xposition = xposition + my_glyph.shift_x + + # bounding_box + return left, top, right - left, bottom - top + + 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: + text = self._text + + self._text = self._replace_tabs(text) + print(f"inside reset_text text: {text}") + + # 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 + + if self.dynamic_height: + print(f"dynamic height, box_y: {box_y}") + self._height = box_y + + # Create the Bitmap unless it can be reused + new_bitmap = None + if ( + self._bitmap is None + or self._bitmap.width != self._width + or self._bitmap.height != self._height + ): + new_bitmap = displayio.Bitmap( + self._width, self._height, len(self._palette) + ) + self._bitmap = new_bitmap + else: + self._bitmap.fill(0) + + # Place the text into the Bitmap + self._place_text( + self._bitmap, + 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=self._width, + tile_height=self._height, + 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 + + self._bounding_box = ( + self._tilegrid.x + self._padding_left, + self._tilegrid.y + self._padding_top, + tight_box_x, + tight_box_y, + ) + print(f"end of reset_text bounding box: {self._bounding_box}") + + 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 + + @property + def height(self) -> int: + """The height of the label determined from the bounding box.""" + return self._height + + @property + def width(self) -> int: + """The width of the label determined from the bounding box.""" + return self._width + + @width.setter + def width(self, width: int) -> None: + self._width = width + self.text = self._text + + @height.setter + def height(self, height: int) -> None: + if height != TextBox.DYNAMIC_HEIGHT: + self._height = height + self.dynamic_height = False + else: + self.dynamic_height = True + self.text = self._text + + @bitmap_label.Label.text.setter + def text(self, text: str) -> None: + self.lines = wrap_text_to_pixels( + text, self._width - self._padding_left - self._padding_right, self.font + ) + self._text = self._replace_tabs(text) + self._original_text = self._text + self._text = "\n".join(self.lines) + + self._set_text(self._text, self.scale) + + @property + def align(self): + """Alignment of the text within the TextBox""" + return self._align + + @align.setter + def align(self, align: int) -> None: + if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT): + raise ValueError( + "Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT" + ) + self._align = align diff --git a/python-microcontrollers/meowbit/circuitpython-display-test.py b/python-microcontrollers/meowbit/circuitpython-display-test.py new file mode 100644 index 0000000..9e07605 --- /dev/null +++ b/python-microcontrollers/meowbit/circuitpython-display-test.py @@ -0,0 +1,20 @@ +import board +import displayio +import vectorio +import terminalio +from adafruit_display_text import label + +group = displayio.Group() +board.DISPLAY.root_group = group + +text = "Hello world" +text_area = label.Label(terminalio.FONT, text=text) +text_area.x = 10 +text_area.y = 10 +group.append(text_area) + +palette = displayio.Palette(1) +palette[0] = 0xff00ff + +circle = vectorio.Circle(pixel_shader=palette, radius=5, x=5, y=5) +group.append(circle) \ No newline at end of file diff --git a/python-microcontrollers/meowbit/circuitpython-keypad-test.py b/python-microcontrollers/meowbit/circuitpython-keypad-test.py new file mode 100644 index 0000000..54093cd --- /dev/null +++ b/python-microcontrollers/meowbit/circuitpython-keypad-test.py @@ -0,0 +1,13 @@ +#meowbit + +import keypad +import board + +km = keypad.KeyMatrix( + row_pins = (board.P0, board.P1, board.P2, board.P3), + column_pins = (board.P4, board.P6, board.P8, board.P9) ) + +while True: + event = km.events.get() + if event: + print(event.key_number, event.released) \ No newline at end of file diff --git a/python-microcontrollers/meowbit/cpu.py b/python-microcontrollers/meowbit/cpu.py new file mode 100644 index 0000000..926bd0e --- /dev/null +++ b/python-microcontrollers/meowbit/cpu.py @@ -0,0 +1,327 @@ +# TO USE KEYPAD TO MANIPULATE DATA: +# +# A button: run/halt +# B button: when halted, toggles address/data entry +# Right button: when halted, single-steps +# +# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key) +# Data entry: likewise. After you press the second digit, it will automatically go to the next address. + + +import time +import board +import displayio +import vectorio +import terminalio # for font +from adafruit_display_text import label +import keypad +from digitalio import DigitalInOut, Direction, Pull + + + + +class TwoDigitHexInput: + def __init__(self): + self.digits = [0x0, 0x0] + self.currentDigit = 0 + self.value = 0 + + def input(self, d): + self.digits[self.currentDigit] = d + self.value = (self.digits[0] * 16) + self.digits[1] + print("INPUT", self.digits) + self.currentDigit = 0 if self.currentDigit else 1 + + def clear(self): + self.__init__() + print(self.digits) + + +class CPU: + def __init__(self): + self.running = False + self.IP = 254 + self.acc = 0 + self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False } + self.instruction = { 'opcode': False, 'operand': False } + self.memory = False + + + def load_memory(self, bytes): + self.memory = bytes + bytearray(256 - len(bytes)) + print(type(self.memory)) + print('mem 254', self.memory[254]) + # print(self.memory) + + def start(self): + self.running = True + + def step(self): + if self.IP >= 256: + self.IP = 0 + print("IP:", self.IP) + self.instruction['opcode'] = self.memory[self.IP] + self.IP = self.IP+1 + self.instruction['operand'] = self.memory[self.IP] + self.IP = self.IP+1 + self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand']) + + print("instr:", self.instruction['opcode'], self.instruction['operand']) + print("mnem:", self.nums2mnems[self.instruction['opcode']]) + print("acc:", self.acc) + print("running:", self.running) + print() + # self.print_screen() + print("byte 26 (keyboard):", self.memory[26]) + print() + + def hlt(self, operand): + self.running = False + + def nop(self, operand): + pass + + def lda_lit(self, operand): + self.acc = operand + + def lda_mem(self, operand): + self.acc = memory[operand] + + def sta_lit(self, operand): + memory[operand] = self.acc + + def sta_mem(self, operand): + memory[memory[operand]] = self.acc + + def add_lit(self, operand): + self.acc = self.acc + operand + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def add_mem(self, operand): + self.acc = self.acc + self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_lit(self, operand): + self.acc = self.acc - operand + if self.acc < 0: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_mem(self, operand): + self.acc = self.acc - self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def jmp_lit(self, operand): + self.IP = operand + + def jmp_mem(self, operand): + self.IP = memory[operand] + + def ske(self, operand): + if self.flags['Eq']: + self.IP += 2 + + def skz(self, operand): + if self.flags['Z']: + self.IP += 2 + + def skn(self, operand): + if self.flags['N']: + self.IP += 2 + + def skc(self, operand): + if self.flags['C']: + self.IP += 2 + + def cst(self, operand): + self.flags['C'] = True + + def ccl(self, operand): + self.flags['C'] = False + + nums2mnems = { + 0: hlt, + 1: nop, + 2: lda_lit, + 3: sta_lit, + 4: add_lit, + 5: sub_lit, + 6: jmp_lit, + 7: ske, + 8: skz, + 9: skn, + 10: skc, + 11: cst, + 12: ccl, + 16: hlt, + 17: nop, + 18: lda_mem, + 19: sta_mem, + 20: add_mem, + 21: sub_mem, + 22: jmp_mem, + 23: ske, + 24: skz, + 25: skn, + 26: skc, + 27: cst, + 28: ccl, + } + + +### MEOWBIT-SPECIFIC STUFF ### + +# to list board features: print(board.__dir__) + +btna = DigitalInOut(board.BTNA) +btna.direction = Direction.INPUT +btna.pull = Pull.UP # down doesn't work + +btnb = DigitalInOut(board.BTNB) +btnb.direction = Direction.INPUT +btnb.pull = Pull.UP # down doesn't work + +btnr = DigitalInOut(board.RIGHT) +btnr.direction = Direction.INPUT +btnr.pull = Pull.UP # down doesn't work + +km = keypad.KeyMatrix( + row_pins = (board.P0, board.P1, board.P2, board.P3), + column_pins = (board.P4, board.P6, board.P8, board.P9) ) + +# This is global because that way you can update the text by just altering text_area.text +displayGroup = displayio.Group() +board.DISPLAY.root_group = displayGroup +text_area = label.Label(terminalio.FONT, text="") +text_area.x = 10 +text_area.y = 10 +displayGroup.append(text_area) + +palette = displayio.Palette(1) +palette[0] = 0xff00ff + + +class Monitor: + def __init__(self, cpu): + self.cpu = cpu + self.monitorMode = 'addressEntry' # or dataEntry + self.monitorAddressInput = TwoDigitHexInput() + self.monitorDataInput = TwoDigitHexInput() + + def handleKeys(self): + keypad_event = km.events.get() + keyPressed = True if (keypad_event and keypad_event.released) else False + key = keypad_event.key_number if keyPressed else False + + if self.cpu.running: + if btna.value == False: + print("HALT PRESSED") + self.cpu.running = False + time.sleep(0.5) # lazy debounce + # km.events.clear() # don't track keypresses from during the run + + if keyPressed: + self.cpu.memory[26] = key + + elif not self.cpu.running: + if btna.value == False: + self.cpu.running = True + print("\nSTARTING") + time.sleep(0.5) # lazy debounce + + if btnb.value == False: + self.monitorMode = 'addressEntry' if self.monitorMode != 'addressEntry' else 'dataEntry' + print("\nENTERING", self.monitorMode, "MODE") + self.monitorDataInput.currentDigit = 0 + self.monitorAddressInput.currentDigit = 0 + time.sleep(0.5) # lazy debounce + + if btnr.value == False: + print("\nSINGLE STEP FROM MONITOR ADDR") + # self.IP = self.monitorAddressInput.value + self.cpu.step() + time.sleep(0.5) # lazy debounce + + if keypad_event and keypad_event.released: + if self.monitorMode == 'addressEntry': + self.monitorAddressInput.input(keypad_event.key_number) + self.cpu.IP = self.monitorAddressInput.value + print("MA", self.IP) + + else: + self.monitorDataInput.input(keypad_event.key_number) + self.cpu.memory[self.IP] = self.monitorDataInput.value + print("MD", self.monitorDataInput.value) + if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so go to the next addresss + self.cpu.IP = (self.cpu.IP + 1) % 256 + print("ADVANCING") + print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n") + + def printMonitor(self): + text = "IP " + str(self.cpu.IP) + "\tDATA " + str(self.cpu.memory[self.cpu.IP]) + "\tACC " + str(self.cpu.acc) + "\nRunning: " + str(self.cpu.running) + text_area.text = text + + + def printScreen(self): + for i in range(5): + for j in range(5): + memory_index = (i * 5) + j + if self.cpu.memory[memory_index] > 0: + print("#", end=" ") + circle = vectorio.Circle(pixel_shader=palette, radius=8, x=(10 + (j * 20)), y=(40 + (i * 20))) + displayGroup.append(circle) + else: + print("_", end=" ") + print() + + def run(self): + self.cpu.start() + t = time.time() + while (time.time() - t) < 30: + self.handleKeys() + if self.cpu.running: + self.cpu.step() + self.printMonitor() + # self.printScreen() + time.sleep(0.5) + print("timeout") + print(self.cpu.memory) + + +cpu = CPU() +monitor = Monitor(cpu) + +prog = '04 FF 04 01 14 01 00 00 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01' +#prog = '00' +program_bytes = bytearray.fromhex(prog.replace(" ", "")) +# Add jmp at addr 254: +program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') +cpu.load_memory(program_with_jump) + +monitor.run() diff --git a/python-microcontrollers/pi-pico/cpu-pi-pico.py b/python-microcontrollers/pi-pico/cpu-pi-pico.py new file mode 100644 index 0000000..ac3b280 --- /dev/null +++ b/python-microcontrollers/pi-pico/cpu-pi-pico.py @@ -0,0 +1,353 @@ +# TO USE KEYPAD TO MANIPULATE DATA: +# +# A button: run/halt +# B button: when halted, toggles address/data entry +# Right button: when halted, single-steps +# +# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key) +# Data entry: likewise. After you press the second digit, it will automatically go to the next address. + + +import time +import board +import keypad +import digitalio +from tm1637_display import TM1637Display +import board +import busio # for led matrix +from adafruit_ht16k33 import matrix + +class TwoDigitHexInput: + def __init__(self): + self.digits = [0x0, 0x0] + self.currentDigit = 0 + self.value = 0 + + def input(self, d): + self.digits[self.currentDigit] = d + self.value = (self.digits[0] * 16) + self.digits[1] + print("INPUT", self.digits, "current digit: " + str(self.currentDigit), "value: " + str(self.value)) + self.currentDigit = 0 if self.currentDigit else 1 + + def clear(self): + self.__init__() + print(self.digits) + + def set(self, n): + self.value = n + self.digits[0] = n >> 4 + self.digits[1] = n & 0xF + + + + +class CPU: + def __init__(self): + self.running = False + self.IP = 254 + self.acc = 0 + self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False } + self.instruction = { 'opcode': False, 'operand': False } + self.memory = False + + + def load_memory(self, bytes): + self.memory = bytes + bytearray(256 - len(bytes)) + # print(self.memory) + + def start(self): + self.running = True + + def step(self): + if self.IP >= 255: # TODO CHECK + self.IP = 0 + print("IP:", toHex(self.IP)) + self.instruction['opcode'] = self.memory[self.IP] + self.IP = self.IP+1 + self.instruction['operand'] = self.memory[self.IP] + self.IP = self.IP+1 + self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand']) + + print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand'])) + print("mnem:", self.nums2mnems[self.instruction['opcode']]) + print("acc:", self.acc, "N:", self.flags['N']) + print("running:", self.running) + print() + # self.print_screen() + print("byte 26 (keyboard):", self.memory[26]) + print() + + def hlt(self, operand): + self.running = False + + def nop(self, operand): + pass + + def lda_lit(self, operand): + self.acc = operand + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def lda_mem(self, operand): + self.acc = self.memory[operand] + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sta_lit(self, operand): + self.memory[operand] = self.acc + + def sta_mem(self, operand): + self.memory[self.memory[operand]] = self.acc + + def add_lit(self, operand): + self.acc = self.acc + operand + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def add_mem(self, operand): + self.acc = self.acc + self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_lit(self, operand): + self.acc = self.acc - operand + if self.acc < 0: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_mem(self, operand): + self.acc = self.acc - self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def jmp_lit(self, operand): + self.IP = operand + + def jmp_mem(self, operand): + self.IP = self.memory[operand] + + def ske(self, operand): # FIXME +# if self.flags['Eq']: +# self.IP += 2 + if self.acc == operand: + self.IP += 2 + + def skz(self, operand): + if self.flags['Z']: + self.IP += 2 + + def skn(self, operand): + if self.flags['N']: + self.IP += 2 + + def skc(self, operand): + if self.flags['C']: + self.IP += 2 + + def cst(self, operand): + self.flags['C'] = True + + def ccl(self, operand): + self.flags['C'] = False + + nums2mnems = { + 0: hlt, # x0 + 1: nop, # x1 + 2: lda_lit, # 02 + 3: sta_lit, # 03 + 4: add_lit, # 04 + 5: sub_lit, # 05 + 6: jmp_lit, # 06 + 7: ske, # x7 + 8: skz, # x8 + 9: skn, # x9 + 10: skc, # A + 11: cst, # B + 12: ccl, # C + 16: hlt, # + 17: nop, # + 18: lda_mem, # 12 + 19: sta_mem, # 13 + 20: add_mem, # 14 + 21: sub_mem, # 15 + 22: jmp_mem, # 16 + 23: ske, + 24: skz, + 25: skn, + 26: skc, + 27: cst, + 28: ccl, + } + + +### PI PICO SPECIFIC STUFF ### + +# to list board features: print(dir(board)) + +display_1 = TM1637Display(board.GP0, board.GP1, length=4) +display_2 = TM1637Display(board.GP2, board.GP3, length=4) + +i2c = busio.I2C(board.GP17, board.GP16) # scl, sda +matrix = matrix.Matrix8x8(i2c) +matrix.brightness = 1 +matrix.blink_rate = 0 + +keymatrix = keypad.KeyMatrix( + row_pins = (board.GP5, board.GP6, board.GP7, board.GP8), + column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) ) + +keymap = { + 15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt", + 10:"4", 11:"5", 12:"6", 13:"7", 14:"step", + 5:"8", 6:"9", 7:"A", 8:"B", 9:"addr", + 0:"C", 1:"D", 2:"E", 3:"F", 4:"data" } + +numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ] + +def toHex(n): + return "%0.2X" % n + +class Monitor: + def __init__(self, cpu): + self.cpu = cpu + self.monitorMode = 'addressEntry' # or dataEntry + self.monitorAddressInput = TwoDigitHexInput() + self.monitorDataInput = TwoDigitHexInput() + + # In data entry mode, when a full byte is keyed in, + # the next keypress advances to the next address and continues entering data there. + # This variable tracks whether it's time to do that or not. + self.advanceDataEntryNextPress = False + + def handleKeys(self): + keypad_event = keymatrix.events.get() + keyPressed = True if (keypad_event and keypad_event.released ) else False + key = keymap[keypad_event.key_number] if keyPressed else False + numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False + + + if self.cpu.running: + if key == "runhalt": + print("HALT PRESSED") + self.cpu.running = False + time.sleep(0.5) # lazy debounce + # km.events.clear() # don't track keypresses from during the run + + if numericKeyPressed: + self.cpu.memory[26] = int(key, 16) + + elif not self.cpu.running: + if key == "runhalt": + self.cpu.running = True + print("\nSTARTING") + time.sleep(0.5) # lazy debounce + + if key == "addr": + self.monitorMode = 'addressEntry' + print("\nENTERING", self.monitorMode, "MODE") + self.monitorAddressInput.currentDigit = 0 + time.sleep(0.5) # lazy debounce + if key == "data": + self.monitorMode = 'dataEntry' + print("\nENTERING", self.monitorMode, "MODE") + self.monitorDataInput.clear() + self.advanceDataEntryNextPress = False + time.sleep(0.5) # lazy debounce + + if key == "step": + print("\nSINGLE STEP FROM MONITOR ADDR") + # self.IP = self.monitorAddressInput.value + self.cpu.step() + time.sleep(0.5) # lazy debounce + + if numericKeyPressed: + if self.monitorMode == 'addressEntry': + self.monitorAddressInput.input(int(key, 16)) + self.cpu.IP = self.monitorAddressInput.value + print("MA", self.cpu.IP) + + if self.monitorMode == 'dataEntry': + if self.advanceDataEntryNextPress: + print("ADVANCING") + self.cpu.IP = (self.cpu.IP + 1) % 256 + # self.monitorDataInput.clear() # reset .currentDigit + self.monitorDataInput.set(self.cpu.memory[self.cpu.IP]) + self.advanceDataEntryNextPress = False + self.monitorDataInput.input(int(key, 16)) + self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value + print("MD", self.monitorDataInput.value) + if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address + self.advanceDataEntryNextPress = True + + print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n") + + + def displayScreen(self): + for x in range(8): + for y in range(8): + matrix[x, y] = self.cpu.memory[x + (8*y)] + + + + def run(self): + #self.cpu.start() + t = time.time() + while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles + self.handleKeys() + display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP])) + # display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP])) + # display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc)) + display_2.print(toHex(self.cpu.acc)) + self.displayScreen() + if self.cpu.running: + self.cpu.step() + # time.sleep(0.5) # TODO ? + print("timeout") + print(self.cpu.memory) + + +cpu = CPU() +monitor = Monitor(cpu) + +# preamble = '00 ' * 64 +# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES +# offset = 64 +# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00' +#prog = '00' +# program_bytes = bytearray.fromhex(prog.replace(" ", "")) + +# Add jmp at addr 254: +#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00 +# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64) + +with open('test-multiply2.bin', 'rb') as file: + program_bytes = bytearray(file.read()) + +cpu.load_memory(program_bytes) + +monitor.run() \ No newline at end of file diff --git a/python-microcontrollers/pi-pico/old/neopixel-test.py b/python-microcontrollers/pi-pico/old/neopixel-test.py new file mode 100644 index 0000000..42df1b1 --- /dev/null +++ b/python-microcontrollers/pi-pico/old/neopixel-test.py @@ -0,0 +1,12 @@ +import time +import board +import neopixel + +pixels = neopixel.NeoPixel(board.A3, 5*5, brightness=0.5, auto_write=True) + +for i in range(50): + pixels.fill((50, 0, 0)) # red + time.sleep(0.5) + pixels.fill((0,0,50)) # blue + time.sleep(0.5) + print(i) \ No newline at end of file diff --git a/python-microcontrollers/pi-pico/old/neopixel.mpy b/python-microcontrollers/pi-pico/old/neopixel.mpy new file mode 100644 index 0000000..5291001 Binary files /dev/null and b/python-microcontrollers/pi-pico/old/neopixel.mpy differ diff --git a/python-microcontrollers/pi-pico/old/neopixel.py b/python-microcontrollers/pi-pico/old/neopixel.py new file mode 100644 index 0000000..715d408 --- /dev/null +++ b/python-microcontrollers/pi-pico/old/neopixel.py @@ -0,0 +1,181 @@ +# 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__ = "6.3.15" +__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. The default is GRB if bpp is set + to 3, otherwise GRBW is used as the 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) diff --git a/python-microcontrollers/pi-pico/read-hex.py b/python-microcontrollers/pi-pico/read-hex.py new file mode 100644 index 0000000..6421f64 --- /dev/null +++ b/python-microcontrollers/pi-pico/read-hex.py @@ -0,0 +1,20 @@ +# Open the binary file in read-binary mode +with open('test-multiply.bin', 'rb') as file: + # Read the entire file contents + binary_data = file.read() + +# Convert the binary data to a string of hex bytes +hex_string = binary_data.hex() + +# Print the hex string +print(hex_string) + + +# Open the binary file in read-binary mode +with open('test-multiply.bin', 'rb') as file: + # Read the entire file contents into a bytearray + byte_data = bytearray(file.read()) + +# Print the bytearray +print(byte_data) + diff --git a/python-microcontrollers/pi-pico/test-multiply.bin b/python-microcontrollers/pi-pico/test-multiply.bin new file mode 100644 index 0000000..1c4d85f Binary files /dev/null and b/python-microcontrollers/pi-pico/test-multiply.bin differ diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-8x8-matrix.py b/python-microcontrollers/pi-pico/tests/pi-pico-8x8-matrix.py new file mode 100644 index 0000000..5826964 --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-8x8-matrix.py @@ -0,0 +1,10 @@ +import board +import busio +from adafruit_ht16k33 import matrix +i2c = busio.I2C(board.GP17, board.GP16) # scl, sda + +matrix = matrix.Matrix8x8(i2c) +matrix.fill(0) # Clear the matrix. +matrix[0, 0] = 1 +matrix.brightness = 1 +matrix.blink_rate = 2 \ No newline at end of file diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-blink.py b/python-microcontrollers/pi-pico/tests/pi-pico-blink.py new file mode 100644 index 0000000..fbc4cab --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-blink.py @@ -0,0 +1,14 @@ +"""Example for pi pico. Blinking LED""" + +import board +import digitalio +import time + +led = digitalio.DigitalInOut(board.LED) +led.direction = digitalio.Direction.OUTPUT + +led.value = True + +for i in range(50): + led.value = not led.value + time.sleep(0.5) \ No newline at end of file diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-button.py b/python-microcontrollers/pi-pico/tests/pi-pico-button.py new file mode 100644 index 0000000..e83c555 --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-button.py @@ -0,0 +1,17 @@ +"""Example for pi pico. Button-controlled LED. + +Wiring: switch to ground pin on pico, and to pin 18. +""" + +import board +import digitalio + +led = digitalio.DigitalInOut(board.LED) +led.direction = digitalio.Direction.OUTPUT + +switch = digitalio.DigitalInOut(board.GP18) +switch.direction = digitalio.Direction.INPUT +switch.pull = digitalio.Pull.UP + +while True: + led.value = not switch.value diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-keypad.py b/python-microcontrollers/pi-pico/tests/pi-pico-keypad.py new file mode 100644 index 0000000..87be7a2 --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-keypad.py @@ -0,0 +1,19 @@ +# pi pico + +import keypad +import board + +keymatrix = keypad.KeyMatrix( + row_pins = (board.GP5, board.GP6, board.GP7, board.GP8), + column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) ) + +keymap = { + 15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt", + 10:"4", 11:"5", 12:"6", 13:"7", 14:"step", + 5:"8", 6:"9", 7:"A", 8:"B", 9:"addrdata", + 0:"C", 1:"D", 2:"E", 3:"F", 4:"NA" } + +while True: + event = keymatrix.events.get() + if event: + print(event.key_number, event.released, keymap[event.key_number]) diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-keypad2.py b/python-microcontrollers/pi-pico/tests/pi-pico-keypad2.py new file mode 100644 index 0000000..92d263b --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-keypad2.py @@ -0,0 +1,19 @@ +import board +import keypad + +keymatrix = keypad.KeyMatrix( + row_pins = (board.GP5, board.GP6, board.GP7, board.GP8), + column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) ) + +keymap = { + 15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt", + 10:"4", 11:"5", 12:"6", 13:"7", 14:"step", + 5:"8", 6:"9", 7:"A", 8:"B", 9:"addr", + 0:"C", 1:"D", 2:"E", 3:"F", 4:"data" } + +while True: + keypad_event = keymatrix.events.get() + keyPressed = True if (keypad_event and keypad_event.released ) else False + key = keymap[keypad_event.key_number] if keyPressed else False + if key: + print(key) diff --git a/python-microcontrollers/pi-pico/tests/pi-pico-seven-segment-display.py b/python-microcontrollers/pi-pico/tests/pi-pico-seven-segment-display.py new file mode 100644 index 0000000..480353f --- /dev/null +++ b/python-microcontrollers/pi-pico/tests/pi-pico-seven-segment-display.py @@ -0,0 +1,14 @@ +"""Example for pi pico. Blinking LED""" + +import board +import digitalio +import time +from tm1637_display import TM1637Display + +display_1 = TM1637Display(board.GP0, board.GP1, length=4) +display_1.print("1234") + +display_2 = TM1637Display(board.GP2, board.GP3, length=4) +display_2.print("ABCD") + +print("end") \ No newline at end of file diff --git a/python-microcontrollers/pi-pico/venv/lib/python3.11/site-packages/idna-3.10.dist-info/LICENSE.md b/python-microcontrollers/pi-pico/venv/lib/python3.11/site-packages/idna-3.10.dist-info/LICENSE.md new file mode 100644 index 0000000..19b6b45 --- /dev/null +++ b/python-microcontrollers/pi-pico/venv/lib/python3.11/site-packages/idna-3.10.dist-info/LICENSE.md @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2013-2024, Kim Davies and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/python/cpu.py b/python/cpu.py new file mode 100644 index 0000000..35b4063 --- /dev/null +++ b/python/cpu.py @@ -0,0 +1,164 @@ +import time + +class CPU: + def __init__(self): + self.running = False + self.IP = 254 + self.acc = 0 + self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False } + self.instruction = { 'opcode': False, 'operand': False } + self.memory = False + + + def load_memory(self, bytes): + self.memory = bytes + bytearray(256 - len(bytes)) + # print(self.memory) + + def start(self): + self.running = True + + def step(self): + if self.IP >= 255: # TODO CHECK + self.IP = 0 + print("IP:", toHex(self.IP)) + self.instruction['opcode'] = self.memory[self.IP] + self.IP = self.IP+1 + self.instruction['operand'] = self.memory[self.IP] + self.IP = self.IP+1 + self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand']) + + print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand'])) + print("mnem:", self.nums2mnems[self.instruction['opcode']]) + print("acc:", self.acc, "N:", self.flags['N']) + print("running:", self.running) + print() + # self.print_screen() + print("byte 26 (keyboard):", self.memory[26]) + print() + + def hlt(self, operand): + self.running = False + + def nop(self, operand): + pass + + def lda_lit(self, operand): + self.acc = operand + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def lda_mem(self, operand): + self.acc = self.memory[operand] + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sta_lit(self, operand): + self.memory[operand] = self.acc + + def sta_mem(self, operand): + self.memory[self.memory[operand]] = self.acc + + def add_lit(self, operand): + self.acc = self.acc + operand + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def add_mem(self, operand): + self.acc = self.acc + self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_lit(self, operand): + self.acc = self.acc - operand + if self.acc < 0: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def sub_mem(self, operand): + self.acc = self.acc - self.memory[operand] + if self.acc > 255: + self.acc = self.acc % 256 + self.flags['C'] = True + else: + self.flags['C'] = False + self.flags['Z'] = True if self.acc == 0 else False + self.flags['Eq'] = True if self.acc == operand else False + self.flags['N'] = True if self.acc > 127 else False + + def jmp_lit(self, operand): + self.IP = operand + + def jmp_mem(self, operand): + self.IP = self.memory[operand] + + def ske(self, operand): # FIXME +# if self.flags['Eq']: +# self.IP += 2 + if self.acc == operand: + self.IP += 2 + + def skz(self, operand): + if self.flags['Z']: + self.IP += 2 + + def skn(self, operand): + if self.flags['N']: + self.IP += 2 + + def skc(self, operand): + if self.flags['C']: + self.IP += 2 + + def cst(self, operand): + self.flags['C'] = True + + def ccl(self, operand): + self.flags['C'] = False + + nums2mnems = { + 0: hlt, # x0 + 1: nop, # x1 + 2: lda_lit, # 02 + 3: sta_lit, # 03 + 4: add_lit, # 04 + 5: sub_lit, # 05 + 6: jmp_lit, # 06 + 7: ske, # x7 + 8: skz, # x8 + 9: skn, # x9 + 10: skc, # A + 11: cst, # B + 12: ccl, # C + 16: hlt, # + 17: nop, # + 18: lda_mem, # 12 + 19: sta_mem, # 13 + 20: add_mem, # 14 + 21: sub_mem, # 15 + 22: jmp_mem, # 16 + 23: ske, + 24: skz, + 25: skn, + 26: skc, + 27: cst, + 28: ccl, + } diff --git a/python/simulator-scrap.py b/python/simulator-scrap.py new file mode 100644 index 0000000..0a4473b --- /dev/null +++ b/python/simulator-scrap.py @@ -0,0 +1,124 @@ +numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ] + +def toHex(n): + return "%0.2X" % n + +class Monitor: + def __init__(self, cpu): + self.cpu = cpu + self.monitorMode = 'addressEntry' # or dataEntry + self.monitorAddressInput = TwoDigitHexInput() + self.monitorDataInput = TwoDigitHexInput() + + # In data entry mode, when a full byte is keyed in, + # the next keypress advances to the next address and continues entering data there. + # This variable tracks whether it's time to do that or not. + self.advanceDataEntryNextPress = False + + def handleKeys(self): + keypad_event = keymatrix.events.get() + keyPressed = True if (keypad_event and keypad_event.released ) else False + key = keymap[keypad_event.key_number] if keyPressed else False + numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False + + + if self.cpu.running: + if key == "runhalt": + print("HALT PRESSED") + self.cpu.running = False + time.sleep(0.5) # lazy debounce + # km.events.clear() # don't track keypresses from during the run + + if numericKeyPressed: + self.cpu.memory[26] = int(key, 16) + + elif not self.cpu.running: + if key == "runhalt": + self.cpu.running = True + print("\nSTARTING") + time.sleep(0.5) # lazy debounce + + if key == "addr": + self.monitorMode = 'addressEntry' + print("\nENTERING", self.monitorMode, "MODE") + self.monitorAddressInput.currentDigit = 0 + time.sleep(0.5) # lazy debounce + if key == "data": + self.monitorMode = 'dataEntry' + print("\nENTERING", self.monitorMode, "MODE") + self.monitorDataInput.clear() + self.advanceDataEntryNextPress = False + time.sleep(0.5) # lazy debounce + + if key == "step": + print("\nSINGLE STEP FROM MONITOR ADDR") + # self.IP = self.monitorAddressInput.value + self.cpu.step() + time.sleep(0.5) # lazy debounce + + if numericKeyPressed: + if self.monitorMode == 'addressEntry': + self.monitorAddressInput.input(int(key, 16)) + self.cpu.IP = self.monitorAddressInput.value + print("MA", self.cpu.IP) + + if self.monitorMode == 'dataEntry': + if self.advanceDataEntryNextPress: + print("ADVANCING") + self.cpu.IP = (self.cpu.IP + 1) % 256 + # self.monitorDataInput.clear() # reset .currentDigit + self.monitorDataInput.set(self.cpu.memory[self.cpu.IP]) + self.advanceDataEntryNextPress = False + self.monitorDataInput.input(int(key, 16)) + self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value + print("MD", self.monitorDataInput.value) + if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address + self.advanceDataEntryNextPress = True + + print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n") + + + def displayScreen(self): + for x in range(8): + for y in range(8): + matrix[x, y] = self.cpu.memory[x + (8*y)] + + + + def run(self): + #self.cpu.start() + t = time.time() + while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles + self.handleKeys() + display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP])) + # display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP])) + # display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc)) + display_2.print(toHex(self.cpu.acc)) + self.displayScreen() + if self.cpu.running: + self.cpu.step() + # time.sleep(0.5) # TODO ? + print("timeout") + print(self.cpu.memory) + + +cpu = CPU() +monitor = Monitor(cpu) + +# preamble = '00 ' * 64 +# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES +# offset = 64 +# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00' +#prog = '00' +# program_bytes = bytearray.fromhex(prog.replace(" ", "")) + +# Add jmp at addr 254: +#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00 +# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64) + +with open('test-multiply2.bin', 'rb') as file: + program_bytes = bytearray(file.read()) + +cpu.load_memory(program_bytes) + +monitor.run() diff --git a/python/simulator.py b/python/simulator.py new file mode 100644 index 0000000..1f3fbd9 --- /dev/null +++ b/python/simulator.py @@ -0,0 +1,5 @@ +import cpu + +c = cpu.CPU() + +print(c) diff --git a/src/opter b/src/opter deleted file mode 160000 index 1d98a07..0000000 --- a/src/opter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1d98a0707c3e61e362d2d3d5413b475437b5de0e