本次活动为电子工程世界与得捷电子创办的Follow me活动的第二期,旨在通过Adafruit推出的Adafruit ESP32-S3 TFT Feather开发板学习CircuitPython开发。本次学习通过渐进式学习,一步一步了解Adafruit ESP32-S3 TFT Feather开发板的基本控制,最终通过各种学习成果的综合利用实现“多功能环境监测器”基本功能。
本项目总体结构如下:
一、总体视频讲解;
二、渐进式学习成果
1、开发环境的配置
2、实现任务一:控制屏幕显示中文;
3、实现任务二:网络功能使用;
4、实现任务三:控制WS2812B;
5、实现任务四:分任务1:日历&时钟;
三、多功能环境监测器的实现
四、代码下载
接下来让我们一起走进CircuitPython下的Adafruit ESP32-S3 TFT Feather开发板的魅力世界。
一、视频展示
二、渐进式学习成果
1、开发环境的配置
(1)获取最新的CircuitPython UF2文件
打开开发板对应的官方介绍界面,下载最新的CircuitPython UF2,我使用的UF2的版本为8.2.3。
(2)双击复位键进入下图模式:这个时候开发板会作为“FTHRS3BOOT”盘被打开。注意:双击复位键的顺序,第一下灯光会变为紫色,时间挺短的,如果没有按下第二下就真的是复位重启了,双击后灯光为绿色。将最新的UF2文件拖到“FTHRS3BOOT”盘中,这个时候变为“CIRCUITPY”盘,开发板界面如下图:
(3)安装Mu编辑器(官方的推荐):
注意最好连接着开发板编辑,不然可能会有警告,开发板默认显示如下:
(4)加载“CIRCUITPY”盘中的code.py文件,更改为print("Hello World! Hello World!")后保存,显示就刷新了:
2、实现任务一:控制屏幕显示中文
在进行具体编程开始前,注意下载对应的adafruit-circuitpython-bundle-8.x-mpy,文章末尾有本次对应的官方库文件,github下载还是太慢了。
网上提供的多个字库都在2M左右,使用本开发环境开发后实际可编译空间可能只剩下几十K了,建议选用比较小的字库,这样我们可以有更多的选择。
import board
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
font = bitmap_font.load_font("lib\sanjiruosixingkai_m_16.pcf")
color_FireBrick = 0xB22222 # 耐火砖红
# 初始化显示屏参数
display = board.DISPLAY
display.brightness = 0.25 # 更改亮度
display.rotation = 0 # 更改方向,0为横屏,90为竖屏
str_area = label.Label(font, scale=1)
str_area.text = "\n EEWorld 电子工程世界\n 得捷电子\n Follow me第2期"
str_area.x = 2
str_area.y = 2
str_area.color = color_FireBrick
# 启动屏幕显示
Chinese_group = displayio.Group()
display.show(Chinese_group)
Chinese_group.append(str_area)
while True:
pass
3、实现任务二:网络功能使用
本开发板使用的主控是ESP32-S3,ESP32的一大优点是内置WiFi功能。
由于WiFi名称与密码都是比较私密的文件,所以例程中创建了一个secret .py用于保存自定义数据,可以有效保护个人隐私。
教程中的secret .py包含如下信息:
secrets = {
'ssid''home_wifi_network'
'password''wifi_password'
'aio_username''my_adafruit_io_username'
'aio_key''my_adafruit_io_key'
'timezone'"America/New_York"# http://worldtimeapi.org/timezones
}
就像C语言中创建一个类一样,如果你想增加一个条目就可以用例如'ssid' : 'home_wifi_network',这样添加,注意用(,)隔开。
(1)首先测试创建wifi热点
代码如下:
import wifi
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("ESP32-S3 AP Test")
print(f"My MAC address: {[hex(i) for i in wifi.radio.mac_address]}")
wifi.radio.start_ap(secrets["aio_username"], secrets["aio_key"])
while True:
pass
效果如下:
不过虽然能搜索并且连接上,却上不了网,不支持双通道的网络。
(2)测试wifi连接功能
本次测试通过连接手机热点进行网络连接测试最后通过时间获取网站获取时间进行打印输出。
代码如下:
import time
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
# URLs to fetch from
TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_QUOTES_URL = "https://www.adafruit.com/api/quotes.php"
JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython"
JSON_TIME_URL = "http://quan.suning.com/getSysTime.do"
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("ESP32-S2 WebClient Test\n")
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to {}!\n".format(secrets["ssid"]))
print("IP:", wifi.radio.ipv4_address)
print(f"My MAC address: {[hex(i) for i in wifi.radio.mac_address]}\n")
ping_ip = ipaddress.IPv4Address("8.8.8.8")
ping = wifi.radio.ping(ip=ping_ip) * 1000
if ping is not None:
print(f"Ping google.com: {ping} ms")
else:
ping = wifi.radio.ping(ip=ping_ip)
print(f"Ping google.com: {ping} ms")
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
time.sleep(6)
print(f"Fetching text from {TEXT_URL}")
response = requests.get(TEXT_URL)
print("-" * 40)
print(response.text)
print("-" * 40)
print()
time.sleep(6)
print(f"Fetching and parsing json from {JSON_TIME_URL}")
response = requests.get(JSON_TIME_URL)
print("-" * 40)
print(f"Time: {response.json()['sysTime2']}")
print("-" * 40)
print("Success")
while True:
response = requests.get(JSON_TIME_URL)
print(f"Time: {response.json()['sysTime2']}")
time.sleep(6)
效果如下:
4、实现任务三:控制WS2812B
控制WS2812B其实就是控制主板内置的RGB LED,可以使用CircuitPython代码来控制这个LED的颜色和亮度。
NeoPixel是WS281x系列可寻址RGB led,它包含三个led——一个红色的,一个绿色的和一个蓝色的——以及一个驱动芯片,它们可以单独使用(如板上的内置LED),也可以串联在一起使用,不过板载只有一个。
这里用到的主要是已经封装好的neopixel模块。
代码如下:
import time
import board
import neopixel
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.3
while True:
pixel.fill((255, 0, 0))
time.sleep(0.5)
pixel.fill((0, 255, 0))
time.sleep(0.5)
pixel.fill((0, 0, 255))
time.sleep(0.5)
以上代码是实现红绿蓝三色的循环展示,接下来我们实现一下呼吸灯的变化,代码如下:
import time
import board
import neopixel
from rainbowio import colorwheel
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.3
def rainbow(delay):
for color_value in range(255):
pixel[0] = colorwheel(color_value)
time.sleep(delay)
while True:
rainbow(0.03)
5、实现任务四:分任务1:日历&时钟
本任务其实是对显示和网络连接的一个组合,并对固定网站的回复进行解析显示,这次对之前的解析使用进行了一下升级,代码如下:
import time
import board
import rtc
import displayio
import ssl
import wifi
import socketpool
import adafruit_requests
import adafruit_ntp
from adafruit_display_text import label, wrap_text_to_lines
from adafruit_bitmap_font import bitmap_font
# 加载字库
font = bitmap_font.load_font("lib\sanjiruosixingkai_m_16.pcf")
color_Dimgray = 0x696969 # 昏灰
color_Gray = 0x808080 # 灰色
color_WhiteSmoke = 0xF5F5F5 # 白烟色
color_FireBrick = 0xB22222 # 耐火砖红
color_StrongRed = 0xE60000 # 鲜红
color_Persimmon = 0xFF4D40 # 柿子橙
color_FreshLeaves = 0x99FF4D # 嫩绿
color_LimeGreen = 0x32CD32 # 柠檬绿
color_TurquoiseBlue = 0x33E6CC # 绿松石蓝
color_DeepSkyBlue = 0x00BFFF # 深天蓝
color_Magenta = 0xFF00FF # 洋红
color_PaleVioletRed = 0xDB7093 # 灰紫红
# 初始化显示屏参数
display = board.DISPLAY
display.brightness = 0.25 # 更改亮度
display.rotation = 0 # 更改方向,0为横屏,90为竖屏
# 初始化参数
Time_cut = 0
# URLs to fetch from
JSON_TIME_URL = "http://quan.suning.com/getSysTime.do"
JSON_weather_URL = "http://t.weather.sojson.com/api/weather/city/{}"
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("ESP32-S2 Time&weather Test\n")
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
# 获取数据
def get_TimeData():
try:
response = requests.get(JSON_TIME_URL)
except ConnectionError as e:
print("Connection Error:", e)
print("Retrying in 60 seconds")
data = response.json()
network_time_str = data['sysTime2'][:19]
print(network_time_str)
date_str, time_part = network_time_str.split(' ')
year_str, month_str, day_str = date_str.split('-')
hour_str, minute_str, second_str = time_part.split(':')
print(month_str)
print(day_str)
time.sleep(5)
# 将拆分后的字符串转换为整数
year = int(year_str)
month = int(month_str)
day = int(day_str)
hour = int(hour_str)
minute = int(minute_str)
second = int(second_str)
network_time = time.struct_time((year, month, day, hour, minute, second, 0, 0, -1))
rtc.RTC().datetime = (
network_time.tm_year,
network_time.tm_mon,
network_time.tm_mday,
network_time.tm_hour,
network_time.tm_min,
network_time.tm_sec, 0, 0, -1
)
def get_WeatherData():
try:
city_code = "101091101" # 秦皇岛市的城市代码
url = JSON_weather_URL.format(city_code)
response = requests.get(url)
except ConnectionError as e:
print("Connection Error:", e)
print("Retrying in 60 seconds")
weather_data = response.json()
# print(dis_str)
return weather_data
while not wifi.radio.ipv4_address:
try:
wifi.radio.connect(secrets["ssid"], secrets["password"])
except ConnectionError as Error1:
print("Connection Error:", Error1)
print("Retrying in 3 seconds")
time.sleep(3)
print("IP:", wifi.radio.ipv4_address)
print("Connected!\n")
# 刷新显示
Time_str_area = label.Label(font, scale=1)
Time_str_area.x = 2
Time_str_area.y = 10
Time_str_area.color = color_WhiteSmoke
City_str_area = label.Label(font, scale=1)
City_str_area.x = 2
City_str_area.y = 30
City_str_area.color = color_LimeGreen
TodayWeather_str_area = label.Label(font, scale=1)
TodayWeather_str_area.x = 2
TodayWeather_str_area.y = 50
TodayWeather_str_area.color = color_TurquoiseBlue
# 刷新显示
def Allrefresh_screen1():
info_weather = get_WeatherData()
Time_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
citydata = info_weather['cityInfo']
City_str_area.text = ""+citydata['parent']+' '+citydata['city']
city_weather = info_weather['data']
TodayWeather_str_area.text = "今日天气"
TodayWeather_str_area.text += " 空气质量:" + city_weather['quality']
TodayWeather_str_area.text += '\n'+"温度:"+city_weather['wendu']
TodayWeather_str_area.text += ' pm2.5' + ":" + str(city_weather['pm25'])
TodayWeather_str_area.text += '\n'+"湿度:" + city_weather['shidu']
TodayWeather_str_area.text += ' pm10 ' + ":" + str(city_weather['pm10'])
splash = displayio.Group()
splash.append(Time_str_area)
splash.append(City_str_area)
splash.append(TodayWeather_str_area)
get_TimeData()
now = time.localtime()
display.show(splash)
Allrefresh_screen1()
while True:
time.sleep(0.1)
Time_cut = Time_cut + 1
if Time_cut % 10 == 0:
Time_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
if Time_cut % 600 == 0:
Allrefresh_screen1()
now = time.localtime()
之前是测试发现不能过于频繁的和目标网站要数据,例如时间,为了显示秒级,每秒索要直接导致被封,这一次通过内置RTC,在开机时获取一次时间,存储到内置RTC,后面的时钟就通过内部RTC提供。
通过长时间的尝试摸索,对不同类型的信息采用不同的颜色进行展示,效果如下图:
由于没有任何python基础,且没有相对测试教程,C语言的流程作祟,导致有一些细节总是不理想,例如内部RTC的应用,各种字符串以及变量之间的转换,不同颜色同屏显示刷新等等,花费了太多的试错成本,建议还是先学习一些相关基础。
三、多功能环境监测器的实现
多功能环境监测器主要是通过IIC接口外接BME680传感器(原本是BME688传感器,可是目前还没有对应的库,就只能和同事换了BME680,幸亏这两个相差不大)来获取环境数据,通过按键进行不同界面的切换,这个也是在日历&时钟任务基础的进一步扩展。
代码如下:
import time
import board
import rtc
import displayio
import digitalio
import touchio
import neopixel
import ssl
import wifi
import socketpool
import adafruit_requests
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
import adafruit_bme680
# 加载字库
font = bitmap_font.load_font("lib\sanjiruosixingkai_m_16.pcf")
color_Dimgray = 0x696969 # 昏灰
color_Gray = 0x808080 # 灰色
color_WhiteSmoke = 0xF5F5F5 # 白烟色
color_FireBrick = 0xB22222 # 耐火砖红
color_StrongRed = 0xE60000 # 鲜红
color_Persimmon = 0xFF4D40 # 柿子橙
color_FreshLeaves = 0x99FF4D # 嫩绿
color_LimeGreen = 0x32CD32 # 柠檬绿
color_TurquoiseBlue = 0x33E6CC # 绿松石蓝
color_DeepSkyBlue = 0x00BFFF # 深天蓝
color_Magenta = 0xFF00FF # 洋红
color_PaleVioletRed = 0xDB7093 # 灰紫红
TurquoiseBlue = (51, 0, 204)
DeepSkyBlue = (0, 191, 255)
# 初始化显示屏参数
display = board.DISPLAY
display.brightness = 0.25 # 更改亮度
display.rotation = 0 # 更改方向,0为横屏,90为竖屏
# 初始化RGB
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.3
# 初始化按键相关变量
button = digitalio.DigitalInOut(board.BUTTON)
button.switch_to_input(pull=digitalio.Pull.UP)
touch_Page1 = touchio.TouchIn(board.D5)
touch_Page1.threshold += 100
print(touch_Page1.threshold)
touch_Page2 = touchio.TouchIn(board.D10)
touch_Page2.threshold += 100
print(touch_Page2.threshold)
# 初始化参数
Time_cut = 0
ModFlag = 0
PageFlag = 1
CutEndFlag = 0
i2c = board.I2C() # uses board.SCL and board.SDA
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c, 0x76)
bme680.sea_level_pressure = 1013.25
# URLs to fetch from
JSON_TIME_URL = "http://quan.suning.com/getSysTime.do"
JSON_weather_URL = "http://t.weather.sojson.com/api/weather/city/{}"
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("ESP32-S2 Time&weather Test\n")
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
while not wifi.radio.ipv4_address:
try:
wifi.radio.connect(secrets["ssid"], secrets["password"])
except ConnectionError as Error1:
print("Connection Error:", Error1)
print("Retrying in 3 seconds")
time.sleep(3)
print("IP:", wifi.radio.ipv4_address)
print("Connected!\n")
# 获取数据
def get_TimeData():
try:
response = requests.get(JSON_TIME_URL)
except ConnectionError as e:
print("Connection Error:", e)
print("Retrying in 60 seconds")
data = response.json()
network_time_str = data["sysTime2"][:19]
print(network_time_str)
date_str, time_part = network_time_str.split(" ")
year_str, month_str, day_str = date_str.split("-")
hour_str, minute_str, second_str = time_part.split(":")
# print(month_str)
# print(day_str)
# time.sleep(5)
# 将拆分后的字符串转换为整数
year = int(year_str)
month = int(month_str)
day = int(day_str)
hour = int(hour_str)
minute = int(minute_str)
second = int(second_str)
network_time = time.struct_time((
year, month, day,
hour, minute, second, 0, 0, -1))
rtc.RTC().datetime = (
network_time.tm_year,
network_time.tm_mon,
network_time.tm_mday,
network_time.tm_hour,
network_time.tm_min,
network_time.tm_sec,
0,
0,
-1,
)
def get_WeatherData():
try:
city_code = "101091101" # 秦皇岛市的城市代码
url = JSON_weather_URL.format(city_code)
response = requests.get(url)
except ConnectionError as e:
print("Connection Error:", e)
print("Retrying in 60 seconds")
weather_data = response.json()
# print(dis_str)
return weather_data
# 刷新显示
Time_str_area = label.Label(font, scale=1)
Time_str_area.x = 2
Time_str_area.y = 10
Time_str_area.color = color_WhiteSmoke
TimePage2_str_area = label.Label(font, scale=1)
TimePage2_str_area.x = 2
TimePage2_str_area.y = 10
TimePage2_str_area.color = color_WhiteSmoke
TimeMod2_str_area = label.Label(font, scale=1)
TimeMod2_str_area.x = 2
TimeMod2_str_area.y = 10
TimeMod2_str_area.color = color_WhiteSmoke
City_str_area = label.Label(font, scale=1)
City_str_area.x = 2
City_str_area.y = 30
City_str_area.color = color_LimeGreen
CityPage2_str_area = label.Label(font, scale=1)
CityPage2_str_area.x = 2
CityPage2_str_area.y = 40
CityPage2_str_area.color = color_LimeGreen
TodayWeather_str_area = label.Label(font, scale=1)
TodayWeather_str_area.x = 2
TodayWeather_str_area.y = 50
TodayWeather_str_area.color = color_TurquoiseBlue
TomorrowWeather_str_area = label.Label(font, scale=1)
TomorrowWeather_str_area.x = 2
TomorrowWeather_str_area.y = 70
TomorrowWeather_str_area.color = color_DeepSkyBlue
Environmental_str_area = label.Label(font, scale=1)
Environmental_str_area.x = 2
Environmental_str_area.y = 10
Environmental_str_area.color = color_PaleVioletRed
# 刷新显示
def Allrefresh_screen1():
info_weather = get_WeatherData()
Time_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
TimePage2_str_area.text = "{:4}-{:2}-{:2}".format(
now.tm_year, now.tm_mon, now.tm_mday
)
citydata = info_weather["cityInfo"]
City_str_area.text = "" + citydata["parent"] + " " + citydata["city"]
CityPage2_str_area.text = " " + citydata["city"]
city_weather = info_weather["data"]
TodayWeather_str_area.text = "今日天气"
TodayWeather_str_area.text += " 空气质量:" + city_weather["quality"]
TodayWeather_str_area.text += "\n" + "温度:" + city_weather["wendu"]
TodayWeather_str_area.text += " pm2.5" + ":" + str(city_weather["pm25"])
TodayWeather_str_area.text += "\n" + "湿度:" + city_weather["shidu"]
TodayWeather_str_area.text += " pm10 " + ":" + str(city_weather["pm10"])
forecast = city_weather["forecast"]
TomorrowWeather_str_area.text = "明日预报" + " " + forecast[1]["type"]
TomorrowWeather_str_area.text += (
"\n最" + forecast[1]["high"] + " 最" + forecast[1]["low"]
)
Environmental_str_area.text = "环境监测 Tem:{:2}\nHum:{:3}".format(
int(bme680.temperature), int(bme680.relative_humidity)
)
# 时间+天气页面1
TimeWeaPage1 = displayio.Group()
TimeWeaPage1.append(Time_str_area)
TimeWeaPage1.append(City_str_area)
TimeWeaPage1.append(TodayWeather_str_area)
# 时间+天气页面2
TimeWeaPage2 = displayio.Group()
TimeWeaPage2.append(TimePage2_str_area)
TimeWeaPage2.append(CityPage2_str_area)
TimeWeaPage2.append(TomorrowWeather_str_area)
# 时间+天气页面2
EnvironmentalPage = displayio.Group()
EnvironmentalPage.append(Environmental_str_area)
get_TimeData()
now = time.localtime()
Allrefresh_screen1()
display.show(TimeWeaPage1)
while True:
time.sleep(0.1)
Time_cut = Time_cut + 1
if not button.value:
pixel.fill((255, 255, 255))
display.show(EnvironmentalPage)
ModFlag = 1
if touch_Page1.value:
pixel.fill(TurquoiseBlue)
display.show(TimeWeaPage1)
ModFlag = 0
if touch_Page2.value:
pixel.fill(DeepSkyBlue)
display.show(TimeWeaPage2)
ModFlag = 0
if Time_cut % 10 == 0:
Time_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
TimePage2_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
TimeMod2_str_area.text = "{:4}-{:2}-{:2} {:2}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday,
now.tm_hour, now.tm_min, now.tm_sec
)
# Environmental_str_area.text = "环境监测 Tem:{:2}\nHum:{:3}".format(
# int(bme680.temperature), int(bme680.relative_humidity)
# )
if ModFlag == 1:
Environmental_str_area.text = "环境监测 Pre:{:3}\nTem:{:2} Hum:{:3}\nGas:{:2}\nAlt :{:2}".format(
bme680.pressure,
int(bme680.temperature),
int(bme680.relative_humidity),
bme680.gas,
bme680.altitude
)
if Time_cut % 3000 == 0:
Allrefresh_screen1()
now = time.localtime()
环境数据的采集是通过BME680进行的,如下:
import adafruit_bme680
i2c = board.I2C() # uses board.SCL and board.SDA
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c, 0x76)
bme680.sea_level_pressure = 1013.25
这里我选择的BME680的地址为0x76,默认的是0x77,需要根据传感器地址配置引脚的状态去选择,刷新的速度大概4s,我将其放到了一个单独的界面去显示“环境监测”,效果如下:
在进行界面切换时注意,我们创建了不同Group进行页面的切换,而同一个str_area只能被一个Group使用,所以哪怕是同一个显示内容,在不同的显示页面也要创建不同的str_area。
四、代码下载
相对应的分步学习项目和总体的综合项目代码已经上传到下载中心,可点击搜索:
本帖最后由 秦天qintian0303 于 2023-10-1 22:36 编辑