基于ESP32-S3的智能天气盒子
基于乐鑫 ESP32-S3-BOX-Lite 轻量级开发套件实现的智能天气盒子。
标签
智能家居
物联网
lvgl
ESP32-S3
语音
天气
ESP32-S3-BOX-LITE
乐鑫
TTS
FreeRTOS
鲜de芒果
更新2023-08-01
1502

一、项目介绍

本项目基于乐鑫 ESP32-S3-BOX-Lite 轻量级开发套件实现的智能天气盒子。

  • 语音指令,用于识别用户指令,例如:使用唤醒词唤醒智能天气盒子后,说出指令 天气 则会实时联网获取天气信息,并使用语音合成进行播报。
  • 语音合成,用于播报天气信息。
  • WIFI连接
  • HTTPS请求
  • JSON数据解析

 

二、硬件介绍

 

2.1 硬件总览

Fo0fPUuOBJaDGs9tR94KYBXQnk6t

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 功能概览

FhebHPRoCpkCT85uciMM4LUfdfmE

智能天气盒子

  • 连接WIFI
  • 天气信息获取
  • 语音唤醒
  • 语音指令识别
  • 语音合成
  • 语音播报
  • 灯光控制

 

3.2 流程图

FoGOT1bArEh0Xpw6-mqEKu7Pq2Ea

 

3.3 实现过程

智能天气盒子上电后,开始连接网络,连网成功后,自动获取一次当前天气信息。通过LCD屏幕进行展示当前天气图片与温度信息,并通过语音播报当前天气信息及推荐语。

3.3.1 连接WIFI

原本是想通过配网的形式连接网络的。通过一段时间的使用体验,乐鑫的APP配网失败率挺高。开发过程中每次烧写了之后,需要进行一次配网,而且经常失败。故改成了连接固定WIFI的形式,通过 idf.py menuconfig 进行配置。如下图所示:

Fg3ylH0KAB4MrPsyh7OHANhLAc4K

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进行分别匹配。

FokPOlFyY-cnA5Hq0yNFXtzlFXVK

...
// 语音指令识别分别通过三个任务进行处理
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空间。语音合成的流程如下图所示:

FiTR5TqgUKe6_mM9kuIBwa4kDPhf

 

...
// 初始化轻量化的语音合成库
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 启动界面

Fm9Qa5lSS_NwOZG0Tdk0qC1bCRaP

4.1.2 天气显示

FibsBiYm22PZKizcFl93b2gYLGFx

 

五、心得体会

之前接触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》 系列活动,对于我来说是个很好的学习机会,理论结合实践。我们下期活动再见!

附件下载
weather-box.zip
源码,包含组件库的完整工程仓库:http://gitlab.akazs.com:30000/bunco/weather-box.git
团队介绍
业余爱好者
团队成员
鲜de芒果
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号