1.项目需求
本项目制作的是Funpack第二季第五期任务一,项目要求如下:
使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。
2.完成的功能
- 交互式WIFI连接,自动弹出配置二维码,扫描后自动进入配置页面输入WIFI信息进行联网;
- 能够显示当前时间、天气和随机一言,并按时自动更新实时数据;
- 具备离线语音唤醒、离线语音识别控制功能,可语音控制模拟LED灯开关;
- 具备离线TTS语音播报,可播报语音识别情况和当前天气信息。
3.未实现功能或计划实现功能
- B站粉丝数获取和播报;
- 在线设置界面,实现参数的网页更改功能。
4.主要实现思路
ESP32-S3 BOXLITE本身具备ESP32S3模块,同时提供了屏幕、按键、麦克风和喇叭,官方提供了较为完备的硬件驱动,可以在官方驱动例程上进行修改移植,其中屏幕显示使用LVGL库进行绘制,官方已进行了移植不用再进行移植,使用ESPIDF和VSCODE进行编程,更加贴近底层,并且由于语音唤醒和TTS功能都是基于ESPIDF进行实现,该任务最好不要使用arduino或者python来实现。
首先先进行联网。联网不成功则进入配网模式,用户连接ESP自带的WIFI后弹出配置网页输入相应的账号密码,系统保存后重启再尝试连接。当连接成功后系统分为几个线程,分别是显示线程、语音识别线程、系统更新线程、状态监控线程,为了保证语音识别的时效性和准确性,并且充分发挥出ESP32S3双核的优势,语音识别线程在核1上运行,其他程序在核0上运行,具体的运行流程图如下所示。
5.主要实现步骤说明
1.sdkconfig配置要点
1.1 Partition必须自定义,因为涉及到识别模型、字库的位置。
本文将16M flash划分为多个分区,具体分区大小、起始地址和功能如图所示
1.2 配置语音唤醒和语音命令
这里配置的唤醒词是(hi,乐鑫),命令主要有用的是开灯、关灯、天气,需要注意对应的ID,这里的命令ID和程序的ID一一对应
1.3 启用PSRAM
非常重要,因为内部ram完全不够,并且需要将wifi堆栈也尽量放入psram中
1.4 http server配置(注意最大长度改成1024)
1.5 LVGL设置
LVGL configuration → Font usage中启用这两项,因为将大字库放在了FLASH中
2.功能实现-交互式WIFI连接
本功能主要参考了强制门户参考中的强制门户功能
主要实现流程是,首先读取存储位置的WIFI信息,如果有信息就用该信息联网,如果没有就启动配网模式,或者使用存储的信息在一定时间内未能联网也进入配网模式,配网模式会弹出二维码,扫码连接或者直接连接,连接成功一般会通过强制门户功能自动弹出配网页面,输入wifi信息后系统回保存wifi信息并重启尝试连接。
3.功能实现-时间等信息显示
3.1时间采用NTP对时,采用LVGL的定时器每隔2s检测时间是否变化,有变化则更新时间,注意尽量使用LVGL本身定时器,虽然定时可能不准确,但是能够在定时器内部安全的更新显示。
3.2天气采用的是和风天气,与目前网上常用的心知天气相比,能输入城市名自动获取城市id,不用再手动查找城市id,并且获取的信息比心知也多。缺点是返回数据采用gzip压缩,必须进行解压,一般使用zlib进行解压,但是我找到了一个更加轻量化的库uzlib(uzlib),并且做了移植,具体实现可以看程序中的UZlibdecompress函数。
3.3 一言采用的是hitokoto网站(hitokoto)提供的数据
3.4 由于涉及到https连接,espidf提供的https例程过于繁琐,实际使用的时候体验非常不好,而且非常慢,这里使用的ESP32 HTTP Client接口进行连接,更加快,具体可以参考(ESP32 HTTP Client例程)
4.功能实现-LVGL字库
理论上来说lvgl也可以直接作为C文件写在程序中,但是由于个人喜欢printf调试,因此如果放在c文件中,每次都会多下载1M左右大小的程序文件,非常浪费时间,因此将两个较大的字库放入flash其他位置中,这里主要参考了(lvgl字库放入flash例程),为了尽可能减少占用,将字体文件放在了flash的末尾处,字体只选用了常用的3000字和英文中文符号(常用字和开源字体参考)。制作字体包括16和20大小,实际使用下来,3000常用字完全能满足需求,不建议全部文字。
5.功能实现-语音唤醒和命令
语音唤醒和命令方面,官方提供了相当成熟的例子,只需要进行稍微的修改便可以进行使用,具体参考了(语音唤醒官方例程),参考的例子是(cn_speech_commands_recognition),不过需要注意的是可能会出现( 'HTTPResponse' object has no attribute 'strict'的错误,参考https://github.com/espressif/esp-idf/issues/11340解决,具体解决方法是进入到xxxx\.espressif\python_env\idf4.4_py3.8_env\Scripts,idf虚拟的python环境中,运行pip install -U "urllib3<2" 即可)。但是由于esp-skainet还没有支持boxlite,因此需要做一定改动,替换掉音频采集部分,其他保持原样即可。
需要注意的是由于为了保证实时性,语音采集和监测是放在一个核上,同时任务优先级一样,也就是freertos会将两个任务以时间片形式运行,任务中是没有延时的,因此建议不要在这个核上运行其他任务。
6.功能实现-TTS语音播报
语音播放,官方提供了一个简单的例子(chinese_tts),例子非常简单,我进行了一定的封装,并且由于采集音频和播放音频都是用一个I2S,因此需要考虑互斥量来防止出错,TTS需要一个voice data模型才能使用,这里采用预先烧录模型方式,官方提供的烧录方式是linux的,我改成了win下的cmd一句话,具体为
(Xxxx\.espressif\python_env\idf4.4_py3.8_env\Scripts\python xxxx\esp-idf\components\esptool_py\esptool\esptool.py --port "COM6" --chip "esp32s3" --before default_reset --after hard_reset write_flash --flash_mode dio --flash_freq 40m --flash_size detect 0x7ff000 "esp_tts_voice_data_xiaoxin.dat") (注意xxx替换为自己的espidf地址,COM6替换为自己的端口,0x0x7ff000替换为自己在Partition中定义的voice_data地址,esp_tts_voice_data_xiaoxin.dat替换为你所选的模型)
模型可以去这里下载(模型下载地址),我采用的是这个模型esp_tts_voice_data_xiaoxin.dat
需要注意的是由于boxlite是双声道芯片,但是生成的是单通道数据,如果直接把数据给芯片,会导致输出的声音很尖很快,且通过语速调整基本不起作用。因此这里需要将数据二倍话,也就是把原来的例如123数据变成112233数据,具体实现程序如下
6.遇到的问题和解决
1.在编写程序时使用了较多的即时分配内存的方式,然后写的语音唤醒和监测任务不运行,系统也并未提示内存错误等信息,是直接不运行任务,排查了很久,最后考虑可能是内存不足导致,尽可能将内存移动至PSRAM后,任务正常开始运行,目前程序内部内存剩余仅剩20多KB,可见语音功能对ESP32的资源占用还是挺大的。
2.初期经常出现奇怪的死机花屏,使用串口参看会发现停在摸一个位置不再执行,非常奇怪,因此我在某些关键位置插入了部分无意义的printf,但是居然可以跑通,非常的奇怪....
3.由于boxlite是双声道芯片,但是生成的是单通道数据,如果直接把数据给芯片,会导致输出的声音很尖很快,且通过语速调整基本不起作用。因此这里需要将数据二倍话,也就是把原来的例如123数据变成112233数据。这个问题也是排查了很久,开始以为设置错了,后来看原理图才知道原来是双声道的芯片。
7.总结和注意
离上次参加已经过去了两年时间了。很感谢硬禾学堂提供的这个平台和机会,只有实践才能促进进步,只有实践才能发现问题并解决问题!
注意:
复刻的话很重要的一点,本软件完全在esp-box框架下运行,版本是0.3.0,需要将components里面的esp-sr替换为最新的,然后将程序放入examples中才可以正常运行,字体文件必须下载到指定位置,比如我的一个是放在0xc80000开头的,一个是放在0xc80000+0x5C000开头的,使用flash_download_tool进行下载,参考功能实现-LVGL字库,整体来说,本项目复刻有一定难度,需要在充分理解基础上去实现。
可以直接下载全部合一这个bin,地址从0x0开始,tts模型需要自己下载,源码阅读从esp-box\examples\u_lv_demos\main\lv_demos.c开始
由于文件过大,附件不能上传,因此存在网盘,链接:链接: https://pan.baidu.com/s/153va-HbWA23gxL-BV6U-6g 提取码: uahi
解压密码为《Funpack2-5》