First push of QtPy RP2040

This commit is contained in:
Axel Rafn
2022-03-06 17:23:50 +00:00
parent e2b38c0ff4
commit 696744bde7
9 changed files with 1932 additions and 0 deletions

BIN
QtPy-RP2040-Tem-Hum/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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)

View File

@ -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"))

View File

@ -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

View File

@ -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"

View File

@ -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 <https://www.adafruit.com/product/4650>`_
**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 <https://www.adafruit.com/product/4650>`_.
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 <https://www.adafruit.com/product/5297>`_.
.. 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 <https://shop.pimoroni.com/products/1-12-oled-breakout>`_
.. 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

View File

@ -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
<https://www.adafruit.com/product/4885>`_ (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

View File

@ -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.