diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..468bf77 Binary files /dev/null and b/.DS_Store differ diff --git a/QtPy-RP2040-Tem-Hum/.DS_Store b/QtPy-RP2040-Tem-Hum/.DS_Store new file mode 100644 index 0000000..52b0f12 Binary files /dev/null and b/QtPy-RP2040-Tem-Hum/.DS_Store differ diff --git a/QtPy-RP2040-Tem-Hum/code.py b/QtPy-RP2040-Tem-Hum/code.py new file mode 100644 index 0000000..ce09173 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/code.py @@ -0,0 +1,55 @@ +import time +import board +import adafruit_sht4x +import busio +import displayio +import terminalio +import microcontroller +from adafruit_display_text import bitmap_label as label +from adafruit_displayio_sh1107 import SH1107, DISPLAY_OFFSET_ADAFRUIT_128x128_OLED_5297 + +displayio.release_displays() + +i2c = busio.I2C(board.SCL1, board.SDA1) +sht = adafruit_sht4x.SHT4x(i2c) +sht.mode = adafruit_sht4x.Mode.NOHEAT_HIGHPRECISION +display_bus = displayio.I2CDisplay(i2c, device_address=0x3D) + +WIDTH = 128 +HEIGHT = 128 +ROTATION = 0 + +BORDER = 2 + +display = SH1107( + display_bus, + width=WIDTH, + height=HEIGHT, + display_offset=DISPLAY_OFFSET_ADAFRUIT_128x128_OLED_5297, + rotation=ROTATION, +) + +while True: + temperature, relative_humidity = sht.measurements; + selfTemp = microcontroller.cpu.temperature; + # Make the display context + splash = displayio.Group() + display.show(splash) + + # Draw some label text + size_text = "%0.fc" % temperature; + size_text_area = label.Label( + terminalio.FONT, text=size_text, scale=3, color=0xFFFFFF, x=38, y=42 + ) + splash.append(size_text_area) + oled_text = "%0.f%%" % relative_humidity; + oled_text_area = label.Label( + terminalio.FONT, text=oled_text, scale=3, color=0xFFFFFF, x=58, y=74 + ) + splash.append(oled_text_area) + self_text = "%0.f" % selfTemp; + self_text_area = label.Label( + terminalio.FONT, text=self_text, scale=1, color=0xFFFFFF, x=115, y=120 + ) + splash.append(self_text_area) + time.sleep(60) diff --git a/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/__init__.py b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/__init__.py new file mode 100755 index 0000000..586c361 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/__init__.py @@ -0,0 +1,448 @@ +# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_display_text` +======================= +""" + +try: + from typing import Optional, Union, List, Tuple + from fontio import BuiltinFont + from adafruit_bitmap_font.bdf import BDF + from adafruit_bitmap_font.pcf import PCF +except ImportError: + pass +from displayio import Group, Palette + + +def wrap_text_to_pixels( + string: str, + max_width: int, + font: Optional[Union[BuiltinFont, BDF, PCF]] = None, + indent0: str = "", + indent1: str = "", +) -> List[str]: + # pylint: disable=too-many-branches, too-many-locals + + """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: ~BuiltinFont, ~BDF, or ~PCF + :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): + return sum(font.get_glyph(ord(c)).shift_x for c in text) + + lines = [] + partial = [indent0] + width = measure(indent0) + swidth = measure(" ") + firstword = True + for line_in_input in string.split("\n"): + 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 ( + measure("".join(partial)) + + measure(cur_part) + + measure(char) + + measure("-") + > max_width + ): + word_parts.append("".join(partial) + cur_part + "-") + cur_part = char + partial = [indent1] + 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 + + 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: ~BuiltinFont, ~BDF, or ~PCF + :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.""" + + def __init__( + self, + font: Union[BuiltinFont, BDF, PCF], + 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", + **kwargs, # pylint: disable=unused-argument + ) -> None: + # pylint: disable=too-many-arguments, too-many-locals + + super().__init__(x=x, y=y, scale=1) + + self._font = font + self._text = text + self._palette = Palette(2) + self._color = 0xFFFFFF + self._background_color = None + self._line_spacing = line_spacing + self._background_tight = background_tight + self._padding_top = padding_top + self._padding_bottom = padding_bottom + self._padding_left = padding_left + self._padding_right = padding_right + self._anchor_point = anchor_point + self._anchored_position = anchored_position + self._base_alignment = base_alignment + self._label_direction = label_direction + self._tab_replacement = tab_replacement + self._tab_text = self._tab_replacement[1] * self._tab_replacement[0] + + if "max_glyphs" in kwargs: + print("Please update your code: 'max_glyphs' is not needed anymore.") + + self._ascent, self._descent = self._get_ascent_descent() + self._bounding_box = None + + self.color = color + self.background_color = background_color + + # local group will hold background and text + # the self group scale should always remain at 1, the self._local_group will + # be used to set the scale of the label + self._local_group = Group(scale=scale) + self.append(self._local_group) + + self._baseline = -1.0 + + if self._base_alignment: + self._y_offset = 0 + else: + self._y_offset = self._ascent // 2 + + def _get_ascent_descent(self) -> Tuple[int, int]: + """ Private function to calculate ascent and descent font values """ + if hasattr(self.font, "ascent") and hasattr(self.font, "descent"): + return self.font.ascent, self.font.descent + + # check a few glyphs for maximum ascender and descender height + glyphs = "M j'" # choose glyphs with highest ascender and lowest + try: + self._font.load_glyphs(glyphs) + except AttributeError: + # Builtin font doesn't have or need load_glyphs + pass + # descender, will depend upon font used + ascender_max = descender_max = 0 + for char in glyphs: + this_glyph = self._font.get_glyph(ord(char)) + if this_glyph: + ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy) + descender_max = max(descender_max, -this_glyph.dy) + return ascender_max, descender_max + + @property + def font(self) -> Union[BuiltinFont, BDF, PCF]: + """Font to use for text display.""" + return self._font + + def _set_font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None: + raise NotImplementedError("{} MUST override '_set_font'".format(type(self))) + + @font.setter + def font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None: + self._set_font(new_font) + + @property + def color(self) -> int: + """Color of the text as an RGB hex number.""" + return self._color + + @color.setter + def color(self, new_color: int): + self._color = new_color + if new_color is not None: + self._palette[1] = new_color + self._palette.make_opaque(1) + else: + self._palette[1] = 0 + self._palette.make_transparent(1) + + @property + def background_color(self) -> int: + """Color of the background as an RGB hex number.""" + return self._background_color + + def _set_background_color(self, new_color): + raise NotImplementedError( + "{} MUST override '_set_background_color'".format(type(self)) + ) + + @background_color.setter + def background_color(self, new_color: int) -> None: + self._set_background_color(new_color) + + @property + def anchor_point(self) -> Tuple[float, float]: + """Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)""" + return self._anchor_point + + @anchor_point.setter + def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None: + if new_anchor_point[1] == self._baseline: + self._anchor_point = (new_anchor_point[0], -1.0) + else: + self._anchor_point = new_anchor_point + + # update the anchored_position using setter + self.anchored_position = self._anchored_position + + @property + def anchored_position(self) -> Tuple[int, int]: + """Position relative to the anchor_point. Tuple containing x,y + pixel coordinates.""" + return self._anchored_position + + @anchored_position.setter + def anchored_position(self, new_position: Tuple[int, int]) -> None: + self._anchored_position = new_position + # Calculate (x,y) position + if (self._anchor_point is not None) and (self._anchored_position is not None): + self.x = int( + new_position[0] + - (self._bounding_box[0] * self.scale) + - round(self._anchor_point[0] * (self._bounding_box[2] * self.scale)) + ) + if self._anchor_point[1] == self._baseline: + self.y = int(new_position[1] - (self._y_offset * self.scale)) + else: + self.y = int( + new_position[1] + - (self._bounding_box[1] * self.scale) + - round(self._anchor_point[1] * self._bounding_box[3] * self.scale) + ) + + @property + def scale(self) -> int: + """Set the scaling of the label, in integer values""" + return self._local_group.scale + + @scale.setter + def scale(self, new_scale: int) -> None: + self._local_group.scale = new_scale + self.anchored_position = self._anchored_position # update the anchored_position + + def _set_text(self, new_text: str, scale: int) -> None: + raise NotImplementedError("{} MUST override '_set_text'".format(type(self))) + + @property + def text(self) -> str: + """Text to be displayed.""" + return self._text + + @text.setter # Cannot set color or background color with text setter, use separate setter + def text(self, new_text: str) -> None: + self._set_text(new_text, self.scale) + + @property + def bounding_box(self) -> Tuple[int, int]: + """An (x, y, w, h) tuple that completely covers all glyphs. The + first two numbers are offset from the x, y origin of this group""" + return tuple(self._bounding_box) + + @property + def height(self) -> int: + """The height of the label determined from the bounding box.""" + return self._bounding_box[3] - self._bounding_box[1] + + @property + def width(self) -> int: + """The width of the label determined from the bounding box.""" + return self._bounding_box[2] - self._bounding_box[0] + + @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 self._tab_text.join(text.split("\t")) diff --git a/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/bitmap_label.py b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/bitmap_label.py new file mode 100755 index 0000000..eaa7146 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/bitmap_label.py @@ -0,0 +1,553 @@ +# 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__ = "2.21.2" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + + +try: + from typing import Union, Optional, Tuple + from fontio import BuiltinFont + from adafruit_bitmap_font.bdf import BDF + from adafruit_bitmap_font.pcf import PCF +except ImportError: + pass + +import displayio + +from adafruit_display_text import LabelBase + + +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: ~BuiltinFont, ~BDF, or ~PCF + :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 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 (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``""" + + def __init__( + self, font: Union[BuiltinFont, BDF, PCF], save_text: bool = True, **kwargs + ) -> None: + + self._bitmap = None + + super().__init__(font, **kwargs) + + self._save_text = save_text + self._text = self._replace_tabs(self._text) + + if self._label_direction == "RTL": + self._text = "".join(reversed(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[Union[BuiltinFont, BDF, PCF]] = None, + text: Optional[str] = None, + line_spacing: Optional[float] = None, + scale: Optional[int] = None, + ) -> None: + # pylint: disable=too-many-branches, too-many-statements + + # 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) + if self._label_direction == "RTL": + self._text = "".join(reversed(self._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) + + 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 + + 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 + box_x = box_x + self._padding_left + self._padding_right + box_y = box_y + self._padding_top + self._padding_bottom + + # Create the bitmap and TileGrid + self._bitmap = displayio.Bitmap(box_x, box_y, len(self._palette)) + + # 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 + + 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, + ) + + if self._label_direction == "UPR": + self._tilegrid.transpose_xy = True + self._tilegrid.flip_x = True + if self._label_direction == "DWR": + self._tilegrid.transpose_xy = True + self._tilegrid.flip_y = True + if self._label_direction == "UPD": + self._tilegrid.flip_x = True + self._tilegrid.flip_y = True + + # 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 + + # 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"): + self._bounding_box = ( + self._tilegrid.x, + self._tilegrid.y, + tight_box_y, + box_x, + ) + else: + self._bounding_box = ( + self._tilegrid.x, + self._tilegrid.y, + 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: Union[BuiltinFont, BDF, PCF], 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: Union[BuiltinFont, BDF, PCF] + ) -> Tuple[int, int, int, int, int, int]: + # pylint: disable=too-many-locals + + ascender_max, descender_max = self._ascent, self._descent + + lines = 1 + + xposition = ( + x_start + ) = yposition = y_start = 0 # starting x and y position (left margin) + + left = None + right = x_start + top = bottom = y_start + + y_offset_tight = self._ascent // 2 + + newline = False + line_spacing = self._line_spacing + + for char in text: + + if char == "\n": # newline + newline = True + + 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 newline: + newline = False + xposition = x_start # reset to left column + yposition = yposition + self._line_spacing_ypixels( + font, line_spacing + ) # Add a newline + lines += 1 + if xposition == x_start: + if left is None: + left = my_glyph.dx + 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, + ) + + def _place_text( + self, + bitmap: displayio.Bitmap, + text: str, + font: Union[BuiltinFont, BDF, PCF], + 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 = my_glyph.dx + 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 + + print( + 'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format( + char + ) + ) + + if (y_blit_target + my_glyph.height) > bitmap.height: + print( + 'Warning: Glyph clipped, exceeds descent property: "{}"'.format( + char + ) + ) + + self._blit( + bitmap, + xposition + my_glyph.dx, + 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, + ) + + 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: Union[BuiltinFont, BDF, PCF]) -> 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: + 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/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/label.py b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/label.py new file mode 100755 index 0000000..f5043b8 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/lib/adafruit_display_text/label.py @@ -0,0 +1,427 @@ +# 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__ = "2.21.2" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + + +try: + from typing import Union, Optional, Tuple + from fontio import BuiltinFont + from adafruit_bitmap_font.bdf import BDF + from adafruit_bitmap_font.pcf import PCF +except ImportError: + pass + +from displayio import Bitmap, Palette, TileGrid + +from adafruit_display_text import LabelBase + + +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: ~BuiltinFont, ~BDF, or ~PCF + :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. + 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 (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. 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: Union[BuiltinFont, BDF, PCF], **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)) + + 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 ("UPR", "DWR", "TTB"): + box_height = ( + self._bounding_box[3] + self._padding_top + self._padding_bottom + ) + x_box_offset = -self._padding_bottom + box_width = ( + (ascent + descent) + + int((lines - 1) * self._width * self._line_spacing) + + self._padding_left + + self._padding_right + ) + 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._base_alignment: + y_box_offset = -ascent - self._padding_top + else: + y_box_offset = -ascent + y_offset - self._padding_top + + 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 = left + x_box_offset + movy = -box_height - x_box_offset + elif self._label_direction == "DWR": + movx = left + x_box_offset + movy = x_box_offset + elif self._label_direction == "TTB": + movx = left + x_box_offset + movy = x_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 + + 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 = glyph.dx + 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 = glyph.dx + else: + right = max(right, glyph.dx) + position_x = x - glyph.width + + elif self._label_direction == "TTB": + if x == 0: + if left is None: + left = glyph.dx + 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: Union[BuiltinFont, BDF, PCF]) -> 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/QtPy-RP2040-Tem-Hum/lib/adafruit_displayio_sh1107.py b/QtPy-RP2040-Tem-Hum/lib/adafruit_displayio_sh1107.py new file mode 100755 index 0000000..d9f2c22 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/lib/adafruit_displayio_sh1107.py @@ -0,0 +1,208 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Mark Roberts for Adafruit Industries +# SPDX-FileCopyrightText: 2021 James Carr +# +# SPDX-License-Identifier: MIT +""" +`adafruit_displayio_sh1107` +================================================================================ + +DisplayIO driver for SH1107 monochrome displays + + +* Author(s): Scott Shawcroft, Mark Roberts (mdroberts1243), James Carr + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit FeatherWing 128 x 64 OLED - SH1107 128x64 OLED `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython (version 6+) firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import sys +import displayio +from micropython import const + +__version__ = "1.5.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107.git" + + +DISPLAY_OFFSET_ADAFRUIT_FEATHERWING_OLED_4650 = const(0x60) +""" +The hardware display offset to use when configuring the SH1107 for the +`Adafruit Featherwing 128x64 OLED `_. +This is the default if not passed in. + +.. code-block:: + + from adafruit_displayio_sh1107 import SH1107, DISPLAY_OFFSET_ADAFRUIT_FEATHERWING_OLED_4650 + + # Constructor for the Adafruit FeatherWing 128x64 OLED + display = SH1107(bus, width=128, height=64, + display_offset=DISPLAY_OFFSET_ADAFRUIT_FEATHERWING_OLED_4650) + # Or as it's the default + display = SH1107(bus, width=128, height=64) +""" + +DISPLAY_OFFSET_ADAFRUIT_128x128_OLED_5297 = const(0x00) +""" +The hardware display offset to use when configuring the SH1107 for the +`Adafruit Monochrome 1.12" 128x128 OLED `_. + +.. code-block:: + + from adafruit_displayio_sh1107 import SH1107, DISPLAY_OFFSET_ADAFRUIT_128x128_OLED_5297 + + # Constructor for the Adafruit Monochrome 1.12" 128x128 OLED + display = SH1107(bus, width=128, height=128, + display_offset=DISPLAY_OFFSET_ADAFRUIT_128x128_OLED_5297, rotation=90) +""" + +DISPLAY_OFFSET_PIMORONI_MONO_OLED_PIM374 = const(0x00) +""" +The hardware display offset to use when configuring the SH1107 for the +`Pimoroni Mono 128x128 OLED `_ + +.. code-block:: + + from adafruit_displayio_sh1107 import SH1107, DISPLAY_OFFSET_PIMORONI_MONO_OLED_PIM374 + + # Constructor for the Pimoroni Mono 128x128 OLED + display = SH1107(bus, width=128, height=128, + display_offset=DISPLAY_OFFSET_PIMORONI_MONO_OLED_PIM374) +""" + + +# Sequence from sh1107 framebuf driver formatted for displayio init +# we fixed sh110x addressing in 7, so we have slightly different setups +if sys.implementation.name == "circuitpython" and sys.implementation.version[0] < 7: + # if sys.implementation.version[0] < 7: + _INIT_SEQUENCE = ( + b"\xae\x00" # display off, sleep mode + b"\xdc\x01\x00" # display start line = 0 (POR = 0) + b"\x81\x01\x2f" # contrast setting = 0x2f + b"\x21\x00" # vertical (column) addressing mode (POR=0x20) + b"\xa0\x00" # segment remap = 1 (POR=0, down rotation) + b"\xcf\x00" # common output scan direction = 15 (n-1 to 0) (POR=0) + b"\xa8\x01\x7f" # multiplex ratio = 128 (POR) + b"\xd3\x01\x60" # set display offset mode = 0x60 + b"\xd5\x01\x51" # divide ratio/oscillator: divide by 2, fOsc (POR) + b"\xd9\x01\x22" # pre-charge/dis-charge period mode: 2 DCLKs/2 DCLKs (POR) + b"\xdb\x01\x35" # VCOM deselect level = 0.770 (POR) + b"\xb0\x00" # set page address = 0 (POR) + b"\xa4\x00" # entire display off, retain RAM, normal status (POR) + b"\xa6\x00" # normal (not reversed) display + b"\xaf\x00" # DISPLAY_ON + ) + _PIXELS_IN_ROW = True + _ROTATION_OFFSET = 0 +else: + _INIT_SEQUENCE = ( + b"\xae\x00" # display off, sleep mode + b"\xdc\x01\x00" # set display start line 0 + b"\x81\x01\x4f" # contrast setting = 0x4f + b"\x20\x00" # vertical (column) addressing mode (POR=0x20) + b"\xa0\x00" # segment remap = 1 (POR=0, down rotation) + b"\xc0\x00" # common output scan direction = 0 (0 to n-1 (POR=0)) + b"\xa8\x01\x7f" # multiplex ratio = 128 (POR=0x7F) + b"\xd3\x01\x60" # set display offset mode = 0x60 + # b"\xd5\x01\x51" # divide ratio/oscillator: divide by 2, fOsc (POR) + b"\xd9\x01\x22" # pre-charge/dis-charge period mode: 2 DCLKs/2 DCLKs (POR) + b"\xdb\x01\x35" # VCOM deselect level = 0.770 (POR) + # b"\xb0\x00" # set page address = 0 (POR) + b"\xa4\x00" # entire display off, retain RAM, normal status (POR) + b"\xa6\x00" # normal (not reversed) display + b"\xaf\x00" # DISPLAY_ON + ) + _PIXELS_IN_ROW = False + _ROTATION_OFFSET = 90 + + +class SH1107(displayio.Display): + """ + SH1107 driver for use with DisplayIO + + :param bus: The bus that the display is connected to. + :param int width: The width of the display. Maximum of 128 + :param int height: The height of the display. Maximum of 128 + :param int rotation: The rotation of the display. 0, 90, 180 or 270. + :param int display_offset: The display offset that the first column is wired to. + This will be dependent on the OLED display and two displays with the + same dimensions could have different offsets. This defaults to + `DISPLAY_OFFSET_ADAFRUIT_FEATHERWING_OLED_4650` + """ + + def __init__( + self, + bus, + display_offset=DISPLAY_OFFSET_ADAFRUIT_FEATHERWING_OLED_4650, + rotation=0, + **kwargs + ): + rotation = (rotation + _ROTATION_OFFSET) % 360 + if rotation in (0, 180): + multiplex = kwargs["width"] - 1 + else: + multiplex = kwargs["height"] - 1 + init_sequence = bytearray(_INIT_SEQUENCE) + init_sequence[16] = multiplex + init_sequence[19] = display_offset + super().__init__( + bus, + init_sequence, + **kwargs, + color_depth=1, + grayscale=True, + pixels_in_byte_share_row=_PIXELS_IN_ROW, # in vertical (column) mode + data_as_commands=True, # every byte will have a command byte preceding + brightness_command=0x81, + single_byte_bounds=True, + rotation=rotation, + # for sh1107 use column and page addressing. + # lower column command = 0x00 - 0x0F + # upper column command = 0x10 - 0x17 + # set page address = 0xB0 - 0xBF (16 pages) + SH1107_addressing=True, + ) + self._is_awake = True # Display starts in active state (_INIT_SEQUENCE) + + @property + def is_awake(self): + """ + The power state of the display. (read-only) + + `True` if the display is active, `False` if in sleep mode. + + :type: bool + """ + return self._is_awake + + def sleep(self): + """ + Put display into sleep mode. The display uses < 5uA in sleep mode + + Sleep mode does the following: + + 1) Stops the oscillator and DC-DC circuits + 2) Stops the OLED drive + 3) Remembers display data and operation mode active prior to sleeping + 4) The MP can access (update) the built-in display RAM + """ + if self._is_awake: + self.bus.send(int(0xAE), "") # 0xAE = display off, sleep mode + self._is_awake = False + + def wake(self): + """ + Wake display from sleep mode + """ + if not self._is_awake: + self.bus.send(int(0xAF), "") # 0xAF = display on + self._is_awake = True diff --git a/QtPy-RP2040-Tem-Hum/lib/adafruit_sht4x.py b/QtPy-RP2040-Tem-Hum/lib/adafruit_sht4x.py new file mode 100755 index 0000000..7451051 --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/lib/adafruit_sht4x.py @@ -0,0 +1,239 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 ladyada for Adafruit +# +# SPDX-License-Identifier: MIT +""" +`adafruit_sht4x` +================================================================================ + +Python library for Sensirion SHT4x temperature and humidity sensors + +* Author(s): ladyada + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit SHT40 Temperature & Humidity Sensor + `_ (Product ID: 4885) + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +* Adafruit's Bus Device library: + https://github.com/adafruit/Adafruit_CircuitPython_BusDevice + +""" + +import time +import struct +from adafruit_bus_device import i2c_device +from micropython import const + +__version__ = "1.0.7" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SHT4x.git" + + +_SHT4X_DEFAULT_ADDR = const(0x44) # SHT4X I2C Address +_SHT4X_READSERIAL = const(0x89) # Read Out of Serial Register +_SHT4X_SOFTRESET = const(0x94) # Soft Reset + + +class CV: + """struct helper""" + + @classmethod + def add_values(cls, value_tuples): + """Add CV values to the class""" + cls.string = {} + cls.delay = {} + + for value_tuple in value_tuples: + name, value, string, delay = value_tuple + setattr(cls, name, value) + cls.string[value] = string + cls.delay[value] = delay + + @classmethod + def is_valid(cls, value): + """Validate that a given value is a member""" + return value in cls.string + + +class Mode(CV): + """Options for ``power_mode``""" + + pass # pylint: disable=unnecessary-pass + + +Mode.add_values( + ( + ("NOHEAT_HIGHPRECISION", 0xFD, "No heater, high precision", 0.01), + ("NOHEAT_MEDPRECISION", 0xF6, "No heater, med precision", 0.005), + ("NOHEAT_LOWPRECISION", 0xE0, "No heater, low precision", 0.002), + ("HIGHHEAT_1S", 0x39, "High heat, 1 second", 1.1), + ("HIGHHEAT_100MS", 0x32, "High heat, 0.1 second", 0.11), + ("MEDHEAT_1S", 0x2F, "Med heat, 1 second", 1.1), + ("MEDHEAT_100MS", 0x24, "Med heat, 0.1 second", 0.11), + ("LOWHEAT_1S", 0x1E, "Low heat, 1 second", 1.1), + ("LOWHEAT_100MS", 0x15, "Low heat, 0.1 second", 0.11), + ) +) + + +class SHT4x: + """ + A driver for the SHT4x temperature and humidity sensor. + + :param ~busio.I2C i2c_bus: The I2C bus the SHT4x is connected to. + :param int address: The I2C device address. Default is :const:`0x44` + + + **Quickstart: Importing and using the SHT4x temperature and humidity sensor** + + Here is an example of using the :class:`SHT4x`. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + import adafruit_sht4x + + Once this is done you can define your `board.I2C` object and define your sensor object + + .. code-block:: python + + i2c = board.I2C() # uses board.SCL and board.SDA + sht = adafruit_sht4x.SHT4x(i2c) + + You can now make some initial settings on the sensor + + .. code-block:: python + + sht.mode = adafruit_sht4x.Mode.NOHEAT_HIGHPRECISION + + Now you have access to the temperature and humidity using the :attr:`measurements`. + It will return a tuple with the :attr:`temperature` and :attr:`relative_humidity` + measurements + + + .. code-block:: python + + temperature, relative_humidity = sht.measurements + + """ + + def __init__(self, i2c_bus, address=_SHT4X_DEFAULT_ADDR): + self.i2c_device = i2c_device.I2CDevice(i2c_bus, address) + self._buffer = bytearray(6) + self.reset() + self._mode = Mode.NOHEAT_HIGHPRECISION # pylint: disable=no-member + + @property + def serial_number(self): + """The unique 32-bit serial number""" + self._buffer[0] = _SHT4X_READSERIAL + with self.i2c_device as i2c: + i2c.write(self._buffer, end=1) + time.sleep(0.01) + i2c.readinto(self._buffer) + + ser1 = self._buffer[0:2] + ser1_crc = self._buffer[2] + ser2 = self._buffer[3:5] + ser2_crc = self._buffer[5] + + # check CRC of bytes + if ser1_crc != self._crc8(ser1) or ser2_crc != self._crc8(ser2): + raise RuntimeError("Invalid CRC calculated") + + serial = (ser1[0] << 24) + (ser1[1] << 16) + (ser2[0] << 8) + ser2[1] + return serial + + def reset(self): + """Perform a soft reset of the sensor, resetting all settings to their power-on defaults""" + self._buffer[0] = _SHT4X_SOFTRESET + with self.i2c_device as i2c: + i2c.write(self._buffer, end=1) + time.sleep(0.001) + + @property + def mode(self): + """The current sensor reading mode (heater and precision)""" + return self._mode + + @mode.setter + def mode(self, new_mode): + + if not Mode.is_valid(new_mode): + raise AttributeError("mode must be a Mode") + self._mode = new_mode + + @property + def relative_humidity(self): + """The current relative humidity in % rH. This is a value from 0-100%.""" + return self.measurements[1] + + @property + def temperature(self): + """The current temperature in degrees Celsius""" + return self.measurements[0] + + @property + def measurements(self): + """both `temperature` and `relative_humidity`, read simultaneously""" + + temperature = None + humidity = None + command = self._mode + + with self.i2c_device as i2c: + self._buffer[0] = command + i2c.write(self._buffer, end=1) + time.sleep(Mode.delay[self._mode]) + i2c.readinto(self._buffer) + + # separate the read data + temp_data = self._buffer[0:2] + temp_crc = self._buffer[2] + humidity_data = self._buffer[3:5] + humidity_crc = self._buffer[5] + + # check CRC of bytes + if temp_crc != self._crc8(temp_data) or humidity_crc != self._crc8( + humidity_data + ): + raise RuntimeError("Invalid CRC calculated") + + # decode data into human values: + # convert bytes into 16-bit signed integer + # convert the LSB value to a human value according to the datasheet + temperature = struct.unpack_from(">H", temp_data)[0] + temperature = -45.0 + 175.0 * temperature / 65535.0 + + # repeat above steps for humidity data + humidity = struct.unpack_from(">H", humidity_data)[0] + humidity = -6.0 + 125.0 * humidity / 65535.0 + humidity = max(min(humidity, 100), 0) + + return (temperature, humidity) + + ## CRC-8 formula from page 14 of SHTC3 datasheet + # https://media.digikey.com/pdf/Data%20Sheets/Sensirion%20PDFs/HT_DS_SHTC3_D1.pdf + # Test data [0xBE, 0xEF] should yield 0x92 + + @staticmethod + def _crc8(buffer): + """verify the crc8 checksum""" + crc = 0xFF + for byte in buffer: + crc ^= byte + for _ in range(8): + if crc & 0x80: + crc = (crc << 1) ^ 0x31 + else: + crc = crc << 1 + return crc & 0xFF # return the bottom 8 bits diff --git a/QtPy-RP2040-Tem-Hum/readme.md b/QtPy-RP2040-Tem-Hum/readme.md new file mode 100644 index 0000000..40411cf --- /dev/null +++ b/QtPy-RP2040-Tem-Hum/readme.md @@ -0,0 +1,2 @@ +# Temperature + Humidity +This is for the QtPy RP2040 from Adafruit with the SHT40 sensor attached as well as the SH1107 OLED display. \ No newline at end of file