Skip to content

Instantly share code, notes, and snippets.

@PaulskPt
Last active March 31, 2024 11:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PaulskPt/9570a8ee3ea50243bbf6c34ac18da45e to your computer and use it in GitHub Desktop.
Save PaulskPt/9570a8ee3ea50243bbf6c34ac18da45e to your computer and use it in GitHub Desktop.
Lolin S3 PRO w CircuitPython V9.0.0-rc.0 test onboard SDCard, external Lolin e-Paper Display and external Adafruit Gamepad QT
# SPDX-FileCopyrightText: 2024 Paulus Schulinck
#
# SPDX-License-Identifier: MIT
###############################
"""
Platform: Lolin S3 PRO.
Attached hardware:
a) on-board: 32GB microSDCard (SPI);
b) external: Lolin 2.13inch Lolin e-Paper 3-color display, 250x122px (SPI);
c) external: Adafruit Gamepad QT (seesaw system) (I2C).
Update 2024-03-02.
Successfully entered a 32GB microSDCard in the SDCard slot on the board. I made some changes to the script below.
The script then successfully mounted the SDCard and showed the files on the SDCard;
2) Connected a Lolin 2.13inch e-Paper 3-color display, 250x122px. Needs SSD1680 driver module.
Downloaded and added the Adafruit_ssd1680 module.
I downloaded and installed adafruit_display_text module.
At start this script establishes WiFi connection, then tries to retrieve a datetime stamp from a NTP time server.
Then the internal realtime clock (RTC) will be set with the NTP datetime.
This script contains a State class which keeps varios data. This eliminates the use of a lot of global variables
(still exist twelve of them).
Update 2024-03-04:
I managed to have the microSDCard, the e-Paper Display and the Gamepad QT all three working.
Added functionality to refresh the e-Paper display every 4 minutes.
Update 2024-03-13:
Added a Date class, used to calculate a number of days difference between two dates. It is used to calculate
the number of days to the start and end of the next dst period.
Until now there does not exist a build of Circuitpython for the Lolin S3 PRO board.
Initially I used the build for the Lolin S3, however I managed to create a build of Circuitpython V9.0.0-rc.0 for
the Lolin S3 PRO.
Today a Pull Request (PR) I created has been accepted. This PR has been merged into the upstream Repo of Adafruit/Circuitpython.
I expect that the build for the Lolin S3 PRO will soon be available in https://circuitpython.org.
"""
import board
import time
import sys, os
import gc
import busio
import storage
import sdcardio
import adafruit_ssd1680
import displayio
import fourwire
import terminalio
import microcontroller
import rtc
import wifi
import socketpool
import adafruit_ntp
import random
import neopixel as neopixel
from micropython import const
from adafruit_display_text import label
from adafruit_seesaw.seesaw import Seesaw
from dst_PRT import dst
time.sleep(5) # Give IDE (e.g. mu V2.1.0) time to run and open REPL.
mRTC = rtc.RTC() # create internal RTC object
epd_bus = None
epd = None
spi = None
sd = None
vfs = None
displayio.release_displays()
spi = board.SPI()
state = None
class State:
def __init__(self, saved_state_json=None):
self.my_debug = False
self.use_TAG = True
self.board_id = None
self.pool = None
# self.tft_led = board.IO14
self.tft_busy = board.IO14
self.tft_reset = board.IO21
self.ts_cs = board.IO45 # touchscreen cs
self.sd_cs = board.IO46
self.tft_dc = board.IO47
self.tft_cs = board.IO48
self.flip_show_t = False
self.dtg_start = None
self.SDCard_mounted = False
self.text_group = None
self.text_group_x = 0
self.text_group_y = 0
self.text_area = None
self.text_area_text = ""
self.flip_show_t = False
self.qt_buttons_start = True
self.dtg = None
self.dt_text_area = None
self.dt_text_area_text = ""
self.dst_offset = 0 # PT wintertime
self.last_epd_refresh = 0.00
self.elapsed_msg_shown = False
self.refresh_in_t = 240 # 4 minutes until next display refresh (3 minutes or 180 seconds still gave an error)
self.pth = ""
self.tag_le_max = 24 # see tag_adj()
self.set_SYS_RTC = True
self.NTP_dt_is_set = False
self.SYS_RTC_is_set = False
self.SYS_dt = None # time.localtime()
self.ssid = None
self.ip = None
self.s__ip = None
self.mac = None
self.NTP_dt = None
self.SYS_dt = None
self.dst = None
self.last_x = 0
self.last_y = 0
self.msg1_shown = False
self.use_neopixel = True
self.do_blink = False
self.led_state = 0
self.pixel = None
self.neopixel_brightness = 0.03
self.delay = 0.5
# e-paper display color defines:
self.disp_fg_color = 0xFF0000
self.disp_bg_color = 0xFFFFFF
self.red_ = 0x00ff00
self.grn_ = 0xff0000
self.blu_ = 0x0000ff
self.cyan = 0x00ffff
self.purple = 0x00b4ff
self.yellow = 0xffff00
self.white = 0xffffff
self.blk_ = 0x000000
self.curr_color_set = self.blk_
self.TFT_show_duration = 5
# See: https://docs.python.org/3/library/time.html#time.struct_time
self.tm_year = 0
self.tm_mon = 1
self.tm_mday = 2
self.tm_hour = 3
self.tm_min = 4
self.tm_sec = 5
self.tm_wday = 6
self.tm_yday = 7
self.tm_isdst = 8
self.TIMEZONE = ""
self.TZ_OFFSET = None
self.COUNTRY = None
self.STATE = None
self.mRTC_DOW = DOW = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday",
}
self.pin_dict = {
"IO11": ["mosi", board.IO11],
"IO12": ["sck", board.IO12],
"IO13": ["miso", board.IO13],
"IO14": ["tft_busy", board.IO14],
"IO21": ["tft_reset", board.IO21],
"IO45": ["ts_cs", board.IO45],
"IO46": ["tf_cs", board.IO46],
"IO47": ["tft_dc", board.IO47],
"IO48": ["tft_cs", board.IO48],
}
self.qt_btns_present = False
self.BUTTON_SELECT = const(0)
self.BUTTON_B = const(1)
self.BUTTON_Y = const(2)
self.BUTTON_A = const(5)
self.BUTTON_X = const(6)
self.BUTTON_START = const(16)
self.button_mask = const(
(1 << self.BUTTON_X) # = 1 << 6 = 64 = 0x00040
| (1 << self.BUTTON_Y) # = 1 << 2 = 4 = 0x00004
| (1 << self.BUTTON_A) # = 1 << 5 = 32 = 0x00020
| (1 << self.BUTTON_B) # = 1 << 1 = 2 = 0x00002
| (1 << self.BUTTON_SELECT) # = 1 << 0 = 1 = 0x00001
| (1 << self.BUTTON_START) # = 1 << 16 = 65536 = 0x10000
) # = 65639 dec or 0x10067
try:
self.TZ_OFFSET = int(os.getenv("TZ_OFFSET"))
self.TIMEZONE = os.getenv("TIMEZONE")
self.COUNTRY = os.getenv("COUNTRY")
self.STATE = os.getenv("STATE")
self.id = board.board_id
except OSError as e:
print("State.__init__(): Error: {e}")
pass
except AttributeError as e:
print("State.__init__(): Error: {e}")
pass
# Create an instance of the State class
state = State()
print(f"\nboard ID: {state.id}\n")
try:
sd = sdcardio.SDCard(spi, state.sd_cs)
except OSError as e:
sd = None
print(f"Error: {e}")
pass
i2c = busio.I2C(board.IO10, board.IO9) # was busio.I2C(board.SCL, board.SDA)
device_address = None
while not i2c.try_lock():
pass
try:
dev_list = []
for device_address in i2c.scan():
dev_list.append(hex(device_address))
if len(dev_list) > 0:
print("I2C devices found:")
for _ in range(len(dev_list)):
print(f"\t#{_+1}: {dev_list[_]}")
else:
print("No I2C devices found")
finally: # unlock the i2c bus when ctrl-c'ing out of the loop
i2c.unlock()
# Cleanup
device_address = None
dev_list = None
if state.my_debug:
print(f"state.TZ_OFFSET= {state.TZ_OFFSET}")
state.pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(state.pool, tz_offset=state.TZ_OFFSET)
# Added pullup resistors to pin 41 (SCL) and pin 42 (SDA)
try:
seesaw = Seesaw(i2c, addr=0x50)
seesaw.pin_mode_bulk(state.button_mask, seesaw.INPUT_PULLUP)
state.qt_btns_present = True
print(f"global: seesaw.chip_id= {hex(seesaw.chip_id)}")
except Exception as e:
print(f"global(): Error while creating an instance seesaw class: {e}")
state.qt_btns_present = False
raise
"""
* @brief This function adds spacing
* to the given string parameter t
* If parameter t is a string
* this function will try to add
* the needed spacing to the end
* of the parameter t
*
* @param state, t
*
* @return Boolean
"""
def tag_adj(state, t):
global tag_le_max
if state.use_TAG:
le = 0
spc = 0
ret = t
if isinstance(t, str):
le = len(t)
if le > 0:
spc = state.tag_le_max - le
# print(f"spc= {spc}")
ret = "" + t + "{0:>{1:d}s}".format("", spc)
# print(f"s=\'{s}\'")
return ret
return ""
"""
* @brief This function prints to REPL
* a list of all the board Pins mapped
* for the board in use
*
* @param None
*
* @return None
"""
def show_pin_to_board_mappings():
for pin in dir(microcontroller.pin):
if isinstance(getattr(microcontroller.pin, pin), microcontroller.Pin):
print("".join(("microcontroller.pin.", pin, "\t")), end=" ")
for alias in dir(board):
if getattr(board, alias) is getattr(microcontroller.pin, pin):
print("".join(("", "board.", alias)), end=" ")
print()
"""
* @brief This function creates:
* a display object named 'epd', a
* main displayio group, a
* displayio group 'text_group' and a
* displayio group 'dt_group'
* The text_group will contain a random selected element
* from a list of World and Continent names.
*
* @param state
*
* @return None
"""
def create_display(state):
global spi, epd_bus, epd, i2c
# Create the displayio connection to the display pins
TAG = tag_adj(state, "create_display(): ")
msg_shown = False
try_cnt = 0
s_lst = []
if state.my_debug:
print(TAG + f"type(spi)= {type(spi)}")
print(TAG + f"type(epd_bus)= {type(epd_bus)}")
# Used to ensure the display is free in CircuitPython
displayio.release_displays()
time.sleep(1)
if epd_bus is None:
while True:
try:
if spi is not None:
epd_bus = fourwire.FourWire(
spi, command=state.tft_dc, chip_select=state.tft_cs, reset=state.tft_reset
) # ,
# baudrate=1_000_000)
time.sleep(1)
if state.my_debug:
print(TAG + f"type(epd_bus)= {type(epd_bus)}")
break
except ValueError as e:
try_cnt += 1
if try_cnt >= 10:
print(
TAG
+ f"Tried {try_cnt} times. Failed to resolve error. Exiting...."
)
raise
s = e.args[0]
if not msg_shown or (s[:4] not in s_lst):
s_lst.append(s[:4])
print(TAG + f"s_lst: {s_lst}")
msg_shown = True
time.sleep(1) # Wait a bit
DISPLAY_WIDTH = 250 # was: 212
DISPLAY_HEIGHT = 122 # was: 104
# Create the display object - the third color is red (0xff0000)
# For issues with display not updating top/bottom rows correctly set colstart to 8
epd = adafruit_ssd1680.SSD1680(
epd_bus,
width=DISPLAY_WIDTH,
height=DISPLAY_HEIGHT, # ,
busy_pin=state.tft_busy,
highlight_color=0xFF0000,
rotation=270,
colstart=0,
)
if state.my_debug:
print(TAG + f"type(epd)= {type(epd)}")
# Create a display group for our screen objects
g = displayio.Group()
# print(f"type(display)= {type(display)}")
# Set a background
background_bitmap = displayio.Bitmap(DISPLAY_WIDTH, DISPLAY_HEIGHT, 1)
# Map colors in a palette
palette = displayio.Palette(1)
palette[0] = state.disp_bg_color # BACKGROUND_COLOR
# Create a Tilegrid with the background and put in the displayio group
t = displayio.TileGrid(background_bitmap, pixel_shader=palette)
g.append(t)
# Draw simple text using the built-in font into a displayio group
state.text_group_x = 10
state.text_group_y = 61
text_group = displayio.Group(scale=2, x=state.text_group_x, y=state.text_group_y)
text_lst = [
"World",
"Europe",
"Asia",
"North America",
"South America",
"Africa",
"Oceania",
"Antarctica",
]
idx = random.randint(0, len(text_lst) - 1)
text = "Hello " + text_lst[idx] + "!"
text_area = label.Label(terminalio.FONT, text=text, color=state.disp_fg_color)
state.text_area = text_area
state.text_area_text = text
text_group.append(text_area) # Add this text to the text group
g.append(text_group)
# Create datetime group
dtg = displayio.Group()
t2 = displayio.TileGrid(background_bitmap, pixel_shader=palette)
dtg.append(t2)
dt_group = displayio.Group(scale=2, x=10, y=10)
dt_text = ""
dt_text_area = label.Label(terminalio.FONT, text=dt_text, color=state.disp_fg_color)
state.dt_text_area = dt_text_area
state.dt_text_area_text = dt_text
dt_group.append(dt_text_area)
dtg.append(dt_group)
state.dtg = dtg
epd.auto_refresh = False
# Place the display group on the screen
epd.root_group = g
# time.sleep(180)
# See: https://learn.adafruit.com/todbot-circuitpython-tricks?view=all#displays-lcd-oled-e-ink-and-displayio
# Paragraph: "Dealing with E-ink "Refresh Too Soon" Error:
if state.my_debug:
print(TAG + f"epd.time_to_refresh= {epd.time_to_refresh}")
time.sleep(epd.time_to_refresh)
# Refresh the display to have everything show on the display
# NOTE: Do not refresh eInk displays more often than 180 seconds!
if state.my_debug:
print(TAG + f"epd.busy= {epd.busy}")
while epd.busy:
if state.my_debug:
print(TAG + f"epd.busy= {epd.busy}")
pass
state.last_epd_refresh = float(time.monotonic())
if state.my_debug:
print(TAG + f"last display refresh= {state.last_epd_refresh}")
epd.refresh()
print(TAG + "display refreshed")
"""
* @brief This function creates a text
* equivalent for the received float
* parameter
*
* @param m
*
* @return string
"""
def mins_to_txt(m):
if m == 4.0:
ret = "4"
elif m == 3.5:
ret = "3 1/2"
elif m == 3.0:
ret = "3"
elif m == 2.5:
ret = "2 1/2"
elif m == 2.0:
ret = "2"
elif m == 1.5:
ret = "1 1/2"
elif m == 1.0:
ret = "1"
elif m == 0.5:
ret = "1/2"
else:
ret = str(m)
return ret
# ldr = last display refresh
"""
* @brief This function checks if the
* time lapse between the last epd refresh
* and the current time is less than
* the value in state.refresh_in_t,
* alias 'max_seconds'.
* This function will print a text,
* given by variable txt, or a default text
* togethere with the value of a string
* equivalent to the value of variable togo_t-
* This function returns a True if the
* elapsed time is less or equal than the max_seconds
* This function returns a False if the
* elapsed time is greater than max_seconds.
*
* @param state, txt, repl_pr
*
* @return Boolean
"""
def wait_for_refresh(state, txt=None, repl_pr=False):
TAG = tag_adj(state, "wait_for_refresh(): ")
ret = True
if txt is None:
txt = "Display refresh in:"
state.flip_show_t = not state.flip_show_t # Negate
start_t = state.last_epd_refresh # first time set in func create_display()
curr_t = float(time.monotonic())
max_seconds = state.refresh_in_t
elapsed_t = curr_t - start_t
# print(TAG+f"repl_pr= {repl_pr}, state.flip_show_t= {state.flip_show_t}, elapsed_t = {elapsed_t}, elapsed_t > 0 and elapsed_t % 30 <= 0.5 : {elapsed_t > 0 and elapsed_t % 30 <= 0.5}")
if elapsed_t >= 0.00 and elapsed_t <= 1 and not state.elapsed_msg_shown:
state.elapsed_msg_shown = True
togo_t = round((max_seconds - elapsed_t) / 60, 1)
m = "minutes"
print(TAG + f"{txt} {mins_to_txt(togo_t)} {m}")
elif repl_pr and state.flip_show_t and elapsed_t > 0 and elapsed_t % 30 <= 0.5:
# print(TAG+f"start_t= {start_t}, curr_t= {curr_t}, elapsed_t= {elapsed_t}")
# print(TAG+ f"Elaped time since last display refresh {elapsed_t} seconds")
togo_t = round((max_seconds - elapsed_t) / 60, 1)
#print(TAG+f"togo_t= {togo_t}")
m = "minutes" if togo_t > 1.0 else "minute"
if togo_t >= 0.30:
if repl_pr:
print(TAG + f"{txt} {mins_to_txt(togo_t)} {m}")
time.sleep(1)
if elapsed_t < max_seconds:
if elapsed_t > 0 and elapsed_t % 5 <= 0.5:
state.do_blink = not state.do_blink
if state.do_blink: # blink half of the time
blink_led(state, state.red_, 1)
# print(TAG+f"elapsed_t= {elapsed_t}")
ret = False
return ret
class Date:
def __init__(self):
self.d1 = 0 # d
self.m1 = 0 # m
self.y1 = 0 # y
self.d2 = 0 # d
self.m2 = 0 # m
self.y2 = 0 # y
self.monthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# To store number of days in all months from
# January to Dec.
# This function counts number of leap years
# before the given date
# d = (dd, mo, yy)
def countLeapYears(self, d):
years = d[2]
# Check if the current year needs to be considered
# for the count of leap years or not
if (d[1] <= 2):
years -= 1
# An year is a leap year if it is a multiple of 4,
# multiple of 400 and not a multiple of 100.
return int(years / 4) - int(years / 100) + int(years / 400)
# This function returns number of days between two
# given dates
def getDifference(self, dt1, dt2):
TAG = "Date.getDifference(): "
# COUNT TOTAL NUMBER OF DAYS BEFORE FIRST DATE 'dt1'
for _ in range(2):
if _ == 0:
p = dt1
elif _ == 1:
p = dt2
if not isinstance(p, tuple):
print(TAG+f"Parameter {"dt1" if _ == 0 else "dt2" if _ == 1 else "?"} has to be of type tuple, received type: {type(p)}")
self.d1 = dt1[0]
self.m1 = dt1[1]
self.y1 = dt1[2]
self.d2 = dt2[0]
self.m2 = dt2[1]
self.y2 = dt2[2]
# initialize count using years and day
n1 = self.y1 * 365 + self.d1
# Add days for months in given date
for i in range(0, self.m1 - 1):
n1 += self.monthDays[i]
# Since every leap year is of 366 days,
# Add a day for every leap year
n1 += self.countLeapYears(dt1)
# SIMILARLY, COUNT TOTAL NUMBER OF DAYS BEFORE 'dt2'
n2 = self.y2 * 365 + self.d2
for i in range(0, self.m2 - 1):
n2 += self.monthDays[i]
n2 += self.countLeapYears(dt2)
# return difference between two counts
return (n2 - n1)
"""
* @brief This function checks if the passed
* param tm falls between the limits of
* 'daylight-saving-time' (dst) of the
* actual country.
* This function:
* - uses the dt dictionary,
* which has been imported from the file
* dst_prt.py
* - If global variable state.my_debug is True, prints to REPL:
* - the dst datetime limits to the REPL
* - nr of days to the start of dst period.
* - nr of days to the end of dst period.
* - returns True if the given
* date is within the dst limits.
* - returns False if the given
* date is not within the dst limits.
*
* @param state, tm (a time structure)
*
* @return Boolean
"""
def is_dst(state, tm):
global ntp
TAG = tag_adj(state, "is_dst(): ")
dst_org = state.dst_offset # get original value
if not tm[state.tm_year] in dst.keys():
print(
TAG
+ f"year: {tm[state.tm_year]} not in dst dictionary {dst.keys()}.\nUpdate the dictionary! Exiting..."
)
raise SystemExit
else:
dst_start_end = dst[tm[state.tm_year]]
if state.my_debug:
print(TAG + f"dst_start_end: {dst_start_end}")
cur_dt = time.localtime()
dst_start1 = dst_start_end[0]
dst_end1 = dst_start_end[1]
dst_start2 = time.localtime(dst_start1)
dst_end2 = time.localtime(dst_end1)
if state.my_debug:
print(
TAG
+ "dst_start1: {:4d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
dst_start2[state.tm_year],
dst_start2[state.tm_mon],
dst_start2[state.tm_mday],
dst_start2[state.tm_hour],
dst_start2[state.tm_min],
dst_start2[state.tm_sec],
)
)
print(
TAG
+ "current date: {:4d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
cur_dt[state.tm_year],
cur_dt[state.tm_mon],
cur_dt[state.tm_mday],
cur_dt[state.tm_hour],
cur_dt[state.tm_min],
cur_dt[state.tm_sec],
)
)
print(
TAG
+ "dst_end1: {:4d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
dst_end2[state.tm_year],
dst_end2[state.tm_mon],
dst_end2[state.tm_mday],
dst_end2[state.tm_hour],
dst_end2[state.tm_min],
dst_end2[state.tm_sec],
)
)
if state.my_debug:
print(
TAG
+ f"year: {tm[state.tm_year]}, dst start: {dst_start2}, dst end: {dst_end2}"
)
current_time = time.time()
if current_time > dst_start1 and current_time < dst_end1:
dst_new = 1
else:
dst_new = 0
if dst_new != dst_org:
state.dst_offset = dst_new
ntp = adafruit_ntp.NTP(state.pool, tz_offset=state.TZ_OFFSET if state.dst_offset else 0)
ret = True if state.dst_offset == 1 else False
if state.my_debug:
# print(TAG+f"state.dst_offset: {state.dst_offset}")
s = "Yes" if state.dst_offset == 1 else "No"
print(
TAG
+ f"Are we in daylight saving time for country: '{state.COUNTRY}', state: '{state.STATE}' ? {s}"
)
myDate = Date()
curr_yr = time.localtime(current_time)[0]
curr_mon = time.localtime(current_time)[1]
curr_mday = time.localtime(current_time)[2]
curr_dt = (curr_mday, curr_mon, curr_yr)
start_yr = time.localtime(dst_start1)[0]
start_mon = time.localtime(dst_start1)[1]
start_mday = time.localtime(dst_start1)[2]
start_dt = (start_mday, start_mon, start_yr)
end_yr = time.localtime(dst_end1)[0]
end_mon = time.localtime(dst_end1)[1]
end_mday = time.localtime(dst_end1)[2]
end_dt = (end_mday, end_mon, end_yr)
if state.dst_offset:
days_to_dst_start = myDate.getDifference(curr_dt, start_dt)
days_to_dst_end = myDate.getDifference(curr_dt, end_dt)
else:
days_to_dst_start = myDate.getDifference(curr_dt, start_dt)
days_to_dst_end = myDate.getDifference(curr_dt, end_dt)
print(TAG+"Days to start of daylight saving time period: {:3d}".format(days_to_dst_start))
print(TAG+"Days to end of daylight saving time period: {:3d}".format(days_to_dst_end))
return ret
"""
* @brief This function gets the datetime for the built-in
* realtime clock. It converts the datetime data to string-
* data in 2 ways: 1 string to add to the displayio dt_group
* object. The other string to print to REPL.
*
* @param state
*
* @return None
"""
def disp_dt(state):
TAG = tag_adj(state, "disp_dt(): ")
"""
Get the datetime from the built-in RTC
After being updated (synchronized) from the AIO time server;
Note: the built-in RTC datetime gives always -1 for tm_isdst
If the datetime stamp is received from the AIO time server,
we can determine is_dst from resp_lst[5] extracted from the AIO time server response text,
however we now call function is_dst() to determine if the datetime is within the dst
datetime limits or not.
"""
ct = mRTC.datetime # read datetime from built_in RTC
tm = time.localtime(time.time()) # was (time.time() + state.UTC_OFFSET)
tm_dst = is_dst(state, tm) # was: -1
tm_dst_s = "Yes" if tm_dst == 1 else "No "
spc = " " * state.tag_le_max
# print(TAG+"datetime from built-in rtc= {}".format(ct), file=sys.stderr)
# weekday (ct[6]) Correct because built-in RTC weekday index is different from the AIO weekday
#
sDt = "{} YrDay: {}\n{:4d}-{:02d}-{:02d} {:02d}:{:02d}\nutc offset: {} Hr\ndst: {}".format(
# wDay yDay yy mo dd hh mm UTC-offset is_dst
state.mRTC_DOW[ct[state.tm_wday]],
ct[state.tm_yday],
ct[state.tm_year],
ct[state.tm_mon],
ct[state.tm_mday],
ct[state.tm_hour],
ct[state.tm_min],
state.TZ_OFFSET,
tm_dst_s,
) # was: ct[8])
# yy mo dd
sDt2 = "{} YrDay: {}\n{:s}{:4d}-{:02d}-{:02d} {:02d}:{:02d}\n{:s}timezone: {}\n{:s}utc offset: {} Hr\n{:s}dst: {}".format(
# wDay yDay yy mo dd hh mm UTC-offset is_dst
state.mRTC_DOW[ct[state.tm_wday]],
ct[state.tm_yday],
spc,
ct[state.tm_year],
ct[state.tm_mon],
ct[state.tm_mday],
ct[state.tm_hour],
ct[state.tm_min ],
spc,
state.TIMEZONE,
spc,
state.TZ_OFFSET,
spc,
tm_dst_s,
) # was: ct[8])
dtg = state.dtg
dt_text_area = state.dt_text_area
text = f"{sDt}" # f"{dt}\n{tm}"
dt_text_area.text = text
state.dt_text_area_text = text
epd.root_group = dtg # Select the dt_group
if not state.my_debug:
print(TAG + f"{sDt2}")
# time.sleep(state.TFT_show_duration) # in seconds
"""
* @brief This function prints to REPL
* a message and prints the same text to
* the attached e-Paper display
* This function is called by main().
*
* @param state
*
* @return None
"""
def disp_done(state):
global epd
TAG = tag_adj(state, "disp_done(): ")
text_area = state.text_area
text = "That's all folks!"
text_area.text = text
state.text_area_text = text
print(TAG + f"Displaying text: {text}")
wait_for_refresh(state, "Final display refresh in: ", True)
if epd:
epd.refresh()
else:
print(TAG + f"type(epd)= {type(epd)}")
"""
* @brief This function mounts the on-board SDCard
* It then prints to the REPL a sorted list from
* the contents of the SDCard
* Finnally the SDCard will be unmounted
* If a SDCard mount attempt was successful,
* this function returns a True
* If not successful this function returns a False.add()*
* @param state
*
* @return Boolean
"""
def mount_sd(state):
global spi, sd, vfs
TAG = tag_adj(state, "mount_sd(): ")
# Setup for SDCard:
msg_shown = False
state.pth = "/sd"
spc = " " * state.tag_le_max
mount_res = None
ret = False
try_cnt = 0
if True:
if state.my_debug:
print(TAG + f"spi= {spi}")
print(TAG + f"sd= {sd}, type(sd)= {type(sd)}")
if not sd and spi is not None:
while True:
try:
try_cnt += 1
sd = sdcardio.SDCard(spi, state.sd_cs)
break
except OSError as e:
print(TAG + f"Error: {e}\n")
break
except ValueError as e:
if try_cnt >= 10:
print(TAG+ f"Tried {try_cnt} times. Failed to resolve error. Exiting....")
raise
if not msg_shown:
msg_shown = True
# print(TAG+f"e.args[0]= {e.args[0]}")
if mount_res:
storage.umount(state.pth)
print(TAG + "SD Card unmounted")
state.SDCard_mounted = False
gc.collect()
if state.my_debug:
print(TAG + f"sd= {sd}")
if sd:
vfs = storage.VfsFat(sd)
if state.my_debug:
print(TAG + f"vfs= {vfs}, state.pth= \'{state.pth}\'")
storage.mount(vfs, state.pth)
mount_res = storage.getmount(state.pth)
# print(TAG+f"mount_res= {mount_res}")
if mount_res:
state.SDCard_mounted = True
ret = True
print(TAG + "SD Card mounted")
print("\nContents of SD Card:", end='')
lst = os.listdir(state.pth)
lst.sort()
for _ in range(len(lst)):
if _ == 0:
print(" \'"+lst[_]+"\'")
else:
print(spc+"\'"+lst[_]+"\'")
print()
storage.umount(state.pth)
print(TAG + "SD Card unmounted")
state.SDCard_mounted = False
return ret
"""
* @brief This test creates a neopixel object instance
* It switches the neopixel led in three colors
* Finally it switches the neopixel led off
*
* @param None
*
* @return Boolean
"""
if state.use_neopixel:
# Note built-in NEOPIXEL Pin = (GPIO38)
# Create a NeoPixel instance
# Brightness of 0.3 is ample for the 1515 sized LED
state.pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
state.pixel.brightness = state.neopixel_brightness
print("\nTesting Neopixel rgb led:")
state.pixel.fill(state.red_)
print("Neopixel RED: 0x{:06x}".format(state.red_))
time.sleep(state.delay)
state.pixel.fill(state.grn_)
print("Neopixel GREEN: 0x{:06x}".format(state.grn_))
time.sleep(state.delay)
state.pixel.fill(state.blu_)
print("Neopixel BLUE: 0x{:06x}".format(state.blu_))
time.sleep(state.delay)
"""
pixel.fill(state.cyan)
print("Neopixel CYAN: 0x{:06x}".format(state.cyan))
time.sleep(state.delay)
pixel.fill(state.purple)
print("Neopixel PURPLE: 0x{:06x}".format(state.purple))
time.sleep(state.delay)
"""
state.pixel.fill(state.blk_)
print("Neopixel BLACK: 0x{:06x}".format(state.blk_))
time.sleep(state.delay)
print("End of neopixel rgb led test\n")
"""
* @brief This function blinks the builtin
* led, in a color
*
* @params: state
* nr_times: integer that determines the number of times the led will blink
* bi_led. Boolean that controls if the builtin led has to blink on not
* blink_slow: Boolean that controls the speed of the blinkin
*
* @return None
"""
def blink_led(state, colr=None, nr_times=None, bi_led=True, blink_slow=False):
if colr is None:
clr = state.purple # grn_
else:
clr = colr
curr_state = state.led_state
# print(f"blink_led(): nr_times: {nr_times}, bi_led= {bi_led}, blink_slow= {blink_slow}")
if nr_times is None:
nr_times = 1
if blink_slow:
delay = 0.5
else:
delay = 0.1
if curr_state == 1:
if bi_led:
state.pixel.fill(state.blk_)
# led.value = False # first switch the led off
time.sleep(delay)
for _ in range(nr_times):
if bi_led:
state.pixel.fill(clr)
# led.value = True
time.sleep(delay)
if bi_led:
state.pixel.fill(state.blk_)
# led.value = False
time.sleep(delay)
if curr_state == 1: # if the led originally was on, switch it back on
if bi_led:
state.pixel.fill(clr)
# led.value = True
"""
* @brief This function prints a message
* containing instructions for use of the
* Adafruit Gamepad QT
*
* @param state
*
* @return None
"""
def test_msg(state):
TAG = tag_adj(state, "gamepad_test(): ")
if not state.msg1_shown:
state.msg1_shown = True
spc = " " * state.tag_le_max
print(TAG + f"We're going to test the Gamepad QT and the {id}.")
print(
spc
+ "or press any of the buttons (X, Y, A, B, Select or Start) on the Gamepad QT.\n"
+ spc
+ f"To soft reset {id} press Gamepad QT button Start.\n"
)
"""
* @brief This function asks the user for input
* It checks if the users inputs one of the
* following letters "Y", "y", "N" or "n".
*
* @param None
*
* @return Boolean
"""
def ck_usr_answr():
spc = " " * state.tag_le_max
ret = False
while True:
answer = input(spc + "Are you sure? (Y/n)+<Enter>: ")
print(spc + f"You answered: '{answer}'")
if answer in ["Y", "y"]:
ret = True
break
elif answer in ["N", "n"]:
break
return ret
"""
* @brief This function performs a software reset
* of this board
*
* @param None
*
* @return None
"""
def reboot():
print("\nRebooting...")
time.sleep(3)
# os.system('sudo shutdown -r now') # for Raspberry Pi boards
microcontroller.reset() # for CircuitPython boards
"""
Adafruit Gamepad QT button values list:
+--------+--------+----------------------------+
| BUTTON | VALUE | Value in CPY when pressed |
+--------+--------+----------------------------+
| SELECT | 1 | 65638 |
+--------+--------+----------------------------+
| B | 2 | 65637 |
+--------+--------+----------------------------+
| Y | 4 | 65635 |
+--------+--------+----------------------------+
| ? | 8 | ? |
+--------+--------+----------------------------+
| ? | 16 | ? |
+--------+--------+----------------------------+
| A | 32 | 65607 |
+--------+--------+----------------------------+
| X | 64 | 65637 !
+--------+--------+----------------------------+
| START | 65636 | 103 |
+--------+--------+----------------------------+
"""
"""
* @brief This function prints the name of the button
* pressed.
* If a button in the list btns is pressed,
* this function calls blink_led()
*
* @param state, res
*
* @return None
"""
def pr_btn_name(state, res):
btns = ["X", "Y", "A", "B", "Select", "Start"]
if res >= 0 and res < len(btns):
blink_led(state)
print((" " * state.tag_le_max) + "Button " + btns[res] + " pressed")
"""
* @brief This function checks and handles
+ button presses or joystick movement
* of the Adafruit Gamepad QT.
* At start or when Start button press is answered with 'n'
* this function will call the function test_msg()
* On button press or joystick movemnet this function
* will call pr_btn_name().
*
* @param state
*
* @return None
"""
# Check for button presses on the Gamepad QT
def ck_qt_btns(state):
if not state.qt_btns_present:
return
TAG = tag_adj(state, "ck_qt_btns(): ")
nr_btns = 6
res_x = res_y = res_a = res_b = res_sel = res_sta = -1
elapsed_t = None
interval_t = 36
interval_cnt = 0
gc.collect()
time.sleep(0.2)
spc = " " * state.tag_le_max
test_msg(state)
stop = False # Set by Y-button
while True:
try:
x = 1023 - seesaw.analog_read(14)
y = 1023 - seesaw.analog_read(15)
if state.qt_buttons_start: # Prevent an inital REPL print
state.qt_buttons_start = False
state.last_x = x
state.last_y = y
except Exception as e:
if e.errno == 121: # Remote I/O Error
print(TAG + f"Error: {e}")
pass
if x >= state.last_x:
diff_x = abs(x - state.last_x)
else:
diff_x = abs(state.last_x - x)
if y >= state.last_y:
diff_y = abs(y - state.last_y)
else:
diff_y = abs(state.last_y - y)
if (diff_x > 3) or (diff_y > 3):
blink_led(state)
print(TAG + f"joystick: (x, y)= ({x}, {y})")
# print(TAG+f"diff_x= {diff_x}, diff_y= {diff_y}")
state.last_x = x
state.last_y = y
# Get the button presses, if any...
buttons = seesaw.digital_read_bulk(state.button_mask)
if state.my_debug:
print("\n" + TAG + f"buttons = {buttons}")
if buttons == 65639:
if state.my_debug:
print(TAG + f"Gamepad QT: no button pressed")
stop = True
break
# time.sleep(0.5)
start_t = time.monotonic()
if buttons:
res = -1
for _ in range(nr_btns):
if _ == 0:
bz = 1 << state.BUTTON_X
if not buttons & (bz):
res = _
if res_x != res:
pr_btn_name(state, res)
res_x = res
break
if _ == 1:
bz = 1 << state.BUTTON_Y
if not buttons & (bz):
res = _
if res_y != res:
pr_btn_name(state, res)
res_y = res
break
if _ == 2:
bz = 1 << state.BUTTON_A
if not buttons & (bz):
res = _
if res_a != res:
pr_btn_name(state, res)
res_a = res
break
if _ == 3:
bz = 1 << state.BUTTON_B
if not buttons & (bz):
res = _
if res_b != res:
pr_btn_name(state, res)
res_b = res
break
if _ == 4:
bz = 1 << state.BUTTON_SELECT
if not buttons & (bz):
res = _
if res_sel != res:
pr_btn_name(state, res)
res_sel = res
break
if _ == 5:
bz = 1 << state.BUTTON_START
if not buttons & (bz):
res = _
if res_sta != res:
pr_btn_name(state, res)
res_sta = res
print(spc + f"About to soft reset the {id}")
if ck_usr_answr():
reboot() # Reboot the board
else:
state.msg1_shown = False
res_sta = -2
test_msg(state)
break
curr_t = time.monotonic()
elapsed_t = (curr_t - start_t) * 1000
if elapsed_t >= interval_t:
interval_cnt += 1
if interval_cnt >= 100:
interval_cnt = 0
res_x = res_y = res_a = res_b = res_sel = res_sta = -2
start_t = curr_t
time.sleep(0.01)
state.pixel.fill(state.blk_)
# led.value = False
state.led_state = 0
"""
* @brief This function checks if exists an ntp object
* If so, it retrieves a datetime stamp from an NTP server
* and sets state.NTP_dt to the retrieved datetime stamp
* It then also sets the state.NTP_dt_is_set flag
*
* @param state
*
* @return boolean
"""
def is_NTP(state):
TAG = tag_adj(state, "is_NTP(): ")
ret = False
dt = None
try:
if ntp is not None:
if not state.NTP_dt_is_set:
dt = ntp.datetime
state.NTP_dt = dt
if state.my_debug:
print(TAG + f"state.NTP_dt: {state.NTP_dt}")
state.NTP_dt_is_set = True
ret = True if dt is not None else False
except OSError as e:
print(f"is_NTP() error: {e}")
return ret
"""
* @brief This function sets the internal
* realtime clock
* It retrieves the datetime stamp from an NTP server
* on internet
*
* @param state
*
* @return None
"""
def set_INT_RTC(state):
global mRTC
if not state.set_SYS_RTC:
return
TAG = tag_adj(state, "set_INT_RTC(): ")
s1 = "Internal (SYS) RTC is set from "
s2 = "datetime stamp: "
dt = None
internal_RTC = True if is_INT_RTC() else False
if internal_RTC:
try:
dt = ntp.datetime
mRTC.datetime = dt
except OSError as e:
print(
TAG + f"Error while trying to set internal RTC from NTP datetime: {e}"
)
raise
except Exception as e:
raise
state.SYS_dt = mRTC.datetime
if state.my_debug:
print(TAG + f"mRTC.datetime: {mRTC.datetime}")
# print(TAG+f"state.SYS_dt: {state.SYS_dt}")
state.SYS_RTC_is_set = True
if state.SYS_dt.tm_year >= 2000:
print(TAG + s1 + "NTP service " + s2)
dt = state.SYS_dt
if not state.my_debug:
print(TAG + "{:d}/{:02d}/{:02d}".format(dt.tm_mon, dt.tm_mday, dt.tm_year))
print(
TAG
+ "{:02d}:{:02d}:{:02d} weekday: {:s}".format(
dt.tm_hour, dt.tm_min, dt.tm_sec, state.mRTC_DOW[dt.tm_wday]
)
)
if internal_RTC:
print(TAG + "Note that NTP weekday starts with 0")
"""
* @brief In this version of CircuitPython one can only check if there is a WiFi connection
* by checking if an IP address exists.
* In the function do_connect() the global variable s_ip is set.
*
* @param state
*
* @return boolean. True if exists an ip address. False if not.
"""
def wifi_is_connected(state):
TAG = tag_adj(state, "wifi_is_connected(): ")
if state.ip is not None:
my_ip = state.ip
else:
my_ip = wifi.radio.ipv4_address
if my_ip is None:
return False
else:
my_s__ip = str(my_ip)
ret = (
True
if my_s__ip is not None and len(my_s__ip) > 0 and my_s__ip != "0.0.0.0"
else False
)
if ret:
state.ssid = os.getenv("CIRCUITPY_WIFI_SSID")
print(TAG + f"Connected to: {state.ssid}")
state.s__ip = my_s__ip
print(TAG + f"IP: {state.s__ip}")
return ret
"""
* @brief function that establish WiFi connection
* Function tries to establish a WiFi connection with the given Access Point
* If a WiFi connection has been established, function will:
* sets the global variables: 'ip' and 's_ip' ( the latter used by function wifi_is_connected() )
*
* @param state
*
* @return None
"""
def do_connect(state):
TAG = tag_adj(state, "do_connect(): ")
# Get env variables from file settings.toml
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
pw = os.getenv("CIRCUITPY_WIFI_PASSWORD")
try:
wifi.radio.connect(ssid=ssid, password=pw)
except ConnectionError as e:
print(TAG + f"WiFi connection Error: '{e}'")
except Exception as e:
print(TAG + f"Error: {dir(e)}")
state.ip = wifi.radio.ipv4_address
if state.ip:
state.s__ip = str(state.ip)
"""
* @brief function returns True
* if RTC object instance mRTC exists
*
* @param state
*
* @return None
"""
def is_INT_RTC():
if mRTC is not None:
return True
return False
"""
* @brief function print hostname to REPL
*
* @param state
*
* @return None
"""
def hostname(state):
TAG = tag_adj(state, "hostname(): ")
print(TAG + f"wifi.radio.hostname= '{wifi.radio.hostname}'")
"""
* @brief function prints mac address to REPL
*
* @param state
*
* @return None
"""
def mac(state):
TAG = tag_adj(state, "mac(): ")
mac = wifi.radio.mac_address
le = len(mac)
if le > 0:
state.mac = mac
print(TAG + "wifi.radio.mac_address= ", end="")
for _ in range(le):
if _ < le - 1:
print("{:x}:".format(mac[_]), end="")
else:
print("{:x}".format(mac[_]), end="")
print("", end="\n")
"""
* @brief this function sets the WiFi.AuthMode.
+ if there is no WiFi connection the function calls
+ the function do_connect() to establish a WiFi connection.
*
* @param state
*
* @return None
"""
def setup(state):
global pixels, my_brightness, mRTC, SYS_dt, tz_o, ntp
TAG = tag_adj(state, "setup(): ")
wifi.AuthMode.WPA2 # set only once
if not wifi_is_connected(state):
do_connect(state)
hostname(state)
if is_NTP(state):
if not state.my_debug:
print(TAG + "We have NTP")
if is_INT_RTC():
state.dtg_start = True # Set flag to call disp_dt()
# dummy = is_dst(state, time.localtime(time.time()) )
if not state.my_debug:
print(TAG + "We have an internal RTC")
print(TAG + "Going to set internal RTC")
set_INT_RTC(state)
if state.SYS_RTC_is_set:
if state.my_debug:
print(TAG + "and the internal RTC is set from an NTP server")
gc.collect()
if not state.my_debug:
print()
"""
* @brief this is the main function.
*
* @param None
*
* @return None
"""
def main():
global spi, state
TAG = tag_adj(state, "main(): ")
loopnr = 0
setup(state)
if state.my_debug:
show_pin_to_board_mappings()
print(TAG + "Going to mount SD Card")
res = mount_sd(state)
if state.my_debug:
print(TAG + f"SD Card mounted= {state.SDCard_mounted}")
print(TAG + "Done with SD Card")
print(TAG + "Going to create and write text to display")
# Used to ensure the display is free in CircuitPython
displayio.release_displays()
create_display(state)
while True:
try:
loopnr += 1
# print(f"\nLoopnr: {loopnr}")
if loopnr >= 1000:
loopnr = 0
ck_qt_btns(state)
# state.msg1_shown = False
# time.sleep(0.5)
if state.dtg_start:
state.dtg_start = False
gc.collect()
if state.my_debug:
print(TAG+f"Memory free: {gc.mem_free()}")
disp_dt(state)
blink_led(state, state.grn_, 1)
if epd:
epd.refresh()
state.elapsed_msg_shown = False
state.last_epd_refresh = int(float(time.monotonic()))
elif wait_for_refresh(state, "Displaying datetime in:", True):
gc.collect()
if state.my_debug:
print(TAG+f"Memory free: {gc.mem_free()}")
disp_dt(state)
blink_led(state, state.grn_, 1)
if epd:
epd.refresh()
state.elapsed_msg_shown = False
state.last_epd_refresh = int(float(time.monotonic()))
except RuntimeError as e:
print(TAG + f"Error: {e.args[0]}")
pass
except KeyboardInterrupt:
print(TAG + "KeyboardInterrrupt. Exiting...")
break
# if loopnr >= 1000:
# break
state.pixel.fill(state.blk_)
print(TAG + "That's all folks!")
# disp_done(state)
time.sleep(5)
sys.exit()
if __name__ == "__main__":
main()
# To auto-connect to Wi-Fi
CIRCUITPY_WIFI_SSID=""
CIRCUITPY_WIFI_PASSWORD=""
TIMEZONE="Europe/Lisbon" # http=//worldtimeapi.org/timezones
TZ_OFFSET="1" # hours difference from utc
# TIMEZONE="America/New_York"
# TZ_OFFSET="-4"
COUNTRY="PRT"
STATE=""
DEBUG_FLAG="0" # No debug
LOCAL_TIME_FLAG="1"
NTP_LOCAL_FLAG= "1"
NTP_LOCAL_URL="pt.pool.ntp.org"
Wednesday 2024-03-13 17h38 utc
Board: Lolin S3 PRO with 32GB SDCard and a Lolin 2.13 INCH e-Paper 250x122 SPI Display
IDE: mu V2.1.0
REPL Output
soft reboot
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
board ID: lolin_s3_pro
I2C devices found:
#1: 0x50
global: seesaw.chip_id= 0x87
Testing Neopixel rgb led:
Neopixel RED: 0x00ff00
Neopixel GREEN: 0xff0000
Neopixel BLUE: 0x0000ff
Neopixel BLACK: 0x000000
End of neopixel rgb led test
wifi_is_connected(): Connected to: __________
wifi_is_connected(): IP: 192.168.x.xxx
hostname(): wifi.radio.hostname= 'LOLIN-S3-PRO'
setup(): We have NTP
setup(): We have an internal RTC
setup(): Going to set internal RTC
set_INT_RTC(): Internal (SYS) RTC is set from NTP service datetime stamp:
set_INT_RTC(): 3/13/2024
set_INT_RTC(): 17:38:16 weekday: Wednesday
set_INT_RTC(): Note that NTP weekday starts with 0
main(): Going to mount SD Card
mount_sd(): SD Card mounted
Contents of SD Card: 'BigBuckBunny2.pcm'
'BigBuckBunny2.rgb'
'Ham_DV.wav'
'System Volume Information'
'example_app_bad_apple.jpg'
'example_app_moon.jpg'
'morse.wav'
'table_720p30.png'
'table_vga60.png'
'two_connectors.jpg'
mount_sd(): SD Card unmounted
create_display(): display refreshed
gamepad_test(): We're going to test the Gamepad QT and the <function>.
or press any of the buttons (X, Y, A, B, Select or Start) on the Gamepad QT.
To soft reset <function> press Gamepad QT button Start.
Button X pressed
Button Y pressed
Button A pressed
Button B pressed
Button Select pressed
Are you sure? (Y/n)+<Enter>: n
You answered: 'n'
gamepad_test(): We're going to test the Gamepad QT and the <function>.
or press any of the buttons (X, Y, A, B, Select or Start) on the Gamepad QT.
To soft reset <function> press Gamepad QT button Start.
wait_for_refresh(): Displaying datetime in: 4 minutes
wait_for_refresh(): Displaying datetime in: 3 1/2 minutes
wait_for_refresh(): Displaying datetime in: 3 minutes
wait_for_refresh(): Displaying datetime in: 2 1/2 minutes
wait_for_refresh(): Displaying datetime in: 2 minutes
wait_for_refresh(): Displaying datetime in: 1 1/2 minutes
wait_for_refresh(): Displaying datetime in: 1 minute
wait_for_refresh(): Displaying datetime in: 1/2 minute
is_dst(): Are we in daylight saving time for country: 'PRT', state: '' ? No
is_dst(): Days to start of daylight saving time period: 18
is_dst(): Days to end of daylight saving time period: 228
disp_dt(): Wednesday YrDay: 73
2024-03-13 17:42
timezone: Europe/Lisbon
utc offset: 0 Hr
dst: No
wait_for_refresh(): Displaying datetime in: 4 minutes
wait_for_refresh(): Displaying datetime in: 3 1/2 minutes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment