一、项目介绍
本项目基于乐鑫 ESP32-S3-BOX-Lite 轻量级开发套件实现的智能天气盒子。
- 语音指令,用于识别用户指令,例如:使用唤醒词唤醒智能天气盒子后,说出指令 天气 则会实时联网获取天气信息,并使用语音合成进行播报。
- 语音合成,用于播报天气信息。
- WIFI连接
- HTTPS请求
- JSON数据解析
二、硬件介绍
2.1 硬件总览
2.2 硬件介绍
ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。为用户提供了一个基于语音助手、传感器、红外控制器和智能 Wi-Fi 网关等功能开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
功能 | ESP32-S3-BOX-Lite |
---|---|
SOC & 存储 | ESP32-S3 Octal SPI 8MB PSRAM Quad SPI 16MB Flash |
AI语音功能 | 声学前端算法,支持远场噪音环境。 离线语音支持自定义200+语音指令 |
LCD显示屏 | 2.4寸(分辨率 320 * 240) |
按键 | 5个按键 (Reset,Boot Mode,3个自定义按键 ) |
外部接口 | 2个 PmodTM 接口(16个可编程GPIO) |
三、设计思路
3.1 功能概览
智能天气盒子
- 连接WIFI
- 天气信息获取
- 语音唤醒
- 语音指令识别
- 语音合成
- 语音播报
- 灯光控制
3.2 流程图
3.3 实现过程
智能天气盒子上电后,开始连接网络,连网成功后,自动获取一次当前天气信息。通过LCD屏幕进行展示当前天气图片与温度信息,并通过语音播报当前天气信息及推荐语。
3.3.1 连接WIFI
原本是想通过配网的形式连接网络的。通过一段时间的使用体验,乐鑫的APP配网失败率挺高。开发过程中每次烧写了之后,需要进行一次配网,而且经常失败。故改成了连接固定WIFI的形式,通过 idf.py menuconfig
进行配置。如下图所示:
3.3.2 UI展示
UI的设计比较简洁,只有一个开机界面和一个主界面。盒子上电启动时显示开机动画,然后进入主界面。连网获取到最新的天气信息后,会更新UI显示当前的天气对应的图标和温度信息。
天气图标使用 spiffs 进行存储,显示的时候需要根据自己配置的标识符拼接相应的天气图标路径。
...
// 初始化 spiffs
bsp_spiffs_init("storage", "/spiffs", 48)
// 拼接天气图标路径,更新UI展示。
void ui_update_weather(const char *code, const char *temp) {
char img_url[256], temp_val[32];
// 更新天气图标
snprintf(img_url, sizeof(img_url), "S:/spiffs/white/%s@2x.png", code);
lv_img_set_src(g_img_weather, img_url);
// 更新气温
snprintf(temp_val, sizeof(temp_val), "%s °C", temp);
lv_label_set_text(g_lab_temp, temp_val);
}
...
3.3.3 天气信息获取
天气信息的获取有很多种方式,由于之前申请过 心知天气 的API。 因此本项目通过心知天气API获取。
心知天气的接口都是基于 https 的,因此需要在代码中植入相应域名的证书才能正确获取天气信息。证书的获取可通过 openssl s_client -showcerts -connect [https域名]:443 </dev/null
命令进行获取。
...
// 全局信息定义
extern const uint8_t seniverse_root_cert_pem_start[] asm("_binary_seniverse_root_cert_pem_start");
extern const uint8_t seniverse_root_cert_pem_end[] asm("_binary_seniverse_root_cert_pem_end");
#define WEB_SERVER "api.seniverse.com"
#define WEB_PORT "443"
#define WEB_URL_NOW "https://api.seniverse.com/v3/weather/now.json?location=zhuzhou&language=zh-Hans&unit=c&key="
#define API_KEY CONFIG_SENIVERSE_API_KEY
static const char *REQUEST_NOW = "GET " WEB_URL_NOW API_KEY " HTTP/1.0\r\n"
"Host: "WEB_SERVER"\r\n"
"User-Agent: esp-idf/1.0 esp32\r\n"
"\r\n";
...
// 证书配置
esp_tls_cfg_t cfg = {
.cacert_pem_buf = seniverse_root_cert_pem_start,
.cacert_pem_bytes = seniverse_root_cert_pem_end - seniverse_root_cert_pem_start,
};
// 建立https连接
struct esp_tls *tls = esp_tls_conn_http_new(WEB_URL_NOW, &cfg);
...
// 发起请求
size_t written_bytes = 0;
do {
ret = esp_tls_conn_write(tls,
REQUEST_NOW + written_bytes,
strlen(REQUEST_NOW) - written_bytes);
if (ret >= 0) {
ESP_LOGI(TAG, "Now weather %d bytes written", ret);
written_bytes += ret;
} else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
ESP_LOGE(TAG, "Now weather esp_tls_conn_write returned 0x%x", ret);
goto exit;
}
} while(written_bytes < strlen(REQUEST_NOW));
...
// 获取响应结果
do
{
len = sizeof(buf) - 1;
bzero(buf, sizeof(buf));
ret = esp_tls_conn_read(tls, (char *)buf, len);
if(ret == MBEDTLS_ERR_SSL_WANT_WRITE || ret == MBEDTLS_ERR_SSL_WANT_READ)
continue;
if(ret < 0)
{
ESP_LOGE(TAG, "esp_tls_conn_read returned -0x%x", -ret);
break;
}
if(ret == 0)
{
ESP_LOGI(TAG, "connection closed");
break;
}
len = ret;
ESP_LOGD(TAG, "%d bytes read", len);
/* Print response directly to stdout as it is read */
for(int i = 0; i < len; i++) {
putchar(buf[i]);
}
} while(1);
...
3.3.4 语音指令
乐鑫的 ESP-SR 项目,使得离线语音唤醒与语音识别的开发变的很简单。
最大可配置200个离线语音指令,通过 idf.py menuconfig
进行配置。配置完成后,可在代码中通过离线语音指令ID进行分别匹配。
...
// 语音指令识别分别通过三个任务进行处理
xTaskCreatePinnedToCore(audio_feed_task, "Feed Task", 4 * 1024, afe_data, 5, NULL, 1);
xTaskCreatePinnedToCore(audio_detect_task, "Detect Task", 6 * 1024, afe_data, 5, NULL, 1);
xTaskCreatePinnedToCore(sr_handler_task, "SR Handler Task", 4 * 1024, g_result_que, 1, NULL, 0);
...
// 匹配到相应指令后,通过 sr_handler_task 函数进行处理。
xQueueReceive(xQueue, &result, portMAX_DELAY);
if (ESP_MN_STATE_TIMEOUT == result.state) {
// 语音指令获取超时
continue;
}
if (AFE_FETCH_WWE_DETECTED == result.fetch_mode) {
// 语音唤醒状态
continue;
}
if (ESP_MN_STATE_DETECTED & result.state) {
// 匹配到语音指令
switch (result.command_id) {
case 0: // 关闭水泵停止浇水
case 1:
app_pump_watering_stop();
break;
case 2: // 打开水泵开始浇水
case 3:
app_pump_watering_start();
break;
case 4: // 打开LED灯
app_pwm_led_set_power(true);
app_tts_play_text("打开电灯!");
break;
case 5: // 关闭LED灯
app_pwm_led_set_power(false);
app_tts_play_text("关闭电灯,谢谢使用!");
break;
case 6: // 天气播报
app_tts_play_text("好的!");
app_weather_get(); // 获取天气
break;
default: // 未知指令,通过语音告知
app_tts_play_text("未知指令, 欢迎使用天气盒子, 请给出正确的指令!");
break;
}
}
...
3.3.5 语音合成与播报
乐鑫 ESP-SR 项目的 TTS 模块提供了面向嵌入式的轻量化中文语音合成库,单个库占用不到4M的Flash空间。语音合成的流程如下图所示:
...
// 初始化轻量化的语音合成库
const esp_partition_t* part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "voice_data");
if (part==0) printf("Couldn't find voice data partition!\n");
spi_flash_mmap_handle_t mmap;
uint16_t* voicedata;
esp_err_t err=esp_partition_mmap(part, 0, part->size, SPI_FLASH_MMAP_DATA, (const void**)&voicedata, &mmap);
esp_tts_voice_t *voice=esp_tts_voice_set_init(&esp_tts_voice_template, voicedata);
tts_handle=esp_tts_create(voice);
ESP_LOGI(TAG, "tts initialized!");
...
// 中文解析
esp_tts_parse_chinese(tts_handle, text)
...
// 合成语音数据
short *pcm_data=esp_tts_stream_play(tts_handle, len, 1);
...
// 音频输出
i2s_write(I2S_NUM_0, (const char*) data_out, out_length, &bytes_write, ticks_to_wait);
...
3.3.6 灯光控制
灯光控制使用PWM控制GPIO引脚输出高低电平,控制模块化的LED灯。
static void update_pwm_led(uint8_t r, uint8_t g, uint8_t b)
{
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, r);
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, g);
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_2, b);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_2);
}
四、功能展示
4.1 UI展示
4.1.1 启动界面
4.1.2 天气显示
五、心得体会
之前接触ESP系列芯片都是使用 Arduino 框架进行开发,此次换成了 ESP-IDF 进行开发。开发过程中各种不习惯,好在乐鑫提供了很多示例可以参考。以下几点需要特别注意:
- 分区表: 此次项目使用了语音识别,图片展示,语音合成等功能,盒子集成的 16M Flash 需要合理规划分区,才能正常工作。目前是牺牲了一个OTA分区的情况下,满足目前的需求。
- TTS语音库的烧写:在 main 目录下 CMakeLists.txt 中有一段注释的代码,放开后,可以通过
idf.py flash
指令把语音库一起烧写进 Flash 中。由于语音库的分区在最后,通常第一次烧写即可。因此,将该部分代码注释了。也可以使用 esp-sr 项目中的 chinese-tts 示例项目中的flash_voicedata.sh
脚本进行烧写。 - https证书:涉及到 https 请求,因此需要加载证书。目前通过将证书文件注册至
COMPONENT_EMBED_TXTFILES
变量中,可现实情况是域名证书通常都是有有效期的。
总结:
ESP-SR 功能强大,能很好的满足一些简单的语音指令场景。一个小小的语音合成库就能实现语音播报功能。美中不足之处在于语音指令不能动态匹配得到语音关键字,那样将会更加智能。中文TTS库合成出来的语音比较生硬,但感觉这部分功能应该可以通过在线API方式进行合成,有待以后进行验证!
最后感谢电子森林与得捷电子联合推出的 《Funpack》 系列活动,对于我来说是个很好的学习机会,理论结合实践。我们下期活动再见!