TL;DR
在ESP32-S3-BOX-lite上,使用CircuitPython 位图显示的方法,从互联网获取信息,浏览书名和展示图书简介的项目
项目介绍、设计思路、简单的硬件介绍
Funpack-5 主要硬件是ESP32-S3-BOX-Lite,在我看来,和S3-BOX 只有触摸屏与按键的这一个区别比较大,静音键什么的都没太大所谓。
软件平台上,当然电子森林也在推荐大家使用ESP-IDF的开放方式,从任务一和任务二来说,站在乐鑫巨人的肩膀上来二次开发,省时省力,而且图形界面,乐鑫已经集成好了LVGL 。事情总是不会很完美,因为对C/C++/C#的不熟悉,对我而言,还是吃力很多,所以不得不把视线投到Python阵营。
MicroPython (以下简称MPY)和CircuitPython(以下简称CPY)。其中CPY,2017 年 1 月,Adafruit 推出了CircuitPython,这是MicroPython编程语言的一个分支,经过优化,可在选定的 Adafruit 产品上运行。2019 年,CircuitPython 的资源转移到了 circuitpython.org,此举表明使用 CircuitPython 的第三方电路板数量已经超过了仅由 Adafruit 制造的电路板数量。这包括用于微控制器的 CircuitPython 和使用名为“Blinka”的兼容层 Adafruit的单板计算机上的CircuitPython,以访问通用输入/输出功能以及与 160 多个传感器和驱动程序库的兼容性。
软件流程图及各功能对应的主要代码片段及说明
- 整个项目入口文件时code.py, 主要包含show_gird_screen() 入口函数;show_detail_screen() 显示页函数。
- project_details.py 主要时辅助函数,用于抓取需要的内容。
- project_btn_detect.py 主要是 用于正面三个按键的检测,此处感谢微信群里的小伙伴们的帮助,因为按键并不是分别连接不同的GPIO,而是都接入了GPIO1,根据不同电压的大小来判断。
- project_constants.py 主要是项目使用的常量。
运行时和Wi-Fi
因为CPY提供了在线的固件刷入服务和Wi-Fi 连接配置,即便没有任何编程经验的人也可以,轻松过完成.
https://circuitpython.org/downloads?q=S3+BOX
固件刷入后,可以网页编辑Wi-Fi信息,也可以手动编辑 settings.toml
CIRCUITPY_WIFI_SSID = "WiFi_SSID"
CIRCUITPY_WIFI_PASSWORD = "12345678"
CIRCUITPY_WEB_API_PASSWORD = "PASSWORD"
CIRCUITPY_WEB_API_PORT = 80
其中,前两行是Wi-Fi信息,后两行是网页编程器的密码和访问端口,等BOX-Lite成功连接网络后,屏幕会显示获取的IP,这时可以在同一网络下访问IP+端口,并使用API_PASSWORD 可以开启网页编辑器。
文本显示的主要函数
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
项目的显示文字部分主要使用这两个外部库,字体建议使用转化后的pcf格式字体,加载会更快。因为阅读器项目,中文字体的字符又非常多,所以,对MCU的性能也是一种挑战。在项目中,使用了得以黑作为一级菜单的字体,谷歌开源的字体作为二级详情页的字体。
首页书籍字段获取部分代码解释
def get_homepage_books():
grid_url = WEBSITE_URL + GRID_PATH
# make a GET request to the URL and store the response
session = session_init()
if session:
response = session.get(grid_url)
html_content = response.text
response.close()
pattern = r'<div class="col-sm-4 col-md-2">\s*<a href=".+?/([^/]+?)">\s*<div class="thumbnail">\s*<img src="(.+?)" alt="(.+?)" style=".+?">'
matches = []
match = re.search(pattern, html_content)
while match:
matches.append(match.groups())
html_content = html_content[match.end():]
match = re.search(pattern, html_content)
return matches if matches else ['获取列表失败,请尝试重启']
else:
return ['网络未连接,请检查settings.toml,\n或按左侧下键重启']
初始化完Session 后,请求页面,然后re.search 正则表达式获取需要的信息,返回一个类似
[('934', '/bp/e9be6e2e-daa7-4e54-b727-f11b743b1f88.jpg', '当代中国的精神力量'), ('932', '/bp/deb215f2-caae-40b8-82fb-3d54e6116441.jpg', '怒航'),...]
分别代表,细节页的URI ,图片URL,书名
对应的在code.py中负责显示的函数:
def show_gird_screen(page_id=0):
gc.collect()
global homepage_info_list
if homepage_info_list:
result_list = homepage_info_list
else:
result_list = get_homepage_books()
homepage_info_list = result_list
available_item_amount = len(result_list)
if available_item_amount == 1:
error_msg = result_list[0]
display.show(text_area_display(error_msg,font_S_20,True))
raise SystemExit
# to-do 多页
# show_pages_amount = math.ceil(available_item_amount / SCREEN_ONEPAGE_LINE_COUNT)
current_index_id = page_id
display.show(text_area_display(formate_grid(result_list[current_index_id][2]),font_S_20,True)
while True:
前半段是,判断是否已经已经获取,尤其是从细节页面返回的时候,就不重复获取
display.show()默认开机显示第一条记录, while True: 后为按键判断逻辑。
显示效果为
详情页显示部分代码解释
初始化部分和首页类似
def get_details_by_id(book_id):
gc.collect()
book_url = WEBSITE_URL + DETAIL_PATH + str(book_id)
‘’‘
省略 Session初始化和Htm获取代码
’‘’
if div_start != -1 and ul_start != -1 and ul_end != -1:
ul_content = html_content[ul_start: ul_end]
# 提取标题
title_start = ul_content.find('<h3>')
title_end = ul_content.find('</h3>', title_start)
title = ul_content[title_start + 4: title_end]
# 提取其他元素
publish_time_start = ul_content.find('发布时间:', title_end) + 5
publish_time_end = ul_content.find('</li>', publish_time_start)
publish_time = ul_content[publish_time_start: publish_time_end]
views_start = ul_content.find('浏览次数:', publish_time_end) + 5
views_end = ul_content.find('</li>', views_start)
views = ul_content[views_start: views_end]
authors_start = ul_content.find('作者:', views_end) + 3
authors_end = ul_content.find('</li>', authors_start)
authors = ul_content[authors_start: authors_end]
summary_start = ul_content.find('内容简介:', authors_end) + 5
summary_end = ul_content.find('</li>', summary_start)
summary = ul_content[summary_start: summary_end]
summary = re.sub(r'(&#\d+;)|(<.*?>)', '', summary)
summary = '内容简介:'+summary
summary = add_newline(summary)
text_info = '《'+title + '》\n作者:'+ authors + '\n发布时间:'+ publish_time +' 浏览次数:'+ views +'\n'+ summary +'(完)'
return split_string_by_lines(text_info)
返回的结果样例为:
(3, ['《时代广场的蟋蟀》\n作者:(美)乔治&#8226;塞尔登\n发布时间:2015/12/9 浏览次数:24330\n内容简介:柴斯特,一只出生在康涅狄克州\n草原的蟋蟀,因为贪吃,被意外带到了纽约\n时代广场的地铁站。乡下来的小蟋蟀第一次', '出远门,就来到了全世界最繁华的地方。在\n这个全然陌生的世界,他幸运地被一个好心\n的男孩,报摊老板的儿子玛利欧收养,并与\n两个地铁站里的老住户——亨利猫和塔克鼠\n成为了好朋友。柴斯特有着惊人的音乐天赋\n,他“演奏”的美妙音乐,打动了听过的每', '个人,成为了纽约最受瞩目的明星,改变了\n玛利欧一家人贫穷的生活。令人意外的是,\n柴斯特收获了那么多的掌声和鲜花,依旧念\n念不忘家乡的自由生活,于是他告别了朋友\n们,踏上了回家之路。\n(完)'])
在内容介绍部分,增加了‘/n’换行符来适应屏幕宽度。
效果如下:
上下键翻页,中间键返回首页
按键判断逻辑代码解析
以详情页为例
new_v = round((project_analog_in.value * 3.3) / 65536, 1)
if check_button(new_v, 2.4):
# print('left btn')
if current_page_id - 1 >= 0:
current_page_id -= 1
print(current_page_id)
display.show(text_area_display(book_content[0][current_page_id]))
elif check_button(new_v, 2.0):
print('detail mid btn')
print('detail back pass_current_index_id',pass_current_index_id)
time.sleep(0.18)
show_gird_screen(pass_current_index_id)
# use grid list
elif check_button(new_v, 0.8):
print('right btn')
if current_page_id + 1 < book_pages:
current_page_id += 1
print(current_page_id)
display.show(text_area_display(book_content[0][current_page_id]))
可以看到ADC检查电压的方法来判断三个键是否被按下,左右键相对简单直接改变显示字段的ID 来变化内容,中间键绑定不同的函数,实现返回或者进入。
其他部分
因为CPY 没有beautifulSoup类的解析html的库,只能使用正则表达式来解析内容,在项目的前后使用了两种解析方法,其中一种参考了ChatGPT的回答
pattern = r'<div class="col-sm-4 col-md-2">\s*<a href=".+?/([^/]+?)">\s*<div class="thumbnail">\s*<img src="(.+?)" alt="(.+?)" style=".+?">'
matches = []
match = re.search(pattern, html_content)
可以看到HTML解析部分,使用了另外一种方法,更为直观,比首页字段获取的一行式正则表达式。
此外,Session的构造也很重要
def session_init():
try:
# configure the WiFi connection
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))
except ConnectionError:
print('ConnectionError')
return False
# configure the SSL context and socket pool
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
# create an instance of the Adafruit Requests session
session = adafruit_requests.Session(pool, ssl_context=context)
return session
功能展示及说明
首页
详情页
更多详见视频
值得分享的资料:
字体BDF转PCF 网站 https://adafruit.github.io/web-bdftopcf/
个人总结:
第一次参加电子森林的活动,不管是项目和这篇报告,都还有很多值得提升的地方。这个项目给我最大的感受,是如何使用ESP32-S3这个MCU,不断的妥协和更换解决办法,来实现目标的这个过程,简单来说痛苦又快乐。因为在PC项目上有着大量知名的库,可以直接使用。也可能因为水平有限,在MCU上总是束手束脚,代码写的也很不优雅,还有很多需要改进的地方,时间的原因没办法完成,包括:
- 将详情页保存到本地,不用每次都抓取
- 加一个加载loading的界面,不用每次干等
- 增加图书图片作为背景
- ...
再次感谢电子森林和得捷电子,以为微信群里的各位小伙伴。谢谢和大家一起积极帮助和相互学习。