【得捷电子Follow me第2期】CircuitPython入门到完成任务1-5详细教程
[复制链接]
本帖最后由 ttxiaotaoge 于 2023-8-24 11:19 编辑
本帖记录我这两天使用CircuitPython固件完成任务1-5的过程
可完成任务:
任务1:控制屏幕显示中文(必做任务)----- 完成屏幕的控制,并且能显示中文
任务2:网络功能使用(必做任务)----- 完成网络功能的使用,能够创建热点和连接到WiFi
任务3:控制WS2812B(必做任务)---- 使用按键控制板载Neopixel LED的显示和颜色切换
任务4:从下方5个分任务中选择1个感兴趣的完成即可(必做任务)
■ 分任务1:日历&时钟——完成一个可通过互联网更新的万年历时钟,并显示当地的天气信息
■ 分任务2:WS2812B效果控制——完成一个Neopixel(12灯珠或以上)控制器,通过按键和屏幕切换展示效果
任务5:通过网络控制WS2812B(可选任务,非必做)----- 结合123,在手机上通过网络控制板载Neopixel LED的显示和颜色切换,屏幕同步显示状态
先贴上实验现象给大伙看下,由于活动提供的板子还没到,只能用手头上现有的板子给大家演示,实际代码区别仅是部分IO的定义不同和显示的坐标不同,可以轻松移植
本套固件支持功能:
1. 代码运行后将自动联网,如联网成功会自动同步网络时间和获取天气信息并显示在LCD屏幕上,时间显示为经过同步的RTC内部时钟,天气地区配置在代码中设定,使用知心天气API
若未成功联网将会开启AP模式,默认为SSID为ESP32无密码。显示屏会显示当前网络模式和访问ip,同时显示本地时间(RTC内部时钟)
2. 电脑or手机在同一网络下(esp32连接同一个wifi or 连接esp32发出的wifi)访问‘ip’:1080/client 可进入WEB控制中心,该界面下可控制板载的RGB灯,控制灯光效果,LCD屏将同步显示当前RGB颜色配置。
3. 板载按键也可以控制灯光效果,同时将状态上报WEB控制中心
4. 支持web编辑界面,可直接访问IP进入相关circuitpyhon菜单。
相关演示如下:
演示
---
代码解析
先看下代码结构,主要为lib,fonts, code.py, settings.toml
其中lib里存放了我们需要的一些外部库(来源adafruit-circuitpython-bundle-8.x-mpy-20230815)
fonts中存放了我们用到的中文字体库(格式为pcf,通过fontforge和bdftopcf font converter (adafruit.github.io)进行转换),字体生成具体方法可参考官方教程Use FontForge | Custom Fonts for CircuitPython Displays | Adafruit Learning System
code.py就是我们的实际工作代码了。
settings.toml 将宏定义我们需要连接的wifi名称和密码(注意esp32只能连接2.4G频段的wifi),以及使用web编辑器的连接密码
下面贴下我们code.py中的源代码,并对他进行初步讲解。
import board
import digitalio
import terminalio
import busio
import neopixel
import rtc
import time
#网络相关库
import wifi
import socketpool
import ssl
import adafruit_requests
from adafruit_httpserver import Server, Request, Response, Websocket, GET
#显示相关库
import displayio
from adafruit_st7735r import ST7735R
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
# displayDriver_Begin
displayio.release_displays()
spi = busio.SPI(board.GPIO18, board.GPIO17)
tft_cs = board.GPIO14
tft_dc = board.GPIO15
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.GPIO16)
display = ST7735R(display_bus, width=128, height=160,rowstart=0,colstart=0,rotation=0,bgr = 1)#160 * 128
# display = ST7735R(display_bus, width=160, height=80,rowstart=0,colstart=24,rotation=270)#160 * 80
my_display_group = displayio.Group()
print("Display Init Success\n")
# displayDriver_End
# WiFi状态查询,未连接WIFI则不联网更新数据,开启AP模式
wificonnect_State = wifi.radio.connected
print("wificonnect_State",wificonnect_State)
if(wificonnect_State == False):
wifi.radio.start_ap(ssid = 'Esp32', password = '')
Wifi_Mode = 'STA' if(wificonnect_State == True) else 'AP'
Wifi_ip = wifi.radio.ipv4_address if(wificonnect_State == True) else wifi.radio.ipv4_address_ap
#构造函数——按键控制RGB
i = 0
Enter_flag = False
def Button0_Work():
global i
global Enter_flag
global RGBr, RGBg, RGBb
if(Button0.value == False and Enter_flag == False):
time.sleep(0.05)#延时消抖
if(Button0.value == False and Enter_flag == False):
i+=1
RGBr, RGBg, RGBb = 8*i, 8*i, 8*i
pixels.fill((RGBr, RGBg, RGBb))
Enter_flag = True
print("pixels change\n",i)
if(i>20):
i = 0
elif(Button0.value == True and Enter_flag == True):
time.sleep(0.05)#延时消抖
if(Button0.value == True and Enter_flag == True):
Enter_flag = False
#构造函数——解码天气
def parse_weather(weather_str):
# Extract the time part from the datetime string
area = weather_str['results'][0]['location']['name']
temp = weather_str['results'][0]['now']['temperature']
weather = weather_str['results'][0]['now']['text']
return area, temp, weather
#构造函数——解码时间
def parse_time(datetime_str):
# Extract the time part from the datetime string
date_str = datetime_str.split("T")[0]
time_str = datetime_str.split("T")[1].split(".")[0]
hour, minute, second = map(int, time_str.split(":"))
date = date_str
return hour, minute, second, date
#构造函数——更新网络数据
def Update_Data():
global r
global current_area, current_temp, current_weather
current_area = current_temp = current_weather = None
r = rtc.RTC()
if(wificonnect_State == True):
#WeatherUpdate Begin
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response_Weather = requests.get(WEATHER_DATA_SOURCE)
data_Weather = response_Weather.json()
response_Weather= requests.get(WEATHER_DATA_SOURCE)
response_Weather.socket.close()
current_area, current_temp, current_weather = parse_weather(data_Weather)
print("WeatherUpdate Success\n")
#WeatherUpdate End
#ClockTimeUpdate Begin
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response_clock = requests.get(TIME_DATA_SOURCE)
data_clock = response_clock.json()
response_clock= requests.get(WEATHER_DATA_SOURCE)
response_clock.socket.close()
current_hour, current_minute, current_second, current_date = parse_time(data_clock["datetime"])
r.datetime = time.struct_time((int(current_date[0:4]), int(current_date[5:7]), int(current_date[8:10]), \
current_hour, current_minute, current_second, 0, -1, -1))
print("ClockTimeUpdate Success\n")
#ClockTimeUpdate Begin
# RGBDriver_Begin
pixel_pin = board.GPIO10
num_pixels = 16
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.2, auto_write=True)
pixels.fill((8, 8, 8))
print("RGBDriver Init Success\n")
# RGBDriver_End
# ButtonDriver_Begin
Button0 = digitalio.DigitalInOut(board.GPIO0)
Button0.direction = digitalio.Direction.INPUT
print("Button0 Init Success\n")
# ButtonDriver_End
# 添加中文字体
font_file = "fonts/test16-16.pcf"
# font_file = "fonts/test16-16.bdf"
# font_file = "fonts/test24-24.bdf"
# font_file = "fonts/opposans_m_12.pcf"
# font_file = "fonts/sanjiruosixingkai_m_16.pcf"
font_Chinese = bitmap_font.load_font(font_file)
print("fonts Init Success\n")
# 显示WiFi IP 和 Mode
text_Wifi = "WiFi:{}\n{}".format(Wifi_Mode,Wifi_ip)#更新RGB状态
text_Wifi_title = Label(terminalio.FONT, text=text_Wifi, color = 0xffffff)
text_Wifi_title.x = 5
text_Wifi_title.y = 5
my_display_group.append(text_Wifi_title)
# 显示RGB状态,和按键状态,RGB文字同RGB灯颜色
RGBr, RGBg, RGBb = 255,255,255
text_RGB = "RGB:{:03}:{:03}:{:03}\nKEY:{:02}".format(RGBr, RGBg, RGBb, i)#更新RGB状态
text_RGB_title = Label(terminalio.FONT, text=text_RGB, color = (RGBr, RGBg, RGBb))
text_RGB_title.x = 5
text_RGB_title.y = 30
my_display_group.append(text_RGB_title)
# ShowChinese_End
# GetandSetHttp_Begin
TIME_DATA_SOURCE = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"
WEATHER_DATA_SOURCE = "http://api.seniverse.com/v3/weather/now.json?key=SBmmNRnXdiTFi8UQw&location=nanjing&language=zh-Hans&unit=c"
pool = socketpool.SocketPool(wifi.radio) # 网络管理,触发http操作
server = Server(pool, debug=True)
websocket: Websocket = None
next_message_time = time.monotonic()#数据更新时钟,以秒为单位变化
HTML_TEMPLATE = """
<html lang="en">
<head>
<!--注释:强制html使用utf-8编码-->
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Web控制中心</title>
</head>
<body>
<p>按键状态: <strong>-</strong></p>
<p>RGB 颜色: <input type="color"></p>
<script>
const Button_count = document.querySelector('strong');
const colorPicker = document.querySelector('input[type="color"]');
let ws = new WebSocket('ws://' + location.host + '/connect-websocket');
ws.onopen = () => console.log('WebSocket connection opened');
ws.onclose = () => console.log('WebSocket connection closed');
ws.onmessage = event => Button_count.textContent = event.data;
ws.onerror = error => Button_count.textContent = error;
colorPicker.oninput = debounce(() => ws.send(colorPicker.value), 200);
<!--注释:创建一个延时函数,消除RGB调整抖动-->
function debounce(callback, delay = 1000) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
callback(...args)
}, delay)
}
}
</script>
</body>
</html>
"""
#创建Http服务器
@server.route("/client", GET)
def client(request: Request):
return Response(request, HTML_TEMPLATE, content_type="text/html")
#创建websocket服务
@server.route("/connect-websocket", GET)
def connect_client(request: Request):
global websocket # pylint: disable=global-statement
if websocket is not None:
print("websocket.close")
websocket.close() # Close any existing connection
websocket = Websocket(request)
return websocket
# GetTimeandTemp_Beign
Update_Data()
# GetTimeandTemp_End
# DrawTimeandTemp_Beign
time_label_text = "{}-{:02}-{:02}\n"\
"时间:{:02}:{:02}:{:02}\n"\
"地区:{}\n"\
"温度:{}\n"\
"天气:{}\n".format(r.datetime.tm_year, r.datetime.tm_mon, r.datetime.tm_mday, \
r.datetime.tm_hour, r.datetime.tm_min, r.datetime.tm_sec, \
current_area, current_temp, current_weather)
time_label = Label(font = font_Chinese, text=time_label_text, color = 0xff00ff)
time_label.x = 10
time_label.y = 60
my_display_group.append(time_label)
# DrawTimeandTemp_End
#UI show Begin
display.show(my_display_group)
print("display show Success\n")
#UI show End
#判断网络模式,设定location.host
if(wificonnect_State == True):
server.start(str(wifi.radio.ipv4_address), port = 1080)
else:
server.start(str(wifi.radio.ipv4_address_ap), port = 1080)
# GetandSetHttp_Begin
time.sleep(2)#等待2秒进入循环
while True:
#httpSeverLoop_Begin
server.poll()
# Check for incoming messages from client
if websocket is not None:
if (data := websocket.receive(True)) is not None:
RGBr, RGBg, RGBb = int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)
pixels.fill((RGBr, RGBg, RGBb))
# Send a message every second
if websocket is not None and next_message_time < time.monotonic():
time.sleep(0.2)
if websocket is not None:
Button_count = i
websocket.send_message(str(Button_count))
next_message_time = time.monotonic() + 1
#httpSeverLoop_End
#Other_Loop
text_RGB_title.color = (RGBr, RGBg, RGBb)
text_RGB_title.text = "RGB:{:03}:{:03}:{:03}\nKEY:{:02}".format(RGBr, RGBg, RGBb, i)#更新RGB状态
time_label.text = "{}-{:02}-{:02}\n"\
"时间:{:02}:{:02}:{:02}\n"\
"地区:{}\n"\
"温度:{}\n"\
"天气:{}\n".format(r.datetime.tm_year, r.datetime.tm_mon, r.datetime.tm_mday, \
r.datetime.tm_hour, r.datetime.tm_min, r.datetime.tm_sec, \
current_area, current_temp, current_weather)
Button0_Work()
第一部分,添加需要的库文件和初始屏幕,如果是使用活动开发板的同学,可以直接在初始化的地方输入 display = board.DISPLAY即可
由于我使用的ST3375驱动的160x128的屏幕,所以需要添加如下代码
import board
import digitalio
import terminalio
import busio
import neopixel
import rtc
import time
#网络相关库
import wifi
import socketpool
import ssl
import adafruit_requests
from adafruit_httpserver import Server, Request, Response, Websocket, GET
#显示相关库
import displayio
from adafruit_st7735r import ST7735R
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
# displayDriver_Begin
displayio.release_displays()
spi = busio.SPI(board.GPIO18, board.GPIO17)
tft_cs = board.GPIO14
tft_dc = board.GPIO15
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.GPIO16)
display = ST7735R(display_bus, width=128, height=160,rowstart=0,colstart=0,rotation=0,bgr = 1)#160 * 128
# display = ST7735R(display_bus, width=160, height=80,rowstart=0,colstart=24,rotation=270)#160 * 80
my_display_group = displayio.Group()
print("Display Init Success\n")
# displayDriver_End
第二部分,判断WiFi状态并设定WiFi状态
# WiFi状态查询,未连接WIFI则不联网更新数据,开启AP模式
wificonnect_State = wifi.radio.connected
print("wificonnect_State",wificonnect_State)
if(wificonnect_State == False):
wifi.radio.start_ap(ssid = 'Esp32', password = '')
Wifi_Mode = 'STA' if(wificonnect_State == True) else 'AP'
Wifi_ip = wifi.radio.ipv4_address if(wificonnect_State == True) else wifi.radio.ipv4_address_ap
第三部分,构造封装一些我们需要用到的函数,为了让代码更加清晰
#构造函数——按键控制RGB
i = 0
Enter_flag = False
def Button0_Work():
global i
global Enter_flag
global RGBr, RGBg, RGBb
if(Button0.value == False and Enter_flag == False):
time.sleep(0.05)#延时消抖
if(Button0.value == False and Enter_flag == False):
i+=1
RGBr, RGBg, RGBb = 8*i, 8*i, 8*i
pixels.fill((RGBr, RGBg, RGBb))
Enter_flag = True
print("pixels change\n",i)
if(i>20):
i = 0
elif(Button0.value == True and Enter_flag == True):
time.sleep(0.05)#延时消抖
if(Button0.value == True and Enter_flag == True):
Enter_flag = False
#构造函数——解码天气
def parse_weather(weather_str):
# Extract the time part from the datetime string
area = weather_str['results'][0]['location']['name']
temp = weather_str['results'][0]['now']['temperature']
weather = weather_str['results'][0]['now']['text']
return area, temp, weather
#构造函数——解码时间
def parse_time(datetime_str):
# Extract the time part from the datetime string
date_str = datetime_str.split("T")[0]
time_str = datetime_str.split("T")[1].split(".")[0]
hour, minute, second = map(int, time_str.split(":"))
date = date_str
return hour, minute, second, date
#构造函数——更新网络数据
def Update_Data():
global r
global current_area, current_temp, current_weather
current_area = current_temp = current_weather = None
r = rtc.RTC()
if(wificonnect_State == True):
#WeatherUpdate Begin
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response_Weather = requests.get(WEATHER_DATA_SOURCE)
data_Weather = response_Weather.json()
response_Weather= requests.get(WEATHER_DATA_SOURCE)
response_Weather.socket.close()
current_area, current_temp, current_weather = parse_weather(data_Weather)
print("WeatherUpdate Success\n")
#WeatherUpdate End
#ClockTimeUpdate Begin
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response_clock = requests.get(TIME_DATA_SOURCE)
data_clock = response_clock.json()
response_clock= requests.get(WEATHER_DATA_SOURCE)
response_clock.socket.close()
current_hour, current_minute, current_second, current_date = parse_time(data_clock["datetime"])
r.datetime = time.struct_time((int(current_date[0:4]), int(current_date[5:7]), int(current_date[8:10]), \
current_hour, current_minute, current_second, 0, -1, -1))
print("ClockTimeUpdate Success\n")
#ClockTimeUpdate Begin
第四部分,相关模块的初始化,具体操作可以看代码中的相关注释
其中API获取网络天气和时间,需要根据自身的需求进行修改,包含知心天气的KEY和地区
TIME_DATA_SOURCE = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"
WEATHER_DATA_SOURCE = "http://api.seniverse.com/v3/weather/now.json?key=SBmmNRnXdiTFi8UQw&location=nanjing&language=zh-Hans&unit=c"
# RGBDriver_Begin
pixel_pin = board.GPIO10
num_pixels = 16
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.2, auto_write=True)
pixels.fill((8, 8, 8))
print("RGBDriver Init Success\n")
# RGBDriver_End
# ButtonDriver_Begin
Button0 = digitalio.DigitalInOut(board.GPIO0)
Button0.direction = digitalio.Direction.INPUT
print("Button0 Init Success\n")
# ButtonDriver_End
# 添加中文字体
font_file = "fonts/test16-16.pcf"
# font_file = "fonts/test16-16.bdf"
# font_file = "fonts/test24-24.bdf"
# font_file = "fonts/opposans_m_12.pcf"
# font_file = "fonts/sanjiruosixingkai_m_16.pcf"
font_Chinese = bitmap_font.load_font(font_file)
print("fonts Init Success\n")
# 显示WiFi IP 和 Mode
text_Wifi = "WiFi:{}\n{}".format(Wifi_Mode,Wifi_ip)#更新RGB状态
text_Wifi_title = Label(terminalio.FONT, text=text_Wifi, color = 0xffffff)
text_Wifi_title.x = 5
text_Wifi_title.y = 5
my_display_group.append(text_Wifi_title)
# 显示RGB状态,和按键状态,RGB文字同RGB灯颜色
RGBr, RGBg, RGBb = 255,255,255
text_RGB = "RGB:{:03}:{:03}:{:03}\nKEY:{:02}".format(RGBr, RGBg, RGBb, i)#更新RGB状态
text_RGB_title = Label(terminalio.FONT, text=text_RGB, color = (RGBr, RGBg, RGBb))
text_RGB_title.x = 5
text_RGB_title.y = 30
my_display_group.append(text_RGB_title)
# ShowChinese_End
# GetandSetHttp_Begin
TIME_DATA_SOURCE = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"
WEATHER_DATA_SOURCE = "http://api.seniverse.com/v3/weather/now.json?key=SBmmNRnXdiTFi8UQw&location=nanjing&language=zh-Hans&unit=c"
pool = socketpool.SocketPool(wifi.radio) # 网络管理,触发http操作
server = Server(pool, debug=True)
websocket: Websocket = None
next_message_time = time.monotonic()#数据更新时钟,以秒为单位变化
HTML_TEMPLATE = """
<html lang="en">
<head>
<!--注释:强制html使用utf-8编码-->
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Web控制中心</title>
</head>
<body>
<p>按键状态: <strong>-</strong></p>
<p>RGB 颜色: <input type="color"></p>
<script>
const Button_count = document.querySelector('strong');
const colorPicker = document.querySelector('input[type="color"]');
let ws = new WebSocket('ws://' + location.host + '/connect-websocket');
ws.onopen = () => console.log('WebSocket connection opened');
ws.onclose = () => console.log('WebSocket connection closed');
ws.onmessage = event => Button_count.textContent = event.data;
ws.onerror = error => Button_count.textContent = error;
colorPicker.oninput = debounce(() => ws.send(colorPicker.value), 200);
<!--注释:创建一个延时函数,消除RGB调整抖动-->
function debounce(callback, delay = 1000) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
callback(...args)
}, delay)
}
}
</script>
</body>
</html>
"""
#创建Http服务器
@server.route("/client", GET)
def client(request: Request):
return Response(request, HTML_TEMPLATE, content_type="text/html")
#创建websocket服务
@server.route("/connect-websocket", GET)
def connect_client(request: Request):
global websocket # pylint: disable=global-statement
if websocket is not None:
print("websocket.close")
websocket.close() # Close any existing connection
websocket = Websocket(request)
return websocket
# GetTimeandTemp_Beign
Update_Data()
# GetTimeandTemp_End
# DrawTimeandTemp_Beign
time_label_text = "{}-{:02}-{:02}\n"\
"时间:{:02}:{:02}:{:02}\n"\
"地区:{}\n"\
"温度:{}\n"\
"天气:{}\n".format(r.datetime.tm_year, r.datetime.tm_mon, r.datetime.tm_mday, \
r.datetime.tm_hour, r.datetime.tm_min, r.datetime.tm_sec, \
current_area, current_temp, current_weather)
time_label = Label(font = font_Chinese, text=time_label_text, color = 0xff00ff)
time_label.x = 10
time_label.y = 60
my_display_group.append(time_label)
# DrawTimeandTemp_End
#UI show Begin
display.show(my_display_group)
print("display show Success\n")
#UI show End
第五部分,在wile循环中更新屏幕的显示状态,和相应web控制中心的操作指令
while True:
#httpSeverLoop_Begin
server.poll()
# Check for incoming messages from client
if websocket is not None:
if (data := websocket.receive(True)) is not None:
RGBr, RGBg, RGBb = int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)
pixels.fill((RGBr, RGBg, RGBb))
# Send a message every second
if websocket is not None and next_message_time < time.monotonic():
time.sleep(0.2)
if websocket is not None:
Button_count = i
websocket.send_message(str(Button_count))
next_message_time = time.monotonic() + 1
#httpSeverLoop_End
#Other_Loop
text_RGB_title.color = (RGBr, RGBg, RGBb)
text_RGB_title.text = "RGB:{:03}:{:03}:{:03}\nKEY:{:02}".format(RGBr, RGBg, RGBb, i)#更新RGB状态
time_label.text = "{}-{:02}-{:02}\n"\
"时间:{:02}:{:02}:{:02}\n"\
"地区:{}\n"\
"温度:{}\n"\
"天气:{}\n".format(r.datetime.tm_year, r.datetime.tm_mon, r.datetime.tm_mday, \
r.datetime.tm_hour, r.datetime.tm_min, r.datetime.tm_sec, \
current_area, current_temp, current_weather)
Button0_Work()
至此,该代码就实现了我们follow me第二期的1到5的全部功能啦,是不是非常简单呢
在这两天的学习中还是遇到了很多小bug的,建议大家从以下几个官方网站获取API的相关教程
Adafruit Learning System 在这个网站中有大量的实战案例和api的使用教程,建议直接搜索需要用到的模块来参考示例,如直接搜索fonts来了解如何生成字体库
CircuitPython — Adafruit CircuitPython 8.2.0 documentation 这个是circuitpyhon固件的使用文档,可以在里面找到所有api的使用说明,了解其传入传出的参数及配置先后顺序
这个官方仓库中包含了我们需要用到的所有库文件以及对应的例程,可以参考其中的内容进行代码的编写和集成。
这次分享就到这里了,在最后附上我们本次工程的全部代码供大家参考。(活动提供的板子,板载内存可以使用的空间只有2M左右,请按照需求添加lib和fonts文件)
|