diff --git a/adafruit_ht16k33/ht16k33.py b/adafruit_ht16k33/ht16k33.py index 83b4e8f..50fee2d 100644 --- a/adafruit_ht16k33/ht16k33.py +++ b/adafruit_ht16k33/ht16k33.py @@ -7,7 +7,7 @@ `adafruit_ht16k33.ht16k33` =========================== -* Authors: Radomir Dopieralski & Tony DiCola for Adafruit Industries +* Authors: Radomir Dopieralski, Tony DiCola, and Melissa LeBlanc-Williams for Adafruit Industries """ @@ -15,7 +15,7 @@ from micropython import const try: - from typing import Optional + from typing import Union, List, Tuple, Optional from busio import I2C except ImportError: pass @@ -43,25 +43,32 @@ class HT16K33: def __init__( self, i2c: I2C, - address: int = 0x70, + address: Union[int, Tuple, List] = 0x70, auto_write: bool = True, brightness: float = 1.0, ) -> None: - self.i2c_device = i2c_device.I2CDevice(i2c, address) + if isinstance(address, (tuple, list)): + self.i2c_device = [] + for addr in address: + self.i2c_device.append(i2c_device.I2CDevice(i2c, addr)) + else: + self.i2c_device = [i2c_device.I2CDevice(i2c, address)] self._temp = bytearray(1) - self._buffer = bytearray(17) + self._buffer_size = 17 + self._buffer = bytearray((self._buffer_size) * len(self.i2c_device)) self._auto_write = auto_write self.fill(0) - self._write_cmd(_HT16K33_OSCILATOR_ON) + for i, _ in enumerate(self.i2c_device): + self._write_cmd(_HT16K33_OSCILATOR_ON, i) self._blink_rate = None self._brightness = None self.blink_rate = 0 self.brightness = brightness - def _write_cmd(self, byte: bytearray) -> None: + def _write_cmd(self, byte: bytearray, i2c_index: int = 0) -> None: self._temp[0] = byte - with self.i2c_device: - self.i2c_device.write(self._temp) + with self.i2c_device[i2c_index]: + self.i2c_device[i2c_index].write(self._temp) @property def blink_rate(self) -> int: @@ -74,7 +81,10 @@ def blink_rate(self, rate: Optional[int] = None) -> None: raise ValueError("Blink rate must be an integer in the range: 0-3") rate = rate & 0x03 self._blink_rate = rate - self._write_cmd(_HT16K33_BLINK_CMD | _HT16K33_BLINK_DISPLAYON | rate << 1) + for index, _ in enumerate(self.i2c_device): + self._write_cmd( + _HT16K33_BLINK_CMD | _HT16K33_BLINK_DISPLAYON | rate << 1, index + ) @property def brightness(self) -> float: @@ -91,7 +101,8 @@ def brightness(self, brightness: float) -> None: self._brightness = brightness xbright = round(15 * brightness) xbright = xbright & 0x0F - self._write_cmd(_HT16K33_CMD_BRIGHTNESS | xbright) + for index, _ in enumerate(self.i2c_device): + self._write_cmd(_HT16K33_CMD_BRIGHTNESS | xbright, index) @property def auto_write(self) -> bool: @@ -107,10 +118,13 @@ def auto_write(self, auto_write: bool) -> None: def show(self) -> None: """Refresh the display and show the changes.""" - with self.i2c_device: - # Byte 0 is 0x00, address of LED data register. The remaining 16 - # bytes are the display register data to set. - self.i2c_device.write(self._buffer) + for index, i2c_dev in enumerate(self.i2c_device): + with i2c_dev: + # Byte 0 is 0x00, address of LED data register. The remaining 16 + # bytes are the display register data to set. + offset = index * self._buffer_size + buffer = self._buffer[offset : offset + self._buffer_size] + i2c_dev.write(buffer) def fill(self, color: bool) -> None: """Fill the whole display with the given color. @@ -119,13 +133,16 @@ def fill(self, color: bool) -> None: """ fill = 0xFF if color else 0x00 - for i in range(16): - self._buffer[i + 1] = fill + for device, _ in enumerate(self.i2c_device): + for i in range(self._buffer_size - 1): + self._buffer[device * self._buffer_size + i + 1] = fill if self._auto_write: self.show() def _pixel(self, x: int, y: int, color: Optional[bool] = None) -> Optional[bool]: - addr = 2 * y + x // 8 + offset = ((x // 16) + (y // 8)) * self._buffer_size + addr = 2 * (y % 8) + ((x % 16) // 8) + addr = (addr % 16) + offset mask = 1 << x % 8 if color is None: return bool(self._buffer[addr + 1] & mask) diff --git a/adafruit_ht16k33/matrix.py b/adafruit_ht16k33/matrix.py index a881bab..db8acca 100644 --- a/adafruit_ht16k33/matrix.py +++ b/adafruit_ht16k33/matrix.py @@ -11,7 +11,8 @@ from adafruit_ht16k33.ht16k33 import HT16K33 try: - from typing import Optional + from typing import Union, List, Tuple, Optional + from busio import I2C from PIL import Image except ImportError: pass @@ -169,6 +170,16 @@ class Matrix16x8(Matrix8x8): _columns = 16 + def __init__( + self, + i2c: I2C, + address: Union[int, Tuple, List] = 0x70, + auto_write: bool = True, + brightness: float = 1.0, + ) -> None: + super().__init__(i2c, address, auto_write, brightness) + self._columns *= len(self.i2c_device) + def pixel(self, x: int, y: int, color: Optional[bool] = None) -> Optional[bool]: """Get or set the color of a given pixel. @@ -179,13 +190,14 @@ def pixel(self, x: int, y: int, color: Optional[bool] = None) -> Optional[bool]: :rtype: bool """ - if not 0 <= x <= 15: + if not 0 <= x <= self._columns - 1: return None - if not 0 <= y <= 7: + if not 0 <= y <= self._rows - 1: return None - if x >= 8: + while x >= 8: x -= 8 y += 8 + return super()._pixel(y, x, color) # pylint: disable=arguments-out-of-order @@ -202,9 +214,9 @@ def pixel(self, x: int, y: int, color: Optional[bool] = None) -> Optional[bool]: :rtype: bool """ - if not 0 <= x <= 15: + if not 0 <= x <= self._columns - 1: return None - if not 0 <= y <= 7: + if not 0 <= y <= self._rows - 1: return None return super()._pixel(x, y, color) diff --git a/adafruit_ht16k33/segments.py b/adafruit_ht16k33/segments.py index 90766cc..5cb79dd 100755 --- a/adafruit_ht16k33/segments.py +++ b/adafruit_ht16k33/segments.py @@ -142,7 +142,31 @@ class Seg14x4(HT16K33): - """Alpha-numeric, 14-segment display.""" + """Alpha-Numeric 14-segment display. + + :param I2C i2c: The I2C bus object + :param 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 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, Tuple, List] = 0x70, + auto_write: bool = True, + chars_per_display: int = 4, + ): + 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 def print(self, value: Union[str, int, float], decimal: int = 0) -> None: """Print the value to the display. @@ -189,30 +213,38 @@ def scroll(self, count: int = 1) -> None: offset = 0 else: offset = 2 - for i in range(6): - self._set_buffer(i + offset, self._get_buffer(i + 2 * count)) + for i in range((self._chars - 1) * 2): + self._set_buffer( + self._adjusted_index(i + offset), + self._get_buffer(self._adjusted_index(i + 2 * count)), + ) def _put(self, char: str, index: int = 0) -> None: """Put a character at the specified place.""" - if not 0 <= index <= 3: + if not 0 <= index < self._chars: return if not 32 <= ord(char) <= 127: return if char == ".": self._set_buffer( - index * 2 + 1, self._get_buffer(index * 2 + 1) | 0b01000000 + 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(index * 2, CHARS[1 + character]) - self._set_buffer(index * 2 + 1, CHARS[character]) + 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(7) & 0b01000000: + if ( + char != "." + or self._get_buffer(self._char_buffer_index(self._chars - 1) + 1) + & 0b01000000 + ): self.scroll() - self._put(" ", 3) - self._put(char, 3) + self._put(" ", self._chars - 1) + self._put(char, self._chars - 1) def _text(self, text: str) -> None: """Display the specified text.""" @@ -237,7 +269,7 @@ def _number(self, number: Union[int, float], decimal: int = 0) -> str: stnum = str(number) dot = stnum.find(".") - if (len(stnum) > 5) or ((len(stnum) > 4) and (dot < 0)): + 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) @@ -251,7 +283,7 @@ def _number(self, number: Union[int, float], decimal: int = 0) -> str: if places <= 0 < decimal: self.fill(False) - places = 4 + places = self._chars if "." in stnum: places += 1 @@ -262,7 +294,7 @@ def _number(self, number: Union[int, float], decimal: int = 0) -> str: elif places > 0: txt = stnum[:places] - if len(txt) > 5: + if len(txt) > self._chars + 1: self._auto_write = auto_write raise ValueError("Output string ('{0}') is too long!".format(txt)) @@ -271,6 +303,21 @@ def _number(self, number: Union[int, float], decimal: int = 0) -> str: return txt + def _adjusted_index(self, index): + # 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): + return self._chars // len(self.i2c_device) + + def _bytes_per_buffer(self): + return self._bytes_per_char * self._chars_per_buffer() + + def _char_buffer_index(self, char_pos): + offset = (char_pos // self._chars_per_buffer()) * self._buffer_size + return offset + (char_pos % self._chars_per_buffer()) * self._bytes_per_char + def set_digit_raw(self, index: int, bitmask: Union[int, List, Tuple]) -> 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. @@ -279,8 +326,10 @@ def set_digit_raw(self, index: int, bitmask: Union[int, List, Tuple]) -> None: :param bitmask: A 2 byte number corresponding to the segments to set :type bitmask: int, or a list/tuple of bool """ - if not isinstance(index, int) or not 0 <= index <= 3: - raise ValueError("Index value must be an integer in the range: 0-3") + 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) @@ -289,8 +338,8 @@ def set_digit_raw(self, index: int, bitmask: Union[int, List, Tuple]) -> None: bitmask &= 0xFFFF # Set the digit bitmask value at the appropriate position. - self._set_buffer(index * 2, bitmask & 0xFF) - self._set_buffer(index * 2 + 1, (bitmask >> 8) & 0xFF) + 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() @@ -328,15 +377,22 @@ def _scroll_marquee(self, text: str, delay: float) -> None: class _AbstractSeg7x4(Seg14x4): POSITIONS = (0, 2, 6, 8) # The positions of characters. - def __init__( + def __init__( # pylint: disable=too-many-arguments self, i2c: I2C, - address: int = 0x70, + address: Union[int, Tuple, List] = 0x70, auto_write: bool = True, char_dict: Optional[Dict[str, int]] = None, + chars_per_display: int = 4, ) -> None: - super().__init__(i2c, address, auto_write) + super().__init__(i2c, address, auto_write, chars_per_display) self._chardict = char_dict + self._bytes_per_char = 1 + + def _adjusted_index(self, index): + # 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. @@ -348,9 +404,10 @@ def scroll(self, count: int = 1) -> None: offset = 0 else: offset = 1 - for i in range(3): + for i in range(self._chars - 1): self._set_buffer( - self.POSITIONS[i + offset], self._get_buffer(self.POSITIONS[i + count]) + self._adjusted_index(i + offset), + self._get_buffer(self._adjusted_index(i + count)), ) def _push(self, char: str) -> None: @@ -358,17 +415,20 @@ def _push(self, char: str) -> None: if char in ":;": self._put(char) else: - if char != "." or self._get_buffer(self.POSITIONS[3]) & 0b10000000: + if ( + char != "." + or self._get_buffer(self._adjusted_index(self._chars - 1)) & 0b10000000 + ): self.scroll() - self._put(" ", 3) - self._put(char, 3) + 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 <= 3: + if not 0 <= index < self._chars: return - index = self.POSITIONS[index] + index = self._adjusted_index(index) if self._chardict and char in self._chardict: self._set_buffer(index, self._chardict[char]) return @@ -406,15 +466,16 @@ def set_digit_raw(self, index: int, bitmask: int) -> None: 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 bitmask: A 2 byte number corresponding to the segments to set - :type bitmask: int, or a list/tuple of bool + :param int bitmask: A single byte number corresponding to the segments to set """ - if not isinstance(index, int) or not 0 <= index <= 3: - raise ValueError("Index value must be an integer in the range: 0-3") + 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.POSITIONS[index], bitmask & 0xFF) + self._set_buffer(self._adjusted_index(index), bitmask & 0xFF) if self._auto_write: self.show() @@ -425,19 +486,22 @@ class Seg7x4(_AbstractSeg7x4): supports displaying a limited set of characters. :param I2C i2c: The I2C bus object - :param int address: The I2C address for the display + :param 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 int chars_per_display: A number between 1-8 represesenting the number of characters + on each display. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, i2c: I2C, - address: int = 0x70, + address: Union[int, Tuple, List] = 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) + 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) diff --git a/examples/ht16k33_matrix_multi_display.py b/examples/ht16k33_matrix_multi_display.py new file mode 100644 index 0000000..c4f77e7 --- /dev/null +++ b/examples/ht16k33_matrix_multi_display.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Basic example of clearing and drawing pixels on two LED matrix displays. +# This example and library is meant to work with Adafruit CircuitPython API. +# Author: Melissa LeBlanc-Williams +# License: Public Domain + +# Import all board pins. +import time +import board +import busio + +# Import the HT16K33 LED matrix module. +from adafruit_ht16k33 import matrix + + +# Create the I2C interface. +i2c = busio.I2C(board.SCL, board.SDA) + +# Create the matrix class. +# This creates a 16x8 matrix with multiple displays: +matrix = matrix.Matrix16x8(i2c, address=(0x70, 0x71)) + +# Clear the matrix. +matrix.fill(0) + +# Set a pixel in the origin 0, 0 position. +matrix[0, 0] = 1 +# Set a pixel in the middle 8, 4 position. +matrix[8, 4] = 1 +# Set a pixel in the opposite 15, 7 position. +matrix[15, 7] = 1 + +# Set pixels in the second display. +matrix[16, 7] = 1 +matrix[24, 4] = 1 +matrix[31, 0] = 1 + +time.sleep(2) + +# Draw a Smiley Face +matrix.fill(0) + +for row in range(2, 6): + matrix[row, 0] = 1 + matrix[row, 7] = 1 + +for column in range(2, 6): + matrix[0, column] = 1 + matrix[7, column] = 1 + +matrix[1, 1] = 1 +matrix[1, 6] = 1 +matrix[6, 1] = 1 +matrix[6, 6] = 1 +matrix[2, 5] = 1 +matrix[5, 5] = 1 +matrix[2, 3] = 1 +matrix[5, 3] = 1 +matrix[3, 2] = 1 +matrix[4, 2] = 1 + +# Move the Smiley Face Around +while True: + for frame in range(0, 24): + matrix.shift_right(True) + time.sleep(0.05) + for frame in range(0, 8): + matrix.shift_down(True) + time.sleep(0.05) + for frame in range(0, 24): + matrix.shift_left(True) + time.sleep(0.05) + for frame in range(0, 8): + matrix.shift_up(True) + time.sleep(0.05) diff --git a/examples/ht16k33_segments_multi_display.py b/examples/ht16k33_segments_multi_display.py new file mode 100644 index 0000000..e9058dd --- /dev/null +++ b/examples/ht16k33_segments_multi_display.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Basic example of setting digits on two LED segment displays. +# This example and library is meant to work with Adafruit CircuitPython API. +# Author: Melissa LeBlanc-Williams +# License: Public Domain + +import time + +# Import all board pins. +import board +import busio + +# Import the HT16K33 LED segment module. +from adafruit_ht16k33 import segments + +# Create the I2C interface. +i2c = busio.I2C(board.SCL, board.SDA) + +# Create the LED segment class. +# This creates a 7 segment 4 character display: +display = segments.Seg7x4(i2c, address=(0x70, 0x71)) +# Or this creates a 14 segment alphanumeric 4 character display: +# display = segments.Seg14x4(i2c, address=(0x70, 0x71)) + +# Clear the display. +display.fill(0) + +# Can just print a number +display.print(12345678) + +time.sleep(2) + +# Or, can print a hexadecimal value +display.fill(0) +display.print_hex(0xFF23) +time.sleep(2) + +# Or, print the time +display.fill(0) +display.print("12:30") +time.sleep(2) + +display.colon = False + +display.fill(0) +# Or, can set indivdual digits / characters +# Set the first character to '1': +display[0] = "1" +# Set the second character to '2': +display[1] = "2" +# Set the third character to 'A': +display[2] = "A" +# Set the forth character to 'B': +display[3] = "B" +display[4] = "C" +display[5] = "D" +display[6] = "E" +display[7] = "F" +time.sleep(2) + +# Or, can even set the segments to make up characters +if isinstance(display, segments.Seg7x4): + # 7-segment raw digits + display.set_digit_raw(0, 0xFF) + display.set_digit_raw(1, 0b11111111) + display.set_digit_raw(2, 0x79) + display.set_digit_raw(3, 0b01111001) +else: + # 14-segment raw digits + display.set_digit_raw(0, 0x2D3F) + display.set_digit_raw(1, 0b0010110100111111) + display.set_digit_raw(2, (0b00101101, 0b00111111)) + display.set_digit_raw(3, [0x2D, 0x3F]) +time.sleep(2) + +# Show a looping marquee +display.marquee("Deadbeef 192.168.100.102... ", 0.2)