# SPDX-FileCopyrightText: Radomir Dopieralski 2016 for Adafruit Industries
# SPDX-FileCopyrightText: Tony DiCola 2016 for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
adafruit_ht16k33.segments
=========================
"""
import time
from adafruit_ht16k33.ht16k33 import HT16K33
try:
from typing import Union, List, Tuple, Optional, Dict
from busio import I2C
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HT16K33.git"
# fmt: off
CHARS = (
0b00000000, 0b00000000, #
0b01000000, 0b00000110, # !
0b00000010, 0b00100000, # "
0b00010010, 0b11001110, # #
0b00010010, 0b11101101, # $
0b00001100, 0b00100100, # %
0b00100011, 0b01011101, # &
0b00000100, 0b00000000, # '
0b00100100, 0b00000000, # (
0b00001001, 0b00000000, # )
0b00111111, 0b11000000, # *
0b00010010, 0b11000000, # +
0b00001000, 0b00000000, # ,
0b00000000, 0b11000000, # -
0b00000000, 0b00000000, # .
0b00001100, 0b00000000, # /
0b00001100, 0b00111111, # 0
0b00000000, 0b00000110, # 1
0b00000000, 0b11011011, # 2
0b00000000, 0b10001111, # 3
0b00000000, 0b11100110, # 4
0b00100000, 0b01101001, # 5
0b00000000, 0b11111101, # 6
0b00000000, 0b00000111, # 7
0b00000000, 0b11111111, # 8
0b00000000, 0b11101111, # 9
0b00010010, 0b00000000, # :
0b00001010, 0b00000000, # ;
0b00100100, 0b01000000, # <
0b00000000, 0b11001000, # =
0b00001001, 0b10000000, # >
0b01100000, 0b10100011, # ?
0b00000010, 0b10111011, # @
0b00000000, 0b11110111, # A
0b00010010, 0b10001111, # B
0b00000000, 0b00111001, # C
0b00010010, 0b00001111, # D
0b00000000, 0b11111001, # E
0b00000000, 0b01110001, # F
0b00000000, 0b10111101, # G
0b00000000, 0b11110110, # H
0b00010010, 0b00000000, # I
0b00000000, 0b00011110, # J
0b00100100, 0b01110000, # K
0b00000000, 0b00111000, # L
0b00000101, 0b00110110, # M
0b00100001, 0b00110110, # N
0b00000000, 0b00111111, # O
0b00000000, 0b11110011, # P
0b00100000, 0b00111111, # Q
0b00100000, 0b11110011, # R
0b00000000, 0b11101101, # S
0b00010010, 0b00000001, # T
0b00000000, 0b00111110, # U
0b00001100, 0b00110000, # V
0b00101000, 0b00110110, # W
0b00101101, 0b00000000, # X
0b00010101, 0b00000000, # Y
0b00001100, 0b00001001, # Z
0b00000000, 0b00111001, # [
0b00100001, 0b00000000, # \
0b00000000, 0b00001111, # ]
0b00001100, 0b00000011, # ^
0b00000000, 0b00001000, # _
0b00000001, 0b00000000, # `
0b00010000, 0b01011000, # a
0b00100000, 0b01111000, # b
0b00000000, 0b11011000, # c
0b00001000, 0b10001110, # d
0b00001000, 0b01011000, # e
0b00000000, 0b01110001, # f
0b00000100, 0b10001110, # g
0b00010000, 0b01110000, # h
0b00010000, 0b00000000, # i
0b00000000, 0b00001110, # j
0b00110110, 0b00000000, # k
0b00000000, 0b00110000, # l
0b00010000, 0b11010100, # m
0b00010000, 0b01010000, # n
0b00000000, 0b11011100, # o
0b00000001, 0b01110000, # p
0b00000100, 0b10000110, # q
0b00000000, 0b01010000, # r
0b00100000, 0b10001000, # s
0b00000000, 0b01111000, # t
0b00000000, 0b00011100, # u
0b00100000, 0b00000100, # v
0b00101000, 0b00010100, # w
0b00101000, 0b11000000, # x
0b00100000, 0b00001100, # y
0b00001000, 0b01001000, # z
0b00001001, 0b01001001, # {
0b00010010, 0b00000000, # |
0b00100100, 0b10001001, # }
0b00000101, 0b00100000, # ~
0b00111111, 0b11111111,
)
# fmt: on
NUMBERS = (
0x3F, # 0
0x06, # 1
0x5B, # 2
0x4F, # 3
0x66, # 4
0x6D, # 5
0x7D, # 6
0x07, # 7
0x7F, # 8
0x6F, # 9
0x77, # a
0x7C, # b
0x39, # C
0x5E, # d
0x79, # E
0x71, # F
0x3D, # G
0x76, # H
0x30, # I
0x1E, # J
0x40, # -
0x38, # L
0x40, # -
0x54, # n
0x5C, # o
0x73, # P
0x67, # q
0x50, # R
0x6D, # S
0x78, # t
0x3E, # U
0x1C, # v
0x40, # -
0x40, # -
0x6E, # y
0x40, # -
0x40, # -
)
[docs]
class Seg14x4(HT16K33):
"""Alpha-Numeric 14-segment display.
:param I2C i2c: The I2C bus object
:param int|list|tuple address: The I2C address(es) for the display. Can be a tuple or
list for multiple displays.
:param bool auto_write: True if the display should immediately change when set. If False,
`show` must be called explicitly.
:param int chars_per_display: A number between 1-8 represesenting the number of characters
on each display.
"""
def __init__(
self,
i2c: I2C,
address: Union[int, List[int], Tuple[int, ...]] = 0x70,
auto_write: bool = True,
chars_per_display: int = 4,
) -> None:
super().__init__(i2c, address, auto_write)
if not 1 <= chars_per_display <= 8:
raise ValueError(
"Input overflow - The HT16K33 only supports up 1-8 characters!"
)
self._chars = chars_per_display * len(self.i2c_device)
self._bytes_per_char = 2
self._last_nb_scroll_time = -1
self._nb_scroll_text = None
self._nb_scroll_index = -1
self._nb_prev_char_is_dot = False
[docs]
def print(self, value: Union[str, float], decimal: int = 0) -> None:
"""Print the value to the display.
:param str|float value: The value to print
:param int decimal: The number of decimal places for a floating point
number if decimal is greater than zero, or the input number is an
integer if decimal is zero.
"""
if isinstance(value, str):
self._text(value)
elif isinstance(value, (int, float)):
self._number(value, decimal)
else:
raise ValueError("Unsupported display value type: {}".format(type(value)))
if self._auto_write:
self.show()
[docs]
def print_hex(self, value: Union[int, str]) -> None:
"""Print the value as a hexidecimal string to the display.
:param int|str value: The number to print
"""
if isinstance(value, int):
self.print("{0:X}".format(value))
else:
self.print(value)
def __setitem__(self, key: int, value: str) -> None:
self._put(value, key)
if self._auto_write:
self.show()
def _put(self, char: str, index: int = 0) -> None:
"""Put a character at the specified place."""
if not 0 <= index < self._chars:
return
if not 32 <= ord(char) <= 127:
return
if char == ".":
self._set_buffer(
self._adjusted_index(index * 2 + 1),
self._get_buffer(self._adjusted_index(index * 2 + 1)) | 0b01000000,
)
return
character = ord(char) * 2 - 64
self._set_buffer(self._adjusted_index(index * 2), CHARS[1 + character])
self._set_buffer(self._adjusted_index(index * 2 + 1), CHARS[character])
def _push(self, char: str) -> None:
"""Scroll the display and add a character at the end."""
if (
char != "."
or self._get_buffer(self._char_buffer_index(self._chars - 1) + 1)
& 0b01000000
):
self.scroll()
self._put(" ", self._chars - 1)
self._put(char, self._chars - 1)
def _text(self, text: str) -> None:
"""Display the specified text."""
for character in text:
self._push(character)
def _number(self, number: float, decimal: int = 0) -> str:
"""
Display a floating point or integer number on the Adafruit HT16K33 based displays
:param float number: The floating point or integer number to be displayed, which must be
in the range 0 (zero) to 9999 for integers and floating point or integer numbers
and between 0.0 and 999.0 or 99.00 or 9.000 for floating point numbers.
:param int decimal: The number of decimal places for a floating point number if decimal
is greater than zero, or the input number is an integer if decimal is zero.
:return: The output text string to be displayed
"""
auto_write = self._auto_write
self._auto_write = False
stnum = str(number)
dot = stnum.find(".")
if (len(stnum) > self._chars + 1) or ((len(stnum) > self._chars) and (dot < 0)):
self._auto_write = auto_write
raise ValueError(
"Input overflow - {0} is too large for the display!".format(number)
)
if dot < 0:
# No decimal point (Integer)
places = len(stnum)
else:
places = len(stnum[:dot])
if places <= 0 < decimal:
self.fill(False)
places = self._chars
if "." in stnum:
places += 1
# Set decimal places, if number of decimal places is specified (decimal > 0)
txt = stnum
if places > 0 < decimal < len(stnum[places:]) and dot > 0:
txt = stnum[: dot + decimal + 1]
elif places > 0:
txt = stnum[:places]
if len(txt) > self._chars + 1:
self._auto_write = auto_write
raise ValueError("Output string ('{0}') is too long!".format(txt))
self._text(txt)
self._auto_write = auto_write
return txt
def _adjusted_index(self, index: int) -> int:
# Determine which part of the buffer to use and adjust index
offset = (index // self._bytes_per_buffer()) * self._buffer_size
return offset + index % self._bytes_per_buffer()
def _chars_per_buffer(self) -> int:
return self._chars // len(self.i2c_device)
def _bytes_per_buffer(self) -> int:
return self._bytes_per_char * self._chars_per_buffer()
def _char_buffer_index(self, char_pos: int) -> int:
offset = (char_pos // self._chars_per_buffer()) * self._buffer_size
return offset + (char_pos % self._chars_per_buffer()) * self._bytes_per_char
[docs]
def set_digit_raw(
self, index: int, bitmask: Union[int, List[int], Tuple[int, int]]
) -> None:
"""Set digit at position to raw bitmask value. Position should be a value
of 0 to 3 with 0 being the left most character on the display.
:param int index: The index of the display to set
:param bitmask: A 2 byte number corresponding to the segments to set
:type bitmask: int, or a list/tuple of int
"""
if not isinstance(index, int) or not 0 <= index <= self._chars - 1:
raise ValueError(
f"Index value must be an integer in the range: 0-{self._chars - 1}"
)
if isinstance(bitmask, (tuple, list)):
bitmask = ((bitmask[0] & 0xFF) << 8) | (bitmask[1] & 0xFF)
# Use only the valid potion of bitmask
bitmask &= 0xFFFF
# Set the digit bitmask value at the appropriate position.
self._set_buffer(self._adjusted_index(index * 2), bitmask & 0xFF)
self._set_buffer(self._adjusted_index(index * 2 + 1), (bitmask >> 8) & 0xFF)
if self._auto_write:
self.show()
[docs]
def non_blocking_marquee(
self,
text: str,
delay: float = 0.25,
loop: bool = True,
space_between: bool = False,
) -> bool:
"""
Scroll the text at the specified delay between characters. Must be called
repeatedly from main loop faster than delay time.
:param str text: The text to display
:param float delay: (optional) The delay in seconds to pause before scrolling
to the next character (default=0.25)
:param bool loop: (optional) Whether to endlessly loop the text (default=True)
:param bool space_between: (optional) Whether to seperate the end and beginning of
the text with a space. (default=False)
"""
# pylint: disable=too-many-nested-blocks
if isinstance(text, str):
now = time.monotonic()
# if text is the same
if text == self._nb_scroll_text:
# if we delayed long enough, and it's time to scroll
if now >= self._last_nb_scroll_time + delay:
# if there are chars left in the text
if self._nb_scroll_index + 1 < len(text):
self._nb_scroll_index += 1
_character = text[self._nb_scroll_index]
if _character != "." or self._nb_prev_char_is_dot:
self._last_nb_scroll_time = now
self.print(text[self._nb_scroll_index])
self._nb_prev_char_is_dot = text[self._nb_scroll_index] == "."
else:
self._nb_scroll_index = -1
if loop:
if space_between:
self._last_nb_scroll_time = now
self.print(" ")
else:
return True
else:
# different text
self._nb_scroll_index = 0
self.fill(False)
self._nb_scroll_text = text
self._last_nb_scroll_time = now
self.print(text[0])
return False
[docs]
def marquee(
self, text: str, delay: float = 0.25, loop: bool = True, space_between=False
) -> None:
"""
Automatically scroll the text at the specified delay between characters
:param str text: The text to display
:param float delay: (optional) The delay in seconds to pause before scrolling
to the next character (default=0.25)
:param bool loop: (optional) Whether to endlessly loop the text (default=True)
:param bool space_between: (optional) Whether to seperate the end and beginning of
the text with a space. (default=False)
"""
if isinstance(text, str):
self.fill(False)
while True:
if self.non_blocking_marquee(
text=text, delay=delay, loop=loop, space_between=space_between
):
return
class _AbstractSeg7x4(Seg14x4):
POSITIONS = (0, 2, 6, 8) # The positions of characters.
def __init__( # pylint: disable=too-many-arguments
self,
i2c: I2C,
address: Union[int, List[int], Tuple[int, ...]] = 0x70,
auto_write: bool = True,
char_dict: Optional[Dict[str, int]] = None,
chars_per_display: int = 4,
) -> None:
super().__init__(i2c, address, auto_write, chars_per_display)
self._chardict = char_dict
self._bytes_per_char = 1
def _adjusted_index(self, index: int) -> int:
# Determine which part of the buffer to use and adjust index
offset = (index // self._bytes_per_buffer()) * self._buffer_size
return offset + self.POSITIONS[index % self._bytes_per_buffer()]
def scroll(self, count: int = 1) -> None:
"""Scroll the display by specified number of places.
:param int count: The number of places to scroll
"""
if count >= 0:
offset = 0
else:
offset = 1
for i in range(self._chars - 1):
self._set_buffer(
self._adjusted_index(i + offset),
self._get_buffer(self._adjusted_index(i + count)),
)
def _push(self, char: str) -> None:
"""Scroll the display and add a character at the end."""
if char in ":;":
self._put(char)
else:
if (
char != "."
or self._get_buffer(self._adjusted_index(self._chars - 1)) & 0b10000000
):
self.scroll()
self._put(" ", self._chars - 1)
self._put(char, self._chars - 1)
def _put(self, char: str, index: int = 0) -> None:
"""Put a character at the specified place."""
# pylint: disable=too-many-return-statements
if not 0 <= index < self._chars:
return
index = self._adjusted_index(index)
if self._chardict and char in self._chardict:
self._set_buffer(index, self._chardict[char])
return
char = char.lower()
if char == ".":
self._set_buffer(index, self._get_buffer(index) | 0b10000000)
return
if char in "abcdefghijklmnopqrstuvwxy":
character = ord(char) - 97 + 10
elif char == "-":
character = 36
elif char in "0123456789":
character = ord(char) - 48
elif char == " ":
self._set_buffer(index, 0x00)
return
elif char == ":":
self._set_buffer(4, 0x02)
return
elif char == ";":
self._set_buffer(4, 0x00)
return
elif char in "lL":
self._set_buffer(index, 0b00111000)
return
elif char in "oO":
self._set_buffer(index, 0b00111111)
return
else:
return
self._set_buffer(index, NUMBERS[character])
def set_digit_raw(self, index: int, bitmask: int) -> None:
"""Set digit at position to raw bitmask value. Position should be a value
of 0 to 3 with 0 being the left most digit on the display.
:param int index: The index of the display to set
:param int bitmask: A single byte number corresponding to the segments to set
"""
if not isinstance(index, int) or not 0 <= index < self._chars:
raise ValueError(
f"Index value must be an integer in the range: 0-{self._chars - 1}"
)
# Set the digit bitmask value at the appropriate position.
self._set_buffer(self._adjusted_index(index), bitmask & 0xFF)
if self._auto_write:
self.show()
[docs]
class Seg7x4(_AbstractSeg7x4):
"""Numeric 7-segment display. It has the same methods as the alphanumeric display, but only
supports displaying a limited set of characters.
:param I2C i2c: The I2C bus object
:param int|list|tuple address: The I2C address for the display. Can be a tuple or list for
multiple displays.
:param bool auto_write: True if the display should immediately change when set. If False,
`show` must be called explicitly.
:param dict char_dict: An optional dictionary mapping strings to bit settings integers used
for defining how to display custom letters
:param int chars_per_display: A number between 1-8 represesenting the number of characters
on each display.
"""
def __init__( # pylint: disable=too-many-arguments
self,
i2c: I2C,
address: Union[int, List[int], Tuple[int, ...]] = 0x70,
auto_write: bool = True,
char_dict: Optional[Dict[str, int]] = None,
chars_per_display: int = 4,
) -> None:
super().__init__(i2c, address, auto_write, char_dict, chars_per_display)
# Use colon for controling two-dots indicator at the center (index 0)
self._colon = Colon(self)
@property
def colon(self) -> bool:
"""Simplified colon accessor"""
return self._colon[0]
@colon.setter
def colon(self, turn_on: bool) -> None:
self._colon[0] = turn_on
[docs]
class BigSeg7x4(_AbstractSeg7x4):
"""Numeric 7-segment display. It has the same methods as the alphanumeric display, but only
supports displaying a limited set of characters.
:param I2C i2c: The I2C bus object
:param int|list|tuple address: The I2C address(es) for the display
:param bool auto_write: True if the display should immediately change when set. If False,
`show` must be called explicitly.
:param dict char_dict: An optional dictionary mapping strings to bit settings integers used
for defining how to display custom letters
"""
def __init__(
self,
i2c: I2C,
address: Union[int, List[int], Tuple[int, ...]] = 0x70,
auto_write: bool = True,
char_dict: Optional[Dict[str, int]] = None,
) -> None:
super().__init__(i2c, address, auto_write, char_dict)
# Use colon for controling two-dots indicator at the center (index 0)
# or the two-dots indicators at the left (index 1)
self.colons = Colon(self, 2)
def _setindicator(self, index: int, value: bool) -> None:
"""Set side LEDs (dots)
Index is as follow :
* 0 : two dots at the center
* 1 : top-left dot
* 2 : bottom-left dot
* 3 : right dot (also ampm indicator)
"""
bitmask = 1 << (index + 1)
current = self._get_buffer(0x04)
if value:
self._set_buffer(0x04, current | bitmask)
else:
self._set_buffer(0x04, current & ~bitmask)
if self._auto_write:
self.show()
def _getindicator(self, index: int) -> int:
"""Get side LEDs (dots)
See setindicator() for indexes
"""
bitmask = 1 << (index + 1)
return self._get_buffer(0x04) & bitmask
@property
def top_left_dot(self) -> bool:
"""The top-left dot indicator."""
return bool(self._getindicator(1))
@top_left_dot.setter
def top_left_dot(self, value: bool) -> None:
self._setindicator(1, value)
@property
def bottom_left_dot(self) -> bool:
"""The bottom-left dot indicator."""
return bool(self._getindicator(2))
@bottom_left_dot.setter
def bottom_left_dot(self, value: bool) -> None:
self._setindicator(2, value)
@property
def ampm(self) -> bool:
"""The AM/PM indicator."""
return bool(self._getindicator(3))
@ampm.setter
def ampm(self, value: bool) -> None:
self._setindicator(3, value)
[docs]
class Colon:
"""Helper class for controlling the colons. Not intended for direct use."""
# pylint: disable=protected-access
MASKS = (0x02, 0x0C)
def __init__(self, disp: _AbstractSeg7x4, num_of_colons: int = 1) -> None:
self._disp = disp
self._num_of_colons = num_of_colons
def __setitem__(self, key: int, value: bool) -> None:
if key > self._num_of_colons - 1:
raise ValueError("Trying to set a non-existent colon.")
current = self._disp._get_buffer(0x04)
if value:
self._disp._set_buffer(0x04, current | self.MASKS[key])
else:
self._disp._set_buffer(0x04, current & ~self.MASKS[key])
if self._disp.auto_write:
self._disp.show()
def __getitem__(self, key: int) -> bool:
if key > self._num_of_colons - 1:
raise ValueError("Trying to access a non-existent colon.")
return bool(self._disp._get_buffer(0x04) & self.MASKS[key])