任务目的
使用ESP32的WiFi和TTS功能,实现一个语音播报系统,联网获取获取天气并播报。
设计思路
本项目大致可分为三部分设计
- 天气状况获取。
- 天气状态显示。
- 语音播报。
具体如下:
天气状况获取
- 访问网络:通过esp32 wifi接口去连接家中路由器,进一步去访问网络。
- 获取天气:通过心知天气接口获取当前天气状况。
- JSON解析:解析收到的心知天气数据返回,解析出具体内容并使用。
天气状态显示
- 屏幕驱动:直接使用box驱动库去初始化屏幕并使用。
- 图片显示:使用lvgl驱动屏幕,初始化spifs文件系统,将天气图片放入其中。
语音播报
- 声卡驱动:直接使用box驱动库去初始化声卡并使用。
- 播报音频:使用esp32 tts功能生成具体音频并播放出来。
硬件介绍
ESP32-S3-BOX-LITE
ESP32-S3-BOX AI语音开发套件是乐鑫打造的一个智能语音设备开发平台
ESP32-S3-BOX 既可以用于构建智能音箱,也可以赋能更多物联网设备实现人机语音交互。同时,它还集触摸屏控制、传感器、红外控制器和智能网关等多功能于一体,能够作为全屋设备的控制中枢,支持用户通过语音命令控制或触屏控制,轻松实现圈内智能联动。
ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。为用户提供了一个基于语音助手、传感器、红外控制器和智能 Wi-Fi 网关等功能开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
支持特性:
- 双麦克风支持远场语音交互
- 高唤醒率的离线语音唤醒
- 高识别率的离线中英文命令词识别
- 可动态配置 200+ 中英文命令词
- 连续识别和唤醒打断
- 灵活可复用的 GUI 框架
- 端到端一站式接入云平台
- Pmod™ 兼容接口支持多种外设扩展
核心微控制器:ESP32-S3
ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。
- Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz
- 内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM
- 额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions)
- 45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC 主机控制器和 TWAITM 控制器等
- 基于 AES-XTS 算法的 Flash 加密和基于 RSA 算法的安全启动,数字签名和 HMAC 模块,“世界控制器 (World Controller)”模块
代码流程图
主要代码
main代码
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(bsp_board_init());
ESP_ERROR_CHECK(bsp_board_power_ctrl(POWER_MODULE_AUDIO, true));
ESP_ERROR_CHECK(bsp_spiffs_init_default());
ESP_ERROR_CHECK(lv_port_init());
bsp_lcd_set_backlight(true);
bsp_codec_set_voice_volume(100);
user_init();
app_event_group = xEventGroupCreate();
wifi_init_sta();
xTaskCreate(sntp_task, "sntp_task", 2048, NULL, 9, NULL);
xTaskCreate(weather_task, "weather_task", 3072, NULL, 8, NULL);
xTaskCreate(user_task, "user_task", 4096, NULL, 10, NULL);
do {
lv_task_handler();
} while (vTaskDelay(1), true);
}
wifi连接部分
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) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < WIFI_MAX_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
WIFI_SSID, WIFI_PASSWORD);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
WIFI_SSID, WIFI_PASSWORD);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
vEventGroupDelete(s_wifi_event_group);
xEventGroupSetBits(app_event_group, WIFI_INIT_BIT);
}
天气获取部分
void weather_task(void *param)
{
int content_length = 0;
esp_http_client_config_t http_client_cfg = {
.url = weather_url,
};
esp_http_client_handle_t http_client_handle = esp_http_client_init(&http_client_cfg);
esp_http_client_set_method(http_client_handle, HTTP_METHOD_GET);
memset(&weather, 0, sizeof(weather_t));
memset(&response_body, 0, sizeof(response_body));
while (1)
{
esp_err_t err = esp_http_client_open(http_client_handle, 0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
goto __fail_delay;
}
content_length = esp_http_client_fetch_headers(http_client_handle);
if (content_length < 0 || content_length > RESPONSE_BODY_MAX_SIZE)
{
ESP_LOGE(TAG, "HTTP client fetch headers failed");
goto __fail_delay;
}
if (esp_http_client_read_response(http_client_handle, response_body, RESPONSE_BODY_MAX_SIZE) < 0)
{
ESP_LOGE(TAG, "Failed to read response");
goto __fail_delay;
}
ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(http_client_handle),
esp_http_client_get_content_length(http_client_handle));
printf("%s\n", response_body);
cjson_parse_xinzhi_weather(response_body);
esp_http_client_close(http_client_handle);
memset(&response_body, 0, sizeof(response_body));
__fail_delay:
for (size_t i = 0; i < REQUEST_INTERVAL * 60; i++)
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
显示与播报部分
void user_task(void *param)
{
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_partition_mmap(part, 0, part->size, SPI_FLASH_MMAP_DATA, (const void**)&voicedata, &mmap); //3*1024*1024
esp_tts_voice_t *voice=esp_tts_voice_set_init(&esp_tts_voice_xiaole, voicedata);
esp_tts_handle_t *tts_handle=esp_tts_create(voice);
char *text="欢迎使用天气小助手";
size_t bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
while (1)
{
EventBits_t app_event_bit = xEventGroupWaitBits(app_event_group,
NTP_UPDATE_BIT | WEATHER_UPDATE_BIT,
pdTRUE,
pdFALSE,
portMAX_DELAY);
if (app_event_bit & WEATHER_UPDATE_BIT)
{
char text[45];
sprintf(text, "S:/spiffs/%d@2x.png", weather.code_day[0]);
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, text);
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
sprintf(text, "今天白天天气:%s", weather.text_day[0]);
size_t bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
vTaskDelay(300 / portTICK_PERIOD_MS);
sprintf(text, "夜晚天气:%s", weather.text_night[0]);
bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
vTaskDelay(300 / portTICK_PERIOD_MS);
sprintf(text, "最高气温:%d度", weather.degree_high[0]);
bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
vTaskDelay(300 / portTICK_PERIOD_MS);
sprintf(text, "最低气温:%d度", weather.degree_low[0]);
bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
vTaskDelay(300 / portTICK_PERIOD_MS);
sprintf(text, "湿度:百分之%d", weather.humidity[0]);
bytes_written = 0;
if (esp_tts_parse_chinese(tts_handle, text)) { // parse text into pinyin list
int len[1]={0};
do {
short *data=esp_tts_stream_play(tts_handle, len, 4); // streaming synthesis
i2s_write(I2S_NUM_0, data, len[0]*2, &bytes_written, portMAX_DELAY);
} while(len[0]>0);
i2s_zero_dma_buffer(I2S_NUM_0);
}
}
vTaskDelay(5 / portTICK_PERIOD_MS);
}
}
功能演示
显示多云图片
获取心知天气返回
语音播报”欢迎使用天气小助手“
语音播报”今天白天天气多云 夜晚天气多云“
代码使用说明
需要下载音频数据包,设置如下。
由于此代码大量依赖了box的驱动代码,因此需要放到xxxx\esp-box\examples\demo目录下。
因为tts生成的音频数据为16KHz,单声道的,因此需要修改box音频驱动。
需要修改wifi名称与密码为自己可连接的wifi
需要修改心知天气私钥,用于访问心知天气服务。
心得体会
代码中也设计了ntp时间获取功能,下一步计划设计白天显示白天的天气,夜晚显示夜晚的天气。
5分钟播报一次还是太吵了,下一步计划将语音识别加进去,识别到播报天气命令后再播报今天的天气情况。
活动真不错,板子也挺好,题目也有趣,学习了不少新知识,挺好,新一期也下单了,马上第二期就要结束了,希望以后继续办下去。