大家好!我来介绍一下参加本次FUNPACK活动的项目:基于乐鑫SmartConfig的智能配网诗词助手。项目的思路如下:
- 使用乐鑫的smart config智能配网功能来发送连接互联网所需要的SSID和PASSWORD;
- 使用乐鑫的IDF框架下的WIFI组件来实现连接互联网;
- 使用乐鑫的IDF框架下的HTTPS组件并使用今日诗词API来获取JSON格式的诗词;
- 使用乐鑫的IDF框架下的cJSON组件来解析Json格式数据并存储在UTF-8数据中;
- 使用乐鑫的skainet框架下的TTS功能来朗读步骤四中的数据
经过上述步骤,即可在任意的网络环境中都可以让ESP-BOX-LITE化身为可联网的诗词小助手。程序主流程图如下:
一、硬件介绍
ESP32-S3-BOX-Lite搭载 ESP32-S3 AI SoC,在芯片内置的 512 KB SRAM 之外,还集成了16 MB QSPI flash 和 8 MB Octal PSRAM。它板载一块2.4 寸显示屏(分辨率 320 x 240),双麦克风,一个扬声器和两个用于硬件拓展的 Pmod™ 兼容接口;采用 Type-C USB 连接器,提供 5 V 电源输入和串口/JTAG 调试接口。ESP32-S3-BOX-Lite的优秀表现离不开其主控芯片ESP32-S3,ESP32-S3是一款低功耗的 MCU 系统级芯片 (SoC),支持 2.4 GHz Wi-Fi 和低功耗蓝牙 (Bluetooth® LE) 双模无 线通信。芯片集成了高性能的 Xtensa® 32 位 LX7 双核处理器、超低功耗协处理器、Wi-Fi 基带、蓝牙基带、RF 模块以及外设,其功能框图如下所示:
二、基于smart config的联网功能开发
乐鑫自主研发的 ESP-TOUCH 协议采用的是 Smart Config(智能配置)技术来帮助用户将嵌入了 ESP8266EX 的设备连接至 Wi-Fi 网络。用户只需在手机上进行简单操作即可实现智能配置。
下载乐鑫官方App:
上面是手机app端,下面简要介绍一下wifi初始化以及代码层面如何使用smart config一键配网功能。
(1)初始化LwIP:创建LwIP核心任务并初始化与LwIP相关的工作。下面的函数在代码中由static void initialise_wifi(void)调用。
static void initialise_wifi(void) --- > ESP_ERROR_CHECK(esp_netif_init());
(2)初始化事件。Wi-Fi驱动程序会将事件发送到默认事件循环。应用程序可以在相应的回调函数中处理这些事件。
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
ESP_ERROR_CHECK( esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL) );
ESP_ERROR_CHECK( esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL) );
ESP_ERROR_CHECK( esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL) );
(3)初始化Wi-Fi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
(4)配置ESP32S3-BOX-LITE为STA模式,并启动Wi-Fi.
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK( esp_wifi_start() );
(5)等待Wi-Fi驱动程序上报相应的Wi-Fi事件,并在回调函数中进行处理。
在步骤四中,启动了Wi-Fi,因此根据下面的代码,会创建一个名称为“smartconfig_example_task”的任务。在这个任务里,会进行基于ESP-Touch技术的smart config一键配网功能的实现。
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
xTaskCreate(smartconfig_example_task, "smartconfig_example_task", 4096, NULL, 3, NULL);
}
.......
static void smartconfig_example_task(void * parm)
{
EventBits_t uxBits;
ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH) );
smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
ESP_ERROR_CHECK( esp_smartconfig_start(&cfg) );
while (1) {
uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
if(uxBits & CONNECTED_BIT) {
ESP_LOGI(TAG, "WiFi Connected to ap");
}
if(uxBits & ESPTOUCH_DONE_BIT) {
ESP_LOGI(TAG, "smartconfig over");
esp_smartconfig_stop();
vTaskDelete(NULL);
}
}
}
在没有打开手机app进行配网的时候,控制台输出窗口如下:
I (0) cpu_start: App cpu up.
I (302) cpu_start: Pro cpu start user code
I (302) cpu_start: cpu freq: 160000000
I (302) cpu_start: Application information:
I (305) cpu_start: Project name: smart_config
I (310) cpu_start: App version: 1
I (315) cpu_start: Compile time: Jun 24 2023 10:35:56
I (321) cpu_start: ELF file SHA256: 0dc4089d76d58dab...
I (327) cpu_start: ESP-IDF: v4.4.4-dirty
I (332) heap_init: Initializing. RAM available for dynamic allocation:
I (339) heap_init: At 3FCA2348 len 000473C8 (284 KiB): D/IRAM
I (346) heap_init: At 3FCE9710 len 00005724 (21 KiB): STACK/DRAM
I (352) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (359) heap_init: At 600FE000 len 00002000 (8 KiB): RTCRAM
I (365) spi_flash: detected chip: gd
I (369) spi_flash: flash io: qio
I (374) sleep: Configure to isolate all GPIO pins in sleep state
I (380) sleep: Enable automatic switching of GPIO sleep configuration
I (387) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (409) I2S: DMA Malloc info, datalen=blocksize=1280, dma_buf_count=6
I (409) I2S: DMA Malloc info, datalen=blocksize=1280, dma_buf_count=6
I (419) I2S: I2S1, MCLK output by GPIO2
I (419) gpio: GPIO[46]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (449) pp: pp rom version: e7ae62f
I (449) net80211: net80211 rom version: e7ae62f
I (459) wifi:wifi driver task: 3fced6f4, prio:23, stack:6656, core=0
I (459) system_api: Base MAC address is not set
I (459) system_api: read default base MAC address from EFUSE
I (529) wifi_init: udp mbox: 6
I (529) wifi_init: tcp mbox: 6
I (529) wifi_init: tcp tx win: 5744
I (539) wifi_init: tcp rx win: 5744
I (539) wifi_init: tcp mss: 1440
I (549) wifi_init: WiFi IRAM OP enabled
I (549) wifi_init: WiFi RX IRAM OP enabled
I (559) phy_init: phy_version 540,a5d905b,Oct 20 2022,19:36:11
I (599) wifi:mode : sta (f4:12:fa:d6:6d:64)
I (599) wifi:enable tsf
I (599) Inside app_main: good job
I (649) smartconfig: SC version: V3.0.1
I (5459) wifi:ic_enable_sniffer
I (5459) smartconfig: Start to find channel...
I (5459) smartconfig_example: Scan done
I (10599) Inside app_main: good job
I (20599) Inside app_main: good job
在通过ESP-TOUCH成功配网后,控制台输出如下:
I (110599) Inside app_main: good job
I (120599) Inside app_main: good job
I (127379) smartconfig: TYPE: ESPTOUCH
I (127379) smartconfig: T|AP MAC: 20:6b:e7:bb:8c:3b
I (127379) smartconfig: Found channel on 11-2. Start to get ssid and password...
I (127389) smartconfig_example: Found channel
I (130599) Inside app_main: good job
I (135559) smartconfig: T|pswd: xxx
I (135569) smartconfig: T|ssid: TP-LINK_168
I (135569) smartconfig: T|bssid: 20:6b:e7:bb:8c:3b
I (135569) wifi:ic_disable_sniffer
I (135569) smartconfig_example: Got SSID and password
I (135579) smartconfig_example: SSID:TP-LINK_168
I (135579) smartconfig_example: PASSWORD:xxxxxx
I (135589) wifi:new:<11,2>, old:<11,2>, ap:<255,255>, sta:<11,2>, prof:1
I (135589) wifi:state: init -> auth (b0)
I (135599) wifi:state: auth -> assoc (0)
I (135609) wifi:state: assoc -> run (10)
I (135619) wifi:connected with TP-LINK_168, aid = 6, channel 11, 40D, bssid = 20:6b:e7:bb:8c:3b
I (135619) wifi:security: WPA2-PSK, phy: bgn, rssi: -44
I (135619) wifi:pm start, type: 1
I (135619) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 0, mt_pti: 25000, mt_time: 10000
I (135629) wifi:<ba-add>idx:0 (ifx:0, 20:6b:e7:bb:8c:3b), tid:0, ssn:0, winSize:64
I (135669) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (136629) esp_netif_handlers: sta ip: 192.168.0.106, mask: 255.255.255.0, gw: 192.168.0.1
I (136629) smartconfig_example: Hi Debug: got ip:192.168.0.106
I (136629) smartconfig_example: WiFi Connected to ap
至此,已经实现了通过ESP-TOUCH的智能配网功能,让ESP-BOX-LITE连接到了互联网上。接下来会通过HTTPS组件来获取json格式的诗词数据。
三、通过HTTPS组件获取json格式诗词
在事件回调函数中,设计了如果获取IP地址成功后,会创建一个新的任务:poem_read_task。
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "Hi Debug: got ip:" IPSTR, IP2STR(&event->ip_info.ip));
xTaskCreate(poem_read_task, "poem_read_task", 4096, NULL, 3, NULL);
}
static void poem_read_task(void * parm)
{
VoiceSetInit();
SpeakThisText("欢迎使用在线诗词小助手");
vTaskDelay(pdMS_TO_TICKS(2000));
while (1) {
ESP_LOGI("Inside poem_read_task", "well done");
http_native_request();
vTaskDelay(pdMS_TO_TICKS(1*10*1000));
}
}
一个主要的目的是每10s中获取一次诗词,并朗读。
static void http_native_request(void)
{
memset(output_buffer, 0, sizeof(output_buffer));
//char output_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0}; // Buffer to store response of http request
int content_length = 0;
esp_http_client_config_t config = {
.url = "http://v1.jinrishici.com/all.json",
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// GET Request
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG_HTTP_CLIENT, "Failed to open HTTP connection: %s", esp_err_to_name(err));
} else {
content_length = esp_http_client_fetch_headers(client);
if (content_length < 0) {
ESP_LOGE(TAG_HTTP_CLIENT, "HTTP client fetch headers failed");
} else {
int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
if (data_read >= 0) {
ESP_LOGI(TAG_HTTP_CLIENT, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
//ESP_LOG_BUFFER_HEX(TAG, output_buffer, data_read);
ESP_LOGI( TAG_HTTP_CLIENT, "Data %s \r\n", output_buffer);
parse_poem_json(output_buffer);
} else {
ESP_LOGE(TAG_HTTP_CLIENT, "Failed to read response");
}
}
}
esp_http_client_close(client);
}
四、利用cJson组件解析诗词
解析今日诗词返回的json格式的数据,乐鑫的IDF有个cjson组件,可以非常方便的进行json操作。
#include "cJSON.h"
static bool parse_poem_json(char *analysis_buf)
{
if( analysis_buf == NULL )
{
ESP_LOGI("parse_poem_json: ", "Parse Fail\r\n" );
return false;
}
cJSON * json_root = cJSON_Parse(analysis_buf);
if( json_root != NULL )
{
cJSON *cjson_category = cJSON_GetObjectItem(json_root,"category");
cJSON *cjson_author = cJSON_GetObjectItem(json_root,"author");
cJSON *cjson_origin = cJSON_GetObjectItem(json_root,"origin");
cJSON *cjson_content = cJSON_GetObjectItem(json_root,"content");
ESP_LOGI("parse_poem_json: ", "类别 -> %s\r\n", cjson_category->valuestring );
SpeakThisText(cjson_category->valuestring); vTaskDelay(pdMS_TO_TICKS(1*1*1000));
ESP_LOGI("parse_poem_json: ", "作者 -> %s\r\n", cjson_author->valuestring );
SpeakThisText("作者"); vTaskDelay(pdMS_TO_TICKS(1*1*1000));
SpeakThisText(cjson_author->valuestring); vTaskDelay(pdMS_TO_TICKS(1*1*1000));
ESP_LOGI("parse_poem_json: ", "来源 -> %s\r\n", cjson_origin->valuestring );
SpeakThisText(cjson_origin->valuestring); vTaskDelay(pdMS_TO_TICKS(1*1*1000));
ESP_LOGI("parse_poem_json: ", "内容 -> %s\r\n", cjson_content->valuestring );
SpeakThisText(cjson_content->valuestring); vTaskDelay(pdMS_TO_TICKS(1*1*1000));
cJSON_Delete(json_root);
}
//free(analysis_buf);
return true;
}
五、使用TTS功能朗读诗词
步骤四中会调用“SpeakThisText”对获取到诗词进行朗读。
void SpeakThisText(char * prompt1){
printf("%s\n", prompt1);
if (esp_tts_parse_chinese(tts_handle, prompt1)) {
int len[1]={0};
do {
short *pcm_data=esp_tts_stream_play(tts_handle, len, 3);
esp_audio_play(pcm_data, len[0]*2, portMAX_DELAY);
} while(len[0]>0);
}
esp_tts_stream_reset(tts_handle);
}
控制台输出:
古诗文-山水-西湖
I (853229) tts_parser: unicode:0x53e4 -> gu3
I (853239) tts_parser: unicode:0x8bd7 -> shi1
I (853239) tts_parser: unicode:0x6587 -> wen2
I (853249) tts_parser: -
: I (853249) tts_parser: jian3
I (853259) tts_parser:
I (853259) tts_parser: unicode:0x5c71 -> shan1
I (853269) tts_parser: unicode:0x6c34 -> shui3
I (853269) tts_parser: -
: I (853269) tts_parser: jian3
I (853279) tts_parser:
I (853279) tts_parser: unicode:0x897f -> xi1
I (853289) tts_parser: unicode:0x6e56 -> hu2
I (856199) parse_poem_json: : 作者 -> 仲殊
作者
I (856199) tts_parser: unicode:0x4f5c -> zuo4
I (856199) tts_parser: unicode:0x8005 -> zhe3
仲殊
I (857619) tts_parser: unicode:0x4ef2 -> zhong4
I (857619) tts_parser: unicode:0x6b8a -> shu1
I (859079) parse_poem_json: : 来源 -> 诉衷情·宝月山作
诉衷情·宝月山作
I (859079) tts_parser: unicode:0x8bc9 -> su4
I (859079) tts_parser: unicode:0x8877 -> zhong1
I (859079) tts_parser: unicode:0x60c5 -> qing2
W (859089) tts_parser: Warning:skip unknown word
I (859089) tts_parser: unicode:0x5b9d -> bao3
I (859099) tts_parser: unicode:0x6708 -> yue4
I (859099) tts_parser: unicode:0x5c71 -> shan1
I (859109) tts_parser: unicode:0x4f5c -> zuo4
I (860809) Inside app_main: good job
I (861589) parse_poem_json: : 内容 -> 西湖又还春晚,水树乱莺啼。
西湖又还春晚,水树乱莺啼。
I (861589) tts_parser: unicode:0x897f -> xi1
I (861589) tts_parser: unicode:0x6e56 -> hu2
I (861599) tts_parser: unicode:0x53c8 -> you4
I (861599) tts_parser: unicode:0x8fd8 -> hai2
I (861609) tts_parser: unicode:0x6625 -> chun1
I (861609) tts_parser: unicode:0x665a -> wan3
I (861619) tts_parser: unicode:0xff0c -> short pause
I (861619) tts_parser: unicode:0x6c34 -> shui3
I (861629) tts_parser: unicode:0x6811 -> shu4
I (861629) tts_parser: unicode:0x4e71 -> luan4
I (861639) tts_parser: unicode:0x83ba -> ying1
I (861639) tts_parser: unicode:0x557c -> ti2
I (870809) Inside app_main: good job
六、遇到的问题:
问题1:
解决办法:
问题2:
解决办法:把Flash Size调节到16MB
问题3:
解决办法:
七、总结:
经过本次funpack,比较深入的学习了乐鑫的IDF框架,还接触了esp-touch智能配网,以及通过https获取json格式数据并解析,最后使用tts功能进行朗读。
收货非常大,这都是以前没有尝试的。自己之前对于语音播放比较好奇,这次也算是明白到底怎么读出来的。
感谢硬禾学堂,祝funpack越来越好。