作者是参加比赛申请的一块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 无按键。
http获取心知天气的url
/* 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连接。
UI页面
我是在SquareLIne上生成的UI页面,然后移植lvgl包进我的文件夹里,这个软件只有三十天的试用期,但是网上有很多破解方法。
大概就是做的这样一个UI页面,时间不够就没有过多的追求精美的UI页面。
UI的话,生成的文件基本都是组件的位置大小啥的,具体的画面得去SquareLine里面自己调整布局。
然后我们在main.c函数里面引用对应的函数即可。
void app_lvgl_display(void)
{
bsp_display_lock(0);
ui_init();
bsp_display_unlock();
}
注意:
因为SquareLine生成的不是配套的ESP32S3 BOX lite代码,里面是ESP32S3BOX的配套UI,所以我们要在UI里面去掉触摸功能。
主函数
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的按键采样,当我们按下不同的键时,会播放不同的文字。