diff --git a/.micropico b/.micropico new file mode 100644 index 0000000..3de3977 --- /dev/null +++ b/.micropico @@ -0,0 +1,3 @@ +{ + "info": "This file is just used to identify a project folder." +} \ No newline at end of file diff --git a/ht16k33/ht16k33.py b/ht16k33/ht16k33.py new file mode 100644 index 0000000..4a381d6 --- /dev/null +++ b/ht16k33/ht16k33.py @@ -0,0 +1,119 @@ +class HT16K33: + """ + A simple, generic driver for the I2C-connected Holtek HT16K33 controller chip. + This release supports MicroPython and CircuitPython + + Bus: I2C + Author: Tony Smith (@smittytone) + License: MIT + Copyright: 2023 + """ + + # *********** CONSTANTS ********** + + HT16K33_GENERIC_DISPLAY_ON = 0x81 + HT16K33_GENERIC_DISPLAY_OFF = 0x80 + HT16K33_GENERIC_SYSTEM_ON = 0x21 + HT16K33_GENERIC_SYSTEM_OFF = 0x20 + HT16K33_GENERIC_DISPLAY_ADDRESS = 0x00 + HT16K33_GENERIC_CMD_BRIGHTNESS = 0xE0 + HT16K33_GENERIC_CMD_BLINK = 0x81 + + # *********** PRIVATE PROPERTIES ********** + + i2c = None + address = 0 + brightness = 15 + flash_rate = 0 + + # *********** CONSTRUCTOR ********** + + def __init__(self, i2c, i2c_address): + assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in HT16K33()" + self.i2c = i2c + self.address = i2c_address + self.power_on() + + # *********** PUBLIC METHODS ********** + + def set_blink_rate(self, rate=0): + """ + Set the display's flash rate. + + Only four values (in Hz) are permitted: 0, 2, 1, and 0,5. + + Args: + rate (int): The chosen flash rate. Default: 0Hz (no flash). + """ + assert rate in (0, 0.5, 1, 2), "ERROR - Invalid blink rate set in set_blink_rate()" + self.blink_rate = rate & 0x03 + self._write_cmd(self.HT16K33_GENERIC_CMD_BLINK | rate << 1) + + def set_brightness(self, brightness=15): + """ + Set the display's brightness (ie. duty cycle). + + Brightness values range from 0 (dim, but not off) to 15 (max. brightness). + + Args: + brightness (int): The chosen flash rate. Default: 15 (100%). + """ + if brightness < 0 or brightness > 15: brightness = 15 + self.brightness = brightness + self._write_cmd(self.HT16K33_GENERIC_CMD_BRIGHTNESS | brightness) + + def draw(self): + """ + Writes the current display buffer to the display itself. + + Call this method after updating the buffer to update + the LED itself. + """ + self._render() + + def update(self): + """ + Alternative for draw() for backwards compatibility + """ + self._render() + + def clear(self): + """ + Clear the buffer. + + Returns: + The instance (self) + """ + for i in range(0, len(self.buffer)): self.buffer[i] = 0x00 + return self + + def power_on(self): + """ + Power on the controller and display. + """ + self._write_cmd(self.HT16K33_GENERIC_SYSTEM_ON) + self._write_cmd(self.HT16K33_GENERIC_DISPLAY_ON) + + def power_off(self): + """ + Power on the controller and display. + """ + self._write_cmd(self.HT16K33_GENERIC_DISPLAY_OFF) + self._write_cmd(self.HT16K33_GENERIC_SYSTEM_OFF) + + # ********** PRIVATE METHODS ********** + + def _render(self): + """ + Write the display buffer out to I2C + """ + buffer = bytearray(len(self.buffer) + 1) + buffer[1:] = self.buffer + buffer[0] = 0x00 + self.i2c.writeto(self.address, bytes(buffer)) + + def _write_cmd(self, byte): + """ + Writes a single command to the HT16K33. A private method. + """ + self.i2c.writeto(self.address, bytes([byte])) diff --git a/ht16k33/ht16k33segmentbig.py b/ht16k33/ht16k33segmentbig.py new file mode 100644 index 0000000..1ef5e46 --- /dev/null +++ b/ht16k33/ht16k33segmentbig.py @@ -0,0 +1,162 @@ +# Import the base class +from ht16k33.ht16k33 import HT16K33 + +class HT16K33SegmentBig(HT16K33): + """ + Micro/Circuit Python class for the Adafruit 1.2-in 4-digit, + 7-segment LED matrix backpack and equivalent Featherwing. + + Bus: I2C + Author: Tony Smith (@smittytone) + License: MIT + Copyright: 2023 + """ + + # *********** CONSTANTS ********** + + HT16K33_SEGMENT_COLON_ROW = 0x04 + HT16K33_SEGMENT_MINUS_CHAR = 0x10 + HT16K33_SEGMENT_DEGREE_CHAR = 0x11 + HT16K33_SEGMENT_SPACE_CHAR = 0x13 + + COLON_CENTRE = 0x02 + COLON_LEFT_UPPER = 0x04 + COLON_LEFT_LOWER = 0x08 + COLON_LEFT = 0x0C + DECIMAL_POINT = 0x10 + + # The positions of the segments within the buffer + POS = (0, 2, 6, 8) + + # Bytearray of the key alphanumeric characters we can show: + # 0-9, A-F, minus, degree + CHARSET = b'\x3F\x06\x5B\x4F\x66\x6D\x7D\x07\x7F\x6F\x5F\x7C\x58\x5E\x7B\x71\x40\x63\x00' + + # *********** CONSTRUCTOR ********** + + def __init__(self, i2c, i2c_address=0x70): + self.buffer = bytearray(16) + self.point_pattern = 0x00 + super(HT16K33SegmentBig, self).__init__(i2c, i2c_address) + + # *********** PUBLIC METHODS ********** + + def set_colon(self, pattern=0x02): + """ + Set or unset the segment LED display's colon and decimal point lights. + + This method updates the display buffer, but does not send the buffer to the display itself. + Call 'draw()' to render the buffer on the display. + + Args: + patter (int) An integer indicating which elements to light (OR the values required). Default: 0x00 + 0x00: no colon + 0x02: centre colon + 0x04: left colon, lower dot + 0x08: left colon, upper dot + 0x10: decimal point (upper) + + Returns: + The instance (self) + """ + # Bail on incorrect pattern values + assert 0 <= pattern < 0x1F, "ERROR - bad patter passed to set_colon()" + + self.point_pattern = pattern + self.buffer[self.HT16K33_SEGMENT_COLON_ROW] = pattern + return self + + def set_glyph(self, glyph, digit=0): + """ + Present a user-defined character glyph at the specified digit. + + Glyph values are 8-bit integers representing a pattern of set LED segments. + The value is calculated by setting the bit(s) representing the segment(s) you want illuminated. + Bit-to-segment mapping runs clockwise from the top around the outside of the matrix; the inner segment is bit 6: + + 0 + _ + 5 | | 1 + | | + - <----- 6 + 4 | | 2 + | _ | + 3 + + This method updates the display buffer, but does not send the buffer to the display itself. + Call 'update()' to render the buffer on the display. + + Args: + glyph (int): The glyph pattern. + digit (int): The digit to show the glyph. Default: 0 (leftmost digit). + + Returns: + The instance (self) + """ + # Bail on incorrect row numbers or character values + assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_glyph()" + assert 0 <= glyph < 0x80, "ERROR - Invalid glyph (0x00-0xFF) set in set_glyph()" + + self.buffer[self.POS[digit]] = glyph & 0x7F + return self + + def set_number(self, number, digit=0): + """ + Present single decimal value (0-9) at the specified digit. + + This method updates the display buffer, but does not send the buffer to the display itself. + Call 'update()' to render the buffer on the display. + + Args: + number (int): The number to show. + digit (int): The digit to show the number. Default: 0 (leftmost digit). + + Returns: + The instance (self) + """ + # Bail on incorrect row numbers or character values + assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_number()" + assert 0 <= number < 10, "ERROR - Invalid value (0-9) set in set_number()" + + return self.set_character(str(number), digit) + + def set_character(self, char, digit=0): + """ + Present single alphanumeric character at the specified digit. + + Only characters from the class' character set are available: + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d ,e, f, -. + Other characters can be defined and presented using 'set_glyph()'. + + This method updates the display buffer, but does not send the buffer to the display itself. + Call 'draw()' to render the buffer on the display. + + Args: + char (string): The character to show. + digit (int): The digit to show the number. Default: 0 (leftmost digit). + has_dot (bool): Whether the decimal point to the right of the digit should be lit. Default: False. + + Returns: + The instance (self) + """ + # Bail on incorrect row numbers + assert 0 <= digit < 4, "ERROR - Invalid digit set in set_character()" + + char = char.lower() + char_val = 0xFF + if char == "deg": + char_val = self.HT16K33_SEGMENT_DEGREE_CHAR + elif char == '-': + char_val = self.HT16K33_SEGMENT_MINUS_CHAR + elif char == ' ': + char_val = self.HT16K33_SEGMENT_SPACE_CHAR + elif char in 'abcdef': + char_val = ord(char) - 87 + elif char in '0123456789': + char_val = ord(char) - 48 + + # Bail on incorrect character values + assert char_val != 0xFF, "ERROR - Invalid char string set in set_character()" + + self.buffer[self.POS[digit]] = self.CHARSET[char_val] + return self diff --git a/hybotics_ht16k33/ht16k33.py b/hybotics_ht16k33/ht16k33.py new file mode 100644 index 0000000..9d24d24 --- /dev/null +++ b/hybotics_ht16k33/ht16k33.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Radomir Dopieralski 2016 for Adafruit Industries +# SPDX-FileCopyrightText: Tony DiCola 2016 for Adafruit Industries +# SPDX-FileCopyrightText: Dale Weber 2021 +# +# SPDX-License-Identifier: MIT + +""" +`hybotics_ht16k33.ht16k33` +=========================== + +* Authors: Radomir Dopieralski & Tony DiCola for Adafruit Industries + +* Ported to Micropython by Dale Weber +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/hybotics/Hybotics_Micropython_HT16K33.git" + +from micropython import const +from utime import sleep + +_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 = i2c + self.address = 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 + self.i2c.writeto(self.address, 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.""" + # Byte 0 is 0x00, address of LED data register. The remaining 16 + # bytes are the display register data to set. + print(self._buffer) + self.i2c.writeto(self.address, 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/segments.py b/hybotics_ht16k33/segments.py new file mode 100644 index 0000000..8bebde3 --- /dev/null +++ b/hybotics_ht16k33/segments.py @@ -0,0 +1,557 @@ +# SPDX-FileCopyrightText: Radomir Dopieralski 2016 for Adafruit Industries +# SPDX-FileCopyrightText: Tony DiCola 2016 for Adafruit Industries +# SPDX-FileCopyrightText: Dale Weber 2021 +# +# SPDX-License-Identifier: MIT + +""" +Segment Displays +================= +* Ported to Micropython by Dale Weber +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/hybotics/Hybotics_Micropython_HT16K33.git" + +from hybotics_ht16k33.ht16k33 import HT16K33 +from utime import sleep + +# The number of seconds to delay between writing segments +DEFAULT_CHAR_DELAY_SEC = 0.2 + +# The number of cycles to go for each animation +DEFAULT_CYCLES = 5 + +# Brightness of the display (0 to 15) +DEFAULT_DISPLAY_BRIGHTNESS = 0.3 + +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, auto_round=False): + """Print the value to the display.""" + if isinstance(value, (str)): + self._text(value) + elif isinstance(value, (int, float)): + self._number(value, decimal, auto_round) + 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=None, auto_round=False): + """ + 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. + """ + if number < 0: + raise ValueError("Input underflow: {0} is negative - unable to display!".format(number)) + + # Initialize + auto_write = self._auto_write + self._auto_write = False + txt = "" + dec_places = 0 + num = number + + # Check for floating point numbers (dot > 0) + stnum = str(number) + stnum_len = len(stnum) + dot = stnum.find(".") + + if dot > 0: + whole = len(stnum[:dot]) + dec_places = len(stnum[dot + 1:]) + else: + whole = stnum_len + dec_places = 0 + + if auto_round: + # Automatically round up to when there is no decimal part + while whole + dec_places > 4 and dec_places >= 0: + dec_places -= 1 + num = round(num, dec_places) #dec_places) + + stnum = str(num) + stnum_len = len(stnum) + elif decimal >= 1: + # Round according to the number of decimal places requested + num = round(number, decimal) + dec_places = decimal + stnum = str(num) + dot = stnum.find(".") + + if dot > 0: + whole = len(stnum[:dot]) + stnum = stnum[:dot + decimal + 1] + stnum_len = len(stnum) + + if whole + dec_places > 5: + raise ValueError("Input overflow - '{0}' is too large for the display!".format(number)) + + # Set decimal places, if number of decimal places is specified (decimal > 0) + if dec_places > 0 and dot > 0 and stnum[0] == ".": + txt = "0" + stnum[dot:] + else: + txt = stnum + + 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() + + def animate(self, digits, bitmasks, delay=DEFAULT_CHAR_DELAY_SEC, auto_write=True): + """ + Main driver for all alphanumeric display animations. + Param: digits - a list of the digits to write to, in order, like [0, 1, 3]. The digits are + 0 to 3 starting at the left most digit. + Param: bitmasks - a list of the bitmasks to write, in sequence, to the specified digits. + Param: delay - The delay, in seconds (or fractions of), between writing bitmasks to a digit. + Param: auto_write - Whether to actually write to the display immediately or not. + + Returns: Nothing + """ + if not isinstance(digits, list): + raise ValueError("The first parameter MUST be a list!") + if not isinstance(bitmasks, list): + raise ValueError("The second parameter MUST be a list!") + if delay < 0: + raise ValueError("The delay between frames must be positive!") + for dig in digits: + if not 0 <= dig <= 3: + raise ValueError( + "Digit value must be an integer in the range: 0-3") + + for bits in bitmasks: + if not 0 <= bits <= 0xFFFF: + raise ValueError( + "Bitmask value must be an integer in the range: 0-65535" + ) + + self.set_digit_raw(dig, bits) + + if auto_write: + self.show() + sleep(delay) + + @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/main.py b/main.py new file mode 100644 index 0000000..96094f4 --- /dev/null +++ b/main.py @@ -0,0 +1,406 @@ +import _thread +import gc +import math +import network +import socket +import sys +import time +import ubinascii +from machine import I2C, Pin, ADC +from secrets import secrets +import sht4x.sht4x as sht4x +from web_tools import result_ok, result_notfound +from tools import connect_wifi, set_time, with_fallback, with_fallback_to_str +from ht16k33.ht16k33segmentbig import HT16K33SegmentBig + +# GPIO devices +led = Pin('LED', Pin.OUT) + +pir_sensor = Pin(2, Pin.IN, Pin.PULL_UP) +photoRes = ADC(Pin(26)) + +def blink_onboard_led(num_blinks): + for i in range(num_blinks): + led.value(1) + time.sleep_ms(200) + led.value(0) + time.sleep_ms(200) + +i2c = I2C(0, sda=Pin(0), scl=Pin(1)) +devices = i2c.scan() +if len(devices) != 0: + print('Number of I2C devices found=',len(devices)) + for device in devices: + print("Device Hexadecimel Address=",hex(device)) +else: + print("No device found") + +display = HT16K33SegmentBig(i2c) +display.clear() +display.set_glyph(0b01111100, 0) +display.set_glyph(0b01011100, 1) +display.set_glyph(0b01011100, 2) +display.set_glyph(0b01111000, 3) +display.set_brightness(1) +display.draw() +last_brightness = 1 + +wlan = network.WLAN(network.STA_IF) +ssid = secrets['ssid'] +pw = secrets['pw'] + +connect_wifi(wlan, ssid, pw, blink_onboard_led) + +display.set_colon(0x10) +display.draw() + +wlan_mac = wlan.config('mac') +mac_readable = ubinascii.hexlify(wlan_mac).decode() + +sht4x = sht4x.SHT4X(i2c) + +update_time = False +try: + print('Updating time...') + set_time() + print('Time updated') +except: + print('Could not set time') + update_time = True + +temp_sht4x = 0 +humidity_sht4x = 0 + +enable_temp = True +enable_hum = True + +dim_light = True +enable_auto_brightness = True + +enable_motion_detection = False +motion_timeout_ms = 18000 # Default value, it will be recalculated +motion_started_ms = 0 +motion_state_on = False + + +def web_thread(): + global temp_sht4x + global humidity_sht4x + + global enable_temp + global enable_hum + global dim_light + global enable_auto_brightness + global enable_motion_detection + + GET_PATH_START = 4 + POST_PATH_START = 5 + + addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] + + print('Binding to ' + str(addr)) + serverSocket = socket.socket() + serverSocket.bind(addr) + serverSocket.listen(1) + + while True: + try: + #print('waiting for client') + cl, addr = serverSocket.accept() + #print('client connected from', addr) + request = cl.recv(1024) + + request = str(request) + request = request[2:-1] # remove b' and ' from string + #print(request) + + sensor_lock.acquire() + temperature = temp_sht4x + humidity = humidity_sht4x + sensor_lock.release() + + if request.find('/homeassistant ') == GET_PATH_START: + result_ok(cl, + "{" + + "\"humidity\": \"" + with_fallback_to_str(humidity, "NaN") + "\"," + + "\"temp\": \"" + with_fallback_to_str(temperature, "NaN")+"\""+ + "}", + 'application/json') + elif request.find('/toggle_temp') == GET_PATH_START: + result_ok(cl, '{"is_active": "' + str(enable_temp).lower() + '"}', 'application/json') + elif request.find('/toggle_hum') == GET_PATH_START: + result_ok(cl, '{"is_active": "' + str(enable_hum).lower() + '"}', 'application/json') + elif request.find('/toggle_dim_light') == GET_PATH_START: + result_ok(cl, '{"is_active": "' + str(dim_light).lower() + '"}', 'application/json') + elif request.find('/toggle_auto_light') == GET_PATH_START: + result_ok(cl, '{"is_active": "' + str(enable_auto_brightness).lower() + '"}', 'application/json') + elif request.find('/toggle_motion_detection') == GET_PATH_START: + result_ok(cl, '{"is_active": "' + str(enable_motion_detection).lower() + '"}', 'application/json') + elif request.find('/toggle_temp') == POST_PATH_START: + content_start_pos = request.find('\\r\\n\\r\\n') + content = request[content_start_pos+8:] + + # HomeAssistant sends request body in a second packet + if len(content) == 0: + content = str(cl.recv(1024)) + + is_active = content.find('true') != -1 + enable_temp = is_active + result_ok(cl, 'ok') + elif request.find('/toggle_hum') == POST_PATH_START: + content_start_pos = request.find('\\r\\n\\r\\n') + content = request[content_start_pos+8:] + + # HomeAssistant sends request body in a second packet + if len(content) == 0: + content = str(cl.recv(1024)) + + is_active = content.find('true') != -1 + enable_hum = is_active + result_ok(cl, 'ok') + elif request.find('/toggle_dim_light') == POST_PATH_START: + content_start_pos = request.find('\\r\\n\\r\\n') + content = request[content_start_pos+8:] + + # HomeAssistant sends request body in a second packet + if len(content) == 0: + content = str(cl.recv(1024)) + + is_active = content.find('true') != -1 + dim_light = is_active + result_ok(cl, 'ok') + elif request.find('/toggle_auto_light') == POST_PATH_START: + content_start_pos = request.find('\\r\\n\\r\\n') + content = request[content_start_pos+8:] + + # HomeAssistant sends request body in a second packet + if len(content) == 0: + content = str(cl.recv(1024)) + + is_active = content.find('true') != -1 + enable_auto_brightness = is_active + result_ok(cl, 'ok') + elif request.find('/toggle_motion_detection') == POST_PATH_START: + content_start_pos = request.find('\\r\\n\\r\\n') + content = request[content_start_pos+8:] + + # HomeAssistant sends request body in a second packet + if len(content) == 0: + content = str(cl.recv(1024)) + + is_active = content.find('true') != -1 + enable_motion_detection = is_active + result_ok(cl, 'ok') + elif request.find('/metrics') == GET_PATH_START: + attrs = "{mac=\"""" + mac_readable + "\"} " + content = ( +"""# HELP temperature Temperature in Celsius +# TYPE temperature gauge +temperature""" + attrs + with_fallback_to_str(temperature, "NaN") + +""" +# HELP humidity Relative humidity in % +# TYPE humidity gauge +humidity""" + attrs + with_fallback_to_str(humidity, "NaN")) + result_ok(cl, content) + else: + result_notfound(cl) + except KeyboardInterrupt: + serverSocket.close() + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + time.sleep_ms(100) + + try: + gc.collect() + except: + pass + +def display_thread(): + global temp_sht4x + global humidity_sht4x + + global enable_temp + global enable_hum + global dim_light + global enable_auto_brightness + global enable_motion_detection + global motion_timeout_ms + global motion_started_ms + global motion_state_on + + global last_brightness + global update_time + + global display + + global sensor_lock + + current_time = time.time_ns() + last_display_mode_time = int(current_time // 1_000_000) + display_mode = 0 + + while True: + try: + try: + sensor_lock.acquire() + temp_sht4x, humidity_sht4x = sht4x.measurements + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + sensor_lock.release() + + current_time = time.time_ns() + current_time_ms = int(current_time // 1_000_000) + display.clear() + + ltime = time.localtime() + hour = ltime[3] + minute = ltime[4] + + # Light on a 0 - 65535 scale + light = light_raw = photoRes.read_u16() + + # We want to use the minimum brightness in darker environments + # So we scale the light value to a 0 - 15 scale but uses a 18 (0-17) scale with zeroes at the beggining + # Because of this the lower (18 [extended range] - 16 [real range] + 1 [0 is 0 on the real range]) * 100 / 18 = 16.6% + # the brightness will be 0 + # We also chop of the top so instead of 18 we use a 20 scale but only subtract 3 from + light = light * 19 // 65535 - 3 + if light < 0: + light = 0 + + if enable_auto_brightness: + #if hour <= 7 or hour >= 20: + # brighness = 1 + #else: + # brighness = 15 + brightness = light + + if brightness != last_brightness: + last_brightness = brightness + display.set_brightness(brightness) + else: + if dim_light and last_brightness != 1: + display.set_brightness(1) + last_brightness = 1 + elif not dim_light and last_brightness != 15: + display.set_brightness(15) + last_brightness = 15 + + if motion_state_on and current_time_ms >= motion_started_ms + motion_timeout_ms: + motion_state_on = False + + if motion_state_on == False and enable_motion_detection and pir_sensor.value() == 1: + motion_state_on = True + motion_started_ms = current_time_ms + display_mode = 0 + last_display_mode_time = current_time_ms + motion_timeout_ms = 6000 + + if enable_temp: + motion_timeout_ms = motion_timeout_ms + 6000 + + if enable_hum: + motion_timeout_ms = motion_timeout_ms + 6000 + + if display_mode == 0: + + hour_0 = int(math.floor(hour/10)) + hour_1 = hour - hour_0 * 10 + + display.set_number(hour_0, 0) + display.set_number(hour_1, 1) + + minute_0 = int(math.floor(minute/10)) + minute_1 = minute - minute_0*10 + + display.set_number(minute_0, 2) + display.set_number(minute_1, 3) + + signs = 0 + signs = signs | (0x02 if time.time() % 2 == 0 else 0) # (0x02 if current_time_ms % 1000 < 500 else 0) + + display.set_colon(signs) + + elif display_mode == 1: + temp_i0 = int(temp_sht4x // 10) + temp_i1 = int(temp_sht4x - (temp_i0 * 10)) + + temp_d0 = int(temp_sht4x * 10 - temp_i0 * 100 - temp_i1 * 10) + + display.set_number(temp_i0, 0) + display.set_number(temp_i1, 1) + display.set_number(temp_d0, 2) + display.set_glyph(0x39, 3) + display.set_colon(0x02) + + elif display_mode == 2: + hum_i0 = int(humidity_sht4x // 10) + hum_i1 = int(humidity_sht4x - (hum_i0 * 10)) + + hum_d0 = int(humidity_sht4x * 10 - hum_i0 * 100 - hum_i1 * 10) + + display.set_number(hum_i0, 0) + display.set_number(hum_i1, 1) + display.set_number(hum_d0, 2) + display.set_colon(0x12) + + try: + if enable_motion_detection and not motion_state_on: + display.clear() + + display.draw() + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + + if current_time_ms >= last_display_mode_time + 3000: + last_display_mode_time = current_time_ms + display_mode = display_mode + 1 + + if display_mode == 1 and not enable_temp: + display_mode = display_mode + 1 + + if display_mode == 2 and not enable_hum: + display_mode = display_mode + 1 + + if display_mode > 2: + display_mode = 0 + + gc.collect() + + try: + if update_time: + try: + set_time() + update_time = False + except: + print('Could not set time') + update_time = True + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + + time.sleep_ms(100) + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + time.sleep_ms(100) + +sensor_lock = _thread.allocate_lock() + +print('Starting Display thread') +second_thread = _thread.start_new_thread(display_thread, ()) + +print('Starting Main loop') +web_thread() \ No newline at end of file diff --git a/sht4x/sht4x.py b/sht4x/sht4x.py new file mode 100644 index 0000000..b275bdf --- /dev/null +++ b/sht4x/sht4x.py @@ -0,0 +1,261 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 Jose D. Montoya +# +# SPDX-License-Identifier: MIT +""" +`sht4x` +================================================================================ + +MicroPython Driver fot the Sensirion Temperature and Humidity SHT40, SHT41 and SHT45 Sensor + + +* Author: Jose D. Montoya + + +""" + +import time +import struct +from micropython import const + +try: + from typing import Tuple +except ImportError: + pass + + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/jposada202020/MicroPython_SHT4X.git" + +_RESET = const(0x94) + +HIGH_PRECISION = const(0) +MEDIUM_PRECISION = const(1) +LOW_PRECISION = const(2) +temperature_precision_options = (HIGH_PRECISION, MEDIUM_PRECISION, LOW_PRECISION) +temperature_precision_values = { + HIGH_PRECISION: const(0xFD), + MEDIUM_PRECISION: const(0xF6), + LOW_PRECISION: const(0xE0), +} + +HEATER200mW = const(0) +HEATER110mW = const(1) +HEATER20mW = const(2) +heater_power_values = (HEATER200mW, HEATER110mW, HEATER20mW) + +TEMP_1 = const(0) +TEMP_0_1 = const(1) +heat_time_values = (TEMP_1, TEMP_0_1) + +wat_config = { + HEATER200mW: (0x39, 0x32), + HEATER110mW: (0x2F, 0x24), + HEATER20mW: (0x1E, 0x15), +} + + +class SHT4X: + """Driver for the SHT4X Sensor connected over I2C. + + :param ~machine.I2C i2c: The I2C bus the SHT4X is connected to. + :param int address: The I2C device address. Defaults to :const:`0x44` + + :raises RuntimeError: if the sensor is not found + + **Quickstart: Importing and using the device** + + Here is an example of using the :class:`SHT4X` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + from machine import Pin, I2C + from micropython_sht4x import sht4x + + Once this is done you can define your `machine.I2C` object and define your sensor object + + .. code-block:: python + + i2c = I2C(1, sda=Pin(2), scl=Pin(3)) + sht = sht4x.SHT4X(i2c) + + Now you have access to the attributes + + .. code-block:: python + + temp = sht.temperature + hum = sht.relative_humidity + + """ + + def __init__(self, i2c, address: int = 0x44) -> None: + self._i2c = i2c + self._address = address + self._data = bytearray(6) + + self._command = 0xFD + self._temperature_precision = HIGH_PRECISION + self._heater_power = HEATER20mW + self._heat_time = TEMP_0_1 + + @property + def temperature_precision(self) -> str: + """ + Sensor temperature_precision + + +------------------------------------+------------------+ + | Mode | Value | + +====================================+==================+ + | :py:const:`sht4x.HIGH_PRECISION` | :py:const:`0` | + +------------------------------------+------------------+ + | :py:const:`sht4x.MEDIUM_PRECISION` | :py:const:`1` | + +------------------------------------+------------------+ + | :py:const:`sht4x.LOW_PRECISION` | :py:const:`2` | + +------------------------------------+------------------+ + + """ + values = ("HIGH_PRECISION", "MEDIUM_PRECISION", "LOW_PRECISION") + return values[self._temperature_precision] + + @temperature_precision.setter + def temperature_precision(self, value: int) -> None: + if value not in temperature_precision_values: + raise ValueError("Value must be a valid temperature_precision setting") + self._temperature_precision = value + self._command = temperature_precision_values[value] + + @property + def relative_humidity(self) -> float: + """ + The current relative humidity in % rH + The RH conversion formula (1) allows values to be reported + which are outside the range of 0 %RH … 100 %RH. Relative + humidity values which are smaller than 0 %RH and larger than + 100 %RH are non-physical, however these “uncropped” values might + be found beneficial in some cases (e.g. when the distribution of + the sensors at the measurement boundaries are of interest) + """ + return self.measurements[1] + + @property + def temperature(self) -> float: + """The current temperature in Celsius""" + return self.measurements[0] + + @property + def measurements(self) -> Tuple[float, float]: + """both `temperature` and `relative_humidity`, read simultaneously + If you use t the heater function, sensor will be not give a response + back. Waiting time is added to the logic to account for this situation + """ + + self._i2c.writeto(self._address, bytes([self._command]), False) + if self._command in (0x39, 0x2F, 0x1E): + time.sleep(1.2) + elif self._command in (0x32, 0x24, 0x15): + time.sleep(0.2) + time.sleep(0.2) + self._i2c.readfrom_into(self._address, self._data) + + temperature, temp_crc, humidity, humidity_crc = struct.unpack_from( + ">HBHB", self._data + ) + + if temp_crc != self._crc( + memoryview(self._data[0:2]) + ) or humidity_crc != self._crc(memoryview(self._data[3:5])): + raise RuntimeError("Invalid CRC calculated") + + temperature = -45.0 + 175.0 * temperature / 65535.0 + + humidity = -6.0 + 125.0 * humidity / 65535.0 + humidity = max(min(humidity, 100), 0) + + return temperature, humidity + + @staticmethod + def _crc(buffer) -> int: + """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 + + @property + def heater_power(self) -> str: + """ + Sensor heater power + The sensor has a heater. Three heating powers and two heating + durations are selectable. + The sensor executes the following procedure: + 1. The heater is enabled, and the timer starts its count-down. + 2. Measure is taken after time is up + 3. After the measurement is finished the heater is turned off. + 4. Temperature and humidity values are now available for readout. + The maximum on-time of the heater commands is one second in order + to prevent overheating + + +-------------------------------+---------------+ + | Mode | Value | + +===============================+===============+ + | :py:const:`sht4x.HEATER200mW` | :py:const:`0` | + +-------------------------------+---------------+ + | :py:const:`sht4x.HEATER110mW` | :py:const:`1` | + +-------------------------------+---------------+ + | :py:const:`sht4x.HEATER20mW` | :py:const:`2` | + +-------------------------------+---------------+ + + """ + values = ("HEATER200mW", "HEATER110mW", "HEATER20mW") + return values[self._heater_power] + + @heater_power.setter + def heater_power(self, value: int) -> None: + if value not in heater_power_values: + raise ValueError("Value must be a valid heater power setting") + self._heater_power = value + self._command = wat_config[value][self._heat_time] + + @property + def heat_time(self) -> str: + """ + Sensor heat_time + The sensor has a heater. Three heating powers and two heating + durations are selectable. + The sensor executes the following procedure: + 1. The heater is enabled, and the timer starts its count-down. + 2. Measure is taken after time is up + 3. After the measurement is finished the heater is turned off. + 4. Temperature and humidity values are now available for readout. + The maximum on-time of the heater commands is one second in order + to prevent overheating + + +----------------------------+---------------+ + | Mode | Value | + +============================+===============+ + | :py:const:`sht4x.TEMP_1` | :py:const:`0` | + +----------------------------+---------------+ + | :py:const:`sht4x.TEMP_0_1` | :py:const:`1` | + +----------------------------+---------------+ + """ + values = ("TEMP_1", "TEMP_0_1") + return values[self._heat_time] + + @heat_time.setter + def heat_time(self, value: int) -> None: + if value not in heat_time_values: + raise ValueError("Value must be a valid heat_time setting") + self._heat_time = value + self._command = wat_config[self._heater_power][value] + + def reset(self): + """ + Reset the sensor + """ + self._i2c.writeto(self._address, bytes([_RESET]), False) + time.sleep(0.1) diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..06a2760 --- /dev/null +++ b/tools.py @@ -0,0 +1,93 @@ +import ubinascii +import time +import socket +import struct +import machine + +def with_fallback(value, fallback): + if value is None: + return fallback + else: + return value + +def with_fallback_to_str(value, fallback: str) -> str: + if value is None: + return fallback + else: + return str(value) + +def set_time(): + NTP_DELTA = 2208988800 + HU_CORRECTION = 3600 + host = "pool.ntp.org" + + NTP_QUERY = bytearray(48) + NTP_QUERY[0] = 0x1B + addr = socket.getaddrinfo(host, 123)[0][-1] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.settimeout(10) + res = s.sendto(NTP_QUERY, addr) + msg = s.recv(48) + finally: + s.close() + val = struct.unpack("!I", msg[40:44])[0] + t = val - NTP_DELTA + HU_CORRECTION + tm = time.gmtime(t) + machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0)) + +def connect_wifi(wlan, ssid, pw, blink_onboard_led): + print('Connecting to WiFi ' + ssid + '...') + wlan.active(True) + # If you need to disable powersaving mode + # wlan.config(pm = 0xa11140) + + # See the MAC address in the wireless chip OTP + mac = ubinascii.hexlify(wlan.config('mac'),':').decode() + print('mac = ' + mac) + + # Other things to query + # print(wlan.config('channel')) + # print(wlan.config('essid')) + # print(wlan.config('txpower')) + + wlan.connect(ssid, pw) + + # Wait for connection with 10 second timeout + timeout = 10 + while timeout > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + timeout -= 1 + print('Waiting for connection... Status is ' + str(wlan.status())) + blink_onboard_led(2) + time.sleep(1) + # Handle connection error + # Error meanings + # 0 Link Down + # 1 Link Join + # 2 Link NoIp + # 3 Link Up + # -1 Link Fail + # -2 Link NoNet + # -3 Link BadAuth + + wlan_status = wlan.status() + blink_onboard_led(wlan_status) + + if wlan_status != 3: + blink_onboard_led(5) + print('Wi-Fi connection failed') + return False + else: + blink_onboard_led(1) + print('Connected') + status = wlan.ifconfig() + print('ip = ' + status[0]) + return True + +def disconnect_wifi(wlan): + wlan.disconnect() + wlan.active(False) + wlan.deinit() + print('Disconnected') \ No newline at end of file diff --git a/web_tools.py b/web_tools.py new file mode 100644 index 0000000..54f1972 --- /dev/null +++ b/web_tools.py @@ -0,0 +1,9 @@ +def result_ok(cl, response = None, content_type = "text/plain"): + cl.send('HTTP/1.0 200 OK\r\nContent-type: ' + content_type + '\r\n\r\n') + cl.send(response if response is not None else "Ok") + cl.close() + +def result_notfound(cl, response = None): + cl.send('HTTP/1.0 404 NotFound\r\nContent-type: text/plain\r\n\r\n') + cl.send(response if response is not None else "Not Found") + cl.close() \ No newline at end of file