项目介绍
这里是我参加Funpack第二季第五期活动的任务总结报告,我所完成的是任务一,使用ESP32的WiFi和TTS功能,实现一个语音播报系统。
项目的大概思路如下:
-
ESP32联网:使用ESP32的Wi-Fi功能与无线网络进行连接。可以通过配置您的无线网络的SSID和密码,使ESP32能够连接到本地网络。
-
建立TCP/IP连接:使用ESP32的TCP/IP功能,通过与心知网站的服务器建立连接,发送请求并接收响应数据。需要使用心知网站提供的API接口来获取实时的天气数据。通过向心知网站服务器发送HTTP请求,可以获取天气预报、温度、湿度等相关信息。
-
解析和处理数据:需要解析心知网站服务器返回的数据,提取您所需的天气信息,这里就要使用官方的cjson组件。根据需求,可以提取温度、湿度、天气状况等数据并存储在ESP32的变量中,以便后续处理和使用。
-
TTS语音合成:ESP32官方提供了TTS语音合成转换模型,这使得将文本数据转换为音频数据变得相对简单。可以使用该模型将您获取的天气信息转换为可播放的音频文件。
-
音频播放:为了播放音频,需要使用ESP32上的音频芯片。将转换的音频文件加载到ESP32上,并通过与音频芯片的通信,将音频数据发送到芯片进行解码和播放。选择与音频芯片相匹配的接口(例如I2S)来与芯片进行通信,并配置正确的引脚连接。
主要硬件介绍
ESP-BOX 是乐鑫发布的新一代 AIoT 开发平台,ESP32-S3-BOX-Lite 开发套件配备了一块 2.4 寸 LCD 显示屏、双麦克风、一个扬声器、两个用于硬件拓展的 Pmod™ 兼容接口和3个独立按键,可构建多样的 HMI 人机交互应用。开发板可实现离线语音唤醒和命令词识别,支持乐鑫自研的高性能声学前端算法构建语音交互系统。开发者可利用开源的 SDK轻松构建在线离线语音助手、智能语音设备、HMI 人机交互设备、多协议网关等多样的应用。
搭载 ESP32-S3 芯片的 BOX 系列开发板借助语音助手 + 屏幕控制、传感器、红外控制器和智能 Wi-Fi 网关等功能,为用户提供了一个开发智能家居设备控制系统的平台。
主要软件介绍
在软件开发上,我使用的是官方的esp-idf方式进因为官方为我们提供了行开发,原因是丰富的示例程序,涵盖了对esp32开发板上各种外设的使用,并添加了一些流行的第三方组件,如ADC按键检测和lvgl(GUI库)等。这为我们的二次开发提供了极大的便利,无需从底层开始实现,而是可以直接构建在这些示例程序的基础上进行开发。
通过使用官方示例程序包括GPIO、SPI、I2C、UART、ADC等。通过借鉴示例代码,我们可以了解如何正确地使用这些外设,并且直接在我们的项目中应用这些功能,而无需自己从头编写底层驱动程序。同时官方示例程序还集成了许多流行的第三方组件和库,如ADC按键检测和lvgl等。这些组件已经经过官方认证和测试,确保与esp-idf的API兼容,并且容易集成到我们的项目中。这样,我们无需自己去查找、集成和调试这些组件,而是直接使用官方提供的示例程序和文档。再而通过借助官方示例程序和集成的组件,我们可以更加高效地进行开发。我们可以利用这些示例程序提供的功能和代码结构,快速构建原型和功能验证。同时,这也帮助我们避免一些常见的错误和问题,使开发过程更加顺利和快速。
/* Initialize NVS. */
err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
ESP_ERROR_CHECK(bsp_board_init());
ESP_ERROR_CHECK(bsp_board_power_ctrl(POWER_MODULE_AUDIO, true));
es8156_codec_set_voice_volume(100);
xTaskCreatePinnedToCore(key_task,"key_task",1024,"key_task",2,NULL,tskNO_AFFINITY);
首先,则是各种初始化的流程,包括NVS初始化,NVS(Non-Volatile Storage)是ESP32上的一种非易失性存储器,被用于存储关键的配置和状态信息。在初始化过程中,我们需要调用官方提供的API来初始化NVS,并设置各种初始化参数,如分区表和存储区大小等。
调用官方的例程板子初始化,如果我们需要在开发板上使用音频功能,我们需要调用相应的API来启动音频电源,在音频功能启动后,我们可以使用官方提供的API来调整音量大小。对于使用ADC按键的开发板,我们需要创建一个任务来持续检测ADC引脚的状态,以判断按钮是否被按下。我们可以使用官方提供的ADC API来读取ADC引脚的值,并根据设定的阈值来判断按钮的按下状态。通过创建一个按键任务,我们可以实现对按钮事件的响应,并在按下按钮时执行相应的操作或处理。
void key_task(void *data_t){
adc_calibration_init();
//ADC1 config
ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_DEFAULT));
ESP_ERROR_CHECK(adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11));
while(1){
uint16_t adcval=adc1_get_raw(ADC1_CHANNEL_0)/100;
if(adcval != no_key && adcval != key_state){
ESP_LOGI(TAG,"adc value key :%d",adcval);
key_state=adcval;
}
vTaskDelay(50/portTICK_PERIOD_MS);
}
}
任务中也是对adc引脚进行了初始化处理
void net_connect(){
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_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "*****",
.password = "******",
/* Setting a password implies station will connect to all security modes including WEP/WPA.
* However these modes are deprecated and not advisable to be used. Incase your Access point
* doesn't support WPA2, these mode can be enabled by commenting below line */
.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() );
}
接下来就是联网处理,在开始联网处理之前,我们需要在项目中导入官方提供的网络库。这些库提供了与WiFi和网络协议相关的功能和API,例如WiFi连接、TCP/IP通信等。官方提供了一套API和示例程序,用于配置和连接ESP32到WiFi网络。在联网处理过程中,我们需要指定要连接的WiFi网络名称(SSID)和对应的密码。为了处理WiFi连接状态变化和其他网络事件,我们可以使用官方提供的事件系统。通过注册事件回调函数,我们可以在不同的事件发生时执行相应的操作。例如,我们可以注册一个连接成功的回调函数,当ESP32成功连接到WiFi网络时调用,以执行后续的任务或逻辑操作。同样地,我们可以注册一系列的事件回调函数来处理不同的网络事件,如断开连接、连接超时等。一旦我们完成了上述配置和回调函数的注册,我们可以调用官方提供的API来启动联网处理。这将使ESP32开始执行连接WiFi和处理网络事件的操作。在处理过程中,我们可以根据需要执行其他操作,如更新数据、发送请求或接收数据等。
static void obtain_time(void)
{
initialize_sntp();
// wait for time to be set
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
const int retry_count = 10;
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
time(&now);
char strftime_buf[64];
// Set timezone to China Standard Time
setenv("TZ", "CST-8", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
}
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) {
esp_wifi_connect();
ESP_LOGI(TAG,"retry connect to the AP");
} 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));
obtain_time();
}
}
在事件的函数中,成功连上网络的话则先进行的是ntp校准设备时间。
const esp_partition_t* part=esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "voice_data");
if (part==NULL) {
ESP_LOGI(TAG,"Couldn't find voice data partition!\n");
return 0;
} else {
ESP_LOGI(TAG,"voice_data paration size:%d\n", part->size);
}
void* voicedata;
spi_flash_mmap_handle_t mmap;
err=esp_partition_mmap(part, 0, part->size, SPI_FLASH_MMAP_DATA, &voicedata, &mmap);
if (err != ESP_OK) {
ESP_LOGI(TAG,"Couldn't map voice data partition!\n");
return 0;
}
esp_tts_voice_t *voice=esp_tts_voice_set_init(&esp_tts_voice_template, (int16_t*)voicedata);
esp_tts_handle_t *tts_handle = esp_tts_create(voice); // 创建tts对象
再之后就是参考官方的TTS语音合成例程,首先,我们需要在项目中导入官方提供的TTS库。这个库通常包含了语音合成引擎所需的算法和模型,用于将文本转换为语音。在进行语音合成之前,我们需要设置一些参数以控制合成结果的质量和效果。这些参数可以包括语速、音调、音量等。我们可以使用官方提供的API来设置这些参数。
一旦设置完语音合成参数,我们可以调用官方提供的API来初始化语音合成引擎。这将加载所需的模型和算法,并准备好进行后续的语音合成操作。在语音合成引擎初始化完成后,我们可以使用官方提供的API将文本转换为语音。我们需要传入要合成的文本和相应的参数,通过调用合成API,语音合成引擎将根据参数生成相应的语音数据。
一旦语音合成完成,我们可以使用官方提供的API将合成的语音数据发送给音频输出设备进行播放。
char* get_weather(void)
{
int8_t return_res = 1;
char* city_buffer = NULL;
int content_length = 0;
esp_http_client_config_t config =
{
.event_handler = _http_event_handler,
.url = WEB_PATH,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if( client == NULL )
{
return NULL;
}
// GET Request
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_err_t err = esp_http_client_open(client, 0);
if (return_res && err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
return_res = 0;
}
else
{
content_length = esp_http_client_fetch_headers(client);
if (content_length < 0)
{
ESP_LOGE(TAG, "HTTP client fetch headers failed");
return_res = 0;
} else
{
city_buffer = malloc(300);
memset(city_buffer, 0, 300);
if(city_buffer == NULL)
{
return_res = 0;
}
else
{
int data_read = esp_http_client_read_response(client, city_buffer, 300);
if (data_read >= 0)
{
ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d, data_read = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client),
data_read);
//ESP_LOG_BUFFER_HEX(TAG, city_buffer, data_read);
ESP_LOGI( TAG, "Data %s \r\n", city_buffer);
}
else
{
return_res = 0;
ESP_LOGE(TAG, "Failed to read response");
}
}
}
}
esp_http_client_close(client);
if( return_res == 0 )
{
free(city_buffer);
city_buffer = NULL;
}
return city_buffer;
}
static bool parse_weather_json(char *analysis_buf)
{
uint16_t year,month,day,hour,min;
if( analysis_buf == NULL )
{
return false;
}
cJSON * json_root = cJSON_Parse(analysis_buf);
if( json_root != NULL )
{
cJSON *cjson_arr = cJSON_GetObjectItem(json_root,"results");
cJSON *cjson_location = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0),"location");
ESP_LOGI(TAG, "城市 -> %s", cJSON_GetObjectItem(cjson_location,"name")->valuestring );
cJSON *cjson_now = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0),"now");
ESP_LOGI(TAG, "天气 -> %s", cJSON_GetObjectItem(cjson_now,"text")->valuestring );
ESP_LOGI(TAG, "code -> %s", cJSON_GetObjectItem(cjson_now,"code")->valuestring );
ESP_LOGI(TAG, "气温 -> %s", cJSON_GetObjectItem(cjson_now,"temperature")->valuestring );
sprintf(voice_weather,"当前城市为%s,天气为%s,温度为%s摄氏度",cJSON_GetObjectItem(cjson_location,"name")->valuestring,cJSON_GetObjectItem(cjson_now,"text")->valuestring,cJSON_GetObjectItem(cjson_now,"temperature")->valuestring);
cJSON_Delete(json_root);
}
free(analysis_buf);
return true;
}
之后按下右键后,则会使用官方提供的网络库来建立与心知天气网的连接。这需要我们指定心知天气网的API地址以及其他必要的信息,如API密钥、城市编码等。通过调用合适的API,我们可以建立与心知天气网的连接,向其发送请求并获取响应。一旦连接建立成功,我们可以发送HTTP请求来获取天气数据。根据心知天气网的API文档,我们可以使用合适的API来请求实时的天气数据。心知天气网将以JSON格式返回响应,其中包含有关天气的各种信息,如温度、湿度、风速等。由于返回的天气数据是以JSON格式进行传输的,我们需要使用cJSON组件来解析和提取这些数据。cJSON是一种轻量级的C语言JSON解析器,可以帮助我们将JSON数据转换为可操作的C语言对象。我们可以使用cJSON提供的API来解析JSON响应,并提取其中我们所需的天气信息。根据我们想要获取的天气信息,我们可以遍历解析后的JSON对象,并提取其中的特定字段和值。这可以包括当前温度、天气状况、降雨概率等。通过使用cJSON提供的API,我们可以方便地获取并存储这些信息。最后则是合成为一个字符串。
ESP_LOGI(TAG,"左键");
if(voice_weather[0] != 0){
if(esp_tts_parse_chinese(tts_handle,voice_weather)){
int len[1]={0};
do{
short *pcm_data = esp_tts_stream_play(tts_handle,len,0);
i2s_write(I2S_NUM_0, (const char *)pcm_data, len[0]*2 ,&bytes_write ,portMAX_DELAY);
// esp_audio_play(pcm_data,len[0]*2 ,portMAX_DELAY);
}while(len[0] > 0);
}
memset(voice_weather,0,sizeof(voice_weather));
// esp_tts_stream_reset(tts_handle);
}
当检测到按下左键时,就会调用tts的相应api函数,将上面得到的字符串数据转换成音频数据,并通过I2S的方式音频数据传输给音频芯片进行播放天气情况。
总结
在本次活动中,学习了如何利用esp官方的TTS语音合成模型库。在过程中遇到的问题,通过百度搜索都能找到适合的答案,使自我得到了提升感谢硬禾学堂平台。