diff --git a/README.rst b/README.rst index 1fe13d8..75aefda 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ -Micropython Library for the HT16K33-based LED Matrices +Micropython Library for the HT16K33-based LED Matrices, ported from Adafruit's +Adafruit_CircuitPython_HT16K33 library. This is a library for using the I²C-based LED matrices with the HT16K33 chip. -It supports both 16x8 and 8x8 matrices, as well as 7-Segment Numeric and 14-Segment Alphanumeric displays. +It supports Adafruit's 16x8 and 8x8 matrices, as well as 7-Segment Numeric and +14-Segment Alphanumeric displays. Documentation at http://micropython-ht16k33.readthedocs.io/ diff --git a/examples/ht16k33_segments_simpletest.py b/examples/ht16k33_segments_simpletest.py new file mode 100644 index 0000000..76af28c --- /dev/null +++ b/examples/ht16k33_segments_simpletest.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Basic example of setting digits on a LED segment display. +# This example and library is meant to work with Adafruit CircuitPython API. +# Author: Tony DiCola +# License: Public Domain +from micropython import const +from utime import sleep + +# Import all board pins. +from machine import SoftI2C, Pin +# Import the HT16K33 LED segment module. +from ht16k33 import segments +# Import special stuff for tinyPico +from tinypico import I2C_SDA, I2C_SCL + +LED_YELLOW = const(4) +LED_GREEN = const(5) +LED_BLINK_RATE_SEC = 0.2 +LED_NR_CYCLES = 1 + +TP_SDA = Pin(I2C_SDA) +TP_SCL = Pin(I2C_SCL) + +DELAY_BETWEEN_SEC = 4 + +led_yellow = Pin(LED_YELLOW, Pin.OUT) +led_yellow.value(False) + +led_green = Pin(LED_GREEN, Pin.OUT) +led_green.value(False) + +def blink_led(led, cycles=LED_NR_CYCLES, rate_ms=LED_BLINK_RATE_SEC): + for cyc in range(cycles): + led.value(True) + sleep(rate_ms) + led.value(False) + sleep(rate_ms) + +blink_led(led_yellow, 2) +blink_led(led_green, 2) + +# Create the I2C interface. +i2c = SoftI2C(sda=TP_SDA, scl=TP_SCL, freq=400000) + +# Create the LED segment class. +# This creates a 7 segment 4 character display: +display = segments.Seg14x4(i2c) +# Or this creates a 14 segment alphanumeric 4 character display: +# display = segments.Seg14x4(i2c) +# Or this creates a big 7 segment 4 character display +# display = segments.BigSeg7x4(i2c) +# Finally you can optionally specify a custom I2C address of the HT16k33 like: +# display = segments.Seg7x4(i2c, address=0x70) +print() +print("Before Starting Up") +print() +print("Sleeping for 5 seconds...") +sleep(5.0) + +try: + print() + print("4 Digit, 14 Segment Alphanumeric Demo Starting Up - Ctrl/C to Exit") + print() + # Clear the display. + # display.fill(0) + sleep(DELAY_BETWEEN_SEC) + + # Can just print an integer number + int_number = 6358 + print("Printing an integer number {0}".format(int_number)) + display.print(int_number) + sleep(DELAY_BETWEEN_SEC) + display.fill(0) + + # Can just print a floating point number + float_number = 714.4 + print("Printing a floating point number {0}".format(float_number)) + display.print(float_number, 1) + sleep(DELAY_BETWEEN_SEC) + display.fill(0) + + # Or, can print a hexadecimal value + hex_number = 0xBEAD + print("Printing a hexadecimal number {0}".format(hex(hex_number))) + display.print_hex(hex_number) + sleep(DELAY_BETWEEN_SEC) + display.fill(0) + + # Or, print the time + time_string = "11:50" + print("Printing a time string - '{0}'".format(time_string)) + display.colon = True + display.print(time_string) + sleep(DELAY_BETWEEN_SEC) + 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" + sleep(DELAY_BETWEEN_SEC) + display.fill(0) + + # 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]) + + sleep(DELAY_BETWEEN_SEC) + display.fill(0) + + # Show a looping marquee + display.marquee("Deadbeef 192.168.100.102... ", 0.2) +except KeyboardInterrupt: + # display.fill(0) + led_green.value(False) + led_yellow.value(False) diff --git a/ht16k33_matrix.py b/ht16k33_matrix.py deleted file mode 100644 index 7eeb8f6..0000000 --- a/ht16k33_matrix.py +++ /dev/null @@ -1,104 +0,0 @@ -from micropython import const -import framebuf - -_HT16K33_BLINK_CMD = const(0x80) -_HT16K33_BLINK_DISPLAYON = const(0x01) -_HT16K33_CMD_BRIGHTNESS = const(0xE0) -_HT16K33_OSCILATOR_ON = const(0x21) - -class HT16K33: - def __init__(self, i2c, address=0x70): - self.i2c = i2c - self.address = address - self._temp = bytearray(1) - - self.buffer = bytearray(16) - self._write_cmd(_HT16K33_OSCILATOR_ON) - self.blink_rate(0) - self.brightness(15) - - def _write_cmd(self, byte): - self._temp[0] = byte - self.i2c.writeto(self.address, self._temp) - - def blink_rate(self, rate=None): - if rate is None: - return self._blink_rate - rate = rate & 0x03 - self._blink_rate = rate - self._write_cmd(_HT16K33_BLINK_CMD | - _HT16K33_BLINK_DISPLAYON | rate << 1) - - def brightness(self, brightness): - if brightness is None: - return self._brightness - brightness = brightness & 0x0F - self._brightness = brightness - self._write_cmd(_HT16K33_CMD_BRIGHTNESS | brightness) - - def show(self): - self.i2c.writeto_mem(self.address, 0x00, self.buffer) - - def fill(self, color): - fill = 0xff if color else 0x00 - for i in range(16): - self.buffer[i] = fill - -class HT16K33Matrix(HT16K33): - def __init__(self, i2c, address=0x70): - super().__init__(i2c, address) - - self._fb_buffer = bytearray(self.WIDTH * self.HEIGHT - * self.FB_BPP // 8) - self.framebuffer = framebuf.FrameBuffer( - self._fb_buffer, self.WIDTH, self.HEIGHT, self.FORMAT) - - self.framebuffer.fill(0) - self.pixel = self.framebuffer.pixel - self.fill = self.framebuffer.fill - - def show(self): - self._copy_buf() - super().show() - -class Matrix16x8(HT16K33Matrix): - WIDTH = 16 - HEIGHT = 8 - FORMAT = framebuf.MONO_HLSB - FB_BPP = 1 - - def _copy_buf(self): - for y in range(8): - self.buffer[y * 2] = self._fb_buffer[y] - -class Matrix8x8(HT16K33Matrix): - WIDTH = 8 - HEIGHT = 8 - FORMAT = framebuf.MONO_HLSB - FB_BPP = 1 - - def _copy_buf(self): - for y in range(8): - b = self._fb_buffer[y] - self.buffer[y * 2] = (b >> 1) | (b << 7) - - -class Matrix8x8x2(HT16K33Matrix): - WIDTH = 8 - HEIGHT = 8 - FORMAT = framebuf.GS4_HMSB - FB_BPP = 4 - - def _copy_buf(self): - pixel = self.framebuffer.pixel - _buffer = self.buffer - for y in range(8): - b = 0 - for x in range(8): - color = pixel(x, y) - if color & 0x01: - b |= 0x01 << x - if color & 0x02: - b |= 0x01 << (x + 8) - _buffer[y * 2] = b & 0xff - _buffer[y * 2 + 1] = (b >> 8) & 0xff diff --git a/ht16k33_seg.py b/ht16k33_seg.py deleted file mode 100644 index 36e5871..0000000 --- a/ht16k33_seg.py +++ /dev/null @@ -1,258 +0,0 @@ -from ht16k33.ht16k33_matrix import HT16K33 - - -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, -) -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 - 0x40, # - -) - - -class Seg14x4(HT16K33): - # If True, debugging code will be executed - debug = False - - def scroll(self, count=1): - if count >= 0: - offset = 0 - else: - offset = 2 - for i in range(6): - self.buffer[i + offset] = self.buffer[i + 2 * count] - - def put(self, char, index=0): - if not 0 <= index <= 3: - return - if not 32 <= ord(char) <= 127: - return - if char == '.': - self.buffer[index * 2 + 1] |= 0b01000000 - return - c = ord(char) * 2 - 64 - self.buffer[index * 2] = CHARS[1 + c] - self.buffer[index * 2 + 1] = CHARS[c] - - def push(self, char): - if char != '.' or self.buffer[7] & 0b01000000: - self.scroll() - self.put(' ', 3) - self.put(char, 3) - - def text(self, text): - for c in text: - self.push(c) - - ''' - Display a floating point or integer number on the Adafruit HT16K33 based displays - - Param: 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: 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. - - Returns: The output text string to be displayed. - ''' - def number(self, number, decimal = 0): - s = "{:f}".format(number) - places = 0 - - if self.debug: - print("(1) number = {0}, places = {1}, decimal = {2}, s = '{3}'".format(number, places, decimal, s)) - - if (len(s) > 5): - dot = s.find('.') - - if (dot > 5): - raise ValueError("Input overflow - {0} is too large for the display!".format(number)) - elif ((dot > 0) and (decimal == 0)): - places = dot - - if ((places <= 0) and (decimal > 0)): - self.fill(False) - places = 4 - - if '.' in s: - places += 1 - - if self.debug: - print("(2) places = {0}, dot = '{1}', decimal = {2}, s = '{3}'".format(places, dot, decimal, s)) - - # Set decimal places, if number of decimal places is specified (decimal > 0) - if ((places > 0) and (decimal > 0) and (dot > 0) and (len(s[places:]) > decimal)): - txt = s[:dot + decimal + 1] - elif (places > 0): - txt = s[:places] - - if self.debug: - print("(3) places = {0}, s = '{1}', decimal = {2}, txt = '{3}'".format(places, s, decimal, txt)) - print() - - if (len(txt) > 5): - raise ValueError("Output string ('{0}') is too long!".format(txt)) - - self.text(txt) - - return txt - - def hex(self, number): - s = "{:x}".format(number) - if len(s) > 4: - raise ValueError("Overflow") - self.fill(False) - self.text(s) - - -class Seg7x4(Seg14x4): - P = [0, 2, 6, 8] # The positions of characters. - - def scroll(self, count=1): - if count >= 0: - offset = 0 - else: - offset = 1 - for i in range(3): - self.buffer[self.P[i + offset]] = self.buffer[self.P[i + count]] - - def push(self, char): - if char in ':;': - self.put(char) - else: - super().push(char) - - def put(self, char, index=0): - if not 0 <= index <= 3: - return - char = char.lower() - if char == '.': - self.buffer[self.P[index]] |= 0b10000000 - return - elif char in 'abcdef': - c = ord(char) - 97 + 10 - elif char == '-': - c = 16 - elif char in '0123456789': - c = ord(char) - 48 - elif char == ' ': - c = 0x00 - elif char == ':': - self.buffer[4] = 0x02 - return - elif char == ';': - self.buffer[4] = 0x00 - return - else: - return - self.buffer[self.P[index]] = NUMBERS[c] diff --git a/hybotics_ht16k33/__init__.py b/hybotics_ht16k33/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hybotics_ht16k33/ht16k33.py b/hybotics_ht16k33/ht16k33.py new file mode 100644 index 0000000..2f359f0 --- /dev/null +++ b/hybotics_ht16k33/ht16k33.py @@ -0,0 +1,111 @@ +from micropython import const +from utime import sleep +from bus_device import i2c_device + +_HT16K33_BLINK_CMD = const(0x80) +_HT16K33_BLINK_DISPLAYON = const(0x01) +_HT16K33_CMD_BRIGHTNESS = const(0xE0) +_HT16K33_OSCILATOR_ON = const(0x21) + +RETRY_MAX = 10 +RETRY_WAIT_SEC = 1.0 + +class HT16K33: + """The base class for all HT16K33-based backpacks and wings.""" + + + def __init__(self, i2c, address=0x70, auto_write=True, brightness=1.0): + self.i2c_device = i2c_device.I2CDevice(i2c, address) + self._temp = bytearray(1) + self._buffer = bytearray(17) + self._auto_write = auto_write + self.fill(0) + self._write_cmd(_HT16K33_OSCILATOR_ON) + self._blink_rate = None + self._brightness = None + self.blink_rate = 0 + self.brightness = brightness + self.fill(0) + + def _write_cmd(self, byte): + self._temp[0] = byte + with self.i2c_device: + self.i2c_device.write(self._temp) + + @property + def blink_rate(self): + """The blink rate. Range 0-3.""" + return self._blink_rate + + @blink_rate.setter + def blink_rate(self, rate=None): + if not 0 <= rate <= 3: + 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) + + @property + def brightness(self): + """The brightness. Range 0.0-1.0""" + return self._brightness + + @brightness.setter + def brightness(self, brightness): + if not 0.0 <= brightness <= 1.0: + raise ValueError( + "Brightness must be a decimal number in the range: 0.0-1.0" + ) + + self._brightness = brightness + xbright = round(15 * brightness) + xbright = xbright & 0x0F + self._write_cmd(_HT16K33_CMD_BRIGHTNESS | xbright) + + @property + def auto_write(self): + """Auto write updates to the display.""" + return self._auto_write + + @auto_write.setter + def auto_write(self, auto_write): + if isinstance(auto_write, bool): + self._auto_write = auto_write + else: + raise ValueError("Must set to either True or False.") + + def show(self): + """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) + + def fill(self, color): + """Fill the whole display with the given color.""" + fill = 0xFF if color else 0x00 + for i in range(16): + self._buffer[i + 1] = fill + if self._auto_write: + self.show() + + def _pixel(self, x, y, color=None): + addr = 2 * y + x // 8 + mask = 1 << x % 8 + if color is None: + return bool(self._buffer[addr + 1] & mask) + if color: + # set the bit + self._buffer[addr + 1] |= mask + else: + # clear the bit + self._buffer[addr + 1] &= ~mask + if self._auto_write: + self.show() + return None + + def _set_buffer(self, i, value): + self._buffer[i + 1] = value # Offset by 1 to move past register address. + + def _get_buffer(self, i): + return self._buffer[i + 1] # Offset by 1 to move past register address. diff --git a/hybotics_ht16k33/matrix.py b/hybotics_ht16k33/matrix.py new file mode 100644 index 0000000..2aaa13b --- /dev/null +++ b/hybotics_ht16k33/matrix.py @@ -0,0 +1,52 @@ +from ht16k33.ht16k33 import HT16K33 + +class Matrix16x8(HT16K33): + """The double matrix.""" + + def pixel(self, x, y, color=None): + """Set a single pixel to specified color.""" + if not 0 <= x <= 15: + return + if not 0 <= y <= 7: + return + if x >= 8: + x -= 8 + y += 8 + return super()._pixel(y, x, color) + + +class Matrix8x8(HT16K33): + """The single matrix.""" + + def pixel(self, x, y, color=None): + """Set a single pixel to specified color.""" + if not 0 <= x <= 7: + return + if not 0 <= y <= 7: + return + x = (x - 1) % 8 + return super()._pixel(x, y, color) + + +class Matrix8x8x2(HT16K33): + """The bi-color matrix.""" + + def pixel(self, x, y, color=None): + """Set a single pixel to specified color.""" + if not 0 <= x <= 7: + return + if not 0 <= y <= 7: + return + if color is not None: + super()._pixel(y, x, (color & 0x01)) + super()._pixel(y + 8, x, (color >> 1) & 0x01) + else: + return super()._pixel(y, x) | super()._pixel(y + 8, x) << 1 + + def fill(self, color): + """Fill the display with given color.""" + fill1 = 0xff if color & 0x01 else 0x00 + fill2 = 0xff if color & 0x02 else 0x00 + for i in range(8): + self.buffer[i * 2] = fill1 + self.buffer[i * 2 + 1] = fill2 diff --git a/hybotics_ht16k33/segments.py b/hybotics_ht16k33/segments.py new file mode 100644 index 0000000..ea08fe7 --- /dev/null +++ b/hybotics_ht16k33/segments.py @@ -0,0 +1,479 @@ +from ht16k33.ht16k33 import HT16K33 +from utime import sleep + +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, +) +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 + 0x40, # - +) + + +class Seg14x4(HT16K33): + """Alpha-numeric, 14-segment display.""" + + POSITIONS = (0, 2, 6, 8) # The positions of characters. + + def __init__(self, i2c, address=0x70, auto_write=True): + super().__init__(i2c, address, auto_write) + # Use colon for controling two-dots indicator at the center (index 0) + self._colon = Colon(self) + + def print(self, value, decimal=0): + """Print the value to the display.""" + 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() + + def print_hex(self, value): + """Print the value as a hexidecimal string to the display.""" + if isinstance(value, int): + self.print("{0:X}".format(value)) + else: + self.print(value) + + def __setitem__(self, key, value): + self._put(value, key) + if self._auto_write: + self.show() + + def scroll(self, count=1): + """Scroll the display by specified number of places.""" + if count >= 0: + offset = 0 + else: + offset = 2 + for i in range(6): + self._set_buffer(i + offset, self._get_buffer(i + 2 * count)) + + def _put(self, char, index=0): + """Put a character at the specified place.""" + if not 0 <= index <= 3: + return + if not 32 <= ord(char) <= 127: + return + if char == ".": + self._set_buffer( + index * 2 + 1, self._get_buffer(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]) + + def _push(self, char): + """Scroll the display and add a character at the end.""" + if char != "." or self._get_buffer(7) & 0b01000000: + self.scroll() + self._put(" ", 3) + self._put(char, 3) + + def _text(self, text): + """Display the specified text.""" + for character in text: + self._push(character) + + def _number(self, number, decimal=0): + """ + Display a floating point or integer number on the Adafruit HT16K33 based displays + + Param: 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: 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. + + Returns: The output text string to be displayed. + """ + + auto_write = self._auto_write + self._auto_write = False + stnum = str(number) + dot = stnum.find(".") + colon_pos = stnum.find(":") + + if (len(stnum) > 5) or ((len(stnum) > 4) and (dot < 0)): + 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 = 4 + + if "." in stnum: + places += 1 + + # Set decimal places, if number of decimal places is specified (decimal > 0) + if places > 0 < decimal < len(stnum[places:]) and dot > 0: + txt = stnum[: dot + decimal + 1] + elif places > 0: + txt = stnum[:places] + + if len(txt) > 5: + raise ValueError("Output string ('{0}') is too long!".format(txt)) + + self._text(txt) + self._auto_write = auto_write + + return txt + + def set_digit_raw(self, index, bitmask): + """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. + + bitmask should be 2 bytes such as: 0xFFFF + If can be passed as an integer, list, or tuple + """ + if not isinstance(index, int) or not 0 <= index <= 3: + raise ValueError("Index value must be an integer in the range: 0-3") + + 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(index * 2, bitmask & 0xFF) + self._set_buffer(index * 2 + 1, (bitmask >> 8) & 0xFF) + + if self._auto_write: + self.show() + + def marquee(self, text, delay=0.25, loop=True): + """ + 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) + + """ + if isinstance(text, str): + self.fill(False) + if loop: + while True: + self._scroll_marquee(text, delay) + else: + self._scroll_marquee(text, delay) + + def _scroll_marquee(self, text, delay): + """Scroll through the text string once using the delay""" + char_is_dot = False + for character in text: + self.print(character) + # Add delay if character is not a dot or more than 2 in a row + if character != "." or char_is_dot: + sleep(delay) + char_is_dot = character == "." + self.show() + + @property + def colon(self): + """Simplified colon accessor""" + return self._colon[0] + + @colon.setter + def colon(self, turn_on): + self._colon[0] = turn_on + + +class Seg7x4(Seg14x4): + """Numeric 7-segment display. It has the same methods as the alphanumeric display, but only + supports displaying a limited set of characters.""" + + POSITIONS = (0, 2, 6, 8) # The positions of characters. + + def __init__(self, i2c, address=0x70, auto_write=True): + super().__init__(i2c, address, auto_write) + # Use colon for controling two-dots indicator at the center (index 0) + self._colon = Colon(self) + + def scroll(self, count=1): + """Scroll the display by specified number of places.""" + if count >= 0: + offset = 0 + else: + offset = 1 + for i in range(3): + self._set_buffer( + self.POSITIONS[i + offset], self._get_buffer(self.POSITIONS[i + count]) + ) + + def _push(self, char): + """Scroll the display and add a character at the end.""" + if char in ":;": + self._put(char) + else: + if char != "." or self._get_buffer(self.POSITIONS[3]) & 0b10000000: + self.scroll() + self._put(" ", 3) + self._put(char, 3) + + def _put(self, char, index=0): + """Put a character at the specified place.""" + if not 0 <= index <= 3: + return + char = char.lower() + index = self.POSITIONS[index] + if char == ".": + self._set_buffer(index, self._get_buffer(index) | 0b10000000) + return + if char in "abcdef": + character = ord(char) - 97 + 10 + elif char == "-": + character = 16 + 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 + else: + return + + self._set_buffer(index, NUMBERS[character]) + + def set_digit_raw(self, index, bitmask): + """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. + """ + if not isinstance(index, int) or not 0 <= index <= 3: + raise ValueError("Index value must be an integer in the range: 0-3") + + # Set the digit bitmask value at the appropriate position. + self._set_buffer(self.POSITIONS[index], bitmask & 0xFF) + + if self._auto_write: + self.show() + + @property + def colon(self): + """Simplified colon accessor""" + return self._colon[0] + + @colon.setter + def colon(self, turn_on): + self._colon[0] = turn_on + + +class BigSeg7x4(Seg7x4): + """Numeric 7-segment display. It has the same methods as the alphanumeric display, but only + supports displaying a limited set of characters.""" + + def __init__(self, i2c, address=0x70, auto_write=True): + super().__init__(i2c, address, auto_write) + # Use colon for controling two-dots indicator at the center (index 0) + # or the two-dots indicators at the left (index 1) + self.colon = Colon(self, 2) + + def _setindicator(self, index, value): + """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): + """Get side LEDs (dots) + See setindicator() for indexes + """ + bitmask = 1 << (index + 1) + return self._get_buffer(0x04) & bitmask + + @property + def top_left_dot(self): + """The top-left dot indicator.""" + return bool(self._getindicator(1)) + + @top_left_dot.setter + def top_left_dot(self, value): + self._setindicator(1, value) + + @property + def bottom_left_dot(self): + """The bottom-left dot indicator.""" + return bool(self._getindicator(2)) + + @bottom_left_dot.setter + def bottom_left_dot(self, value): + self._setindicator(2, value) + + @property + def ampm(self): + """The AM/PM indicator.""" + return bool(self._getindicator(3)) + + @ampm.setter + def ampm(self, value): + self._setindicator(3, value) + + +class Colon: + """Helper class for controlling the colons. Not intended for direct use.""" + + # pylint: disable=protected-access + + MASKS = (0x02, 0x0C) + + def __init__(self, disp, num_of_colons=1): + self._disp = disp + self._num_of_colons = num_of_colons + + def __setitem__(self, key, value): + 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): + 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]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f675e3b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +Adafruit-Blinka +adafruit-circuitpython-busdevice