Via I2C the Pico W is conntected to: an Adafruit AHT20 temperature/humidity sensor; a SparkFun 4x20 serLCD. At start the built-in RTC is updated with NTP host datetime and next at hourly intervals. The time is updated every second using the values from the built-in RTC. The Temperature and Humidity values are updated at the same moment as the time is updated.
I discovered that the time this script displays is always 12 seconds behind the time that is displayed on another
NTP Synchronized Clock (M5Stack Core2) that I have. So I added a correction:
In function set_time() I added a variable 'NTP_ERROR_CORR = 12'.
The line 't = val - NTP_DELTA' I changed into:
't = val - NTP_DELTA - NTP_ERROR_CORR'
Now both clocks at displaying equal times.
Here is the current structure of files in my PicoW.
I copied them from another of my projects. I am sure that some files can be deleted.
Here is the code of the script:
Code: Select all
#
# Idea: downloaded on 2022-07-17 23h45 utc+1
# Idea: downloaded from: https://gist.github.com/aallan/581ecf4dc92cd53e3a415b7c33a1147c
# Idea: aallan/picow_ntp_client.py
# 2022-07-19. In this version added readings from an Adafruit AHT20 temperature/humidity sensor
# Added DST timezone data for ten years 2022-2031, timezone: Europe/Portugal
# 2022-07-20: Added functionality to display data on a SparkFun 4x20 serLCD
# 2022-07-21: Added functionality to import the contents of file: dst_data.json as a dictionary.
# This file now contains the timezone in the first line, followed by ten lines with dst data.
# In this way - when necessary - the timezone and/or the dst data can be updated without altering the script.
#
import network
import socket
import time
import utime
import struct
# added to have not to show credentials in this code
from secrets import secrets
#from machine import Pin
import machine
import busio
import board
import json
# +--------------------------+
# | Imports for LCD control |
# +--------------------------+
from sparkfun_serlcd import Sparkfun_SerLCD_I2C
my_debug = False
use_aht20_sensor = True
time_is_set = False
show_NTP_upd_msg_on_lcd = True
"""
Note about time.ticks_diff(ticks12, ticks2)
Note: Do not pass time() values to ticks_diff(), you should use normal mathematical operations on them.
But note that time() may (and will) also overflow.
This is known as https://en.wikipedia.org/wiki/Year_2038_problem .
Note about time.time()
Returns the number of seconds, as an integer, since the Epoch, assuming that underlying RTC is set
and maintained as described above. If an RTC is not set, this function returns number of seconds since
a port-specific reference point in time (for embedded boards without a battery-backed RTC,
usually since power up or reset). If you want to develop portable MicroPython application,
ou should not rely on this function to provide higher than second precision.
See: https://docs.micropython.org/en/latest/library/time.html
"""
# Initialize I2C
# busio.I2C(SCL, SDA)
# Mod by @PaulskPt: SCL, SDA pins to GP3 and GP2 so that GP0 is free to toggle the built-in LED
i2c = busio.I2C(board.GP3, board.GP2)
# +-------------------------------------------------+
# | Definitions for 20x4 character LCD (I2C serial) |
# +-------------------------------------------------+
# --- STATIC (not to be changed!) -------
lcd_columns = 20
lcd_rows = 4
lcd_max_row_cnt = 3 # 0 - 3 = 4 lines
my_row1 = 1
my_row2 = 2
my_col = 3
# --- End-of STATIC ----------------------
lcd_max_row_cnt = 3 # 0 - 3 = 4 lines
lcd_row = 0
lcd_col = 0
lcd_data = None # See setup()
# It happens when that SerLCD gets locked-up e.g. caused by touching with a finger the RX pin (4th pin fm left). This pin is very sensitive!
# A locked-up situation is often shown as that all the 8x5 segments of the LCD are 'filled with inverse pixels.
# see: https://github.com/KR0SIV/SerLCD_Reset for a reset tool. But this is not what I want. I want to be able to reset the LCD from within this script.
while True:
try:
lcd = Sparkfun_SerLCD_I2C(i2c)
break
except ValueError as msg: #.
print("The LCD is locked-up. Please connect RS with GND for a second or so.")
time.sleep(10) # wait a bit
"""
Talk to the LCD at I2C address 0x27.
The number of rows and columns defaults to 4x20, so those
arguments could be omitted in this case.
Note @paulsk: I experienced that one needs to use the 'num_rows =' and 'num_cols ='
when these parameters are used.
"""
if use_aht20_sensor:
import adafruit_ahtx0
sensor = adafruit_ahtx0.AHTx0(i2c)
# See: https://docs.python.org/3/library/time.html#time.struct_time
tm_year = 0
tm_mon = 1 # range [1, 12]
tm_mday = 2 # range [1, 31]
tm_hour = 3 # range [0, 23]
tm_min = 4 # range [0, 59]
tm_sec = 5 # range [0, 61] in strftime() description
tm_wday = 6 # range 8[0, 6] Monday = 0
tm_yday = 7 # range [0, 366]
tm_isdst = 8 # 0, 1 or -1
tm_tmzone = 'Europe/Lisbon' # default (set in dst_data.json) abbreviation of timezone name
timezone_set = False
#tm_tmzone_dst = "WET0WEST,M3.5.0/1,M10.5.0"
# Timezone and dst data are in external file "dst_data.json"
# See: https://www.epochconverter.com/
# The "start" and "end" values are for timezone "Europe/Portugal"
dst = None # See setup()
tm_gmtoff = 3600 # offset east of GMT in seconds
# added '- tm_gmtoff' to subtract 1 hour = 3600 seconds to get GMT + 1 for Portugal
NTP_DELTA = 2208988800 - tm_gmtoff # mod by @PaulskPt.
# modified to use Portugues ntp host
host = "pt.pool.ntp.org" # mod by @PaulskPt
dim = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:31}
weekdays = {0:"Monday", 1:"Tuesday", 2:"Wednesday", 3:"Thursday", 4:"Friday", 5:"Saturday", 6:"Sunday"}
led = machine.Pin("LED", machine.Pin.OUT)
"""
function searches the dst dictionary for prensence of the year
Parameter: integer: yr, the year to search
Return: the index of the found key to the dst dictionary. If not found: -1
"""
def yr_in_dst_dict(yr):
TAG = "yr_in_dst_dict(): "
dst_idx = -1
if my_debug:
print(TAG+"type(dst)=", type(dst))
print(TAG+"type(dst[\"dst\"][0])={}, contents={}".format(type(dst["dst"][0]), dst["dst"]))
le = len(dst["dst"])
if my_debug:
print(TAG+"length dst =", le)
t_dst = int(dst["dst"][1]["year"]) # the first record holds the timezone
print(TAG+"1st item in dst[\"dst\"]= {}, type={}".format(t_dst, type(t_dst)))
for i in range(1,le):
if yr == int(dst["dst"][i]["year"]):
if my_debug:
print(TAG+"year {} found in dst dict, index: {}".format(dst["dst"][i]["year"], i))
dst_idx = i
break
if dst_idx < 0:
print(TAG+"year {} not found in dst dict".format(yr))
return dst_idx
"""
Determine if the current date and time are within the Daylight Saving Time period
for the TimeZone: Europe/Lisbon.
Parameters: None
Return: Boolean
If the Year of current date is outside of dst.keys()
then print a message on the REPL and raise a SystemExit
"""
def is_dst():
TAG = "is_dst(): "
# We only can call utime.time() if the built-in RTC has been set by set_time()
if not time_is_set:
print("is_dst(): cannot continue: built-in RTC has not been set with time of NTP host")
return False
t = utime.time()
yr = utime.localtime(t)[0]
dst_idx = yr_in_dst_dict(yr)
if dst_idx >= 0:
t_dst = dst["dst"][dst_idx]
if my_debug:
print(TAG+"t_dst= ",end='')
print(*sorted(t_dst.items()))
start = int(t_dst["start"]) # integer
end = int(t_dst["end"]) # integer
start_info = t_dst["start_info"] # string
end_info = t_dst["end_info"] # string
if my_debug:
print(TAG+"start={}, end={}".format(start,end))
print(TAG+"start_info=\"{}\", end_info=\"{}\"".format(start_info,end_info))
return False if t < start or t > end else True
else:
print("year: {} not in dst dictionary ({}).\nUpdate the dictionary! Exiting...".format(yr, dst.keys()))
raise SystemExit
"""
Get the date and time from a NTP host
Set the built-in RTC
Parameters: None
Return: None
"""
def set_time():
global time_is_set
TAG = "set_time(): "
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1B
NTP_ERROR_CORR = 12 # deduct 12 seconds from the time received (empirecally determined while comparing with another NTP synchronized clock I use)
addr = socket.getaddrinfo(host, 123)[0][-1]
print(TAG+"Time zone: \"{}\"".format(tm_tmzone))
print(TAG+"NTP host address: \"{}\"".format(addr[0]))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
tm = None
while True:
msg = None
res = None
try:
s.settimeout(1)
res = s.sendto(NTP_QUERY, addr)
msg = s.recv(48)
if my_debug:
print(TAG+"msg received:", msg)
except OSError as exc:
if exc.args[0] == 110: # ETIMEDOUT
print(TAG+"ETIMEDOUT. Returning False")
time.sleep(2)
continue
finally:
s.close()
break
val = struct.unpack("!I", msg[40:44])[0]
t = val - NTP_DELTA - NTP_ERROR_CORR
tm = time.gmtime(t)
dst_idx = yr_in_dst_dict(tm[tm_year])
if dst_idx < 0:
print(TAG+"year: {} not in dst dictionary ({}).\nUpdate the dictionary! Exiting...".format(tm[tm_year], dst.keys()))
raise SystemExit
machine.RTC().datetime((tm[tm_year], tm[tm_mon], tm[tm_mday], tm[tm_wday] + 1,
tm[tm_hour], tm[tm_min], tm[tm_sec], 0))
if not time_is_set:
time_is_set = True # Flag, only set once
if not my_debug:
print(TAG+"built-in RTC has been set with time from NTP host")
if show_NTP_upd_msg_on_lcd:
lcd.clear()
lcd.set_cursor(0,1)
lcd.write(TAG)
lcd.set_cursor(0,2)
lcd.write("RTC set fm NTC host")
time.sleep(2)
if tm is not None:
print(TAG+"date/time updated from: \"{}\"".format(host))
"""
setup() function. Called by main()
Parameters: None
Return: None
"""
def setup():
global dst, dst_info, tm_tmzone, timezone_set
TAG = "setup(): "
t = {0:"RPi PicoW",1:"NTP-Client",2:"Temp/Humidity Sensor",3:"(c) 2022 @PaulskPt"}
fn = 'dst_data.json'
f = None
dst_data = None
# lcd.backlight();
lcd.system_messages(False)
lcd.set_contrast(120) # Set lcd contrast to default value
lcd.set_backlight(0xFF8C00) # orange backlight color
lcd.cursor(1) # hide cursor was (2) show cursor
lcd.blink(0) # don't blink. Was (1) blink cursor
lcd.clear()
for i in range(len(t)):
lcd.set_cursor(lcd_col, lcd_row+i) # ATTENTION: the SerLCD module set_cursor(col,row)
lcd.write(t[i])
lcd.set_cursor(19,3)
time.sleep(3) # <--------------- DELAY ---------------
# Load dst and dst_info dictionaries
try:
with open(fn) as f:
dst_data = f.read()
if my_debug:
print(TAG+"Data type data1 b4 reconstruction: ", type(dst_data))
print(TAG+"Contents dst_data b4 \"{}\"".format(dst_data))
# reconstructing the data as a dictionary
dst = json.loads(dst_data)
dst_data = None # Cleanup
if my_debug:
print(TAG+"Data type after reconstruction: ", type(dst))
print(TAG+"contents dst:", dst)
except OSError as exc:
if exc.args[0] == 2: # File not found (ENOENT)
print(TAG+"ERROR: File: \"{}\" not found. Exiting...".format(fn))
raise SystemExit
else:
print(TAG+"Error \"{}\" occurred. Exiting...".format(exc))
raise
# Read the timezone from file:
if not timezone_set:
tz = dst["dst"][0]["timezone"]
if my_debug:
print(TAG+"Timezone imported from file: \"{}\" = \"{}\"".format(fn, tz))
if isinstance(tz, str) and len(tz) > 0:
tm_tmzone = tz
timezone_set = True
# Load login data from different file for safety reasons
ssid = secrets['ssid']
password = secrets['pw']
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
max_wait = 10
while max_wait > 0:
if wlan.status() < 0 or wlan.status() >= 3:
break
max_wait -= 1
print('waiting for connection...')
time.sleep(1)
if wlan.status() != 3:
raise RuntimeError('network connection failed')
else:
print('connected')
status = wlan.ifconfig()
print( 'ip = ' + status[0] )
"""
Show on LCD and print on REPL:
- Date,
- HH:MM:SS
- Day-of-the-Year
- DST Yes/No
- Temperature and Humidity (depending flag: use_aht20_sensor)
Parameters: t: time.localtime() struct;
start: if True display all on LCD. if False display only HH:MM:SS and Temperature and Humidity (depending flag: use_ath20_sensor)
Return: None
"""
def show_dt_t_h(t, start):
dst = "Yes" if is_dst() else "No"
tp = "\nLocal date/time: {} {}-{:02d}-{:02d}, {:02d}:{:02d}:{:02d}, day of the year: {}. DST: {}".format(weekdays[t[tm_wday]], t[tm_year],
t[tm_mon], t[tm_mday], t[tm_hour], t[tm_min], t[tm_sec], t[tm_yday], dst)
if start:
t0 = "{} {}-{:02d}-{:02d}".format(weekdays[t[tm_wday]][:3],
t[tm_year], t[tm_mon], t[tm_mday])
t1 = "Day {} DST {}".format(t[tm_yday], dst)
t2 = "Hr {:02d}:{:02d}:{:02d}".format(t[tm_hour], t[tm_min], t[tm_sec])
if use_aht20_sensor:
tmp = "{:4.1f}C".format(sensor.temperature)
hum = "{:4.1f}%".format(sensor.relative_humidity)
t3 = "T {} H {}".format(tmp, hum)
print(tp)
if start:
lcd.clear()
lcd.set_cursor(0, 0)
lcd.write(t0)
lcd.set_cursor(0, 1)
lcd.write(t1)
lcd.set_cursor(0, 2)
lcd.write(t2)
if use_aht20_sensor:
lcd.set_cursor(0, 3)
lcd.write(t3)
print("Temperature: {}, Humidity: {}".format(tmp, hum))
lcd.set_cursor(19,3) # Park cursor
"""
main() function
Parameters: None
Return: None
"""
def main():
global time_is_set
setup()
set_time() # call at start
t = time.localtime()
o_mday = t[tm_mday]
o_hour = t[tm_hour] # Set hour for set_time() call interval
o_sec = t[tm_sec]
start = True
while True:
try:
t = time.localtime()
# yr, mo, dd, hh, mm, ss, wd, yd, dst
if o_mday != t[tm_mday]:
o_mday = t[tm_mday] # remember current day of the month
start = True # Force to rebuild the whole LCD
if o_hour != t[tm_hour]:
o_hour = t[tm_hour] # remember current hour
set_time()
t = time.localtime() # reload RTC time after being synchronized
start = True
if o_sec != t[tm_sec]:
o_sec = t[tm_sec] # remember current second
led.on()
show_dt_t_h(t, start)
if start:
start = False
led.off()
except KeyboardInterrupt:
print("Keyboard interrupt...exiting...")
led.off()
#lcd.clear()
raise SystemExit
except OSError as exc:
if exc.args[0] == 110: # ETIMEDOUT
time.sleep(2)
pass
if __name__ == '__main__':
main()
Code: Select all
# The MIT License (MIT)
#
# Copyright (c) 2019 Gaston Williams
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`sparkfun_serlcd`
================================================================================
CircuitPython driver library for the Sparkfun Serial LCD displays
* Author(s): Gaston Williams
* Based on the Arduino library for the Sparkfun SerLCD displays
Written by Gaston Williams, August 22, 2018
* Based on sample code provided with the SparkFun Serial OpenLCD display.
The original LiquidCrystal library was written by David A. Mellis and
modified by Limor Fried @ Adafruit and the OpenLCD code was written by
Nathan Seidle @ SparkFun.
Implementation Notes
--------------------
**Hardware:**
* This is library is for the SparkFun Serial LCD displays
* SparkFun sells these at its website: www.sparkfun.com
* Do you like this library? Help support SparkFun. Buy a board!
* 16x2 SerLCD Black on RGB https://www.sparkfun.com/products/14072
* 16x2 SerLCD RGB on Black https://www.sparkfun.com/products/14073
* 20x4 SerLCD Black on RGB https://www.sparkfun.com/products/14074
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/fourstix/Sparkfun_CircuitPython_SerLCD.git"
# imports
# // from abc import ABC, abstractmethod // Please no abstractmethods, CircuitPython is not Python 3
from time import sleep
from micropython import const
# public constants
DEFAULT_I2C_ADDR = const(0x72)
"""Default I2C address for SerLCD"""
# private constants
_MAX_ROWS = const(4)
_MAX_COLS = const(20)
# Character to reset display Splash Screen to default
_DEFAULT_SPLASH_SCREEN = const(0xFF)
# OpenLCD command characters
_SPECIAL_COMMAND = const(254)
_SETTING_COMMAND = const(0x7C)
# OpenLCD commands
# 45, -, the dash character: command to clear and home the display
_CLEAR_COMMAND = const(0x2D)
# Command to change the contrast setting
_CONTRAST_COMMAND = const(0x18)
# Command to change the i2c address
_ADDRESS_COMMAND = const(0x19)
# 43, +, the plus character: command to set backlight RGB value
_SET_RGB_COMMAND = const(0x2B)
# 46, ., command to enable system messages being displayed
_ENABLE_SYSTEM_MESSAGE_DISPLAY = const(0x2E)
# 47, /, command to disable system messages being displayed
_DISABLE_SYSTEM_MESSAGE_DISPLAY = const(0x2F)
# 48, 0, command to enable splash screen at power on
_ENABLE_SPLASH_DISPLAY = const(0x30)
# 49, 1, command to disable splash screen at power on
_DISABLE_SPLASH_DISPLAY = const(0x31)
# 10, Ctrl+j, command to save current text on display as splash
_SAVE_CURRENT_DISPLAY_AS_SPLASH = const(0x0A)
# Show firmware version
_SHOW_VERSION_COMMAND = const(0x2C)
# Software reset of the system
_RESET_COMMAND = const(0x08)
# special commands
_LCD_RETURNHOME = const(0x02)
_LCD_ENTRYMODESET = const(0x04)
_LCD_DISPLAYCONTROL = const(0x08)
_LCD_CURSORSHIFT = const(0x10)
_LCD_SETDDRAMADDR = const(0x80)
# flags for display entry mode
_LCD_ENTRYRIGHT = const(0x00)
_LCD_ENTRYLEFT = const(0x02)
_LCD_ENTRYSHIFTINCREMENT = const(0x01)
_LCD_ENTRYSHIFTDECREMENT = const(0x00)
# flags for display on/off control
_LCD_DISPLAYON = const(0x04)
_LCD_DISPLAYOFF = const(0x00)
_LCD_CURSORON = const(0x02)
_LCD_CURSOROFF = const(0x00)
_LCD_BLINKON = const(0x01)
_LCD_BLINKOFF = const(0x00)
# flags for display/cursor shift
_LCD_DISPLAYMOVE = const(0x08)
_LCD_CURSORMOVE = const(0x00)
_LCD_MOVERIGHT = const(0x04)
_LCD_MOVELEFT = const(0x00)
# private functions
def _map_range(value, in_min, in_max, out_min, out_max):
"""Map an integer value from a range into a value in another range."""
result = (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
return int(result)
# base class
class Sparkfun_SerLCD:
"""Abstract base class for Sparkfun AVR-Based Serial LCD display.
Use the appropriate driver communcation subclass Sprarkfun_SerLCD_I2C()
for I2C, Sparkfun_SerLCD_SPI() for SPI or Sparkfun_SerLCD_UART for UART.
"""
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
def __init__(self):
self._display_control = _LCD_DISPLAYON | _LCD_CURSOROFF | _LCD_BLINKOFF
self._display_mode = _LCD_ENTRYLEFT | _LCD_ENTRYSHIFTDECREMENT
self._begin()
def command(self, command):
# pylint: disable=line-too-long
"""
Send a command to the display.
**Command cheat sheet:**
* ASCII / DEC / HEX
* '|' / 124 / 0x7C - Put into setting mode
* Ctrl+c / 3 / 0x03 - Change width to 20
* Ctrl+d / 4 / 0x04 - Change width to 16
* Ctrl+e / 5 / 0x05 - Change lines to 4
* Ctrl+f / 6 / 0x06 - Change lines to 2
* Ctrl+g / 7 / 0x07 - Change lines to 1
* Ctrl+h / 8 / 0x08 - Software reset of the system
* Ctrl+i / 9 / 0x09 - Enable/disable splash screen
* Ctrl+j / 10 / 0x0A - Save currently displayed text as splash
* Ctrl+k / 11 / 0x0B - Change baud to 2400bps
* Ctrl+l / 12 / 0x0C - Change baud to 4800bps
* Ctrl+m / 13 / 0x0D - Change baud to 9600bps
* Ctrl+n / 14 / 0x0E - Change baud to 14400bps
* Ctrl+o / 15 / 0x0F - Change baud to 19200bps
* Ctrl+p / 16 / 0x10 - Change baud to 38400bps
* Ctrl+q / 17 / 0x11 - Change baud to 57600bps
* Ctrl+r / 18 / 0x12 - Change baud to 115200bps
* Ctrl+s / 19 / 0x13 - Change baud to 230400bps
* Ctrl+t / 20 / 0x14 - Change baud to 460800bps
* Ctrl+u / 21 / 0x15 - Change baud to 921600bps
* Ctrl+v / 22 / 0x16 - Change baud to 1000000bps
* Ctrl+w / 23 / 0x17 - Change baud to 1200bps
* Ctrl+x / 24 / 0x18 - Change the contrast. Follow Ctrl+x with number 0 to 255. 120 is default.
* Ctrl+y / 25 / 0x19 - Change the TWI address. Follow Ctrl+x with number 0 to 255. 114 (0x72) is default.
* Ctrl+z / 26 / 0x1A - Enable/disable ignore RX pin on startup (ignore emergency reset)
* '+' / 43 / 0x2B - Set RGB backlight with three following bytes, 0-255
* ',' / 44 / 0x2C - Display current firmware version
* '-' / 45 / 0x2D - Clear display. Move cursor to home position.
* '.' / 46 / 0x2E - Enable system messages (ie, display 'Contrast: 5' when changed)
* '/' / 47 / 0x2F - Disable system messages (ie, don't display 'Contrast: 5' when changed)
* '0' / 48 / 0x30 - Enable splash screen
* '1' / 49 / 0x31 - Disable splash screen
* / 128-157 / 0x80-0x9D - Set the primary backlight brightness. 128 = Off, 157 = 100%.
* / 158-187 / 0x9E-0xBB - Set the green backlight brightness. 158 = Off, 187 = 100%.
* / 188-217 / 0xBC-0xD9 - Set the blue backlight brightness. 188 = Off, 217 = 100%.
* For example, to change the baud rate to 115200 send 124 followed by 18.
"""
data = bytearray()
data.append(_SETTING_COMMAND)
data.append(command & 0xFF)
self._write_bytes(data)
# Wait a bit longer for special display commands
sleep(0.010)
def clear(self):
"""Clear the display"""
self.command(_CLEAR_COMMAND)
def home(self):
"""Send the cursor home"""
self._special_command(_LCD_RETURNHOME)
def write(self, message):
"""Write a character string to the display."""
# Value -> String -> Bytes
text = str(message).encode()
self._write_bytes(text)
def set_cursor(self, col, row):
"""Set the cursor position."""
row_offsets = [0x00, 0x40, 0x14, 0x54]
# keep variables in bounds
# row cannot be less than 0
row = max(0, row)
# row cannot be greater than max rows
row = min(row, _MAX_ROWS - 1)
# send the command
self._special_command(_LCD_SETDDRAMADDR | (col + row_offsets[row]))
def create_character(self, location, charmap):
"""Create a customer character
location - character number 0 to 7
charmap - bytes for character as 8 x 5 bit map"""
# There are only 8 locations 0-7
location &= 0x07
data = bytearray()
# Send request to create a customer character
data.append(_SETTING_COMMAND)
data.append(27 + location)
for i in range(8):
# Only the lowest 5 bits are used
data.append(charmap[i] & 0x1F)
self._write_bytes(data)
# This takes a bit longer
sleep(0.050)
def write_character(self, location):
"""Write a customer character to the display
location - character number 0 to 7"""
# There are only locations 0-7
location &= 0x07
self.command(35 + location)
def set_backlight(self, rgb):
"""Set the backlight with 24-bit RGB value."""
red = (rgb >> 16) & 0x0000FF
green = (rgb >> 8) & 0x0000FF
blue = rgb & 0x0000FF
self.set_backlight_rgb(red, green, blue)
def set_backlight_rgb(self, red, green, blue):
"""Set the backlight with byte values for r, g, b"""
# map the byte value range to backlight command range
r_value = 128 + _map_range(red, 0, 255, 0, 29)
g_value = 158 + _map_range(green, 0, 255, 0, 29)
b_value = 188 + _map_range(blue, 0, 255, 0, 29)
# send commands to the display to set backlights
data = bytearray()
# Turn display off to hide confirmation messages
self._display_control &= ~_LCD_DISPLAYON
data.append(_SPECIAL_COMMAND)
data.append(_LCD_DISPLAYCONTROL | self._display_control)
# Set the red, green and blue values
data.append(_SETTING_COMMAND)
data.append(r_value)
data.append(_SETTING_COMMAND)
data.append(g_value)
data.append(_SETTING_COMMAND)
data.append(b_value)
# Turn display back on and end
self._display_control |= _LCD_DISPLAYON
data.append(_SPECIAL_COMMAND)
data.append(_LCD_DISPLAYCONTROL | self._display_control)
# Send data
self._write_bytes(data)
# This one is a bit slow
sleep(0.050)
def set_fast_backlight(self, rgb):
"""Set the backlight color by a 24-bit value in one pass."""
# Convert from hex triplet to byte values
red = (rgb >> 16) & 0x0000FF
green = (rgb >> 8) & 0x0000FF
blue = rgb & 0x0000FF
self.set_fast_backlight_rgb(red, green, blue)
def set_fast_backlight_rgb(self, red, green, blue):
"""Set the backlight color in one pass."""
# Mask values into 0-255 range
red &= 0x00FF
green &= 0x00FF
blue &= 0x00FF
# Send commands to the display to set backlights
data = bytearray()
data.append(_SETTING_COMMAND)
# Send the set RGB character '+' or plus
data.append(_SET_RGB_COMMAND)
data.append(red)
data.append(green)
data.append(blue)
self._write_bytes(data)
sleep(0.010)
def display(self, value):
"""Turn the display on and off quickly."""
if bool(value):
self._display_control |= _LCD_DISPLAYON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
else:
self._display_control &= ~_LCD_DISPLAYON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
def cursor(self, value):
"""Turn the underline cursor on and off."""
if bool(value):
self._display_control |= _LCD_CURSORON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
else:
self._display_control &= ~_LCD_CURSORON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
def blink(self, value):
"""Turn the blink cursor on and off."""
if bool(value):
self._display_control |= _LCD_BLINKON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
else:
self._display_control &= ~_LCD_BLINKON
self._special_command(_LCD_DISPLAYCONTROL | self._display_control)
def system_messages(self, enable):
"""Enable or disable the printint of messages like 'UART: 57600' or 'Contrast: 5'"""
if bool(enable):
# Send the set '.' character
self.command(_ENABLE_SYSTEM_MESSAGE_DISPLAY)
else:
# Send the set '/' character
self.command(_DISABLE_SYSTEM_MESSAGE_DISPLAY)
sleep(0.010)
def autoscroll(self, enable):
"""Turn autoscrolling on and off."""
if bool(enable):
self._display_mode |= _LCD_ENTRYSHIFTINCREMENT
self._special_command(_LCD_ENTRYMODESET | self._display_mode)
else:
self._display_mode &= ~_LCD_ENTRYSHIFTINCREMENT
self._special_command(_LCD_ENTRYMODESET | self._display_mode)
sleep(0.010)
def set_contrast(self, value):
"""Set the display contrast."""
data = bytearray()
data.append(_SETTING_COMMAND)
data.append(_CONTRAST_COMMAND)
data.append(value & 0x00FF)
self._write_bytes(data)
sleep(0.010)
def scroll_display_left(self, count=1):
"""Scroll the display to the left"""
self._special_command(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | _LCD_MOVELEFT, count)
def scroll_display_right(self, count=1):
"""Scroll the display to the right"""
self._special_command(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | _LCD_MOVERIGHT, count)
def move_cursor_left(self, count=1):
"""Move the cursor to the left"""
self._special_command(_LCD_CURSORSHIFT | _LCD_CURSORMOVE | _LCD_MOVELEFT, count)
def move_cursor_right(self, count=1):
"""Scroll the display to the right"""
self._special_command(_LCD_CURSORSHIFT | _LCD_CURSORMOVE | _LCD_MOVERIGHT, count)
def splash_screen(self, enable):
"""Enable or disable the splash screem."""
if bool(enable):
self.command(_ENABLE_SPLASH_DISPLAY)
else:
self.command(_DISABLE_SPLASH_DISPLAY)
sleep(0.010)
def save_splash_screen(self):
"""Save the current display as the splash screem."""
self.command(_SAVE_CURRENT_DISPLAY_AS_SPLASH)
sleep(0.010)
def left_to_right(self):
"""Set the text to flow from left to right. This is the direction
that is common to most Western languages."""
self._display_mode |= _LCD_ENTRYLEFT
self._special_command(_LCD_ENTRYMODESET | self._display_mode)
def right_to_left(self):
"""Set the text to flow from right to left."""
self._display_mode &= ~_LCD_ENTRYLEFT
self._special_command(_LCD_ENTRYMODESET | self._display_mode)
def show_version(self):
"""Show the firmware version on the display."""
self.command(_SHOW_VERSION_COMMAND)
def reset(self):
"""Perform a software reset on the dislay."""
self.command(_RESET_COMMAND)
def default_splash_screen(self):
""" Result to the default splash screen"""
# Clear the display
self.clear()
# put the default charater
self._put_char(_DEFAULT_SPLASH_SCREEN)
# Wait a bit
sleep(0.200)
self.save_splash_screen()
# abstract methods
# @abstractmethod
def _write_bytes(self, data):
pass
# @abstractmethod
def _change_i2c_address(self, addr):
pass
# private functions
def _begin(self):
"""Initialize the display"""
data = bytearray()
# Send special command character
data.append(_SPECIAL_COMMAND)
# Send the display command
data.append(_LCD_DISPLAYCONTROL | self._display_control)
# Send special command character
data.append(_SPECIAL_COMMAND)
# Send the entry mode command
data.append(_LCD_ENTRYMODESET | self._display_mode)
# Put LCD into setting mode
data.append(_SETTING_COMMAND)
# Send clear display command
data.append(_CLEAR_COMMAND)
self._write_bytes(data)
sleep(0.050)
def _special_command(self, command, count=1):
"""Send a special command to the display. Used by other functions."""
data = bytearray()
data.append(_SPECIAL_COMMAND)
for _ in range(count):
data.append(command & 0xFF)
self._write_bytes(data)
# Wait a bit longer for special display commands
sleep(0.050)
def _put_char(self, char):
"""Send a character byte directly to display, no encoding"""
data = bytearray()
data.append(char & 0xFF)
self._write_bytes(data)
# concrete subclass for I2C
class Sparkfun_SerLCD_I2C(Sparkfun_SerLCD):
"""Driver subclass for Sparkfun Serial Displays over I2C communication"""
def __init__(self, i2c, address=DEFAULT_I2C_ADDR):
import adafruit_bus_device.i2c_device as i2c_device
self._i2c_device = i2c_device.I2CDevice(i2c, address)
self._i2c = i2c
super().__init__()
def _write_bytes(self, data):
with self._i2c_device as device:
device.write(data)
def _change_i2c_address(self, addr):
import adafruit_bus_device.i2c_device as i2c_device
self._i2c_device = i2c_device.I2CDevice(self._i2c, addr)
# concrete subclass for SPI
class Sparkfun_SerLCD_SPI(Sparkfun_SerLCD):
"""Driver subclass for Sparkfun Serial LCD display over SPI communication"""
def __init__(self, spi, cs):
import adafruit_bus_device.spi_device as spi_device
self._spi_device = spi_device.SPIDevice(spi, cs)
super().__init__()
def _write_bytes(self, data):
with self._spi_device as device:
#pylint: disable=no-member
device.write(data)
def _change_i2c_address(self, addr):
# No i2c address change for SPI
pass
# concrete subclass for UART
class Sparkfun_SerLCD_UART(Sparkfun_SerLCD):
"""Driver subclass for Sparkfun Serial LCD display over Serial communication"""
def __init__(self, uart):
self._uart = uart
super().__init__()
def _write_bytes(self, data):
self._uart.write(data)
def _change_i2c_address(self, addr):
# No i2c address change for UART
pass
Here is the default secrets.py:
Code: Select all
secrets = {
'ssid': 'your SSID here',
'pw': 'your PASSWORD here'
}