作者是参加比赛申请的一块ESP32S3 BOX Lite的开发板。
图片如下:
别的不说,用过乐鑫的idf的同学都知道,这开发环境真的一言难尽,我是用vscode+espidf4.4.4+python3.8.7。插一嘴这里最好不要下py3.11容易安装报错,然后去换源。
因为我当时做的是跟农业相关的,所以这块开发板当时是做的交互模块。(其实也没做啥)
我用这块开发板,通过http协议访问心知天气的url,然后我们不是看见这块开发板有三个按钮吗,于是我设置了一个按键检测,当我按下某个键的时候,播放当地的实时天气情况。然后做了一个UI页面,用的SquareLine,制作了一个简易的静态UI画面。
大概就做成了这个样子,然后按中间的按键就能实时播放当地的天气情况。
先去github上,找到esp-skainet框架,然后找到chinese-tts,也就就是找到它的语音框架,我们在这个基础上进行开发。
我们这块BOX lite采用的是es8311这块语音芯片。
- esp_tts_voice_t *voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件
- esp_tts_handle_t *tts_handle = esp_tts_create(voice); // 创建tts对象
- esp_tts_handle_t *tts = esp_tts_create(voice); // 创建tts对象
- es8156_codec_set_voice_volume(100);
我们可以这主函数里面看见,我们首先就是要配置tts的声音配置文件,然后创建一个tts对象,然后函数调节我们想要的音量。
- if (esp_tts_parse_chinese(tts_handle, buff))
- {
- int len[1] = {0};
- do
- {
- short *pcm_data = esp_tts_stream_play(tts_handle, len, 0);
- esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);
- // printf("data:%d \n", len[0]);
- } while (len[0] > 0);
- vTaskDelay(10000 / portTICK_PERIOD_MS);
- }
- esp_tts_stream_reset(tts_handle);
- }
然后这块开发板是每次一个字节的输出,所以可能听起来有点机械感。
当然更多的配置文件,全是编译的时候,我们的espidf给我们下载一大堆。
可见这里面的配置文件都是一些底层,驱动寄存器。
我是采用ADC采样获取按键的数据的。
- //ADC按键
- int key=4; // 0 右键 2 中间键 3 左键 4 无按键
- static esp_adc_cal_characteristics_t adc1_chars;
- #define ADC1_EXAMPLE_CHAN0 ADC1_CHANNEL_0
- static bool adc_calibration_init(void)
- {
- esp_err_t ret;
- bool cali_enable =false;
- ret = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP_FIT);
- if(ret == ESP_ERR_NOT_SUPPORTED)
- ESP_LOGW(TAG,"Calibration scheme not supported,skip software calibration");
- else if(ret == ESP_ERR_INVALID_VERSION)
- {
- ESP_LOGW(TAG,"eFuse");
- }
- else if(ret == ESP_OK)
- {
- cali_enable =true;
- esp_adc_cal_characterize(ADC_UNIT_1,ADC_ATTEN_DB_11,ADC_WIDTH_BIT_DEFAULT,0,&adc1_chars);
- }
- else{
- ESP_LOGW(TAG,"Invalid arg");
- }
- return cali_enable;
- }
因为ESP32S3自带FreeRTOS操作系统(如果不知道什么是FreeRTOS的读者,可以查看我前几篇博客),所以我这里直接创建一个监听任务。
- int listen_key_task()
- {
- adc_calibration_init();
- adc1_config_width(ADC_WIDTH_BIT_DEFAULT);
- adc1_config_channel_atten(ADC1_EXAMPLE_CHAN0,ADC_ATTEN_DB_11);
- u16_t adcval=adc1_get_raw(ADC1_EXAMPLE_CHAN0) / 1000;
-
- if(adcval !=4 &&adcval !=key )
- {
- ESP_LOGI(TAG,"Adc value key:%d",adcval);
- key = adcval;
- }
- vTaskDelay(400 / portTICK_PERIOD_MS);
-
- return key;
- }
我设定了一个KEY的初始值为4,当按下按钮时,数据就会变化。0 右键 2 中间键 3 左键 4 无按键。
- /* Constants that aren't configurable in menuconfig */
- // #define WEB_SERVER "example.com"
- // #define WEB_PORT "80"
- // #define WEB_PATH "/"
- //http组包宏,获取天气的http接口参数
- #define WEB_SERVER "api.thinkpage.cn"
- #define WEB_PORT "80"
- #define WEB_URL "/v3/weather/now.json?key="
- #define host "api.thinkpage.cn"
- #define APIKEY "S1owwyz1J-WgwE_k3"
- #define city "changsha"
- #define language "zh-Hans"
我们首先定义http组的宏,我们提前去心知天气,获取APIKEY,我这个是14天的,现在已经过期了,然后我们修改city的地区,然后language可以修改语音。
- //天气解析结构体
- typedef struct
- {
- char cit[20];
- char weather_text[20];
- char weather_code[2];
- char temperatur[3];
- char feels_like[3]; //体感温度
- char pressure[4]; //气压,单位为mb百帕或in英寸
- char humidity[3]; //相对湿度,0~100,单位为百分比
- char visibility[5]; //能见度,单位为km公里或mi英里
- char wind_direction[20]; //风向文字
- char wind_direction_degree[4]; //风向角度,范围0~360,0为正北,90为正东,180为正南,270为正西
- char wind_speed[6]; //风速,单位为km/h公里每小时或mph英里每小时
- char wind_scale[3]; //风力等级,请参考:http://baike.baidu.com/view/465076.htm
- }weather_info;
-
- weather_info weathe;
-
- static const char *REQUEST = "GET "WEB_URL""APIKEY"&location="city"&language="language" HTTP/1.1\r\n"
- "Host: "WEB_SERVER"\r\n"
- "Connection: close\r\n"
- "\r\n";
然后我们定义一个结构体,因为我们从心知天气上获取的是cjson的数据包,所以我们要进行cjson的数据解析。
-
- /*解析json数据 只处理 解析 城市 天气 天气代码 温度 其他的自行扩展
- * @param[in] text :json字符串
- * @retval void :无
- * @note 修改日志
- * Ver0.0.1:
- hx-zsj, 2018/08/10, 初始化版本\n
- */
- void cjson_to_struct_info(char *text)
- {
- cJSON *root,*psub;
- cJSON *arrayItem;
- //截取有效json
- char *index=strchr(text,'{');
- strcpy(text,index);
-
- root = cJSON_Parse(text);
-
- if(root!=NULL)
- {
- psub = cJSON_GetObjectItem(root, "results");
- arrayItem = cJSON_GetArrayItem(psub,0);
-
- cJSON *locat = cJSON_GetObjectItem(arrayItem, "location");
- cJSON *now = cJSON_GetObjectItem(arrayItem, "now");
- if((locat!=NULL)&&(now!=NULL))
- {
- psub=cJSON_GetObjectItem(locat,"name");
- sprintf(weathe.cit,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[city:%s]",weathe.cit);
-
- psub=cJSON_GetObjectItem(now,"text");
- sprintf(weathe.weather_text,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[weather:%s]",weathe.weather_text);
-
- psub=cJSON_GetObjectItem(now,"code");
- sprintf(weathe.weather_code,"%s",psub->valuestring);
- // ESP_LOGI(TAG,"%s",weathe.weather_code);
-
- psub=cJSON_GetObjectItem(now,"temperature");
- sprintf(weathe.temperatur,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[temperatur:%s]",weathe.temperatur);
-
- psub=cJSON_GetObjectItem(now,"feels_like");
- sprintf(weathe.feels_like,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[feels_like:%s]",weathe.feels_like);
-
- psub=cJSON_GetObjectItem(now,"pressure");
- sprintf(weathe.pressure,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[pressure:%s]",weathe.pressure);
-
- psub=cJSON_GetObjectItem(now,"humidity");
- sprintf(weathe.humidity,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[humidity:%s]",weathe.humidity);
-
- psub=cJSON_GetObjectItem(now,"visibility");
- sprintf(weathe.visibility,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[visibility:%s]",weathe.visibility);
-
- psub=cJSON_GetObjectItem(now,"wind_direction");
- sprintf(weathe.wind_direction,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[wind_direction:%s]",weathe.wind_direction);
-
- psub=cJSON_GetObjectItem(now,"wind_direction_degree");
- sprintf(weathe.wind_direction_degree,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[wind_direction_degree:%s]",weathe.wind_direction_degree);
-
- psub=cJSON_GetObjectItem(now,"wind_speed");
- sprintf(weathe.wind_speed,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[wind_speed:%s]",weathe.wind_speed);
-
- psub=cJSON_GetObjectItem(now,"wind_scale");
- sprintf(weathe.wind_scale,"%s",psub->valuestring);
- ESP_LOGI(TAG,"[wind_scale:%s]",weathe.wind_scale);
-
- sprintf(buff,"%s天气%s温度%s体感温度%s气压%s相对湿度%s能见度%s风向%s风向角度%s风速%s风力等级%s",weathe.cit,weathe.weather_text,weathe.temperatur,weathe.feels_like,
- weathe.pressure,weathe.humidity,weathe.visibility,weathe.wind_direction,weathe.wind_direction_degree,weathe.wind_speed,weathe.wind_scale);
-
- // lv_label_set_text(ui_Celsius,weathe.weather_text);
-
- }
- }
- cJSON_Delete(root);
- }
一开始我是解析json数据 只处理 解析 城市 天气 天气代码 温度,后面的可以自己添加,根据个人需求。
- static void http_get_task(void *pvParameters)
- {
- const struct addrinfo hints = {
- .ai_family = AF_INET,
- .ai_socktype = SOCK_STREAM,
- };
- struct addrinfo *res;
- struct in_addr *addr;
- int s, r;
- char recv_buf[1024];
- char mid_buf[1024];
-
- while(1) {
- //DNS域名解析
- int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &res);
- if(err != 0 || res == NULL) {
- ESP_LOGE(TAG, "DNS lookup failed err=%d res=%p", err, res);
- vTaskDelay(1000 / portTICK_PERIOD_MS);
- continue;
- }
-
- //打印获取的IP
- addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
- ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
-
- //新建socket
- s = socket(res->ai_family, res->ai_socktype, 0);
- if(s < 0) {
- ESP_LOGE(TAG, "... Failed to allocate socket.");
- freeaddrinfo(res);
- vTaskDelay(1000 / portTICK_PERIOD_MS);
- continue;
- }
- ESP_LOGI(TAG, "... allocated socket");
-
- //连接ip
- if(connect(s, res->ai_addr, res->ai_addrlen) != 0) {
- ESP_LOGE(TAG, "... socket connect failed errno=%d", errno);
- close(s);
- freeaddrinfo(res);
- vTaskDelay(4000 / portTICK_PERIOD_MS);
- continue;
- }
- ESP_LOGI(TAG, "... connected");
- freeaddrinfo(res);
-
- //发送http包
- if (write(s, REQUEST, strlen(REQUEST)) < 0) {
- ESP_LOGE(TAG, "... socket send failed");
- close(s);
- vTaskDelay(4000 / portTICK_PERIOD_MS);
- continue;
- }
- ESP_LOGI(TAG, "http write:%s",REQUEST);
- ESP_LOGI(TAG, "... socket send success");
- //设置http超时
- struct timeval receiving_timeout;
- receiving_timeout.tv_sec = 5;
- receiving_timeout.tv_usec = 0;
- if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout,
- sizeof(receiving_timeout)) < 0) {
- ESP_LOGE(TAG, "... failed to set socket receiving timeout");
- close(s);
- vTaskDelay(4000 / portTICK_PERIOD_MS);
- continue;
- }
- ESP_LOGI(TAG, "... set socket receiving timeout success");
-
- //清缓存
- memset(mid_buf,0,sizeof(mid_buf));
-
- //获取http应答包
- do {
- //清缓存
- bzero(recv_buf, sizeof(recv_buf));
- //读取http应答包
- r = read(s, recv_buf, sizeof(recv_buf)-1);
- strcat(mid_buf,recv_buf);
- // for(int i = 0; i < r; i++) {
- // putchar(recv_buf[i]);
- // }
- } while(r > 0);
-
- ESP_LOGI(TAG, "http read:%s", mid_buf);
-
- //json解析
- cjson_to_struct_info(mid_buf);
- vTaskDelay(10000 / portTICK_PERIOD_MS);
- //关闭socket,http是短连接
- ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d.", r, errno);
- close(s);
- vTaskDelay(10000 / portTICK_PERIOD_MS);
- }
- }
这里同样的,创建一个进程,上面的注释写的很清楚,我们首先进行DNS域名解析,然后打印获取IP地址,新建socket,连接ip,发送http包,设置异常超时,获取我们得到的http应答包,然后进行json解析,关闭socket连接。
我是在SquareLIne上生成的UI页面,然后移植lvgl包进我的文件夹里,这个软件只有三十天的试用期,但是网上有很多破解方法。
大概就是做的这样一个UI页面,时间不够就没有过多的追求精美的UI页面。
UI的话,生成的文件基本都是组件的位置大小啥的,具体的画面得去SquareLine里面自己调整布局。
然后我们在main.c函数里面引用对应的函数即可。
- void app_lvgl_display(void)
- {
- bsp_display_lock(0);
-
- ui_init();
-
- bsp_display_unlock();
- }
-
- int app_main()
- {
- /* Initialize display and LVGL */
- bsp_display_start();
-
- /* Set default display brightness */
- bsp_display_brightness_set(APP_DISP_DEFAULT_BRIGHTNESS);
-
- /* Add and show objects on display */
- app_lvgl_display();
-
- ESP_LOGI(TAG, "Example initialization done.");
- ESP_ERROR_CHECK( nvs_flash_init() );
- ESP_ERROR_CHECK(esp_netif_init());
- ESP_ERROR_CHECK(esp_event_loop_create_default());
-
- /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
- * Read "Establishing Wi-Fi or Ethernet Connection" section in
- * examples/protocols/README.md for more information about this function.
- */
- ESP_ERROR_CHECK(example_connect());
- xTaskCreate(&http_get_task, "http_get_task", 8196, NULL, 5, NULL);
- /*** 2. play prompt text ***/
- ESP_ERROR_CHECK(esp_board_init(AUDIO_HAL_16K_SAMPLES, 1, 16));
- esp_tts_voice_t *voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件
- esp_tts_handle_t *tts_handle = esp_tts_create(voice); // 创建tts对象
- esp_tts_handle_t *tts = esp_tts_create(voice); // 创建tts对象
- es8156_codec_set_voice_volume(100);
- bool tts_flag;
- bool flag=0;
- while(1)
- {
- listen_key_task(key);
- switch(key)
- {
- case 2:
- {
- key =4;
- tts_flag=true;
- break;
-
- }
- case 0:
- {
- key=4;
- flag=1;
- break;
- }
-
- default:
- break;
- }
-
- if(tts_flag)
- {
- tts_flag=false;
- if (esp_tts_parse_chinese(tts_handle, buff))
- {
- int len[1] = {0};
- do
- {
- short *pcm_data = esp_tts_stream_play(tts_handle, len, 0);
- esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);
- // printf("data:%d \n", len[0]);
- } while (len[0] > 0);
- vTaskDelay(10000 / portTICK_PERIOD_MS);
- }
- esp_tts_stream_reset(tts_handle);
- }
-
- if(flag)
- {
- flag=0;
- char *prompt1 = "我是ljt2333,大家关注我吧!";
- if (esp_tts_parse_chinese(tts, prompt1))
- {
- int len[1] = {0};
- do
- {
- short *pcm_data = esp_tts_stream_play(tts, len, 2);
-
- esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);
- // printf("data:%d \n", len[0]);
- } while (len[0] > 0);
- vTaskDelay(1000 / portTICK_PERIOD_MS);
- }
- esp_tts_stream_reset(tts);
- }
-
-
- }
- return 0;
- }
媒体播放器 2023-09-22 17-37-06
视频放在最后是成品,可以看一下。
总体来说,这是一块通过http协议获取心知天气的实时天气的数据包,进行cjson解析,然后通过乐鑫的esp-skainet语音框架播报语音,创建两个任务,一个adc的按键采样,当我们按下不同的键时,会播放不同的文字。