前面的例程,都是单一功能的,本部分将会把之前的几个功能结合在一起使用。
第16章 桌面天气助手
前面章节的例程,都是从一个基本例程开始,然后教大家一步一步添加代码完成对应功能。从本章开始,由于综合例程的步骤比较多,如果还是像之前那样讲解,会比较繁琐。从本章例程开始,我们直接讲已经写好的例程。本章例程,是一个天气时钟面板,可以作为一个桌面摆件。
例程代码如下:
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:14-desktop_weather.zip
16.1 让例程跑起来
下载例程到你的开发板之前,需要完成一些设置才可以。 复制开发板提供的例程(desktop_weather)到的你的实验文件夹,然后使用VSCode打开文件夹。 第一步:点击左侧工程面板最后一个文件sdkconfig.defaults打开它,前两行,修改WiFi名称和密码为你自己的。
CONFIG_EXAMPLE_WIFI_SSID="WIFI名称"
CONFIG_EXAMPLE_WIFI_PASSWORD="WIFI密码"
2
第二步:点击打开spi_lcd_touch_example_main.c文件,在大概100多行的位置,找到下面的宏定义,把这个里面的地理位置ID号和密钥号,改成你自己的。关于如何获取自己的地理位置ID号和密钥号,看第15章。
#define QWEATHER_DAILY_URL "https://devapi.qweather.com/v7/weather/3d?&location=xxx&key=xxx"
#define QWEATHER_NOW_URL "https://devapi.qweather.com/v7/weather/now?&location=xxx&key=xxx"
#define QAIR_NOW_URL "https://devapi.qweather.com/v7/air/now?&location=xxx&key=xxx"
2
3
第三步:设置目标芯片为esp32c3。 第四步:设置menuconfig。 1、把FLASH大小设置为8MB。
2、修改分区文件为自定义的。 3、把LVGL的缓存大小,修改为64K 4、勾选LVGL的GIF解码库 保存,并退出menuconfig。 第五步,设置好串口号,下载方式设置为串口,编译下载。 正常的话,首先进入第一个界面,等待网络连接、获取网络时间、获取天气信息,最后进入主界面。获取网络时间如果失败,会自动重新启动。获取完全部信息后,就会进入主界面。在主界面上显示时钟和天气信息,还有室内温湿度。 (注意,此刻你显示的地理位置,是:太原市|小店区,如果要更改成自己的地理位置,比如:深圳市|福田区,需要重新制作字库,制作字库的方法在下面16.3.1开机界面章节讲解。)16.2 程序流程讲解
本例程是在第11章的lvgl_demo例程上修改而来,结合了第13章的“WiFi连接”,第14章的“网络授时”,以及第15章的“获取天气信息”,和第5章的获取“室内温湿度”,最终做了一个在液晶屏上显示的时钟天气看板,显示界面使用LVGL设计。
另外新增的功能或者叫知识点是,使用PWM调节液晶屏背光亮度,以及LVGL新增字体、GIF图片解码等。
点击spi_lcd_touch_example_main.c文件,找到app_main函数,可以先从上往下浏览一遍主函数。
程序上电运行后,进入主函数,先是进行了一番初始化工作,包括NVS初始化、温湿度传感器初始化、液晶屏初始化、触摸屏初始化、LVGL初始化、液晶背光控制初始化。
初始化完成以后,使用lv_gui_start()显示了一个开机界面。
然后创建了7个任务函数,分别是获取室内温湿度任务、连接WiFi任务、获取网络时间任务、获取每日天气信息任务、获取实时天气信息任务、获取实时空气质量任务、主界面任务。其中,获取室内温湿度任务和主界面任务会一直运行,其余几个任务都是一次性任务。
这些任务是依次执行,使用freeRTOS事件组控制它们的先后执行顺序。比如,从网络上获取信息,必须先执行完连接WiFi任务。
进入主界面任务后,创建了一个lv_timer定时器,每隔一秒钟更新一下主界面的数据。在定时器回调函数中,设置 了各模块更新周期。日期、星期、时分秒,是1秒钟更新一次。室内温湿度数据,是10秒钟更新一次。室外温湿度以及天气图标和天气状况,是30分钟更新一次。每日温度范围和日出日落时间,是每小时更新一次。
具体的代码讲解,可以看下面的章节。
16.3 图片和字库制作
本例程中用到了1个图片和4种字体。图片是一个GIF动图,太空人旋转的画面。四种字体分别是:阿里普惠字体、awesome字体、LED数码管字体、和风天气图标字体。下面分别介绍它们的详细制作方法。
16.3.1 图片文件制作
我们使用的图片,如下图所示,是一张80*80像素的gif动图。
esp32-idf提供了gif图片的第三方解码库,我们使用这个库显示图片。大概分为两个步骤,第一个是把图片转换为C语言数组文件,放到源文件中。第二个是编写几行代码调用它。比较简单。 图片转换成C语言数组,我们使用LVGL官方提供的图片转换在线工具进行转换。 LVGL在线图片转换器 https://lvgl.io/tools/imageconverter 使用转换器的步骤是,先点击Browse选择我们要转换的图片,然后填写图片名称,颜色格式,我们选择CF_RAW,输出格式,选择C数组,最后点击Convert转换,转换完成的c代码,会自动从浏览器下载。如上图片显示,就是我转换太空人图片的设置。 下载好的C文件,我放到了工程main目录下,在VSCode中点击打开它。刚下载好的C文件,前面的13行有关于怎么包含lvgl.h头文件的条件编译,我已经全部删除,然后只写一个包含lvgl.h的include。 原来的代码:#ifdef __has_include
#if __has_include("lvgl.h")
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
#define LV_LVGL_H_INCLUDE_SIMPLE
#endif
#endif
#endif
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
2
3
4
5
6
7
8
9
10
11
12
13
修改后的代码
#include "lvgl.h"
然后需要在main下面的CMakeLists.txt文件中添加它。
idf_component_register(SRCS "image_taikong.c" 后面省略
然后我们需要在显示gif图片的C文件里面,使用下面语句声明一下。在lvgl_demo_ui.c文件的开始处,我已经添加进去。
LV_IMG_DECLARE(image_taikong);
LVGL显示gif图片的代码,也非常简洁。我们的例程,在开机界面和主界面都显示了这个动图,开机界面显示函数lv_gui_start()和主界面显示函数lv_main_page()都位于main下面的lvgl_demo_ui.c文件中,我们现在可以打开lv_gui_start()函数,看一下调用图片的方法。在函数中看到,显示图片只需要3行代码,首先使用lv_gif_create创建一个对象,然后使用lv_gif_set_src给这个对象指定图片名称,最后使用lv_obj_align设置图片的显示位置。
16.3.2 字库文件制作
本例程需要使用了4种字体。
- 阿里普惠字体:用来显示汉字和数字。 LVGL提供的中文字库,只有16像素大小的宋体字。在我们的开发板上看起来很小,费眼睛,而且宋体字也逐步在现在的电子产品中淘汰了。我们这里使用接近于手机显示效果的一种字体,阿里普惠体。这款字体是阿里巴巴提供的一款可以免费商用的字体,可以从下面的网站下载到。 阿里巴巴字体网站:https://www.alibabafonts.com/ 里面针对字体粗细有好几个版本,我下载的是Alibaba PuHuiTi 3.0 - 45 Light。
代码下载
链接在资料下载中心
章节百度网盘中的字库文件!!
文件名称:AlibabaPuHuiTi-3-45-Light.ttf
- LED数码管字体:用来显示时分秒。字体来自网络。
代码下载
链接在资料下载中心
章节百度网盘中的字库文件!!
文件名称:Ni7seg.ttf
和风天气图标字体:用来显示天气图标。 和风天气官方提供了每个天气状况对应的图标,这些图标可以做成图片显示,也可以做成字体显示,本例程中,我们把它作为字体显示。
和风天气图标官方链接:https://icons.qweather.com/install/
代码下载
链接在资料下载中心
章节百度网盘中的字库文件!!
文件名称:qweather-icons.ttf
- awesome字体:用来显示温湿度的符号。 LVGL已经包含了57个awesome图标,如下图所示。
但是这里面,没有我们需要的可以代表温度和湿度的图标,所以我们需要自己再制作两个温湿度图标。
awesome字体的下载链接如下。
官方下载链接:https://fontawesome.com/download
代码下载
链接在资料下载中心
章节百度网盘中的字库文件!!
文件名称:FontAwesome5-Solid+Brands+Regular.woff
以上是各个字体的介绍和字体文件下载方法,下面开始制作每个字体文件。
把字体转换成C语言数组,可以使用LVGL官方提供的在线转换工具。 lvgl字体制作工具:https://lvgl.io/tools/fontconverter
上图所示,是阿里普惠字体的转换示例。首先定义一个自己的名字,这里我起名为font_alipuhui。
然后像素大小,我这里填了20。
接下来Bpp这里,有3个选项,分别是1 2 3 4 8bit-per-pixel,数字越大,显示效果越好,当然生成的C代码也越大,这里因为我们只需要提取几十个字,不必在乎C代码体积,所以选择效果最好的8。
然后是Fallback,这个是字体回退机制,作用是,在这个字体中,找不到你要用的字符时,就会尝试从这个fallback指定的字体中寻找。在这个例程中,我们没有用到字体回退功能,所以这里空下不填就可以。(如果要填的话,也必须要填已经存在的字体名称)
接下来点击“选择文件”选择我们刚才下载好的字体文件。然后在Symbols窗口中填入我们要显示的文字。最后点击Convert按钮生成字体C文件,会自动下载。 在本例程中,我们使用到的文字如下所示:
0123456789-℃%:√~ 太原市|小店区年月日正在连接WiFi成功获取天气信息网络时间晴多云少间阴阵雨强雷伴有冰雹小中大极端降毛细暴特冻到雪夹薄雾霾扬沙浮尘浓度重严热冷未知星期一二三四五六室内外温湿空优良轻落出
(注意在“0123456789-℃%:√~”后面还有一个空格) (你可以把其中的“太原市小店区”更改成你的城市和行政区)
LED数码管字体的制作,如下图所示。
名称写为font_led,像素大小设置为32,Bpp设置为8,Fallback不用填。在Symbols一栏,写入要生成的符号,这里我写的是 0123456789: 注意这里面还有一个冒号,是英文的冒号,不要写成中文的。除了显示时分秒这几个数字以外,显示时钟还需要冒号,例如:12:25:18。生成awesome字体的配置如下图所示。
文件名称写为font_myawesome。像素大小设置为20,因为这个图标要和刚才生成的汉字在用一行显示,所以这里的像素也设置为20。Bpp设置为8,Fallback不用填。选择刚才下载的字体文件。在Range一栏里面写入温湿度的Unicode码。因为温湿度的符号用键盘打不出来,所以只能在Range一栏写它的Unicode码。 温湿度符号的Unicode码是多少,可以在awesome官方网站对应符号页面看到。温度符号页面链接:(https://fontawesome.com/icons/temperature-three-quarters?f=classic&s=solid)
湿度符号页面链接:(https://fontawesome.com/icons/droplet?f=classic&s=solid)
和风天气图标字体制作配置如下图所示:
文件名称写为font_qweather,像素大小设置为80,Bpp选择8,Fallback不用填。文件选择刚才下载好的qweather字体。然后还是在Range一栏写入我们需要的图标Unicode码。 天气图标的Unicode码查看,需要使用一个字体编辑软件查看,比如Font Creator。Font Creator软件下载地址:(https://www.xitongzhijia.net/soft/116980.html)
软件仅供交流学习使用,尊重版权,拒绝盗版,从你我做起。 使用Font Creator软件打开qweather-icon.ttf字体文件,就可以看到每个图标的Unicode码了,如下图所示。
https://dev.qweather.com/docs/resource/icons/ 上面这个链接可以查到各种天气图标对应的图标代码,如下图所示: 然后根据图标代码,在下面这个链接可以查看各个图标代码对应的图标形状。 https://icons.qweather.com/icons/#sunny 然后把所有的图标代码对应的图标样子的Unicode码,在Font Creator软件中找出来,最后的Unicode码就是 0xF101-0xF13B和0xF144-0xF146 所以在Range中填入:0xF101-0xF13B,0xF144-0xF146,就可以生成我们需要的全部天气图标了。经过上面的一番操作,已经生成了4个字体c文件,接下来看看怎么在工程中使用它们。
把生成的字体c文件,放到了工程中main文件夹下面,如下图所示:
可以在VSCode软件中,点击打开它。文件源文件最前面关于怎么包含头文件lvgl.h的条件编译已经删除,修改为直接包含lvgl.h文件。 原来的:
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
2
3
4
5
修改后的:
#include "lvgl.h"
在main下面的CMakeLists.txt文件里面把这些字体文件添加进去。
idf_component_register(SRCS "font_alipuhui.c" "font_myawesome.c" "font_qweather.c" "font_led.c" 后面省略
使用这个字体,还需要在使用字体的文件中,声明一下。在lvgl_demo_ui.c文件的开始处,我已经添加进去。
LV_FONT_DECLARE(font_alipuhui);
LV_FONT_DECLARE(font_qweather);
LV_FONT_DECLARE(font_led);
LV_FONT_DECLARE(font_myawesome);
2
3
4
然后就可以写程序使用了,使用lv_obj_set_style_text_font函数指定字体,使用lv_label_set_text函数设置显示的内容,在lv_gui_start()函数中就可以看到。 刚才在制作字库的时候,如果字体是在Symbols栏中直接输入字符生成的,就可以直接在lv_label_set_text函数中使用,如下代码所示:
lv_label_set_text(label_wifi, "正在连接WiFi");
如果字体是在Range一栏中使用Unicode码生成的,显示的内容需要使用UTF-8编码,如下代码所示:
lv_label_set_text(temp_symbol_label2, "\xEF\x8B\x88"); // 温度图标
我们生成字体文件的时候,用的是Unicode码,这里需要的是UTF-8码,所以,需要把Unicode码转换成UTF-8编码才行。转换编码,可以使用下面的在线工具。 UTF-8工具:https://www.cogsci.ed.ac.uk/~richard/utf-8.cgi?input=F146&mode=hex 例如,温度的Unicode是F2C8,转换成UTF-8码后就是EF 8B 88,如下图所示:
到这里,总结一下,上面讲解了图片和字库的c文件制作方法,以及如何把这些C文件中添加到工程中,以及如何在代码中使用它们。16.4 程序模块讲解
接下来我们以程序执行流程的顺序,来逐个讲解程序各模块的实现。
16.4.1 开机界面
显示开机界面的函数是 lv_gui_start(),在主函数初始化完成后调用,主要用来提示我们进度。// 开机界面
void lv_gui_start(void)
{
// 设置背景色
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x080808), 0);
// 显示太空人GIF图片
lv_obj_t *gif_start = lv_gif_create(lv_scr_act());
lv_gif_set_src(gif_start, &image_taikong);
lv_obj_align(gif_start, LV_ALIGN_TOP_MID, 0, 20);
// 连接wifi
label_wifi = lv_label_create(lv_scr_act());
lv_label_set_text(label_wifi, "正在连接WiFi");
lv_obj_set_style_text_font(label_wifi, &font_alipuhui, 0);
lv_obj_set_style_text_color(lv_scr_act(), lv_color_hex(0x888888), LV_PART_MAIN);
lv_obj_set_pos(label_wifi, 85 ,110);
// 获取网络时间
label_sntp = lv_label_create(lv_scr_act());
lv_label_set_text(label_sntp, "");
lv_obj_set_style_text_font(label_sntp, &font_alipuhui, 0);
lv_obj_set_style_text_color(lv_scr_act(), lv_color_hex(0x888888), LV_PART_MAIN);
lv_obj_set_pos(label_sntp, 70 ,135);
// 获取天气信息
label_weather = lv_label_create(lv_scr_act());
lv_label_set_text(label_weather, "");
lv_obj_set_style_text_font(label_weather, &font_alipuhui, 0);
lv_obj_set_style_text_color(lv_scr_act(), lv_color_hex(0x888888), LV_PART_MAIN);
lv_obj_set_pos(label_weather, 70 ,160);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
程序第一条语句,设置屏幕背景色,080808接近于纯黑色(000000),之所以没有设置为纯黑色,是因为接下来要显示的太空人图片,太空人gif图片的背景色就是080808,我们把屏幕背景色设置为080808后,太空人gif的图片就和屏幕融为一体,比较好看。 接下来我们再看lv_gui_start()函数中剩下的3个板块,连接wifi,获取网络时间和获取天气信息的3个label。分别使用lv_label_create创建。因为这3个label的名称,需要在未来的任务函数中修改文字,所以这里的3个label,都定义成了全局变量。在这个函数的上边,可以找到。
lv_obj_t * label_wifi;
lv_obj_t * label_sntp;
lv_obj_t * label_weather;
2
3
lv_label_set_text函数指定label显示什么内容,lv_obj_set_style_text_font函数指定显示内容使用的字体,lv_obj_set_style_text_color函数指定显示内容的颜色,lv_obj_set_pos函数指定显示内容在屏幕中的位置。
16.4.2 获取室内温湿度任务
获取室内温湿度任务函数如下所示:
// 获取温湿度的任务函数
static void get_th_task(void *args)
{
esp_err_t ret;
int time_cnt = 0, date_cnt = 0;
float temp_sum = 0.0, humi_sum = 0.0;
while(1)
{
ret = gxhtc3_get_tah(); // 获取一次温湿度
if (ret!=ESP_OK) {
ESP_LOGE(TAG,"GXHTC3 READ TAH ERROR.");
}
else{ // 如果成功获取数据
temp_sum = temp_sum + temp; // 温度累计和
humi_sum = humi_sum + humi; // 湿度累计和
date_cnt++; // 记录累计次数
}
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒
time_cnt++; // 每秒+1
if(time_cnt>10) // 10秒钟到
{
// 取平均数 且把结果四舍五入为整数
temp_value = round(temp_sum/date_cnt);
humi_value = round(humi_sum/date_cnt);
// 各标志位清零
time_cnt = 0; date_cnt = 0; temp_sum = 0; humi_sum = 0;
// 标记温湿度有新数值
th_update_flag = 1;
ESP_LOGI(TAG, "TEMP:%d HUMI:%d", temp_value, humi_value);
}
}
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
此任务函数,每隔1秒钟,采集一次温湿度数据,每采集10次,计算一次平均值,round函数是C语言库函数,用来四舍五入取整,需要调用头文件<math.h>。最终的温湿度值赋值给全局变量temp_value和humi_value,供其它显示函数调用。th_update_flag变量置1,表示计算好了新的温湿度数据。
这个任务函数从建立开始就会一直执行。
16.4.3 WiFi连接任务
WiFi连接任务函数如下所示:
// WIFI连接 任务函数
static void wifi_connect_task(void *pvParameters)
{
my_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
ESP_LOGI(TAG, "Successfully Connected to AP");
if(reset_flag == 1) // 如果是刚开机
{
lv_label_set_text(label_wifi, "√ WiFi连接成功");
lv_label_set_text(label_sntp, "正在获取网络时间");
}
xEventGroupSetBits(my_event_group, WIFI_CONNECTED_BIT);
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
第1行代码,创建了一个事件组,事件组句柄my_event_group是个全局变量,在C文件开始处定义。
static EventGroupHandle_t my_event_group;
接下来的6~9行代码,用来连接WiFi。 接下来的if条件语句,判断是否是刚开机,如果是刚开机,需要在开机界面显示一些信息。如果是进入主界面后重新连接wifi,就不用显示这两行字了。 第17行,给事件组设置了WIFI_CONNECTED_BIT这个位。这里的BIT位,在C文件最开始处进行了宏定义。
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_GET_SNTP_BIT BIT1
#define WIFI_GET_DAILYWEATHER_BIT BIT2
#define WIFI_GET_RTWEATHER_BIT BIT3
#define WIFI_GET_WEATHER_BIT BIT4
2
3
4
5
16.4.4 获取网络时间任务
获取网络时间任务函数如下所示:
// 获得日期时间 任务函数
static void get_time_task(void *pvParameters)
{
xEventGroupWaitBits(my_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
esp_netif_sntp_init(&config);
// wait for time to be set
int retry = 0;
const int retry_count = 6;
while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
}
if(retry>5)
{
esp_restart(); // 没有获取到时间的话 重启ESP32
}
esp_netif_sntp_deinit();
// 设置时区
setenv("TZ", "CST-8", 1);
tzset();
// 获取系统时间
time(&now);
localtime_r(&now, &timeinfo);
if(reset_flag == 1) // 如果是刚开机
{
lv_label_set_text(label_sntp, "√ 网络时间获取成功");
lv_label_set_text(label_weather, "正在获取天气信息");
}
xEventGroupSetBits(my_event_group, WIFI_GET_SNTP_BIT);
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
函数一开始,等待WIFI_CONNECTED_BIT事件,等到WIFI_CONNECTED_BIT事件发生,程序才会向下执行,保证了先连接上WiFi,再从网络获取时间。
第6~20行,设置网络时间的服务器,并连接服务器获取时间。这里设置最大获取次数是6,如果达到了6次还没有获取到,那就使用esp_restart()函数重启ESP32。
第22、23行,setenv("TZ", "CST-8", 1)和tzset(),用来把当前时间的时区修改为北京时间。
第25行,time()函数是<time.h>头文件中的库函数,用来获取系统时间,获取到的数,赋值给全局变量now,现在获取到的值,其实是一个比较大的数字。
第26行,localtime_r()函数也是<time.h>头文件中的库函数,用来把刚才获取到的数转换成我们能看懂的数,赋值到timeinfo结构体里面。该结构体的原始定义如下所示,注意这个结构体定义是在time.h文件中,我们的工程中找不到。
struct tm {
int tm_sec; /* 秒 - 取值区间为[0,59] */
int tm_min; /* 分 - 取值区间为[0,59] */
int tm_hour; /* 时 - 取值区间为[0,23] */
int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
int tm_mon; /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
int tm_year; /* 年份,其值等于实际年份减去1900 */
int tm_wday; /* 星期–取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
int tm_yday; /* 从每年的1月1日开始的天数 – 取值区间为[0,365],其中0代表1月1日,以此类推 */
int tm_isdst; /*夏令时标识符,实行夏令时的时候,为1。不实行夏令时的进候,为0;不了解情况时,为-1。*/
};
2
3
4
5
6
7
8
9
10
11
第28~32行,如果是刚开机,显示一些信息,如果是已经开机后调用的,就不显示这些信息。 第34行,告知事件组WIFI_GET_SNTP_BIT位已经完成。
16.4.5 获取天气信息任务
例程中,获取了3次天气信息,分别是每日天气预报、实时天气信息和实时天气质量。从每日天气预报信息中,我们需要获取当天的最高温度和最低温度以及日出和日落时间。从实时天气信息中,我们需要获取实时的温湿度、天气图标和天气状况。从实时天气质量中,获取当前的空气质量等级。 获取这些信息的链接,在spi_lcd_touch_example_main.c文件中可以看到。
#define QWEATHER_DAILY_URL "https://devapi.qweather.com/v7/weather/3d?&location=xxx&key=xxx"
#define QWEATHER_NOW_URL "https://devapi.qweather.com/v7/weather/now?&location=xxx&key=xxx"
#define QAIR_NOW_URL "https://devapi.qweather.com/v7/air/now?&location=xxx&key=xxx"
2
3
这些链接的不同之处是,在v7后面。weather/3d是获取3日每日天气信息。weather/now是获取实时天气信息。air/now是获取实时天气质量信息。 获取这3种信息,分别创建了3个任务函数,函数结构都差不多一样。关于如何解码获取到的JSON数据,在第15章已经详细讲过了,这里就不再重复。
16.4.6 主界面任务
主界面任务函数如下所示:// 主界面 任务函数
static void main_page_task(void *pvParameters)
{
int tm_cnt1 = 0;
int tm_cnt2 = 0;
xEventGroupWaitBits(my_event_group, WIFI_GET_WEATHER_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
example_disconnect();
vTaskDelay(pdMS_TO_TICKS(100));
lv_obj_clean(lv_scr_act());
vTaskDelay(pdMS_TO_TICKS(100));
lv_main_page();
th_update_flag = 0;
qwnow_update_flag = 0;
qair_update_flag = 0;
qwdaily_update_flag = 0;
lv_timer_create(value_update_cb, 1000, NULL); // 创建一个lv_timer 每秒更新一次数据
reset_flag = 0; // 标记开机完成
while (1)
{
tm_cnt1++;
if (tm_cnt1 > 1800) // 30分钟更新一次实时天气和实时空气质量
{
tm_cnt1 = 0; // 计数清零
example_connect(); // 连接wifi
get_now_weather(); // 获取实时天气信息
get_air_quality(); // 获取实时空气质量
tm_cnt2++;
if (tm_cnt2 > 1) // 60分钟更新一次每日天气
{
tm_cnt2 = 0;
get_daily_weather(); // 获取每日天气信息
}
example_disconnect(); // 断开wifi
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
上面代码中,第7行,等到获取完天气信息,程序才会向下执行。 第10行,lv_obj_clean(lv_scr_act()),用来清除开机界面。 第12行,lv_main_page()用来显示如上图所示的主界面。 第14~17行,是几个flag标志,表示各数值是否更新,这里清0表示还没有更新。 第19行,lv_timer_create函数创建了一个lv_timer,函数中的第1个参数,定义了回调函数名称,第2个参数,1000表示1000毫秒进入一次回调函数。 后面的while循环里面,tm_cnt1变量,每1秒钟加1,加到1800就是30分钟到。到30分钟后,获取实时天气信息和实时空气质量,然后tm_cnt2是判断是否到60分钟,到了60分钟后,获取每日天气预报。在获取前,先连接wifi,获取到后,断开wifi。
接下来我们再看两个函数。一个是lv_main_page()主界面显示函数,另外一个是value_update_cb()定时器回调函数。 主界面显示函数如下所示。
// 主界面
void lv_main_page(void)
{
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x000000), 0); // 修改背景为黑色
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10); // 设置圆角半径
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 10);
lv_style_set_width(&style, 320); // 设置宽
lv_style_set_height(&style, 240); // 设置高
/*Create an object with the new style*/
lv_obj_t * obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(obj, &style, 0);
// 显示地理位置
lv_obj_t * addr_label = lv_label_create(obj);
lv_obj_set_style_text_font(addr_label, &font_alipuhui, 0);
lv_label_set_text(addr_label, "太原市|小店区");
lv_obj_align_to(addr_label, obj, LV_ALIGN_TOP_LEFT, 0, 0);
// 显示年月日
date_label = lv_label_create(obj);
lv_obj_set_style_text_font(date_label, &font_alipuhui, 0);
lv_label_set_text_fmt(date_label, "%d年%02d月%02d日", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday);
lv_obj_align_to(date_label, obj, LV_ALIGN_TOP_RIGHT, 0, 0);
// 显示分割线
lv_obj_t * above_bar = lv_bar_create(obj);
lv_obj_set_size(above_bar, 300, 3);
lv_obj_set_pos(above_bar, 0 , 30);
lv_bar_set_value(above_bar, 100, LV_ANIM_OFF);
// 显示天气图标
qweather_icon_label = lv_label_create(obj);
lv_obj_set_style_text_font(qweather_icon_label, &font_qweather, 0);
lv_obj_set_pos(qweather_icon_label, 0 , 40);
lv_qweather_icon_show();
// 显示空气质量
static lv_style_t qair_level_style;
lv_style_init(&qair_level_style);
lv_style_set_radius(&qair_level_style, 10); // 设置圆角半径
lv_style_set_bg_color(&qair_level_style, lv_palette_main(LV_PALETTE_GREEN)); // 绿色
lv_style_set_text_color(&qair_level_style, lv_color_hex(0xffffff)); // 白色
lv_style_set_border_width(&qair_level_style, 0);
lv_style_set_pad_all(&qair_level_style, 0);
lv_style_set_width(&qair_level_style, 50); // 设置宽
lv_style_set_height(&qair_level_style, 26); // 设置高
qair_level_obj = lv_obj_create(obj);
lv_obj_add_style(qair_level_obj, &qair_level_style, 0);
lv_obj_align_to(qair_level_obj, qweather_icon_label, LV_ALIGN_OUT_RIGHT_TOP, 5, 0);
qair_level_label = lv_label_create(qair_level_obj);
lv_obj_set_style_text_font(qair_level_label, &font_alipuhui, 0);
lv_obj_align(qair_level_label, LV_ALIGN_CENTER, 0, 0);
lv_qair_level_show();
// 显示当天室外温度范围
qweather_temp_label = lv_label_create(obj);
lv_obj_set_style_text_font(qweather_temp_label, &font_alipuhui, 0);
lv_label_set_text_fmt(qweather_temp_label, "%d~%d℃", qwdaily_tempMin, qwdaily_tempMax);
lv_obj_align_to(qweather_temp_label, qweather_icon_label, LV_ALIGN_OUT_RIGHT_MID, 5, 5);
// 显示当天天气图标代表的天气状况
qweather_text_label = lv_label_create(obj);
lv_obj_set_style_text_font(qweather_text_label, &font_alipuhui, 0);
lv_label_set_long_mode(qweather_text_label, LV_LABEL_LONG_SCROLL_CIRCULAR); /*Circular scroll*/
lv_obj_set_width(qweather_text_label, 80);
lv_label_set_text_fmt(qweather_text_label, "%s", qwnow_text);
lv_obj_align_to(qweather_text_label, qweather_icon_label, LV_ALIGN_OUT_RIGHT_BOTTOM, 5, 0);
// 显示时间 小时:分钟:秒钟
led_time_label = lv_label_create(obj);
lv_obj_set_style_text_font(led_time_label, &font_led, 0);
lv_label_set_text_fmt(led_time_label, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
lv_obj_set_pos(led_time_label, 142, 42);
// 显示星期几
week_label = lv_label_create(obj);
lv_obj_set_style_text_font(week_label, &font_alipuhui, 0);
lv_obj_align_to(week_label, led_time_label, LV_ALIGN_OUT_BOTTOM_RIGHT, -10, 6);
lv_week_show();
// 显示日落时间
sunset_label = lv_label_create(obj);
lv_obj_set_style_text_font(sunset_label, &font_alipuhui, 0);
lv_label_set_text_fmt(sunset_label, "日落 %s", qwdaily_sunset);
lv_obj_set_pos(sunset_label, 200 , 103);
// 显示分割线
lv_obj_t * below_bar = lv_bar_create(obj);
lv_obj_set_size(below_bar, 300, 3);
lv_obj_set_pos(below_bar, 0, 130);
lv_bar_set_value(below_bar, 100, LV_ANIM_OFF);
// 显示室外温湿度
static lv_style_t outdoor_style;
lv_style_init(&outdoor_style);
lv_style_set_radius(&outdoor_style, 10); // 设置圆角半径
lv_style_set_bg_color(&outdoor_style, lv_color_hex(0xd8b010)); //
lv_style_set_text_color(&outdoor_style, lv_color_hex(0xffffff)); // 白色
lv_style_set_border_width(&outdoor_style, 0);
lv_style_set_pad_all(&outdoor_style, 5);
lv_style_set_width(&outdoor_style, 100); // 设置宽
lv_style_set_height(&outdoor_style, 80); // 设置高
lv_obj_t * outdoor_obj = lv_obj_create(obj);
lv_obj_add_style(outdoor_obj, &outdoor_style, 0);
lv_obj_align(outdoor_obj, LV_ALIGN_BOTTOM_LEFT, 0, 0);
lv_obj_t *outdoor_th_label = lv_label_create(outdoor_obj);
lv_obj_set_style_text_font(outdoor_th_label, &font_alipuhui, 0);
lv_label_set_text(outdoor_th_label, "室外");
lv_obj_align(outdoor_th_label, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t *temp_symbol_label1 = lv_label_create(outdoor_obj);
lv_obj_set_style_text_font(temp_symbol_label1, &font_myawesome, 0);
lv_label_set_text(temp_symbol_label1, "\xEF\x8B\x88"); // 显示温度图标
lv_obj_align(temp_symbol_label1, LV_ALIGN_LEFT_MID, 10, 0);
outdoor_temp_label = lv_label_create(outdoor_obj);
lv_obj_set_style_text_font(outdoor_temp_label, &font_alipuhui, 0);
lv_label_set_text_fmt(outdoor_temp_label, "%d℃", qwnow_temp);
lv_obj_align_to(outdoor_temp_label, temp_symbol_label1, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *humi_symbol_label1 = lv_label_create(outdoor_obj);
lv_obj_set_style_text_font(humi_symbol_label1, &font_myawesome, 0);
lv_label_set_text(humi_symbol_label1, "\xEF\x81\x83"); // 显示湿度图标
lv_obj_align(humi_symbol_label1, LV_ALIGN_BOTTOM_LEFT, 10, 0);
outdoor_humi_label = lv_label_create(outdoor_obj);
lv_obj_set_style_text_font(outdoor_humi_label, &font_alipuhui, 0);
lv_label_set_text_fmt(outdoor_humi_label, "%d%%", qwnow_humi);
lv_obj_align_to(outdoor_humi_label, humi_symbol_label1, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
// 显示室内温湿度
static lv_style_t indoor_style;
lv_style_init(&indoor_style);
lv_style_set_radius(&indoor_style, 10); // 设置圆角半径
lv_style_set_bg_color(&indoor_style, lv_color_hex(0xfe6464)); //
lv_style_set_text_color(&indoor_style, lv_color_hex(0xffffff)); // 白色
lv_style_set_border_width(&indoor_style, 0);
lv_style_set_pad_all(&indoor_style, 5);
lv_style_set_width(&indoor_style, 100); // 设置宽
lv_style_set_height(&indoor_style, 80); // 设置高
lv_obj_t * indoor_obj = lv_obj_create(obj);
lv_obj_add_style(indoor_obj, &indoor_style, 0);
lv_obj_align(indoor_obj, LV_ALIGN_BOTTOM_MID, 10, 0);
lv_obj_t *indoor_th_label = lv_label_create(indoor_obj);
lv_obj_set_style_text_font(indoor_th_label, &font_alipuhui, 0);
lv_label_set_text(indoor_th_label, "室内");
lv_obj_align(indoor_th_label, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t *temp_symbol_label2 = lv_label_create(indoor_obj);
lv_obj_set_style_text_font(temp_symbol_label2, &font_myawesome, 0);
lv_label_set_text(temp_symbol_label2, "\xEF\x8B\x88"); // 温度图标
lv_obj_align(temp_symbol_label2, LV_ALIGN_LEFT_MID, 10, 0);
indoor_temp_label = lv_label_create(indoor_obj);
lv_obj_set_style_text_font(indoor_temp_label, &font_alipuhui, 0);
lv_label_set_text_fmt(indoor_temp_label, "%d℃", temp_value);
lv_obj_align_to(indoor_temp_label, temp_symbol_label2, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *humi_symbol_label2 = lv_label_create(indoor_obj);
lv_obj_set_style_text_font(humi_symbol_label2, &font_myawesome, 0);
lv_label_set_text(humi_symbol_label2, "\xEF\x81\x83"); // 湿度图标
lv_obj_align(humi_symbol_label2, LV_ALIGN_BOTTOM_LEFT, 10, 0);
indoor_humi_label = lv_label_create(indoor_obj);
lv_obj_set_style_text_font(indoor_humi_label, &font_alipuhui, 0);
lv_label_set_text_fmt(indoor_humi_label, "%d%%", humi_value);
lv_obj_align_to(indoor_humi_label, humi_symbol_label2, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
// 显示太空人
lv_obj_t *tk_gif = lv_gif_create(obj);
lv_gif_set_src(tk_gif, &image_taikong);
lv_obj_align(tk_gif, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
上面函数各模块都分开了,看注释基本上就可以看懂了。lvgl的函数名称也比较易懂,看函数名称就可以知道是做什么用的。
lv_timer创建的回调函数value_update_cb代码如下所示:
// 主界面各值更新函数
void value_update_cb(lv_timer_t * timer)
{
// 更新日期 星期 时分秒
time(&now);
localtime_r(&now, &timeinfo);
lv_label_set_text_fmt(led_time_label, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
lv_label_set_text_fmt(date_label, "%d年%02d月%02d日", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday);
lv_week_show();
// 日出日落时间交替显示 每个5秒切换
if (timeinfo.tm_sec%10 == 0)
lv_label_set_text_fmt(sunset_label, "日落 %s", qwdaily_sunset);
else if(timeinfo.tm_sec%10 == 5)
lv_label_set_text_fmt(sunset_label, "日出 %s", qwdaily_sunrise);
// 更新温湿度
if(th_update_flag == 1)
{
th_update_flag = 0;
lv_label_set_text_fmt(indoor_temp_label, "%d℃", temp_value);
lv_label_set_text_fmt(indoor_humi_label, "%d%%", humi_value);
}
// 更新实时天气
if(qwnow_update_flag == 1)
{
qwnow_update_flag = 0;
lv_qweather_icon_show(); // 更新天气图标
lv_label_set_text_fmt(qweather_text_label, "%s", qwnow_text); // 更新天气情况文字描述
lv_label_set_text_fmt(outdoor_temp_label, "%d℃", qwnow_temp); // 更新室外温度
lv_label_set_text_fmt(outdoor_humi_label, "%d%%", qwnow_humi); // 更新室外湿度
}
// 更新空气质量
if(qair_update_flag ==1)
{
qair_update_flag = 0;
lv_qair_level_show();
}
// 更新每日天气
if(qwdaily_update_flag == 1)
{
qwdaily_update_flag = 0;
lv_label_set_text_fmt(qweather_temp_label, "%d~%d℃", qwdaily_tempMin, qwdaily_tempMax); // 温度范围
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
是否更新数值,取决于各个flag标志,比如是否更新温湿度的标志th_update_flag,这些flag,在对应的函数中获取到值以后,会把这些flag置1。
第17章 掌机示例
本章使用LVGL设计制作了一个简单的类似于手机界面的示例。 开机后,先是开机画面,logo炫酷出场,并伴有开机音乐,然后进入主界面,主界面如下图所示。主界面顶部显示文字和图标示例,下面部分显示六种应用,从上到下,从左到右,依次分别是:温湿度仪、弹力小游戏、回声器、水平仪、指南针和设置。每一种应用,都可以点击屏幕进入,进入后,上滑页面退回到主界面。
例程代码如下:
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:15-handheld.zip
17.1 让例程跑起来
下载例程到你的开发板之前,需要完成一些设置才可以。 复制开发板提供的例程(handheld)到的你的实验文件夹,然后使用VSCode打开文件夹。 第一步:设置目标芯片为esp32c3。 第二步:设置menuconfig。 1、把FLASH大小设置为8MB。
2、修改分区文件为自定义的。 3、把LVGL的缓存大小,修改为64K 4、勾选字库Montserrat14和20。 保存,并退出menuconfig。 第三步,设置好串口号,下载方式设置为串口,编译下载。17.2 代码讲解
本例程是在第16章例程基础上修改而来。
下面将介绍程序每一步的实现代码。
17.2.1 开机logo制作
程序上电,先显示开机logo。开机logo使用一张png格式图片,像素大小120*120。使用lvgl的在线图片转换工具转换成c文件后添加到工程,具体操作方法,在第16章已经讲过。如果你想换成自己的logo,也可以按照第16章的讲解制作。
开机logo的动画,是把图片快速旋转几圈。这里的动画,和第16章的开机动画不同,第16章的太空人旋转,是直接显示的gif图片,gif图片本身就是动图,所以直接显示就可以。这里的动画使用的是lvgl的animations组件实现。
开机界面代码如下所示:
// 设置角度的回调函数
static void set_angle(void * img, int32_t v)
{
lv_img_set_angle(img, v); // 设置图片的旋转角度
}
// 开机界面
void lv_gui_start(void)
{
// 显示logo
LV_IMG_DECLARE(image_lckfb_logo); // 声明图片
lckfb_logo = lv_img_create(lv_scr_act()); // 创建图片对象
lv_img_set_src(lckfb_logo, &image_lckfb_logo); // 设置图片对象的图片源
lv_obj_align(lckfb_logo, LV_ALIGN_CENTER, 0, 0); // 设置图片位置为屏幕正中心
lv_img_set_pivot(lckfb_logo, 60, 60); // 设置图片围绕自己的中心位置旋转
// 设置旋转动画
lv_anim_t a; // 创建动画变量
lv_anim_init(&a); // 初始化动画变量
lv_anim_set_var(&a, lckfb_logo); // 动画变量赋值为logo图片
lv_anim_set_exec_cb(&a, set_angle); // 创建设置角度的回调函数
lv_anim_set_values(&a, 0, 3600); // 设置动画旋转角度的开始值和结尾值
lv_anim_set_time(&a, 200); // 设置转一圈的周期是200毫秒
lv_anim_set_repeat_count(&a, 5); // 设置旋转5次
lv_anim_start(&a); // 动画开始
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
上述代码,看注释基本上就可以懂了。
可能会有问题的地方:第一个是lv_img_set_pivot()这个函数,它的第2、3个参数,是以自身为参考的,因为图片大小是120*120,所以,60和60就是它的中心,初学者可能会把这里的坐标认为是屏幕的坐标,造成困扰。第二个是lv_anim_set_values()这个函数,第2、3个参数,表示的是旋转的角度,范围是0~360°,这个函数又把旋转角度细分到了0.1度,所以3600代表的是360°,就是一圈。
lv_anim_set_exec_cb()函数指定了回调函数的名称,我们在lv_gui_start()前面写好了这个回调函数。这个回调函数中的v,就会依次带入我们刚才设定的0~3600,从而实现旋转一圈的动画。
17.2.2 开机音乐制作
开机logo在旋转的过程中,还伴随有开机音乐。这里的开机音乐,我使用了一个利剑出鞘的音效,从网络下载得到。点击下面链接获得,把鼠标放上去会出现下载图标。
代码下载
链接在资料下载中心
章节百度网盘中的音频文件!!
文件名称:sword.mp3
上面的音乐是mp3格式,需要把它转换成pcm格式才能被esp32使用。我已经把开机音乐转换成pcm格式,放到了main文件中。
下面是我制作开机音乐的完整过程。如果你想用自己的mp3音乐,可以按照下面的方法制作。
安装FFMPEG 转换格式需要使用ffmpeg这个工具,在windows上安装即可使用,安装过程比较简单,几步完成。
- 在WINDOWS程序中找到Windows PowerShell这个命令行工具,以管理员身份打开,如下图所示。
- 然后输入iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex,回车,用来下载choco工具。
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
到此ffmpeg就安装好了。
在Powershell中使用cd命令,切换到你要转换的音乐的文件夹。比如,我把利剑出鞘的音乐放到了D:\esp32c3\handheld这个文件夹下面,那我就输入cd D:\esp32c3\handheld命令,在命令行进入到这个文件。
然后使用ffmpeg -i sword.mp3 -f s16le -ar 16000 -ac -1 -acodec pcm_s16le sword.pcm命令,把sword.mp3转换成sword.pcm格式。ffmpeg -i sword.mp3 -f s16le -ar 16000 -ac -1 -acodec pcm_s16le sword.pcm
命令执行后,会在sword.mp3文件的旁边生成一个sword.pcm文件,把sword.pcm剪切或者复制粘贴放到main文件夹里面就可以了。
注意:如果你找的mp3音乐太长,想剪切自己喜欢的那一部分,可以使用ffmpeg剪切,截切方法在第8章例程的readme.md文件里面有介绍。或者可以使用其它音乐或视频编辑软件也可以完成。
接下来需要在main下面的CMakeList.txt文件中,使用EMBED_FILES把文件添加进去,如下代码所示:
idf_component_register(SRCS "xxx.c" 后面省略
INCLUDE_DIRS "."
EMBED_FILES "sword.pcm")
2
3
然后就可以在c文件中使用了。
例程中,我又新建了audio.c和audio.h文件,放音频相关的代码。
在audio.c文件开始处,先定义了音乐片段的开始和结尾的指针,如果你要换成自己的,把其中的sword换成你的pcm音乐名字。
extern const uint8_t music_pcm_start[] asm("_binary_sword_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_sword_pcm_end");
2
在spi_lcd_touch_example_main.c文件中,执行完开机画面后,执行了一个播放开机音乐的任务函数。
xTaskCreate(power_music_task, "power_music_task", 4096, NULL, 5, NULL); // 播放开机音乐
任务函数在audio.c文件中,代码如下所示:
// 开机音乐 任务函数
void power_music_task(void *pvParameters)
{
esp_err_t ret = ESP_OK;
size_t bytes_write = 0;
uint8_t *data_ptr = (uint8_t *)music_pcm_start;
/* (Optional) Disable TX channel and preload the data before enabling the TX channel,
* so that the valid data can be transmitted immediately */
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write));
data_ptr += bytes_write; // Move forward the data pointer
/* Enable the TX channel */
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
/* Write music to earphone */
ret = i2s_channel_write(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write, portMAX_DELAY);
if (ret != ESP_OK) {
/* Since we set timeout to 'portMAX_DELAY' in 'i2s_channel_write'
so you won't reach here unless you set other timeout value,
if timeout detected, it means write operation failed. */
ESP_LOGE(TAG, "[music] i2s write failed, %s", err_reason[ret == ESP_ERR_TIMEOUT]);
abort();
}
if (bytes_write > 0) {
ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
} else {
ESP_LOGE(TAG, "[music] i2s music play failed.");
abort();
}
gpio_set_level(GPIO_NUM_13, 0); // 输出低电平 关闭音频输出
xEventGroupSetBits(my_event_group, START_MUSIC_DOWN);
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
这个函数是从第8章的音乐播放任务函数复制过来的,然后做了一些修改。
第一个修改的地方是,第8章的音乐播放任务是需要一直循环播放,而这里只需要播放一次,所以把原来的while死循环去掉了。
第二个修改的地方是,在执行完音乐播放后,添加了关闭音频播放的语句,不关闭的话,音频放大器一直在工作,电路板会发热。
第三个修改的地方是,增加了事件组触发标志。因为主界面需要等待音乐播放完以后才能进入,所以这里设置了一个事件标志,用于通知主界面函数。
17.2.3 主界面制作
开机动画和开机音乐结束之后,进入主界面。主界面左上角显示了一个文字label,右上角显示了几个lvgl自带的符号。界面下部分显示了6个应用,这6个应用的图标,使用lvgl的button组件制作,button上面的画面,使用png图片。
这里用到的png图片,来自阿里巴巴矢量图标库,点击网页进入后,搜索框可以输入你想要的图标。
阿里巴巴矢量图标库:https://www.iconfont.cn/
声明:本例程用到的图标图片,仅供学习交流用!商用请联系图标作者。
选择好想要的图标后,点击图标进入,如下图所示,可以修改图片颜色、像素大小。本例程用的应用按钮图标像素是80*80,所以这里我们把像素设置为75,比按钮小一点。例程用到的这些图片颜色,我都改成了白色,你们可以选择自己喜欢的颜色修改。最后点击下载png格式图片,然后使用lvgl在线图片转换工具转成c语言文件。具体的生成方式,在第16章有讲。
本例程中用到的字体一共有3种。第1个是lvgl自带的lv_font_montserrat_20字体。第2个是在16章中用到的2个温湿度符号。第3个是阿里普惠字体,20像素,只把本例程用到的字符做成c文件即可,本例程用到的文字符号如下所示:
欢迎使用立创实战派开发板请对准USB旁边的麦克风说话滑动调节屏幕亮度 -0123456789%℃
具体的生成和使用方式,在第16章有精讲。
主界面函数代码如下所示:
// 主界面
void lv_main_page(void)
{
lv_obj_del(lckfb_logo); // 删除开机logo
// 创建主界面基本对象
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x000000), 0); // 修改背景为黑色
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0x00BF00 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
main_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(main_obj, &style, 0);
// 显示右上角符号
lv_obj_t * sylbom_label = lv_label_create(main_obj);
lv_obj_set_style_text_font(sylbom_label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(sylbom_label, lv_color_hex(0xffffff), 0);
lv_label_set_text(sylbom_label, LV_SYMBOL_CALL" "LV_SYMBOL_USB" "LV_SYMBOL_GPS" "LV_SYMBOL_BLUETOOTH" "LV_SYMBOL_WIFI" "LV_SYMBOL_BATTERY_FULL );
lv_obj_align_to(sylbom_label, main_obj, LV_ALIGN_TOP_RIGHT, -8, 8);
// 显示左上角欢迎语
lv_obj_t * text_label = lv_label_create(main_obj);
lv_obj_set_style_text_font(text_label, &font_alipuhui20, 0);
lv_label_set_long_mode(text_label, LV_LABEL_LONG_SCROLL_CIRCULAR); /*Circular scroll*/
lv_obj_set_width(text_label, 120);
lv_label_set_text(text_label, "欢迎使用立创实战派开发板");
lv_obj_align_to(text_label, main_obj, LV_ALIGN_TOP_LEFT, 8, 8);
// 设置应用图标style
static lv_style_t btn_style;
lv_style_init(&btn_style);
lv_style_set_radius(&btn_style, 16);
lv_style_set_bg_opa( &btn_style, LV_OPA_COVER );
lv_style_set_text_color(&btn_style, lv_color_hex(0xffffff));
lv_style_set_border_width(&btn_style, 0);
lv_style_set_pad_all(&btn_style, 5);
lv_style_set_width(&btn_style, 80);
lv_style_set_height(&btn_style, 80);
// 创建第1个应用图标
lv_obj_t * icon1 = lv_btn_create(main_obj);
lv_obj_add_style(icon1, &btn_style, 0);
lv_obj_set_style_bg_color(icon1, lv_color_hex(0x30a830), 0);
lv_obj_set_pos(icon1, 15, 50);
lv_obj_add_event_cb(icon1, th_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img1 = lv_img_create(icon1);
LV_IMG_DECLARE(image_th_icon);
lv_img_set_src(img1, &image_th_icon);
lv_obj_align(img1, LV_ALIGN_CENTER, 0, 0);
// 创建第2个应用图标
lv_obj_t * icon2 = lv_btn_create(main_obj);
lv_obj_add_style(icon2, &btn_style, 0);
lv_obj_set_style_bg_color(icon2, lv_color_hex(0xf87c30), 0);
lv_obj_set_pos(icon2, 120, 50);
lv_obj_add_event_cb(icon2, game_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img2 = lv_img_create(icon2);
LV_IMG_DECLARE(image_spr_icon);
lv_img_set_src(img2, &image_spr_icon);
lv_obj_align(img2, LV_ALIGN_CENTER, 0, 0);
// 创建第3个应用图标
lv_obj_t * icon3 = lv_btn_create(main_obj);
lv_obj_add_style(icon3, &btn_style, 0);
lv_obj_set_style_bg_color(icon3, lv_color_hex(0x008b8b), 0);
lv_obj_set_pos(icon3, 225, 50);
lv_obj_add_event_cb(icon3, echo_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img3 = lv_img_create(icon3);
LV_IMG_DECLARE(image_mic_icon);
lv_img_set_src(img3, &image_mic_icon);
lv_obj_align(img3, LV_ALIGN_CENTER, 0, 0);
// 创建第4个应用图标
lv_obj_t * icon4 = lv_btn_create(main_obj);
lv_obj_add_style(icon4, &btn_style, 0);
lv_obj_set_style_bg_color(icon4, lv_color_hex(0xd8b010), 0);
lv_obj_set_pos(icon4, 15, 147);
lv_obj_add_event_cb(icon4, att_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img4 = lv_img_create(icon4);
LV_IMG_DECLARE(image_att_icon);
lv_img_set_src(img4, &image_att_icon);
lv_obj_align(img4, LV_ALIGN_CENTER, 0, 0);
// 创建第5个应用图标
lv_obj_t * icon5 = lv_btn_create(main_obj);
lv_obj_add_style(icon5, &btn_style, 0);
lv_obj_set_style_bg_color(icon5, lv_color_hex(0xd8b010), 0);
lv_obj_set_pos(icon5, 120, 147);
lv_obj_add_event_cb(icon5, comp_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img5 = lv_img_create(icon5);
LV_IMG_DECLARE(image_comp_icon);
lv_img_set_src(img5, &image_comp_icon);
lv_obj_align(img5, LV_ALIGN_CENTER, 0, 0);
// 创建第6个应用图标
lv_obj_t * icon6 = lv_btn_create(main_obj);
lv_obj_add_style(icon6, &btn_style, 0);
lv_obj_set_style_bg_color(icon6, lv_color_hex(0xb87fa8), 0);
lv_obj_set_pos(icon6, 225, 147);
lv_obj_add_event_cb(icon6, set_event_handler, LV_EVENT_CLICKED, NULL);
lv_obj_t * img6 = lv_img_create(icon6);
LV_IMG_DECLARE(image_set_icon);
lv_img_set_src(img6, &image_set_icon);
lv_obj_align(img6, LV_ALIGN_CENTER, 0, 0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
这个函数结构清晰,很容易看懂。下面讲解一下可能有疑惑的地方。
最开始使用lv_obj_del()函数,删除开机画面,因为开机画面只有logo,所以只删除logo就可以。
在“创建主界面基本对象”时,使用了一个渐变色设置,使得背景不再是单一的颜色。这个是由上面代码中的第13、14、15行代码实现。lv_style_set_bg_color()函数定义了基本背景色,lv_style_set_bg_grad_color()函数定义了目标颜色,lv_style_set_bg_grad_dir()函数定义了渐变色的方向,可以是垂直方向,也可以是水平方向。
在“显示右上角符号”时,各个符号之间,加了“ ”双引号,双引号之间是空格,这样使得显示的各个符号之间加上空格,不加空格挤到一起看起来不太美观。
在“显示左上角欢迎语”时,定义了label的宽度是120像素,需要显示的文字宽度实际上超过了120像素,所以这里使用了lv_label_set_long_mode()这个函数,使得label标签内容,可以自动循环显示完毕。
在“设置应用图标style”中,统一设置了6个按钮图标的格式,但是没有定义它们的背景色,它们的背景色,是在显示各图标的时候,再单独设置为不同的背景色。
接下来创建了6个按钮图标,每一个按钮图标,都使用lv_obj_add_event_cb()函数创建了它们被按下后要执行的回调函数,也就是我们需要的应用程序处理函数。
每一个应用程序处理函数,都是在界面上再绘制一层新的界面对象,在新的界面对象上,再创建对应的显示对象。退出该应用后,会删除新的界面对象。也就是说,在进入应用程序后,主界面其实一直都在,只是被新的界面覆盖了。在退出新的界面后,主界面自然就会再显示出来。
// 手势处理函数
static void my_gesture_event_cb(lv_event_t * e)
{
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
if(dir == LV_DIR_TOP)
{
if((icon_flag == 1)||(icon_flag == 2)||(icon_flag == 4)||(icon_flag == 5))
{
lv_timer_del(my_lv_timer);
}
lv_obj_del(icon_in_obj);
icon_flag = 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在每个应用程序中,会给界面对象创建一个触摸手势回调函数。上面就是这个回调函数的代码。代码中,判断是不是向上滑动屏幕,如果是的话,就删除界面对象,并把icon_flag赋值为0,表示不在任何一个应用程序里面了。另外,在应用程序1 2 4 5中还创建了lv定时器,所以需要把lv定时器也删除。
lv_obj_del()会把括号里面的参数对象以及它的子对象全部删除。
17.2.4 温湿度应用程序
点击温湿度仪图标,进入温湿度回调函数运行。
// 温湿度应用程序
static void th_event_handler(lv_event_t * e)
{
gxhtc3_get_tah(); // 获取一次温湿度
temp_value = round(temp);
humi_value = round(humi);
// 重新创建一个面板对象
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0x00BF00 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(main_obj);
lv_obj_add_style(icon_in_obj, &style, 0);
// 显示温度表
temp_meter = lv_meter_create(icon_in_obj);
lv_obj_center(temp_meter);
lv_obj_set_size(temp_meter, 220, 220);
lv_obj_remove_style(temp_meter, NULL, LV_PART_INDICATOR);
lv_meter_scale_t * scale = lv_meter_add_scale(temp_meter);
lv_meter_set_scale_ticks(temp_meter, scale, 9, 2, 10, lv_palette_main(LV_PALETTE_GREY));
lv_meter_set_scale_major_ticks(temp_meter, scale, 1, 2, 12, lv_palette_main(LV_PALETTE_GREY), 12);
lv_meter_set_scale_range(temp_meter, scale, -30, 50, 160, 190);
lv_meter_indicator_t * indic;
indic = lv_meter_add_arc(temp_meter, scale, 3, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_meter_set_indicator_start_value(temp_meter, indic, -30);
lv_meter_set_indicator_end_value(temp_meter, indic, 18);
indic = lv_meter_add_scale_lines(temp_meter, scale, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_BLUE), false, 0);
lv_meter_set_indicator_start_value(temp_meter, indic, -30);
lv_meter_set_indicator_end_value(temp_meter, indic, 18);
indic = lv_meter_add_arc(temp_meter, scale, 3, lv_palette_main(LV_PALETTE_GREEN), 0);
lv_meter_set_indicator_start_value(temp_meter, indic, 18);
lv_meter_set_indicator_end_value(temp_meter, indic, 25);
indic = lv_meter_add_scale_lines(temp_meter, scale, lv_palette_main(LV_PALETTE_GREEN), lv_palette_main(LV_PALETTE_GREEN), false, 0);
lv_meter_set_indicator_start_value(temp_meter, indic, 18);
lv_meter_set_indicator_end_value(temp_meter, indic, 25);
indic = lv_meter_add_arc(temp_meter, scale, 3, lv_palette_main(LV_PALETTE_RED), 0);
lv_meter_set_indicator_start_value(temp_meter, indic, 25);
lv_meter_set_indicator_end_value(temp_meter, indic, 50);
indic = lv_meter_add_scale_lines(temp_meter, scale, lv_palette_main(LV_PALETTE_RED), lv_palette_main(LV_PALETTE_RED), false, 0);
lv_meter_set_indicator_start_value(temp_meter, indic, 25);
lv_meter_set_indicator_end_value(temp_meter, indic, 50);
temp_indic = lv_meter_add_arc(temp_meter, scale, 10, lv_palette_main(LV_PALETTE_ORANGE), 13);
lv_meter_set_indicator_start_value(temp_meter, temp_indic, -30);
lv_meter_set_indicator_end_value(temp_meter, temp_indic, temp_value);
lv_obj_t *temp_symbol_label = lv_label_create(temp_meter);
lv_obj_set_style_text_font(temp_symbol_label, &font_myawesome, 0);
lv_label_set_text(temp_symbol_label, "\xEF\x8B\x88"); // 温度图标
lv_obj_align(temp_symbol_label, LV_ALIGN_CENTER, -28, -30);
temp_label = lv_label_create(temp_meter);
lv_obj_set_style_text_font(temp_label, &font_alipuhui20, 0);
lv_label_set_text_fmt(temp_label, "%d℃", temp_value);
lv_obj_align_to(temp_label, temp_symbol_label, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
// 显示湿度表
humi_meter = lv_meter_create(icon_in_obj);
lv_obj_center(humi_meter);
lv_obj_align(humi_meter, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(humi_meter, 220, 220);
lv_obj_set_style_opa(humi_meter, LV_OPA_50, 0);
lv_obj_remove_style(humi_meter, NULL, LV_PART_INDICATOR);
lv_meter_scale_t * humi_scale = lv_meter_add_scale(humi_meter);
lv_meter_set_scale_ticks(humi_meter, humi_scale, 11, 2, 10, lv_palette_main(LV_PALETTE_GREY));
lv_meter_set_scale_major_ticks(humi_meter, humi_scale, 1, 2, 12, lv_palette_main(LV_PALETTE_GREY), 10);
lv_meter_set_scale_range(humi_meter, humi_scale, 0, 100, 160, 10);
lv_meter_indicator_t * indic2;
indic2 = lv_meter_add_arc(humi_meter, humi_scale, 3, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 0);
lv_meter_set_indicator_end_value(humi_meter, indic2, 40);
indic2 = lv_meter_add_scale_lines(humi_meter, humi_scale, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_BLUE), false, 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 0);
lv_meter_set_indicator_end_value(humi_meter, indic2, 40);
indic2 = lv_meter_add_arc(humi_meter, humi_scale, 3, lv_palette_main(LV_PALETTE_GREEN), 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 40);
lv_meter_set_indicator_end_value(humi_meter, indic2, 70);
indic2 = lv_meter_add_scale_lines(humi_meter, humi_scale, lv_palette_main(LV_PALETTE_GREEN), lv_palette_main(LV_PALETTE_GREEN), false, 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 40);
lv_meter_set_indicator_end_value(humi_meter, indic2, 70);
indic2 = lv_meter_add_arc(humi_meter, humi_scale, 3, lv_palette_main(LV_PALETTE_RED), 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 70);
lv_meter_set_indicator_end_value(humi_meter, indic2, 100);
indic2 = lv_meter_add_scale_lines(humi_meter, humi_scale, lv_palette_main(LV_PALETTE_RED), lv_palette_main(LV_PALETTE_RED), false, 0);
lv_meter_set_indicator_start_value(humi_meter, indic2, 70);
lv_meter_set_indicator_end_value(humi_meter, indic2, 100);
humi_indic = lv_meter_add_arc(humi_meter, humi_scale, 10, lv_palette_main(LV_PALETTE_ORANGE), 13);
lv_meter_set_indicator_start_value(humi_meter, humi_indic, 0);
lv_meter_set_indicator_end_value(humi_meter, humi_indic, humi_value);
lv_obj_t *humi_symbol_label = lv_label_create(temp_meter);
lv_obj_set_style_text_font(humi_symbol_label, &font_myawesome, 0);
lv_label_set_text(humi_symbol_label, "\xEF\x81\x83"); // 湿度图标
lv_obj_align(humi_symbol_label, LV_ALIGN_CENTER, -28, 30);
humi_label = lv_label_create(temp_meter);
lv_obj_set_style_text_font(humi_label, &font_alipuhui20, 0);
lv_label_set_text_fmt(humi_label, "%d%%", humi_value);
lv_obj_align_to(humi_label, humi_symbol_label, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
// 创建一个获取温湿度的任务
icon_flag = 1; // 标记已经进入第一个应用
xTaskCreate(get_th_task, "get_th_task", 4096, NULL, 5, NULL);
// 创建一个lv_timer 定时更新数据
my_lv_timer = lv_timer_create(thv_update_cb, 1000, NULL);
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
进入应用后,先获取一次温湿度值,以便在第一次显示的时候用,在最后面,创建了一个温湿度任务,获取温湿度,然后创建了一个lv定时器,定时1秒钟更新一次温湿度数据。 在“重新创建一个面板对象”时,温湿度仪这里使用的渐变颜色,还是和主界面的一样。这里大家可以修改成自己喜欢的渐变色背景,或者修改成单色背景也可以。 温度表和湿度表的显示,使用了lv_meter组件,lv_meter创建的组件都是圆形的,温度表使用圆形的上半部分,湿度表使用圆形的下半部分,温度表和湿度表都创建在了同一个位置,且大小相同,我们把湿度表的透明图设置成了50,这样的话,就可以看到湿度表下面的温度表了,否则就只能看到湿度表。在湿度表中,使用lv_obj_set_style_opa()函数设置湿度表的透明度为50。 lv_meter_set_scale_ticks()函数、lv_meter_set_scale_major_ticks()函数、lv_meter_set_scale_range()函数,这3个函数中,有很多个数字参数,接下来我们挨个介绍它们的意义。 lv_meter_set_scale_ticks()函数 我们在VSCode中,把鼠标放到这个函数的上面,单击右键,选择“转到声明”,注意不是“转到定义”,可以看到它的参数解释,如下所示:
/**
* Set the properties of the ticks of a scale
* @param obj pointer to a meter object
* @param scale pointer to scale (added to `meter`)
* @param cnt number of tick lines
* @param width width of tick lines
* @param len length of tick lines
* @param color color of tick lines
*/
void lv_meter_set_scale_ticks(lv_obj_t * obj, lv_meter_scale_t * scale, uint16_t cnt, uint16_t width, uint16_t len,
lv_color_t color);
2
3
4
5
6
7
8
9
10
11
结合温度表中使用的语句,以及实际上显示出来的样子,一起来看,就可以很快看明白。 lv_meter_set_scale_ticks(temp_meter, scale, 9, 2, 10, lv_palette_main(LV_PALETTE_GREY)); 第3个参数,9代表的是一共有9个刻度。第4、5个参数,2代表的是刻度的宽度是2个像素,10代表的是刻度的长度是10个像素。 lv_meter_set_scale_major_ticks()函数 我们在VSCode中,把鼠标放到这个函数的上面,单击右键,选择“转到声明”,注意不是“转到定义”,可以看到它的参数解释,如下所示:
/**
* Make some "normal" ticks major ticks and set their attributes.
* Texts with the current value are also added to the major ticks.
* @param obj pointer to a meter object
* @param scale pointer to scale (added to `meter`)
* @param nth make every Nth normal tick major tick. (start from the first on the left)
* @param width width of the major ticks
* @param len length of the major ticks
* @param color color of the major ticks
* @param label_gap gap between the major ticks and the labels
*/
void lv_meter_set_scale_major_ticks(lv_obj_t * obj, lv_meter_scale_t * scale, uint16_t nth, uint16_t width,
uint16_t len, lv_color_t color, int16_t label_gap);
2
3
4
5
6
7
8
9
10
11
12
13
结合温度表中使用的语句,以及实际上显示出来的样子,一起来看,就可以很快看明白。
lv_meter_set_scale_major_ticks(temp_meter, scale, 1, 2, 12, lv_palette_main(LV_PALETTE_GREY), 12);
第3个参数,1代表每隔1个刻度为主要刻度。第4、5个参数,2代表主要刻度的宽度是2个像素,12代表的是主要刻度的长度是12个像素。最后一个参数,12代表的是刻度旁边显示的数字和刻度弧线之间的距离,这个数字一般设置为和主要刻度的长度相同,如果这里的数字比主要刻度线的长度小的话,刻度数字就会和主要刻度重叠。 lv_meter_set_scale_range()函数 我们在VSCode中,把鼠标放到这个函数的上面,单击右键,选择“转到声明”,注意不是“转到定义”,可以看到它的参数解释,如下所示:
/**
* Set the value and angular range of a scale.
* @param obj pointer to a meter object
* @param scale pointer to scale (added to `meter`)
* @param min the minimum value
* @param max the maximal value
* @param angle_range the angular range of the scale
* @param rotation the angular offset from the 3 o'clock position (clock-wise)
*/
void lv_meter_set_scale_range(lv_obj_t * obj, lv_meter_scale_t * scale, int32_t min, int32_t max, uint32_t angle_range,
uint32_t rotation);
2
3
4
5
6
7
8
9
10
11
结合温度表中使用的语句,以及实际上显示出来的样子,一起来看,就可以很快看明白。
lv_meter_set_scale_range(temp_meter, scale, -30, 50, 160, 190);
第3、4个参数,-30表示最小刻度值,50代表最大刻度值,因为我们要测的温度范围是-30℃~50℃。第5个参数,160表示刻度圆弧的角度大小是160°。第6个参数,190表示的是刻度圆弧的旋转角度是190°。
上面回调函数中的第87~106行,用来更改刻度圆弧的颜色,分成了3段,分别定义成了3种颜色。
第58~60行,创建了温度刻度圆弧,会随着温度值的变化而变化。
第108~110行,创建了湿度刻度圆弧,会随着湿度值的变化而变化。
第124行,创建了一个获取温湿度的任务。icon_flag用来控制任务的删除,在获取温湿度任务函数中,获取一次温度,就会看一下这个icon_flag,当icon_flag等于0的时候,就会删除获取温湿度任务函数。这里把icon_flag先赋值为1,再进入获取温湿度任务函数。
最后几行,创建了一个提示符,并给界面对象创建了屏幕滑动手势回调函数。注意,滑动屏幕任意位置,都可以退出应用。
温湿度值更新函数如下所示:
// 定时更新温湿度值
void thv_update_cb(lv_timer_t * timer)
{
lv_meter_set_indicator_end_value(temp_meter, temp_indic, temp_value);
lv_meter_set_indicator_end_value(humi_meter, humi_indic, humi_value);
lv_label_set_text_fmt(temp_label, "%d℃", temp_value);
lv_label_set_text_fmt(humi_label, "%d%%", humi_value);
}
2
3
4
5
6
7
8
这个函数每隔1秒钟执行一次,间隔时间是在创建定时器回调函数的时候设置的。
在函数中,会更新圆弧刻度的长度以及温湿度数字值。
17.2.5 弹力球应用程序
点击第2个图标,也就是带弹簧的图标,进入弹力球应用程序。
// 第2个图标 弹力球应用程序
static void game_event_handler(lv_event_t * e)
{
// 创建一个界面对象
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0xcccccc));
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(icon_in_obj, &style, 0);
// 创建一个垫子
static lv_style_t mat_style;
lv_style_init(&mat_style);
lv_style_set_radius(&mat_style, 0);
lv_style_set_border_width(&mat_style, 0);
lv_style_set_pad_all(&mat_style, 0);
lv_style_set_shadow_width(&mat_style, 10);
lv_style_set_shadow_color(&mat_style, lv_color_black());
lv_style_set_shadow_ofs_x(&mat_style, 10);
lv_style_set_shadow_ofs_y(&mat_style, 10);
mat = lv_obj_create(icon_in_obj);
lv_obj_add_style(mat, &mat_style, 0);
lv_obj_set_style_bg_color(mat, lv_color_hex(0x6B8E23), 0);
lv_obj_align(mat, LV_ALIGN_BOTTOM_LEFT, 30, -30);
lv_obj_set_size(mat, 80, 60);
// 创建一个圆球
ball = lv_led_create(icon_in_obj);
lv_led_set_brightness(ball, 150);
lv_led_set_color(ball, lv_palette_main(LV_PALETTE_DEEP_ORANGE));
lv_obj_align_to(ball, mat, LV_ALIGN_OUT_TOP_MID, 0, 0);
// 创建一个lv_timer 用于更新圆球的坐标
icon_flag = 2; // 标记已经进入第2个图标
my_lv_timer = lv_timer_create(game_update_cb, 50, NULL); //
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
与温湿度应用程序相比,不同的是中间创建垫子和小球以及创建lv_timer的地方,其它地方基本一样。
“创建界面对象”部分,这里把底色更改为了单色。
“创建一个垫子”,使用的是基本对象,主要是把这个基本对象的的style更改了一下,把它的圆角设置为0,就变成了矩形,然后把边界线以及内部间距都设置为了0,最后还加了阴影。
“创建一个圆球”,使用的是LED对象,lv_obj_align_to()函数把圆球放到了垫子上面。
然后创建了lv_timer回调函数,50毫秒进入一次,用来实时检测手指按下,更改垫子高度和更改小球的高度。
回调函数代码如下所示:
lv_obj_t * mat; // 创建一个弹力球的垫子
lv_obj_t * ball; // 创建一个弹力球
int mat_flag; // 弹力球标志位
int ball_height = 0; // 弹力球的高度
int ball_dir = 0; // 弹力球的方向
int mat_height = 0; // 垫子的高度
// 弹力球各值更新程序
void game_update_cb(lv_timer_t * timer)
{
if (strength != 0) // 发现有手指按下屏幕
{
if(strength < 31) // 限制手指按下时间最大为30
{
mat_height = 60 - strength; // 计算正方体的高度
lv_obj_set_size(mat, 80, mat_height); // 调整正方体的高度
lv_obj_align_to(ball, mat, LV_ALIGN_OUT_TOP_MID, 0, 0); // 让弹力球跟随垫子一起移动
mat_flag = 1; // 表示垫子已经缩小过
}
}
else if(mat_flag == 1) // 如果垫子已经缩小过
{
lv_obj_set_size(mat, 80, 60); // 垫子回弹到原始值
lv_obj_align_to(ball, mat, LV_ALIGN_OUT_TOP_MID, 0, 0); // 弹力球跟随垫子
mat_flag = 2; // 标记垫子已经回弹
}
else if(mat_flag == 2) // 垫子已经回弹 小球应该向上走了
{
if (ball_dir == 0) // 向上运动
{
if(ball_height < 150) // 限制弹力球上弹高度为150像素
{
ball_height = ball_height + 10; // 每次上升10个像素
lv_obj_align_to(ball, mat, LV_ALIGN_OUT_TOP_MID, 0, -ball_height); // 更新弹力球位置
if (ball_height >= (0 + (60-mat_height)*5)) // 根据力度计算小球最大高度
{
ball_dir = 1; // 如果达到最大高度 更改小球运动方向
}
}
}
else // 向下运动
{
if(ball_height > 0) // 限制小球最低高度
{
ball_height = ball_height - 10; // 每次降低10个像素高度
lv_obj_align_to(ball, mat, LV_ALIGN_OUT_TOP_MID, 0, -ball_height); // 更新弹力球高度
if (ball_height == 0) // 如果弹力球落到了垫子上
{
mat_flag = 0; // 垫子状态恢复
ball_dir = 0; // 小球方向恢复
mat_height = 0; // 垫子高度恢复
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
回调函数中出现的strength这个变量,在触摸屏触摸回调函数中获得。触摸屏回调函数在spi_lcd_touch_example_main.c文件中,代码如下:
int strength = 0;
static void example_lvgl_touch_cb(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
uint16_t touchpad_x[1] = {0};
uint16_t touchpad_y[1] = {0};
uint8_t touchpad_cnt = 0;
/* Read touch controller data */
esp_lcd_touch_read_data(drv->user_data);
/* Get coordinates */
bool touchpad_pressed = esp_lcd_touch_get_coordinates(drv->user_data, touchpad_x, touchpad_y, NULL, &touchpad_cnt, 1);
if (touchpad_pressed && touchpad_cnt > 0) {
data->point.x = touchpad_x[0];
data->point.y = touchpad_y[0];
data->state = LV_INDEV_STATE_PRESSED;
strength++;
printf("x = %d y = %d strength = %d\n", data->point.x, data->point.y, strength);
} else {
data->state = LV_INDEV_STATE_RELEASED;
strength = 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这个触摸屏回调函数,与第16章的触摸屏回调函数的区别就是加了strength变量。原来只是记录触摸的x和y坐标值,这里又加了strength变量,用来记录按下的时间,当手指按着不放,这个值就会一直增加,手指放开以后,这个值会清零。
然后再看回game_update_cb弹力球更新程序。
当发现手指按下后,根据按下的时间,更改垫子的高度即可,更改垫子高度使用lv_obj_set_size()函数。
更改了垫子高度以后,小球的位置还没有动,所以再使用lv_obj_align_to()函数,让小球跟着垫子的高度下降而下降。
当手指放开以后,先把垫子和小球的高度恢复,就是一个瞬间弹起的效果。然后再让更改小球的高度,每次高度上升10个像素大小,达到小球被弹到空中的一个效果,根据刚才垫子被挤压的程度,限制一下小球弹起来的高度。到达最高点后,继续更改小球位置,每次下降10个像素,达到一个落下的效果,最后落到垫子上方停止。
17.2.6 回声应用程序
点击第3个图标,就是带麦克风的那个图标,进入回声应用程序。
static void echo_event_handler(lv_event_t * e)
{
// 创建一个界面对象
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0x00BF00 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(icon_in_obj, &style, 0);
// 创建一个文字label
lv_obj_t * text_label = lv_label_create(icon_in_obj);
lv_obj_set_style_text_color(text_label, lv_color_hex(0xffffff), 0);
lv_obj_set_style_text_font(text_label, &font_alipuhui20, 0);
lv_obj_align(text_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(text_label, "请对准USB旁边的麦克风说话");
// 创建一个音乐回声的任务
icon_flag = 3; // 标记已经进入第3个图标
xTaskCreate(echo_task, "music_echo_task", 8192, NULL, 5, NULL);
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
通过对“温湿度应用程序”和“弹力球应用程序”的讲解,我们已经知道了应用程序的大致结构。
这个应用程序比较简单,界面上只显示了一句话。然后创建了一个回声任务,没有创建lv_timer。回声任务在退出这个应用程序后删除。回声任务函数,从第8章的回声任务函数复制过来,只添加了控制音频放大器开启和关闭的语句,在进入任务时开启,在退出任务函数时关闭。
17.2.7 水平仪应用程序
点击第4个图标,就是显示手机有些倾斜的那个图标,进入水平仪应用程序。
水平仪应用程序界面,参考小米手机自带的水平仪界面设计,比较简洁,只有一个圆点和一个圆圈。圆点会根据开发板的位置更改它在屏幕中的位置,并在左上角和右上角显示当前xy方向的角度。
// 水平仪应用程序
static void att_event_handler(lv_event_t * e)
{
// 创建一个界面对象
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0x00BF00 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(icon_in_obj, &style, 0);
// 画一个外圆
lv_obj_t * arc = lv_arc_create(icon_in_obj);
lv_arc_set_bg_angles(arc, 0, 360);
lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
lv_obj_center(arc);
lv_arc_set_value(arc, 360);
lv_obj_set_size(arc, 200, 200);
// 画一个实心圆点
att_led = lv_led_create(icon_in_obj);
lv_obj_align(att_led, LV_ALIGN_CENTER, 0, 0);
lv_led_set_brightness(att_led, 255);
lv_led_set_color(att_led, lv_palette_main(LV_PALETTE_RED));
// 显示X和Y的角度
att_x_label = lv_label_create(icon_in_obj);
lv_obj_set_style_text_font(att_x_label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(att_x_label, lv_color_hex(0xffffff), 0);
lv_obj_align(att_x_label, LV_ALIGN_TOP_LEFT, 20, 20);
att_y_label = lv_label_create(icon_in_obj);
lv_obj_set_style_text_font(att_y_label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(att_y_label, lv_color_hex(0xffffff), 0);
lv_obj_align(att_y_label, LV_ALIGN_TOP_RIGHT, -20, 20);
// 创建一个lv_timer 用于更新圆的坐标
icon_flag = 4; // 标记已经进入第4个图标
my_lv_timer = lv_timer_create(att_update_cb, 100, NULL);
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
界面中的“外圆”位置固定不变,使用lvgl的arc对象组件创建。
界面中的“圆点”位置会随开发板姿态而变化,使用lvgl的led对象组件创建。
最后创建了一个lv_timer,指定了回调函数,且每隔100毫秒进入一次回调函数。代码如下:
// 定时更新水平仪坐标值
void att_update_cb(lv_timer_t * timer)
{
t_sQMI8658C QMI8658C;
int att_led_x, att_led_y;
qmi8658c_fetch_angleFromAcc(&QMI8658C);
att_led_x = round(QMI8658C.AngleX);
att_led_y = round(QMI8658C.AngleY);
lv_obj_align(att_led, LV_ALIGN_CENTER, -att_led_x, att_led_y);
lv_label_set_text_fmt(att_x_label, "X=%d°", -att_led_x);
lv_label_set_text_fmt(att_y_label, "Y=%d°", att_led_y);
}
2
3
4
5
6
7
8
9
10
11
12
13
回调函数用于更新圆点位置和XY数值。
17.2.8 指南针应用程序
点击第5个图标,就是显示位置箭头的那个图标,进入指南针应用程序。指南针应用程序没有经过微调,在各个地区的显示结果可能不是很准确。
// 指南针应用程序
static void comp_event_handler(lv_event_t * e)
{
// 创建一个界面对象
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0x00BFFF));
lv_style_set_bg_grad_color( &style, lv_color_hex(0x00BF00));
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(icon_in_obj, &style, 0);
// 绘制指南针仪表
compass_meter = lv_meter_create(icon_in_obj);
lv_obj_center(compass_meter);
lv_obj_set_size(compass_meter, 220, 220);
lv_obj_set_style_bg_opa(compass_meter, LV_OPA_TRANSP, 0);
lv_obj_set_style_text_color(compass_meter, lv_color_hex(0xffffff), 0);
lv_obj_remove_style(compass_meter, NULL, LV_PART_INDICATOR);
compass_scale = lv_meter_add_scale(compass_meter);
lv_meter_set_scale_ticks(compass_meter, compass_scale, 61, 1, 10, lv_color_hex(0xffffff));
lv_meter_set_scale_major_ticks(compass_meter, compass_scale, 10, 2, 16, lv_color_hex(0xffffff), 10);
lv_meter_set_scale_range(compass_meter, compass_scale, 0, 360, 360, 270);
// 添加指针
lv_obj_t * arrow_label = lv_label_create(icon_in_obj);
lv_label_set_text(arrow_label, LV_SYMBOL_DOWN);
lv_obj_set_style_text_color(arrow_label, lv_palette_main(LV_PALETTE_RED), 0);
lv_obj_align(arrow_label, LV_ALIGN_CENTER, 0, -100);
// 显示方位角度
comp_label = lv_label_create(icon_in_obj);
lv_obj_set_style_text_font(comp_label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(comp_label, lv_color_hex(0xffffff), 0);
lv_obj_align(comp_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(comp_label, "0");
// 创建一个lv_timer 用于更新方位角的值
icon_flag = 5; // 标记已经进入第5个图标
my_lv_timer = lv_timer_create(comp_update_cb, 500, NULL);
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
指南针仪表,使用meter组件绘制,程序基本上和温湿度仪应用程序的一样,关于程序的精讲请看温湿度仪关于meter的讲解。这里的meter,刻度设置成了360°圆形,而且会随着方位角的变化而旋转。中间位置,显示了当前的角度值,以正北方向为0°。整个meter设置成了透明色,和背景色融为一体,看起来比较美观,大家做自己的设计时,也可以参考这种形式。
回调函数更新仪表的旋转角度和数值,代码如下:
// 定时更新方位角的值
void comp_update_cb(lv_timer_t * timer)
{
t_sQMC5883L QMC5883L;
int comp_angle;
qmc5883l_fetch_azimuth(&QMC5883L);
comp_angle = round(QMC5883L.azimuth);
comp_angle = (comp_angle + 120)%360; // 校准角度 正北为0
lv_label_set_text_fmt(comp_label, "%d°", comp_angle);
comp_angle = 360 - (comp_angle+90)%360; // 计算旋转角度
lv_meter_set_scale_range(compass_meter, compass_scale, 0, 360, 360, comp_angle);
}
2
3
4
5
6
7
8
9
10
11
12
13
图中的数值,是在调整仪表刻度和显示数值的对应,比如数值显示0°的时候,刻度箭头应该指到仪表刻度的0°位置(0°也就是360°),数值显示30°的时候,刻度箭头应该指到仪表刻度的30°位置。
17.2.9 设置应用程序
点击最后1个图标,进入设置界面应用程序。
// 进入设置界面
static void set_event_handler(lv_event_t * e)
{
static lv_style_t style;
lv_style_init(&style);
lv_style_set_radius(&style, 10);
lv_style_set_bg_opa( &style, LV_OPA_COVER );
lv_style_set_bg_color(&style, lv_color_hex(0xE680FF));
lv_style_set_bg_grad_color( &style, lv_color_hex( 0xE68000 ) );
lv_style_set_bg_grad_dir( &style, LV_GRAD_DIR_VER );
lv_style_set_border_width(&style, 0);
lv_style_set_pad_all(&style, 0);
lv_style_set_width(&style, 320);
lv_style_set_height(&style, 240);
icon_in_obj = lv_obj_create(lv_scr_act());
lv_obj_add_style(icon_in_obj, &style, 0);
// 创建文字提示
lv_obj_t * text_label = lv_label_create(icon_in_obj);
lv_obj_set_style_text_color(text_label, lv_color_hex(0xffffff), 0);
lv_obj_set_style_text_font(text_label, &font_alipuhui20, 0);
lv_obj_align(text_label, LV_ALIGN_CENTER, 0, -80);
lv_label_set_text(text_label, "滑动调节屏幕亮度");
// 创建一个滑动条
lv_obj_t * slider = lv_slider_create(icon_in_obj);
lv_slider_set_value(slider, bg_duty*100 , 0);
lv_obj_center(slider);
lv_obj_set_height(slider, 50);
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
icon_flag = 6; // 标记已经进入第6个图标
// 绘制退出提示符
lv_obj_t * label = lv_label_create(icon_in_obj);
lv_label_set_text(label, LV_SYMBOL_UP);
lv_obj_set_style_text_color(label, lv_color_hex(0xffffff), 0);
lv_obj_align(label, LV_ALIGN_BOTTOM_RIGHT, -20, -20);
// 添加向上滑动退出功能
lv_obj_add_event_cb(icon_in_obj, my_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(icon_in_obj, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(icon_in_obj, LV_OBJ_FLAG_CLICKABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
这个应用程序中,在界面上放置了一个滑动条,并且定义了滑动条回调函数。
static void slider_event_cb(lv_event_t * e)
{
int x;
lv_obj_t * slider = lv_event_get_target(e);
lv_slider_set_range(slider, 10, 80);
x = lv_slider_get_value(slider);
bg_duty = (float)x/100; // 根据滑动条的值计算占空比
// 设置占空比
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8191*(1-bg_duty));
// 更新背光
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
2
3
4
5
6
7
8
9
10
11
12
13
在滑动条回调函数中,通过获得滑动条的值,来计算占空比值,给背光调节函数用。
滑动条的值,是从0~100。占空比的值是0%~100%,也就是0~1。所以需要换算一下。
背光调节功能,在刚开机的时候进行了初始化。背光调节初始化函数,如下所示:
// LCD背光调节初始化函数
static void lcd_brightness_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_13_BIT, // Set duty resolution to 13 bits,
.freq_hz = 5000, // Frequency in Hertz. Set frequency at 5 kHz
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = EXAMPLE_PIN_NUM_BK_LIGHT,
.duty = 0, // Set duty
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
该函数对ledc_timer和ledc_channel进行了配置,函数是从官方peripherals/ledc/ledc_basic例程中复制过来的,原函数名称是example_ledc_init,这里改成了lcd_brightness_init,然后需要把给EXAMPLE_PIN_NUM_BK_LIGHT做一个宏定义,对照开发板原理图,控制液晶屏背光的引脚是IO2,所以把它定义为2就可以了。
第18章 桌面对话助手
桌面对话助手这个例程,可以实现人工智能对话,同时双方对话的内容还可以以文字的形式显示到液晶屏上。
开机画面如下,提示正在连接WiFi。
AI的回答,显示文字的同时,也会在喇叭里面播放声音。
如果我们的问题比较长,文字超出了屏幕,会自动滚动显示。
如果AI的回答比较长,文字超出了屏幕,会自动出现滑动条,上下滑动可看全部内容。
例程实现的原理如下:
第1步:从麦克风获取声音,把声音上传到语音识别服务器,把语音转成文字,然后把文字发回单片机。
第2步:把识别后的文字上传到大模型服务器,大模型针对这句话给出文字回答,把回答的文字发回单片机。
第3步:把回答的文字,上传到文字转语音服务器,把生成的语音发回单片机,用喇叭放出来。
上述步骤看似很复杂,实际上,从我们提问到AI回答的时间非常短,因为例程采用了Http流式传输。
大家可以基于这个例程,加上自己的想法,实现更有趣的应用。
例程代码如下:
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:16-ai_chat.zip
18.1 让例程跑起来
本例程代码,使用乐鑫官方的ADF音频框架,所以需要先安装ADF。关于ADF的介绍,请看下面链接。
https://docs.espressif.com/projects/esp-adf/
18.1.1 安装ADF开发环境
下面开始安装ADF开发环境,一共3个步骤,几分钟就可以完成。
第1步:下载esp-adf文件夹
在VSCode中点击“终端”按钮进入终端。终端按钮如下图所示:
我们可以把esp-adf下载到esp-idf文件夹的旁边,或者其它文件夹也可以,只要是纯英文路径就可以,不要有空格。例如,我把esp-adf下载到D盘的esp32文件夹下(在硬盘下得有这个文件夹),输入命令cd D:\esp32,回车执行命令,如下图所示:
进入esp32文件后,使用下面的git命令从gitee下载,gitee是国内服务器,下载速度很快,不会卡壳。
git clone https://gitee.com/EspressifSystems/esp-adf.git
复制上面的git命令,在终端中点击右键,就会自动把命令输入到窗口,然后回车执行命令即可,如下图所示:
刚才下载的esp-adf文件夹中,还有2个子模块需要下载,一个是esp-adf-libs,另外一个是esp-sr,你现在可以打开这两个文件夹看看,里面还是空的,路径是esp-adf\components。 接下来我们下载这两个子模块,需要先进入esp-adf\components文件夹,然后才可以下载,如下图所示:
进入components后,分别输入下面两个命令下载这两个子模块的文件夹。
git clone https://gitee.com/esp-components/esp-sr.git
git clone https://gitee.com/esp-components/esp-adf-libs.git
如下图所示:
至此,esp-adf文件夹就下载好了。
第2步:安装IDF补丁文件
先使用cd命令进入idf文件夹路径,比如,我的idf路径是D:\Espressif\frameworks\esp-idf-v5.1.3,就执行下面的命令,还是和第1步一样,在终端中执行。你需要改成你自己的idf路径。
cd D:\Espressif\frameworks\esp-idf-v5.1.3
然后使用git apply命令打补丁,命令如下所示:
git apply $ADF_PATH/idf_patches/idf_v5.1_freertos.patch
需要注意两点: 一、上面命令中的,$ADF_PATH替换成你的ADF路径,例如,我的是D:/esp32/esp-adf,就执行下面的命令。
git apply D:/esp32/esp-adf/idf_patches/idf_v5.1_freertos.patch
二、上面命令是针对5.1版本的idf做的补丁,如果你的是5.2版本,把命令中的5.1改成5.2就可以,如下所示:
git apply D:/esp32/esp-adf/idf_patches/idf_v5.2_freertos.patch
输入命令后回车执行,没有任何提示,就说明命令执行成功,如果有提示,那应该是错误提示。
第3步:增加ADF路径
需要打开用户配置文件,添加esp-adf路径。 在VSCode软件中间最上方的输入框里面点击一下,出现的菜单中选择“显示并运行命令”,如下图所示:
输入框中会出现一个三角箭头,然后输入“打开用户设置”,点击选择第一个命令,用户配置文件就打开了。如果你的VSCode是英文的,那就输入“Open User Settings”,就找到了。
然后在打开的json文件中,找到"idf.espIdfPathWin",这是idf的路径,在它的下面,写入adf的路径就可以,比如,我的adf路径是D:\esp32\esp-adf,就写入下面的语句,格式按照idf路径写的,不要忘了最后的逗号。
"idf.espAdfPathWin": "D:\\esp32\\esp-adf",
支持,adf的环境就配置好了。 打开一个空的VSCode软件,点击菜单“查看”->“命令面板”,然后输入:ESP-IDF:ESP-IDF:安装ESP-ADF,执行这个命令。
然后选择第1个菜单,选择安装路径。
然后选择使用Github或者Gitee安装。通过Gitee下载时,实际上它的子模块也需要通过Github。能否下载成功,取决于你的网络环境。
如果网络环境不好,可以下载下面百度网盘中的esp-adf,解压放到你的Espressif文件夹内。
链接:https://pan.baidu.com/s/1lC0wbIsGCVkpec5_s5Y5pQ
提取码:p7rl
安装好以后,会出现esp-adf文件夹,如下图所示。
最后,需要打开用户配置文件,看看ADF的路径是否添加好,没有的话,自己添加进去。
用户配置文件路径是:C:\Users\Administrator\AppData\Roaming\Code\User\settings.json
复制开发板提供的例程(ai_chat)到的你的实验文件夹,然后使用VSCode打开文件夹。 下载例程到你的开发板之前,需要“在源程序中添加百度和minimax的密钥信息”以及“修改menuconfig”。然后就可以编译下载程序到开发板运行。
18.1.2 获取密钥
上面说的例程实现原理中,第1步的语音转文字和第3步的文字转语音,我们使用百度智能云,第2步的大模型,我们是用minimax开放平台。
百度智能云密钥获取
浏览器中输入网址:https://console.bce.baidu.com/ 进入百度智能云,使用百度账号登录,如果没有账号就注册后登录,使用百度智能云,需要实名认证,个人认证和企业认证都可以。
进入百度智能云控制台后,点击左上角三个横杠,展开全部应用,找到“语音技术”点击进入,如下图所示:
进入语音技术控制台后,如下图所示,可以看到“操作指引”,一共4个步骤。
第1步去领取免费资源,每个账户会送一点资源体验,用完就得花米了。
第2步去创建应用,创建应用后才能在单片机调用。
第3步去看API文档,看看怎么调用。
第4步去查看用量,可以查看具体的调用数量。
我们先点击“去领取”,进入下面页面。
默认的“服务类型”是“语音识别”,在“待领接口”会列出全部接口,你要用哪个,就勾选哪个。我已经领过了,领的是“短语音识别-中文普通话”,上图中“已领接口”可以看到我领的,你没领的话不会出现。为了避免出现不必要的麻烦,你和我一样领取“短语音识别-中文普通话”就可以。勾选后,先不要点击“0元领取”。
如上图,再打开“语音合成”这个服务类型,勾选“短文本在线合成-基础音库”,然后点击“0元领取”。
上面,我们领取了2个服务,语音识别和语音合成。
领取成功后,就可以“创建应用”了。
如上图所示,在左侧点击“应用列表”,然后点击“创建应用”。
在“创建新应用”页面,填写必要的信息。应用名称,可以自己起名,这里我写了esp32c3。然后“接口选择”里面,在“语音技术”栏目里,选择“短语音识别”和“短文本在线合成”。语音包名选择“不需要”。应用归属选择“个人”,应用描述写入一些简单的提示。最后点击“立即创建”。
出现“创建完毕”提示,点击“返回应用列表”即可。
然后就可以看到刚才创建的应用。在这里,复制API Key和Secret Key到工程main.c文件里面,替换代码中的xxx,在第37行左右。
CONFIG_BAIDU_ACCESS_KEY对应的是API Key。
CONFIG_BAIDU_SECRET_KEY对应的是Secret Key。
这里顺便把WiFi名称和密码也改了。
#define CONFIG_WIFI_SSID "xxx"
#define CONFIG_WIFI_PASSWORD "xxx"
#define CONFIG_BAIDU_ACCESS_KEY "xxx"
#define CONFIG_BAIDU_SECRET_KEY "xxx"
2
3
4
到这里,百度的密钥就修改好了。
minimax密钥获取https://www.minimaxi.com/platform
进入minimax网站,注册登录后,进入“账户管理”,然后再点击“接口密钥”,然后再点击“创建新的密钥”。
const char * minimax_key = "Bearer xxx"
注意:这个密钥只能在上图中创建的时候复制,点击“确定”后,就无法复制了。 这个密钥非常长,大家不要惊奇。 到这里,百度和minimax的密钥就都配置好了。
还有一个GroupID需要获取。进入“账户管理”->“账户信息”,就可以在右上方看到GroupID.
打开minimax_chat.c文件,找到minimax_chat函数,在config结构体定义中的url成员变量中,修改GroupID。下面的代码中,我已经填入了我的GroupId,你需要替换成你自己的。char *minimax_chat(const char *text)
{
char *response_text = NULL;
char *post_buffer = NULL;
char *data_buf = NULL;
esp_http_client_config_t config = {
.url = "https://api.minimax.chat/v1/text/chatcompletion_pro?GroupId=1768536437306691918",
.buffer_size_tx = 1024 // 默认是512 minimax_key很长 512不够 这里改成1024
};
esp_http_client_handle_t client = esp_http_client_init(&config);
......
2
3
4
5
6
7
8
9
10
11
12
13
到这里,代码就修改好了。一共修改了main.c文件中的5处地方和minimax_chat.c文件中的1处地方。
18.1.3 配置menuconfig
**特别注意:**在配置menuconfig之前,一定要先选择目标芯片,因为选择目标会把sdkconfig.defaults文件中的配置加载到menuconfig中,并且把sdkconfig.defaults没有配置的进行复位,对所有例程都如此。 打开menuconfig。 1:确认FLASH大小为8MB。
2:确认分区表选择的是自定义,名称是partitions.csv。 3:确认开发板型号,是自定义的板子。 4:确定勾选Swap the 2 bytes of RGB565 5:确定LVGL的内存设置为下图所示: 6:确认勾选允许大量字符。 7:确认LVGL的第三方库GIF解码库被勾选。经过上面的配置,现在就可以编译下载了,下载好程序后,就可以体验了。
18.2 代码讲解
18.2.1 例程实现过程
本例程是在ESP-ADF的google_translate_device例程上修改而来。 google_translate_device例程的路径是esp-adf\examples\cloud_services\。 google_translate_device是“谷歌翻译机”的源代码例程,这个例程的框架和我们要实现的AI对话,几乎一模一样,所以我们使用它来修改。下图是源例程的文件结构。
translate_device_example.c是主文件,包含主函数。 google_sr.c和google_sr.h是“语音转文本”程序,负责把MIC捕获到的声音上传到谷歌服务器,并把识别后的文字从服务器返回给单片机。这里我们把谷歌服务器改成百度服务器就可以。 google_translate.c和google_translate.h是“汉语文字转英语文字”程序,也就是用于翻译的。负责把刚才语音识别的文字上传到谷歌翻译服务器,并把翻译后的文字传回给单片机。这里我们把谷歌翻译服务器改成minimax大模型服务器就可以。 google_tts.c和google_tts.h是“文字转语音”程序,负责把翻译后的文字上传到谷歌服务器,并把转换后的语音返回给单片机,并使用I2S传输给音频喇叭。这里我们把谷歌服务器改成百度服务器就可以。 除了服务器的更改,还需要修改一些其它细节,修改其它细节,可以参考google_translate_device例程同目录下的pipeline_baidu_speech_mp3例程。这个例程是使用百度服务器,用来“文字转语音”的程序。这个例程可以说是我们要实现的AI对话的第3个完整步骤。 接下来就是音频部分的实现,ADF自带es8311的驱动程序,在包含主函数文件中,直接包含es8311.h就可以。 最后就是lvgl的界面实现,这个参考我们之前的第17章“掌机”例程,就可以实现,然后还在例程中添加了触摸屏功能。18.2.2 例程结构介绍
main文件夹下的例程文件如上图所示。 main.c文件包含主函数。 baidu_vtt.c和baidu_vtt.h是“语音转文字”程序。 minimax_chat.c和minimax_chat.h是“聊天”程序。 baidu_tts.c和baidu_tts.h是“文字转语音”程序。 lv_gui.c和lv_gui.h是lvgl界面程序,大家可以在这里修改自己想要的界面。 img_bilibili120.c是开机时候的那个gif图片的c代码。关于图片转c代码的方法,可以看16.3.1章节。 font_alipuhui20.c是阿里字体文件,这个包含了27780个中文汉字(是GB18030-2022规定的2级要求,覆盖了《通用规范汉字表》),也就是几乎囊括了所有常用和不常用的汉字,因为AI的回答是随机的,不一定会出现什么字,所以只能使用“全部”汉字了。关于字库文件制作,可以看16.3.2章节。不过有一点要注意,之前的字库,都是用到什么字,就输入什么字,而这次是要“全部”汉字,所以需要在Range一栏输入:0x20-0x2FA1F,这个范围,包含了全世界的所有文字的Unicode码。 Unicode字符范围查询网站:https://jrgraphix.net/research/unicode.php componets目录有两个文件,第一个是触摸屏的驱动文件,这个我们在之前的例程章节中已经介绍过。 第二个是针对我们开发板的文件,里面包含了开发板的引脚定义等内容。这个文件夹是从ADF官方例程中复制过来的,路径为esp-adf\examples\get-started\play_mp3_control\components\my_board,如下图所示。 复制过来以后,针对我们的开发板做了一些修改。18.2.3 程序整体讲解
这一小节,我们按照单片机的执行流程,对代码进行解读。
找到app_main函数。
app_main函数中,已经用/**/这种注释分开了各个模块,从上往下看,先是初始化各个部分,然后创建了两个任务。
初始化部分,几乎和第17章的“掌机”例程一模一样,需要注意的是,初始化ES8311音频芯片这里。这里使用的es8311的初始化函数,是ADF的函数,不是IDF的函数。IDF和ADF对于es8311的驱动不一样。在“掌机”这个例程中,我们是先初始化I2C,然后再初始化各个I2C器件。这里,我们没有先初始化I2C,直接初始化es8311,在初始化es8311的函数中,初始化了I2C。大家如果想加入其它I2C器件在例程中,就要写到初始化es8311的模块后面,不要写到它前面。比如我后面放了I2C接口的触摸屏初始化。
初始化完成以后,使用lv_gui_start()函数显示开机界面。
然后创建了2个任务。
main_page_task任务比较简单,如果你学习过第17章掌机例程,看这个任务的代码就很轻松了。
ai_chat_task任务是本例程的主要任务,可以学习到ADF的重要框架思维。
void ai_chat_task(void *pv)
{
ESP_LOGI(TAG, "[ 1 ] Initialize Buttons & Connect to Wi-Fi network, ssid=%s", CONFIG_WIFI_SSID);
// Initialize peripherals management
esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
//periph_cfg.task_stack = 8*1024;
esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);
periph_wifi_cfg_t wifi_cfg = {
.wifi_config.sta.ssid = CONFIG_WIFI_SSID,
.wifi_config.sta.password = CONFIG_WIFI_PASSWORD,
};
esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg);
// Initialize Button peripheral
periph_button_cfg_t btn_cfg = {
.gpio_mask = (1ULL << get_input_mode_id()) | (1ULL << get_input_rec_id()),
};
esp_periph_handle_t button_handle = periph_button_init(&btn_cfg);
// Start wifi & button peripheral
esp_periph_start(set, button_handle);
esp_periph_start(set, wifi_handle);
periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上面代码用于初始化按键和WiFi连接。
// 百度token计算
if (baidu_access_token == NULL) {
// Must freed `baidu_access_token` after used
baidu_access_token = baidu_get_access_token(CONFIG_BAIDU_ACCESS_KEY, CONFIG_BAIDU_SECRET_KEY);
}
2
3
4
5
上面代码用于计算百度token。百度token使用API Key和Screat Key获取,这两个密钥在前面已经介绍了获取方法。计算出的token,用于http的url链接。这个token,其实也可以直接在百度智能云API调试台上通过两个密钥获取,不过,这个token只能用一个月,失效以后,还得重新在百度智能云API调试台获取,比较麻烦,不如直接在程序中获取。
// 百度 语音转文字 初始化
baidu_vtt_config_t vtt_config = {
.record_sample_rates = 16000,
.encoding = ENCODING_LINEAR16,
};
baidu_vtt_handle_t vtt = baidu_vtt_init(&vtt_config);
// 百度 文字转语音 初始化
baidu_tts_config_t tts_config = {
.playback_sample_rate = 16000,
};
baidu_tts_handle_t tts = baidu_tts_init(&tts_config);
2
3
4
5
6
7
8
9
10
11
12
上面是百度“语音转文字”和“文字转语音”的初始化,可以把鼠标放到init初始化函数上,右键菜单选择“转到定义”,就可以找到这个函数的定义。 接下来我们先跳出main函数,看一下这两个初始化函数的实现过程。因为这两个初始化函数中有ADF框架最重要的定义。 先看baidu_vtt_init初始化函数。
baidu_vtt_handle_t baidu_vtt_init(baidu_vtt_config_t *config)
{
// 管道配置
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
baidu_vtt_t *vtt = calloc(1, sizeof(baidu_vtt_t));
AUDIO_MEM_CHECK(TAG, vtt, return NULL);
vtt->pipeline = audio_pipeline_init(&pipeline_cfg);
vtt->buffer_size = config->buffer_size;
if (vtt->buffer_size <= 0) {
vtt->buffer_size = DEFAULT_VTT_BUFFER_SIZE;
}
vtt->buffer = malloc(vtt->buffer_size);
AUDIO_MEM_CHECK(TAG, vtt->buffer, goto exit_vtt_init);
// I2S流配置
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, 16, AUDIO_STREAM_READER);
i2s_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
i2s_cfg.std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
vtt->i2s_reader = i2s_stream_init(&i2s_cfg);
// HTTP流配置
http_stream_cfg_t http_cfg = {
.type = AUDIO_STREAM_WRITER,
.event_handle = _http_stream_writer_event_handle,
.user_data = vtt,
.task_stack = BAIDU_VTT_TASK_STACK,
};
vtt->http_stream_writer = http_stream_init(&http_cfg);
vtt->sample_rates = config->record_sample_rates;
vtt->encoding = config->encoding;
vtt->on_begin = config->on_begin;
// 把 I2S流 和 HTTP流 注册到管道 并链接起来
audio_pipeline_register(vtt->pipeline, vtt->http_stream_writer, "vtt_http");
audio_pipeline_register(vtt->pipeline, vtt->i2s_reader, "vtt_i2s");
const char *link_tag[2] = {"vtt_i2s", "vtt_http"};
audio_pipeline_link(vtt->pipeline, &link_tag[0], 2);
// 设置I2S采样率 位数 等参数
i2s_stream_set_clk(vtt->i2s_reader, config->record_sample_rates, 16, 1);
return vtt;
exit_vtt_init:
baidu_vtt_destroy(vtt);
return NULL;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
这个初始化函数,主要用来配置一个“管道”,让“I2S流”和“HTTP流”在“管道”中连接起来。注意连接顺序是(i2s流->http流),“I2S流”负责读取MIC的声音,“HTTP流”负责把声音发送到服务器。这个地方的优点就是,并不是说完话以后再发送到HTTP服务器,而是边说边往服务器发。
接下来我们再看看baidu_tts_init初始化函数。
baidu_tts_handle_t baidu_tts_init(baidu_tts_config_t *config)
{
// 管道设置
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
baidu_tts_t *tts = calloc(1, sizeof(baidu_tts_t));
AUDIO_MEM_CHECK(TAG, tts, return NULL);
tts->pipeline = audio_pipeline_init(&pipeline_cfg);
tts->buffer_size = config->buffer_size;
if (tts->buffer_size <= 0) {
tts->buffer_size = DEFAULT_TTS_BUFFER_SIZE;
}
tts->buffer = malloc(tts->buffer_size);
AUDIO_MEM_CHECK(TAG, tts->buffer, goto exit_tts_init);
tts->sample_rate = config->playback_sample_rate;
// I2S流设置
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, 16, AUDIO_STREAM_WRITER);
i2s_cfg.std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
i2s_cfg.std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
tts->i2s_writer = i2s_stream_init(&i2s_cfg);
// http流设置
http_stream_cfg_t http_cfg = {
.type = AUDIO_STREAM_READER,
.event_handle = _http_stream_reader_event_handle,
.user_data = tts,
.task_stack = BAIDU_TTS_TASK_STACK,
};
tts->http_stream_reader = http_stream_init(&http_cfg);
// MP3流设置
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
tts->mp3_decoder = mp3_decoder_init(&mp3_cfg);
audio_pipeline_register(tts->pipeline, tts->http_stream_reader, "tts_http");
audio_pipeline_register(tts->pipeline, tts->mp3_decoder, "tts_mp3");
audio_pipeline_register(tts->pipeline, tts->i2s_writer, "tts_i2s");
const char *link_tag[3] = {"tts_http", "tts_mp3", "tts_i2s"};
audio_pipeline_link(tts->pipeline, &link_tag[0], 3);
// I2S流采样率 位数等设置
i2s_stream_set_clk(tts->i2s_writer, config->playback_sample_rate, 16, 1);
return tts;
exit_tts_init:
baidu_tts_destroy(tts);
return NULL;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
这个初始化函数,结构和上面的初始化函数基本上一样。不一样的是,管道中的“流”的顺序(http流->mp3流->i2s流)。另外,在这个初始化函数中,除了“i2s流”和“http流”,还多了一个“mp3流”,注意在管道中的顺序。tts负责的是把文字转成语音。“http流”把转换成的mp3文件发送到单片机,“mp3流”负责解码mp3文件,“i2s流”负责把解码后的文件传输到音频芯片。这个框架的优点就是,并不是把“语音文件”全部接收完再播放,而是边接收边播放。
接下来我们再看回app_main函数。
// 监听“流”
ESP_LOGI(TAG, "[ 4 ] Set up event listener");
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);
ESP_LOGI(TAG, "[4.1] Listening event from the pipeline");
baidu_vtt_set_listener(vtt, evt);
baidu_tts_set_listener(tts, evt);
ESP_LOGI(TAG, "[4.2] Listening event from peripherals");
audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt);
ESP_LOGI(TAG, "[ 5 ] Listen for all pipeline events");
2
3
4
5
6
7
8
9
10
11
12
13
上面是用来对“管道”中的“流”设置监听,用于监测“管道”中“流”的状态。
// 给出 “已经准备好” 信号
xEventGroupSetBits(my_event_group, ALL_REDAY);
监听设置好以后,就可以对着麦克风说话了,这里用来通知main_page_task,可以进入对话界面了。
while (1) {
audio_event_iface_msg_t msg;
if (audio_event_iface_listen(evt, &msg, portMAX_DELAY) != ESP_OK) {
ESP_LOGW(TAG, "[ * ] Event process failed: src_type:%d, source:%p cmd:%d, data:%p, data_len:%d",
msg.source_type, msg.source, msg.cmd, msg.data, msg.data_len);
continue;
}
ESP_LOGI(TAG, "[ * ] Event received: src_type:%d, source:%p cmd:%d, data:%p, data_len:%d",
msg.source_type, msg.source, msg.cmd, msg.data, msg.data_len);
2
3
4
5
6
7
8
9
10
11
12
13
接下来进入while循环,audio_event_iface_listen负责监听事件,并把接收到的信息放到msg里面。如果没有正确接收到信息,就会进入if循环,给串口打印接收到的信息各值,并通过continue返回继续监听。如果正确接收到信息了,就会执行if后面的打印msg信息到串口,并继续向下执行。
if (baidu_tts_check_event_finish(tts, &msg)) {
ESP_LOGI(TAG, "[ * ] TTS Finish");
es8311_pa_power(false); // 关闭音频
continue;
}
2
3
4
5
上面代码,用于监测接收到信息是否是“tts完成”的信息。如果是的话,代表语音回答已经完毕,打印提示信息,并关闭音频。然后通过continue返回继续监听信息。如果不是“tts完成”信息,那么就继续往下执行。
if (msg.source_type != PERIPH_ID_BUTTON) {
continue;
}
2
3
上面代码,如果接收到的消息类型不是按键,那就通过continue返回继续监听。如果是按键引起的,那就继续往下执行。
if ((int)msg.data == get_input_mode_id()) {
break;
}
2
3
上面代码,如果接收到的是MODE按键消息,那就通过break跳出while循环。MODE按键在我们开发板上没有,只是设置到了GPIO18引脚,你可以在外扩接口找到GPIO18,连接一条线,接GND表示按键按下,不过,这个例程,我们一般不会让它退出while循环结束。 如果接收到的按键消息不是MODE引起的,那就继续往下执行。
if ((int)msg.data != get_input_rec_id()) {
continue;
}
2
3
上面代码,如果按键消息不是由REC按键(也就是BOOT按键)引起的,那就通过continue回到前面监听。如果是的话,就继续往下执行。
if (msg.cmd == PERIPH_BUTTON_PRESSED) {
baidu_tts_stop(tts);
ESP_LOGI(TAG, "[ * ] Resuming pipeline");
lcd_clear_flag = 1;
baidu_vtt_start(vtt);
} else if (msg.cmd == PERIPH_BUTTON_RELEASE || msg.cmd == PERIPH_BUTTON_LONG_RELEASE) {
ESP_LOGI(TAG, "[ * ] Stop pipeline");
char *original_text = baidu_vtt_stop(vtt);
if (original_text == NULL) {
minimax_content[0]=0; // 清空minimax 第1个字符写0就可以
continue;
}
ESP_LOGI(TAG, "Original text = %s", original_text);
ask_flag = 1;
char *answer = minimax_chat(original_text);
if (answer == NULL)
{
continue;
}
ESP_LOGI(TAG, "minimax answer = %s", answer);
answer_flag = 1;
es8311_pa_power(true); // 打开音频
baidu_tts_start(tts, answer);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
上面代码,如果是“按键按下”消息,就进入if执行,先通过baidu_tts_stop(tts)停止tts运行。因为按下按键有可能发生在喇叭正在说话的时候,也就是tts正在运行的时候。然后lcd_clear_flag = 1用于通知gui把之前的对话删了,需要重新对话了。baidu_vtt_start(vtt)这个函数执行后,vtt就正式开始运行了,开始把接收到的语音发送到服务器。
如果是“按键放开”消息,就结束vtt,并把获取到的文字给了original_text指针,然后判断original_text 是否为空。
如果是空,就是没有接收到转换好的文字,minimax_content数组负责接收接收到的文字,给他的第一个字符写0就表示清空了数组,因为数组就是以0结尾,后面的就不用管了,然后通过continue返回继续监听。
如果不为空,那就继续往下执行。ask_flag = 1把这个标志位置1,就会告诉GUI,可以把我说的文字显示到液晶屏上了。
接下来,minimax_chat函数负责把我说话的文字传输到服务器,并获取回答给了answer指针。
然后判断answer所执行的内容是否为空,如果为空,通过continue返回继续监听。如果不为空,继续往下执行,answer_flag = 1把这个标志置1,就会告诉GUI,可以把AI的回答文字显示到液晶屏上了。
es8311_pa_power(true)打开音频,baidu_tts_start(tts, answer)启动文字转语音。播放完语音后,会自动回去监听。
}
ESP_LOGI(TAG, "[ 6 ] Stop audio_pipeline");
baidu_vtt_destroy(vtt);
baidu_tts_destroy(tts);
/* Stop all periph before removing the listener */
esp_periph_set_stop_all(set);
audio_event_iface_remove_listener(esp_periph_set_get_event_iface(set), evt);
/* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */
audio_event_iface_destroy(evt);
esp_periph_set_destroy(set);
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
上面代码,只有在按了MODE按键后才会跳出while循环执行。负责“卸载”vtt和tts,以及停止外设、停止监听,最后删除任务。
18.2.4 音频驱动讲解
我们开发板上的音频芯片型号是es8311,ADF中提供了这个型号的驱动代码,这个驱动源码和乐鑫组件管理工具中的es8311驱动源码是不一样的,感兴趣的可以对比一样。 接下来,我们看一下es8311驱动函数在这个例程中的使用。 在app_main()函数中,首先设置了es8311。
/******* 初始化ES8311音频芯片 (注意:这个里面会初始化I2C 后面初始化触摸屏就不用再初始化I2C了) ********/
ESP_LOGI(TAG, "[ 2 ] Start codec chip");
audio_board_handle_t board_handle = audio_board_init();
audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START);
es8311_codec_set_voice_volume(100); // 最大音量
es8311_pa_power(false); // 关闭声音
在audio_board_init()上单击右键,找到它的函数定义,位于components/my_board/board.c文件中。
audio_board_handle_t audio_board_init(void)
{
if (board_handle) {
ESP_LOGW(TAG, "The board has already been initialized!");
return board_handle;
}
board_handle = (audio_board_handle_t) audio_calloc(1, sizeof(struct audio_board_handle));
AUDIO_MEM_CHECK(TAG, board_handle, return NULL);
board_handle->audio_hal = audio_board_codec_init();
return board_handle;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个函数中,实际上执行了一个audio_board_codec_init()函数,该函数的定义就位于上面函数的下面。
audio_hal_handle_t audio_board_codec_init(void)
{
audio_hal_codec_config_t audio_codec_cfg = AUDIO_CODEC_DEFAULT_CONFIG();
audio_hal_handle_t codec_hal = audio_hal_init(&audio_codec_cfg, &AUDIO_CODEC_ES8311_DEFAULT_HANDLE);
AUDIO_NULL_CHECK(TAG, codec_hal, return NULL);
return codec_hal;
}
2
3
4
5
6
7
audio_board_codec_init()函数实际上执行了一个audio_hal_init函数。
audio_hal_init函数在我们的工程中就无法通过右键转到定义找到了,因为这个函数位于ADF源文件中,大家可以使用VSCode打开esp-adf文件,找到它。不过,该函数是怎么定义的,也不需要去深究了,我们只需要知道它怎么使用就可以了。它有两个参数,第二个参数就是我们需要关注的,它决定了你使用哪种芯片的驱动,因为在ADF里面还有很多种音频驱动。
在app_main函数audio_board_init()函数执行后,下一条语句执行了audio_hal_ctrl_codec()函数。该函数也不在我们的工程文件中,而位于ADF源码中,这里知道它的使用方法就可以。它的第一个参数AUDIO_HAL_CODEC_MODE_BOTH代表ADC和DAC全部开启,你可以简单的认为,ADC就是MIC,DAC就是喇叭。它的第二个参数AUDIO_HAL_CTRL_START表示启动es8311。这两的原始定义位于文件audio_hal.h文件中,大家可以在ADF源文件中找到,如下所示:
......
/**
* @brief Select media hal codec mode
*/
typedef enum {
AUDIO_HAL_CODEC_MODE_ENCODE = 1, /*!< select adc */
AUDIO_HAL_CODEC_MODE_DECODE, /*!< select dac */
AUDIO_HAL_CODEC_MODE_BOTH, /*!< select both adc and dac */
AUDIO_HAL_CODEC_MODE_LINE_IN, /*!< set adc channel */
} audio_hal_codec_mode_t;
......
/**
* @brief Select operating mode i.e. start or stop for audio codec chip
*/
typedef enum {
AUDIO_HAL_CTRL_STOP = 0x00, /*!< set stop mode */
AUDIO_HAL_CTRL_START = 0x01, /*!< set start mode */
} audio_hal_ctrl_t;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
执行完audio_hal_ctrl_codec()函数后,接下来使用es8311_codec_set_voice_volume(100)函数把声音设置为最大,你可以根据需求修改声音大小。最后使用es8311_pa_power(false)函数把功放关闭,如果不关闭的话,在喇叭不说话的时候,可能会有一点噪音,而且会耗电引起芯片发热,等到需要使用喇叭播放声音的时候再打开就可以,上一小节介绍整理代码的时候见到过,在收到服务器发过来的语音后,就会打开PA,播放完后再关闭。es8311_codec_set_voice_volume()函数和es8311_pa_power()函数定义位于es8311.c文件中,该文件可以在ADF中找到,路径是:esp-adf\components\audio_hal\driver\es8311。
18.2.5 语音转文字
我们说话的声音,被麦克风采集后,送到服务器,然后服务器识别后,把识别到的文字传回单片机。
“语音转文字”程序位于baidu_vtt.c文件中,这里的vtt是voice to text的首字母。
baidu_vtt_init()函数用来建立“管道”(pipeline),并在“管道”中把“I2S流”和“HTTP流”连接起来。
baidu_vtt_start()函数用来启动“管道”。
baidu_vtt_set_listener()函数用来监听“管道”。
baidu_vtt_stop()函数用来停止“管道”。
以上4个函数,就是这个管道的完整运行过程,先建立,再启动,然后监听,最后停止。不过,建立管道只需一次,之后就可以在“启动”“监听”“停止”之间循环。这个在ai_chat_task()函数中就可以看到。
baidu_vtt_destroy()函数用于“卸载”管道。如果管道确实不用了,就使用这个卸载。这个函数在AI任务函数的while循环后使用。
_http_stream_writer_event_handle()函数是在管道建立的时候,指定的http事件处理函数,接收msg消息。该函数中使用了若干个if条件判断msg消息的类型。 HTTP_STREAM_PRE_REQUEST是http请求前的事件,在这个里面配置http的method和header。
HTTP_STREAM_ON_REQUEST是http正在请求的事件,在这个里面给服务器发送MIC接收到的数据。
HTTP_STREAM_POST_REQUEST是结束请求事件,在这个里面发送结束标志。
HTTP_STREAM_FINISH_REQUEST是完成请求事件,在这个里面接收识别后的文字。
百度短语音识别API参考:https://ai.baidu.com/ai-doc/SPEECH/Jlbxdezuf 我们先看上传的音频。
从百度的API参考中,我们知道,支持的音频格式有很多,这里我们使用的是16位pcm格式,这种格式就是直接I2S采集到的数据,不用转换,直接上传。上传音频有两种方式,一种是JSON方式,一种是RAW方式。JSON方式上传需要编码,会增加上传的数据以及增加编码的时间。RAW就是原始文件上传,节省时间,我们使用RAW方式上传,没有对音频数据进行JSON编码。
我们再看转换后的文字。
在HTTP_STREAM_FINISH_REQUEST事件中,我们使用了下面这条语句打印了收到的数据: ESP_LOGI(TAG, "Got HTTP Response = %s", (char *)vtt->buffer); 大家可以打开终端在程序运行的时候看看这里的输出,另外,在API参考文档里也有,收到的数据如下所示。 比如你说的是“北京天气”。 识别成功会返回:
{
"err_no":0,"err_msg":"success.",
"corpus_no":"15984125203285346378",
"sn":"481D633F-73BA-726F-49EF-8659ACCC2F3D",
"result":["北京天气"]
}
2
3
4
5
6
识别错误会返回:
{
"err_no":2000,
"err_msg":"data empty.",
"sn":"481D633F-73BA-726F-49EF-8659ACCC2F3D"
}
2
3
4
5
如果成功返回,err_no是0,所以我们使用cJSON解析之后先判断err_no是不是0,如果是0,那就把result的内容提取出来,代码如下:
cJSON *root = cJSON_Parse(vtt->buffer);
int err_no = cJSON_GetObjectItem(root,"err_no")->valueint;
if (err_no == 0) // 如果转换成功
{
cJSON *result = cJSON_GetObjectItem(root,"result");
char *text = cJSON_GetArrayItem(result, 0)->valuestring;
strcpy(ask_text, text);
vtt->response_text = ask_text;
ESP_LOGI(TAG, "response_text:%s", vtt->response_text);
}
else{
vtt->response_text = NULL;
}
cJSON_Delete(root);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
转换好的文字,储存到了ask_text数组中,这个数组在文件的前面定义,是一个全局数组变量。 使用vtt->response_text指向了这个数组。 这时候,我们可以再看一下主函数ai_chat_task任务中,放开按键要执行的代码:
char *original_text = baidu_vtt_stop(vtt);
通过baidu_vtt_stop这个函数,返回了收到的文字,我们看baidu_vtt_stop这个函数定义就可以知道,在这个函数最后使用retrun返回了vtt->response_text,就被original_text接收到了。 我们再看一下主函数ai_chat_task任务中,放开按键要执行的代码:
char *original_text = baidu_vtt_stop(vtt);
if (original_text == NULL) {
minimax_content[0]=0; // 清空minimax 第1个字符写0就可以
continue;
}
ESP_LOGI(TAG, "Original text = %s", original_text);
ask_flag = 1;
char *answer = minimax_chat(original_text);
2
3
4
5
6
7
8
9
original_text接收到文字后,先判断是否为空,如果不为空,在串口终端打印出接收到的文字,然后使用ask_flag标志通知液晶屏可以显示接收到的文字,然后使用minimax_chat()函数把接收到的文字发送到大模型服务器,同时这个函数会返回大模型的文字结果,给到answer。
18.2.6 大模型问答
上一节最后,已经提到了minimax_chat()函数会把我们说的文字上传到大模型,并返回大模型的文字回答。
minimax_chat()函数位于minimax_chat.c文件中,这个文件中,也只有这么一个函数。
大模型这里的程序,没有使用ADF的“管道”,直接使用http请求。
这里需要注意两个地方,一个是POST_DATA,一个是获取到的大模型回答JSON结构。
POST_DATA,是每次和大模型通话要发送的内容,格式在MINIMAX的文档中心可以查看,根据它的API文档,我们写好的POST_DATA如下代码所示,它是一个JSON格式的内容。
#define PSOT_DATA "{\
\"model\":\"abab5.5s-chat\",\"tokens_to_generate\": 256,\"temperature\":0.7,\"top_p\":0.7,\"plugins\":[],\"sample_messages\":[],\
\"reply_constraints\":{\"sender_type\":\"BOT\",\"sender_name\":\"小美\"},\
\"bot_setting\":[{\
\"bot_name\":\"小美\",\
\"content\":\"小美,性别女,年龄22岁,在校大学生,性格活泼可爱,说话幽默风趣,擅长撩男生,喜欢美食,爱好旅行,是个话痨。\\n\"}],\
\"messages\":[{\"sender_type\":\"USER\",\"sender_name\":\"靓仔\",\"text\":\"%s\"}]\
}"
2
3
4
5
6
7
8
上面代码,我们使用宏定义写了POST_DATA,因为太长,所以在每一行的最后使用了换行符“\”。除了最前面的双引号和最后面的双引号以外,其余的双引号前面都有一个“\”,起到转义字符作用。从第二行开始,每一行都是顶着最开始处,没有空格,因为每多一个空格,在http发送的时候,都会多发送一个字节。
其中的model、temperature等参数的含义,可以去查看API文档。
接下来看大模型的回复内容,也是JSON格式。
正常回复如下:
{
"created":1713954440,
"model":"abab5.5s-chat",
"reply":"我当然知道啦,你叫靓仔嘛,哈哈哈哈。",
"choices":[
{
"finish_reason":"stop",
"messages":[
{
"sender_type":"BOT",
"sender_name":"小美",
"text":"我当然知道啦,你叫靓仔嘛,哈哈哈哈。"
}]
}],
"usage":
{
"total_tokens":87
},
"input_sensitive":false,
"output_sensitive":false,
"id":"02781188c86c702c424c86b7a509064e",
"base_resp":
{
"status_code":0,
"status_msg":""
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
参数错误回复如下:
{
"created":0,
"model":"",
"reply":"",
"choices":null,
"base_resp":
{
"status_code":2013,
"status_msg":"invalid params"
}
}
2
3
4
5
6
7
8
9
10
11
根据以上对比,我们发现如果错误的话,获取到的created是0,所以我们首先判断created是否为0,如果不为0,我们就把reaply的内容提取出来。
cJSON *root = cJSON_Parse(data_buf);
int created = cJSON_GetObjectItem(root,"created")->valueint;
if(created != 0)
{
char *reply = cJSON_GetObjectItem(root,"reply")->valuestring;
strcpy(minimax_content, reply);
response_text = minimax_content;
ESP_LOGI(TAG, "response_text:%s", response_text);
}
cJSON_Delete(root);
2
3
4
5
6
7
8
9
10
11
minimax_content是一个数组,用来存储大模型回复的文字内容,在函数前面已经定义好。最后使用response_text指针指向minimax_content数组,并返回。 主函数里面,判断返回的内容是否为空,如果不是空,就需要使用baidu_tts_start函数把文字发送到文字转语音服务器了。
char *answer = minimax_chat(original_text);
if (answer == NULL)
{
continue;
}
ESP_LOGI(TAG, "minimax answer = %s", answer);
answer_flag = 1;
es8311_pa_power(true); // 打开音频
baidu_tts_start(tts, answer);
2
3
4
5
6
7
8
9
18.2.7 文字转语音
文字转语音的程序,位于baidu_tts.c文件中,结构和baidu_vtt.c文件内容类似,都是使用了ADF的“管道”和“流”框架。
baidu_tts_init()函数负责建立“管道”和“流”。在baidu_vtt中,只建立了“I2S流”和“HTTP流”,这里又多了一个“MP3流”。因为之前的“语音转文字”中的语音,我们使用的是直接I2S采集到的RAW数据,而这里“文字转语音”后的语音,是mp3格式的音乐,在接收到以后,还需要解码才能用于播放。
baidu_tts_start()函数负责启动管道的运行。
baidu_tts_set_listener()函数负责监听管道的运行状态。
baidu_tts_stop()函数负责停止管道的运行。
_http_stream_reader_event_handle()函数是http事件处理函数,该函数只需要处理一个事件就可以。
HTTP_STREAM_PRE_REQUEST就是需要处理的http事件,作用就是在http请求之前,把需要转换的文字,写入到post_field中。
baidu_tts_destroy()函数用于“卸载”管道,在确定管道不使用了之后,就可以卸载管道了。
baidu_tts_check_event_finish()函数用于判断“文字转语音”是否结束。这个函数在main.c文件中使用,判断完成之后,关闭音频。
关于百度文本转语音的API参考,可以看以下链接。
百度文本合成API链接:https://ai.baidu.com/ai-doc/SPEECH/mlbxh7xie
教程到这里,暂时终结。