PiClock project
This commit is contained in:
3
.micropico
Normal file
3
.micropico
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"info": "This file is just used to identify a project folder."
|
||||
}
|
||||
119
ht16k33/ht16k33.py
Normal file
119
ht16k33/ht16k33.py
Normal file
@@ -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]))
|
||||
162
ht16k33/ht16k33segmentbig.py
Normal file
162
ht16k33/ht16k33segmentbig.py
Normal file
@@ -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
|
||||
127
hybotics_ht16k33/ht16k33.py
Normal file
127
hybotics_ht16k33/ht16k33.py
Normal file
@@ -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 <hybotics.wy@gmail.com>
|
||||
"""
|
||||
|
||||
__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.
|
||||
557
hybotics_ht16k33/segments.py
Normal file
557
hybotics_ht16k33/segments.py
Normal file
@@ -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 <hybotics.wy@gmail.com>
|
||||
"""
|
||||
|
||||
__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])
|
||||
406
main.py
Normal file
406
main.py
Normal file
@@ -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()
|
||||
261
sht4x/sht4x.py
Normal file
261
sht4x/sht4x.py
Normal file
@@ -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)
|
||||
93
tools.py
Normal file
93
tools.py
Normal file
@@ -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')
|
||||
9
web_tools.py
Normal file
9
web_tools.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user