[MCU] 【启明云端WT99P4C5-S1】综合项目:中国天气网获取天气图像,和风天气HTTPS+GZIP解压

zxyyl   2026-3-18 23:56 楼主

这篇综合项目,我将聚焦于复杂并且没人做的项目,由于我非常的喜欢天气,所以这里我尝试做一款天气时钟,但和以往的不同,我将尝试把天气的图像,包括雷达云图,天气现象图和气象卫星拍摄的图像,直接显示出来,这样,我们不仅仅获得了对天气的直观认识,同时也可以看到我们所处地区的权威数据,并且还搭配了天气和时钟功能,方便我们查看,提升人机交互体验。

南京信息工程大学是一个气象专业为名的大学,这个项目是聚焦于嵌入式和气象的融合,让这个专业的同学可以随时的查看对应的气象情况,让所学和实践融合。

在这篇文章,我们将探讨:和风天气的HTTPS证书和返回的GZIP解压缩获取天气JSON的操作。LVGL使用PNG获取中国天气网的PNG天气雷达图,使用JPEG硬件解码器解码JPEG图像,这里的所有图像全部来源于中国天气网而非提前准备好的SD卡图像,这样我们可以方便的定时更新天气图像进而进行实时更新。

我们要解决的难点在于:大内存的处理方面,由于每一个图像占用的空间都很大,我们在解码操作的时候极易触发内存泄漏或者堆损坏等问题;HTTPS获取AC证书来进行HTTPS通信,和风天气返回的GZIP压缩的API数据,我们如何来解压缩获取原始数据,获取的图像如何处理才能在LVGL上显示等问题,做这个项目可以让我对HTTPS,LVGL的图像处理加深理解,提升经验。

项目最终效果图如下:

IMG20260318142356.jpg  

我们上面是一个天气的图标,旁边是文字描述,下面三个是天气的图,经过缩放进行显示,这样我们就可以实现天气的显示功能,我们可以添加定时器,可以定时的进行更新操作,实现图像的自动化更新。

由于天气的图像占用的内存过大,不建议使用官网的动图形式进行展示,这样会快速耗尽PSRAM,不方便项目的拓展,并且由于PNG解码是通过CUP解码,效率不高,所以不建议进行动图的播放,当然使用缓存应该可以,但是不推荐,毕竟这个屏幕太大了,支持复杂的LVGL项目但是向这样的原始大图其实解码和支持起来不太好,这个项目如果使用linux开发板开发的话那拓展性和流畅度会更上一层楼。

我们废话不多说,开始进行项目的讲解吧:

天气图片获取官网:中国天气网

我们的项目使用之前移植好的LVGL工程进行开发,我们移植LVGL的工程在【启明云端WT99P4C5-S1】MIPI屏幕和触摸驱动,移植LVGL图形库 - 国产芯片交流 - 电子工程世界-论坛

ESP32P4本身不支持wifi和蓝牙,我们使用ESP_WIFI_REMOTE组件实现使用开发板上的ESP32C5进行联网的操作,这个联网的操作请见:【启明云端ESP32P4】使用WIFI Remote联网(使用ESP32C3作为协处理器) - 国产芯片交流 - 电子工程世界-论坛(ESP32C5原理同上)

1.和风天气HTTPS的CA证书获取:

我们之所以使用和风天气来获取天气,是因为和风天气提供的天气现象是最多的,并且免费的额度也很多,可获取的天气现象和API也很多,所以我们这里演示获取实时天气的例子,如果这个例子调通了,那么剩下的也就是换API和改JSON解码获取的操作了,整体使用起来还是很方便的,我搜索整个互联网和论坛,没人做在这个,有做的讲的也不详细,这里我详细说一下,对于JPEG解码器,LVGL和屏幕的驱动,我往期的测评写过,这里就不再赘述直接使用里面的函数进行操作。

首先想使用和风天气,我们需要注册密钥,获取我们对应的Key和对应的访问API,这里官网讲的很详细了,我就不赘述如何创建自己的项目了API 配置 | 和风天气开发服务

当我们获取了属于自己的开发者API Host,我们可以通过这个API_Host访问服务器进行,我们同时需要我们对应的API_key,作为参数放入请求API里让服务器进行认证,当认证结束,就会返回GZIP格式压缩的HTTPS流。我们创建缓冲区接收这些流,然后使用ZLIB组件进行解压缩,得到最终的原始Json数据,我们再使用cJSON读取里面的信息,更新到LVGL的标签上,就实现了天气预报的实现了。

对应里面的SNTP更新时间的操作,这是很有必要的,因为HTTPS服务器会校验设备时间,如果误差过大,那就不会正常的与之通信,所以必须在进行HTTPS请求之前进行SNTP更新,并且ESP32内部实现了时间的自动更新,我们只要初始化并且使用了SNTP更新时间,他就会根据我们设定的间隔(默认一小时)进行时间的校准,防止时间误差过大(毕竟嵌入式系统的晶振和温度会影响频率,必须校准,如果不使用SNTP更新时间,使用时钟芯片也可以,但是同样需要定时获取时间进行更新)。

进行HTTPS服务器获取操作,我们需要对应的CA证书,让ESP32校验服务器是否是真正的目标服务器,防止劫持,让把CA证书防在对应的pem_cert配置项里面,这样就可以正常的和服务器校验了,和风天气和中国天气网同样需要获取CA证书进行配置,我这里就演示一下如何获取和风天气的CA证书,中国天气网的方法一样可以。

首先进入和风天气首页,点击左上角的锁图标,进入如下界面:

image.png  

点击“连接安全”,然后点击右上角那个类似于证书的图标:

image.png  

然后进入如下界面,点击详细信息:

image.png  

然后点击中间的末尾是“CA  CV R36”的选项,点击右下角的导出,导出为pem格式的证书,然后通过记事本打开:

image.png  

打开以后就可以看到和风天气对应的CA证书了,我们接下来把它导入到文件里面,导入成常量字符串,由于把一整个当成字符串,我们需要在末尾补上看不见的回车'\n'。

image.png  

应该弄成这个样子即可:

image.png  

这样我们就完成了对应的CA证书的提取了,我们看到,即使使用的是和风天气对应的API服务,由于是使用的同一台根服务器的,所以CA证书的可以嵌套的,我们就导入他们官网的CA证书就可以同样对API服务器进行校验了。

中国天气网的CA证书提取的流程和上面一致,这里就不重复演示了。

接下来我们进行HTTPS的部分编写:

2.WIFI连接和HTTPS事件编写:

我们使用了WIFIremot进行网络桥接以后,我们可以直接使用ESP32的WIFI函数进行联网操作,底层会自动和ESP32C5进行通信联网,我们仅需关注应用层的WIFI的API就可以了,对于WIFI的初始化,我们首先需要初始化对应的NVS,Netif,默认的WIFI_STA和事件循环,然后创建WIFI的配置结构体,配置好对应的WIFI的STA的名字和密码,然后创建两个事件循环,包括WIFI的事件循环和IP事件的事件循环,我们在获取IP的时候启动SNTP的时间校准,然后我们初始化WIFI,设置wifi的模式和把配置结构体传入进行配置,最后开启WIFI进入事件循环的WIFI初始化完成的事件,在这个事件里面连接WIFI,然后会触发WIFI连接成功的事件,这里我们可以切换LVGL的WIFI显示图标,设置LED灯的亮灭等,最后在路由器分配完IP以后,就进入了IP获取到的事件了,在这个事件里面我们就调用SNTP更新的函数,进行SNTP的更新,SNTP的更新和网络有关,我们无法准确的知道SNTP究竟会更新多久,所以我们可以使用回调函数,当更新完毕就置为事件组,触发其他的HTTPS任务,这就是WIFI和SNTP的更新流程。

初始化代码如下:

void WIFI_Init(void)
{
    wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = SSID,
            .password = PASSWORD,
        },
        
    };
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();
    esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,esp_wifi_event_handler,NULL);
    esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,esp_wifi_event_handler,NULL);
    
    esp_wifi_init(&wifi_init_config);
    esp_wifi_set_config(ESP_IF_WIFI_STA,&wifi_config);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

}
extern lv_ui guider_ui;
void esp_wifi_event_handler(void* event_handler_arg,
                                    esp_event_base_t event_base,
                                    int32_t event_id,
                                    void* event_data)
{
    if(event_base == WIFI_EVENT)
    {
        switch(event_id)
        {
            case WIFI_EVENT_STA_DISCONNECTED:
                esp_wifi_connect();
                lv_obj_set_style_bg_color(guider_ui.screen_label_13, lv_color_hex(0xFF0000), LV_STATE_DEFAULT); 
                vTaskDelay(pdMS_TO_TICKS(5000));
            break;
            case WIFI_EVENT_STA_START:
                esp_wifi_connect();
            default:
            
        }
    }
    else if(event_base == IP_EVENT)
    {
        switch(event_id)
        {
            case IP_EVENT_STA_GOT_IP:
                ESP_LOGI(TAG,"获取IP");
                lv_obj_set_style_bg_color(guider_ui.screen_label_13, lv_color_hex(0x00FF00), LV_STATE_DEFAULT);  
                SNTP_Init();
            break;
            default:
            
        }
    }
}

SNTP的更新和回调函数如下:

static void SNTP_Init(void)
{
    esp_sntp_config_t sntp_config = ESP_NETIF_SNTP_DEFAULT_CONFIG_MULTIPLE(1,ESP_SNTP_SERVER_LIST("ntp1.aliyun.com"));
    sntp_config.start = true;
    esp_netif_sntp_init(&sntp_config);
    sntp_set_time_sync_notification_cb(&sntp_callback);
}

void sntp_callback(struct timeval *tv)
{
    ESP_LOGI(TAG,"SNTP时间更新完毕");
    xSemaphoreGive(Sntp_Update_Down);
}

这样我们就实现了SNTP的时间跟新的操作了,当时间更新完成以后就会触发事件组,这样我们的HTTPS任务就会解除阻塞,开始运行。

接下来开始进行和风天气的天气数据获取(这里我的网址API使用常量字符串封装,这里不展示因为这是私人的API):

配置结构体如下:

第一个参数就是我们的HTTPS的API网址,第二个是获取方式,我们使用GET获取数据,第三个超时时间我们给大一点,防止网络波动,我这里给10秒绝对够了,实际上可以根据需要适当缩小超时时间,第四个的传输模式,我们由于是HTTPS方式,需要使用SSL库进行加密传输,第五个是跳过服务器的CN证书,我没搞定CN证书部分,所以选择调过,第六个是认证配置的部分,我们不需要认证就填写NONE,最后一个就是咱上面说的CA证书了,我们填入这个CA证书字符串的首地址即可,内部会自动根据我们的HTTPS的url解析这个CA证书解析校验的。

esp_http_client_config_t Get_Weather_Config = {
    .url = URL_QWeather,
    .method = HTTP_METHOD_GET,
    .timeout_ms = 10000,
    .transport_type = HTTP_TRANSPORT_OVER_SSL,
    .skip_cert_common_name_check = true,
    .auth_type = HTTP_AUTH_TYPE_NONE,
    .cert_pem = Pem_qweather_data,
};

接下来就可以开始传输了,我们调用esp_http_client_init(&Get_Weather_Config);初始化HTTP的客户端,由于ESP32对HTTPS和HTTP进行了融合,我们可以调用相同的API进行请求,内部会根据我们的传入参数和url自动的解析使用是否加密传输。同时我们需要设置传输的请求头部分,请求返回GZIP压缩的数据esp_http_client_set_header(get_weather_cilent,"Accept-Encoding","gzip");这样就会返回GZIP压缩的JSON数据,当然,不加这个请求照样返回GZIP压缩的数据,这是官方为了节省带宽使用的方式,无法避免和跳过,所以想使用和风天气必须想办法进行GZIP解压缩。

我们的传输需要使用userdata部分,把我们创建的结构体指针放里面,我们的结构体是包含原始申请缓冲区指针,接收数据大小和解码后的指针这三部分,我们通过累加获取最后的数据长度,指针用来接收数据,然后在传输完成的事件里面调用zlib的解码函数,解码得到的缓冲区指针给自定义结构体的解码指针,我们可以在后续释放接收指针对应的内存和读取解码内存进行数据提取操作。

size_t Out_Len = 0;
esp_err_t https_event_handle_cb(esp_http_client_event_t *evt)
{
    switch(evt->event_id)
    {
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGI(TAG,"和服务器取得连接");
            ((HTTP_GET_Data*)evt->user_data)->Data_Size = 0;
        break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG,"和服务器断开连接");
        break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            if(evt->data && evt->data_len > 0)
            {
                memcpy((char *)((HTTP_GET_Data*)evt->user_data)->Data_Buffer + ((HTTP_GET_Data*)evt->user_data)->Data_Size,evt->data,evt->data_len);
                ((HTTP_GET_Data*)evt->user_data)->Data_Size+= evt->data_len;
            }
        break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGI(TAG,"传输完成");
            ESP_LOGI(TAG,"长度:%d",((HTTP_GET_Data*)evt->user_data)->Data_Size);
            gunzip(((HTTP_GET_Data*)evt->user_data)->Data_Buffer,((HTTP_GET_Data*)evt->user_data)->Data_Size,(char**)&((HTTP_GET_Data*)evt->user_data)->Decode_Data_Buffer,&Out_Len);
            ((HTTP_GET_Data*)evt->user_data)->Data_Size = Out_Len;
            ESP_LOGI(TAG,"解码后长度:%d",((HTTP_GET_Data*)evt->user_data)->Data_Size);
            if(((HTTP_GET_Data*)evt->user_data)->Data_Size != 0)
            {
                ESP_LOGI(TAG,"%s",((HTTP_GET_Data*)evt->user_data)->Decode_Data_Buffer);
            }
        break;
        default:
        break;
    }
    return ESP_OK;
}

void Http_Cilent_Get_Weather(void)
{
    HTTP_GET_Data HTTP_Get_Data = {0};
    Get_Weather_Config.event_handler = https_event_handle_cb;
    HTTP_Get_Data.Data_Buffer = calloc(1024,sizeof(uint8_t));
    Get_Weather_Config.user_data = &HTTP_Get_Data;
    esp_http_client_handle_t get_weather_cilent = esp_http_client_init(&Get_Weather_Config);
    esp_http_client_set_header(get_weather_cilent,"Accept-Encoding","gzip");
    esp_http_client_perform(get_weather_cilent);
    esp_http_client_cleanup(get_weather_cilent);
    Weather_Decode_And_Update(&HTTP_Get_Data);
    free(HTTP_Get_Data.Data_Buffer);
    free(HTTP_Get_Data.Decode_Data_Buffer);
    Get_Weather_Config.user_data = NULL;
}

我们的GZIP数据需要使用zlib库进行解码操作,好在ESP32的组件管理器里面官方为我们移植好了zlib的库,我们可以使用这个命令为main组件添加最新的zlib库:idf.py add-dependency "espressif/zlib^1.3.2"

espressif/zlib • v1.3.2 • ESP Component Registry

image.png  

添加完了我们就可开始进行zlib的Gzip解码了,函数如下:

我们分配30KB的空间给缓冲区来进行解码,防止解码其他的数据(比如一周天气)的API的时候缓冲区不够导致解码失败。

bool gunzip(const void* gz_data, size_t gz_len, char** out, size_t* out_len) {
    z_stream strm = {0};
    int ret = inflateInit2(&strm, 16 + MAX_WBITS); // 支持 GZIP
    if (ret != Z_OK) return false;

    // 分配输出缓冲区
    *out = malloc(30*1024);
    if (!*out) {
        inflateEnd(&strm);
        ESP_LOGI(TAG,"缓冲区分配失败");
        return false;
    }

    strm.next_in = (Bytef*)gz_data;
    strm.avail_in = gz_len;
    strm.next_out = (Bytef*)(*out);
    strm.avail_out = 30*1024;

    ret = inflate(&strm, Z_FINISH);
    inflateEnd(&strm);

    if (ret != Z_STREAM_END) {
        free(*out);
        *out = NULL;
        ESP_LOGI(TAG,"Z_STREAM_END");
        return false;
    }

    *out_len = 30*1024 - strm.avail_out;
    (*out)[*out_len] = '\0'; // 确保字符串结尾
    return true;
}

通过这个函数解码以后,我们就可以得到和风天气的对应json数据了,解码必须调用inflate(&strm, Z_FINISH);进行,使用Z_FINISH以支持GZIP解压,我们使用zlib这个库,当然可以在以后使用HTTP服务器的时候向手机发送压缩的数据,以节省带宽,加快数据传输的速度。解码结束或者失败都需要调用inflateEnd(&strm);进行释放操作,否则会导致内存泄露。

当解码完成以后,需要使用inflateEnd(&strm);来释放zlib解码产生的一些空间,我们仅需要里面的解码缓冲区,我们返回出去给我们的解码指针,接收到了以后就可以把这个自定义的结构体给到我们的Josn解码函数里面了,是cJSON进行解码操作的,我们自定义的结构体如下:

typedef struct{
    uint8_t *Data_Buffer;
    uint8_t *Decode_Data_Buffer;
    size_t Data_Size;

}HTTP_GET_Data;

解码函数如下,由于每一个API对应里面的数据不同,我们需要对每一个API编写属于他自己的cJSON解压函数:

我们这里使用保存数据的方式,把解码得到的JSON数据保存在天气结构体里面,我们就可以在需要的时候进行LVGL的标签更新,让数据的更新按照我们规定的节拍走。

数据结构体如下:

typedef struct{
    char Weather[50];
    char temp[50];
    char Hyd[50];
    char Wind_scale[50];
    char Wind_Dir[50];
    char Air_Pressure[50];
}Weather_NOW;

里面是字符数组的格式,在每一次更新之前我都会清空这个结构体来进行更新(虽然字符串末尾就存在'\0'但是为了安全还是手动清除一下最好)。

解码的函数如下:

void Weather_Decode_And_Update(HTTP_GET_Data *Data)
{
    cJSON *root = cJSON_Parse((char*)Data->Decode_Data_Buffer);
    if(root == NULL)
    {
        ESP_LOGE(TAG,"解码失败");
        return;
    }
    char Buffer[50] = {0};
    cJSON *now = cJSON_GetObjectItem(root, "now");
    cJSON *text = cJSON_GetObjectItem(now, "text");
    cJSON *windDir = cJSON_GetObjectItem(now, "windDir");
    cJSON *humidity = cJSON_GetObjectItem(now, "humidity");
    cJSON *temp = cJSON_GetObjectItem(now, "temp");
    cJSON *Pressure = cJSON_GetObjectItem(now,"pressure");
    cJSON *windScale  = cJSON_GetObjectItem(now,"windScale");
    Weather_NOW *Weather_Now_P = &Weather_Now;
    memset(Weather_Now_P,0,sizeof(Weather_NOW));
    sprintf(Buffer,"%s%%",humidity->valuestring);
    memcpy(Weather_Now_P->Hyd,Buffer,sizeof(Buffer));
    sprintf(Buffer,"%s℃",temp->valuestring);
    memcpy(Weather_Now_P->temp,Buffer,sizeof(Buffer));

    memcpy(Weather_Now_P->Weather,text->valuestring,strlen(text->valuestring));
    memcpy(Weather_Now_P->Wind_Dir,windDir->valuestring,strlen(windDir->valuestring));
    memcpy(Weather_Now_P->Wind_scale,windScale->valuestring,strlen(windScale->valuestring));
    memcpy(Weather_Now_P->Air_Pressure,Pressure->valuestring,strlen(Pressure->valuestring));

    cJSON_Delete(root);
    
}

通过cJSON的解码进行操作然后通过内存拷贝函数进行拷贝,就实现了数据保存的功能。

这样我们的天气的获取就结束了,整体不难实现,主要是zlib的解码部分,但是仅需要调用一个解码函数进行解码即可,难度不大。

3.中国天气网的图像获取:

中国天气网的图像,由于是国家平台,没有对我们的访问加入任何的限制,返回的就是纯PNG或者JPEG图像,我们之前使用ESP32P4内部的JPEG硬件解码器,解码效率高得离谱,那这里我们就使用硬件JPEG解码器解码返回的JPEG图像,我这里再搭配一个Lodepng解码返回的PNG图像,我这里使用的是LVGL8.3.10版本,里面集成了Lodepng,我们无需使用外部的PNG解码器进行解码了。

我们需要获取三个图像:南京气象台的雷达图,FY4B的真彩色云图和全国气候舒适度预报图,由于原始图像像素很大,我们需要对其进行缩放,但是无需担心,LVGL为我们提供了方便的图像操作函数。

我们通过GUI_Guider进行界面的绘制:

image.png  

我们的图片是放在容器里面的,可以帮我们限制图像的显示区域,当图像的大小大于容器的大小,就会直接截断,防止大图侵占其他的地方导致显示问题。

我们的第二个屏幕放置的是所有的天气的汉字,我目前没有好的方法把所有需要使用的汉字预存在系统里面,只能采用创建一个屏幕存放汉字的操作来实现:

image.png  

我们请求的函数和请求天气API类似,函数如下,去除了设置请求头的返回GZIP的要求:

这就是获取FY4B卫星云图的函数,我们直接请求,使用堆内存接收,由于图片过大,我们需要申请大内存,我们申请600KB的空间存放图片,必须使用PSRAM,否则仅依靠内部的RAM肯定不行。结构和上面的获取天气的函数一样,去除了GZIP解码的部分,而是直接读取数据然后返回,我们在这个函数里面使用的JPEG解码的函数,这个函数我在之前的测评:【启明云端WT99P4C5-S1】硬件JPEG解码器解码图片搭配LVGL显示在MIPI屏幕上 - 国产芯片交流 - 电子工程世界-论坛说过了,就不讲解了。

#define URL_Weather_Nanjing "https://pi.weather.com.cn/i/product/pic/sl/z_rada_c_babj_20260318052911_p_dor_z9250_cref_20260318_052145.png"
esp_err_t https_weather_event_handle_cb(esp_http_client_event_t *evt)
{
    switch(evt->event_id)
    {
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGI(TAG,"和服务器取得连接");
            ((HTTP_GET_Data*)evt->user_data)->Data_Size = 0;
        break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG,"和服务器断开连接");
        break;
        case HTTP_EVENT_ON_DATA:
            //ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            if(evt->data && evt->data_len > 0)
            {
                memcpy((char *)((HTTP_GET_Data*)evt->user_data)->Data_Buffer + ((HTTP_GET_Data*)evt->user_data)->Data_Size,evt->data,evt->data_len);
                ((HTTP_GET_Data*)evt->user_data)->Data_Size+= evt->data_len;
            }
        break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGI(TAG,"传输完成");
            ESP_LOGI(TAG,"长度:%d",((HTTP_GET_Data*)evt->user_data)->Data_Size);
            ESP_LOGI(TAG,"解码后长度:%d",((HTTP_GET_Data*)evt->user_data)->Data_Size);
        break;
        default:
        break;
    }
    return ESP_OK;
}

void Https_Cilent_Get_Weather_Earth(void)
{
    esp_http_client_config_t Get_Weather_Config = {
        .url = URL_Weather_Earth,
        .method = HTTP_METHOD_GET,
        .timeout_ms = 100000,
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .skip_cert_common_name_check = true,
        .auth_type = HTTP_AUTH_TYPE_NONE,
        .cert_pem = NULL,
    };
    HTTP_GET_Data HTTP_Get_Data = {0};
    Get_Weather_Config.event_handler = https_weather_event_handle_cb;
    HTTP_Get_Data.Data_Buffer = heap_caps_calloc(600*1024,sizeof(uint8_t),MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    Get_Weather_Config.user_data = &HTTP_Get_Data;
    esp_http_client_handle_t get_weather_cilent = esp_http_client_init(&Get_Weather_Config);
    esp_http_client_perform(get_weather_cilent);
    esp_http_client_cleanup(get_weather_cilent);
    if(Weather_Picture.Earth.Is_Weather_Earth_Loaded == true)
    {
        free(Weather_Picture.Earth.Weather_Earth);
    }
    Weather_Picture.Earth.Is_Weather_Earth_Loaded = true;
    Weather_Picture.Earth.Weather_Earth = (char *)JEPG_Decoder_Init(HTTP_Get_Data.Data_Buffer,HTTP_Get_Data.Data_Size);
    free(HTTP_Get_Data.Data_Buffer);
    Get_Weather_Config.user_data = NULL;
}

JPEG解码函数:


jpeg_decoder_handle_t jepg_decoder_handle = NULL;
uint8_t* JEPG_Decoder_Init(uint8_t* Jepg_Buffer,size_t Jepg_Size)
{
    size_t input_size = 0,output_size = 0;
    uint32_t out_size = 0;
    jpeg_decode_engine_cfg_t jpeg_decode_engine_cfg = {
        .intr_priority = 0,
        .timeout_ms = 40,
    };
    jpeg_decode_memory_alloc_cfg_t INPUT_Config = {
        .buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
    };
    jpeg_decode_memory_alloc_cfg_t OUTPUT_Config = {
        .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
    };
    jpeg_decode_cfg_t jpeg_decode_cfg = {
        .rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
        .output_format = JPEG_DECODE_OUT_FORMAT_RGB565,
    };
    ESP_ERROR_CHECK(jpeg_new_decoder_engine(&jpeg_decode_engine_cfg,&jepg_decoder_handle));
    uint8_t *Input_Buffer = jpeg_alloc_decoder_mem(Jepg_Size,&INPUT_Config,&input_size);
    uint8_t *Output_Buffer = jpeg_alloc_decoder_mem(1024*1024*2,&OUTPUT_Config,&output_size);
    memcpy(Input_Buffer,Jepg_Buffer,Jepg_Size);
    jpeg_decoder_process(jepg_decoder_handle,&jpeg_decode_cfg,Input_Buffer,Jepg_Size,Output_Buffer,1024*1024*2,&out_size);
    free(Input_Buffer);
    return Output_Buffer;
}

我们的JPEG的缓冲区给到的是2MB的大小,因为解码的原始RAW图像实在是大的离谱,使用这个大小防止缓冲区不足导致的解码失败。(一定注意我们的解码大小一定要小于等于我们申请的空间,否则就会写超过分配的大小,导致堆被损坏)。

这样我们在获取到原始数据以后就进行解码,把解码好的内存首地址给到我们创建的图像结构体,来存储我们解码的指针,数据的大小和是否是第一次解码(因为LVGL的显示依赖我们的缓冲区数据,不可以更新以后就释放,否则会导致LVGL崩溃),当检测到不是第一次获取图像就会先释放前一个图像内存然后再获取新的图像。

如此三次我们就获取到了三个图像,仅当获取JPEG的时候我们需要调用JPEG解码函数,获取PNG图像的时候仅保存原始的图像数据即可,我们依赖LVGL的内部解码库进行解码。

由于三个函数调用的原理是类似的,我们使用同一个回调函数就可以完成这个操作,反转就是接收HTTPS的数据流然后分段写入我们分配好的缓冲区里面。

void Https_Cilent_Get_Weather_Nanjing(void)
{
    esp_http_client_config_t Get_Weather_Config = {
        .url = URL_Weather_Nanjing,
        .method = HTTP_METHOD_GET,
        .timeout_ms = 100000,
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .skip_cert_common_name_check = true,
        .auth_type = HTTP_AUTH_TYPE_NONE,
        .cert_pem = NULL,
    };
    HTTP_GET_Data HTTP_Get_Data = {0};
    Get_Weather_Config.event_handler = https_weather_event_handle_cb;
    //HTTP_Get_Data.Data_Buffer = calloc(10240*85,sizeof(uint8_t));
    HTTP_Get_Data.Data_Buffer =heap_caps_calloc(1024*1024,sizeof(uint8_t),MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (HTTP_Get_Data.Data_Buffer == NULL) {
        // 处理分配失败
        ESP_LOGE(TAG,"内存分配失败");
        return;
    }
    else
    {
        ESP_LOGI(TAG,"内存分配成功");
    }
    Get_Weather_Config.user_data = &HTTP_Get_Data;
    esp_http_client_handle_t get_weather_cilent = esp_http_client_init(&Get_Weather_Config);
    esp_http_client_perform(get_weather_cilent);
    esp_http_client_cleanup(get_weather_cilent);
    if(Weather_Picture.Nanjing.Is_Weather_NanJing_Loaded == true)
    {
        ESP_LOGI(TAG,"准备释放南京的内存");
        free(Weather_Picture.Nanjing.Weather_NanJing);
    }
    Weather_Picture.Nanjing.Is_Weather_NanJing_Loaded = true;
    Weather_Picture.Nanjing.Weather_NanJing = (char *)HTTP_Get_Data.Data_Buffer;
    Weather_Picture.Nanjing.Data_size = HTTP_Get_Data.Data_Size;
    ESP_LOGI(TAG,"退出南京云图的获取函数");
    //Get_Weather_Config.user_data = NULL;
}
void Https_Cilent_Get_Weather_Sutable(void)
{
    esp_http_client_config_t Get_Weather_Config = {
        .url = URL_Weather_Suitable,
        .method = HTTP_METHOD_GET,
        .timeout_ms = 100000,
        .transport_type = HTTP_TRANSPORT_OVER_SSL,
        .skip_cert_common_name_check = true,
        .auth_type = HTTP_AUTH_TYPE_NONE,
        .cert_pem = NULL,
    };
    HTTP_GET_Data HTTP_Get_Data = {0};
    Get_Weather_Config.event_handler = https_weather_event_handle_cb;
    HTTP_Get_Data.Data_Buffer = heap_caps_calloc(600*1024,sizeof(uint8_t),MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    Get_Weather_Config.user_data = &HTTP_Get_Data;
    esp_http_client_handle_t get_weather_cilent = esp_http_client_init(&Get_Weather_Config);
    esp_http_client_perform(get_weather_cilent);
    esp_http_client_cleanup(get_weather_cilent);
    if(Weather_Picture.Suitable_weather.Is_Suitable_Weather_Loaded == true)
    {
        free(Weather_Picture.Suitable_weather.Suitable_Weather);
    }
    Weather_Picture.Suitable_weather.Is_Suitable_Weather_Loaded = true;
    Weather_Picture.Suitable_weather.Suitable_Weather = (char *)JEPG_Decoder_Init(HTTP_Get_Data.Data_Buffer,HTTP_Get_Data.Data_Size);
    free(HTTP_Get_Data.Data_Buffer);
    Get_Weather_Config.user_data = NULL;
}

这样我们就实现了三张图片的获取操作了,我们获取到图像之后就可以进行显示了,由于除了PNG是需要使用内部的PNG解码器解码的,剩下的是解码好的RAW数据,这里就直接显示即可我。

我们需要创建对应的图像描述符,是三个结构体:

static lv_img_dsc_t img_dsc;
static lv_img_dsc_t img_NanJing ={
    .header.always_zero = 0,
    .header.w = 0,
    .header.h = 0,
};
static lv_img_dsc_t img_Suitable ={
    .header.always_zero = 0,
    .header.w = 0,
    .header.h = 0,
};

 然后就可以向里面填入数据了,我们设置图像的格式,长宽和使用的空间(图像大小)和内存的首地址即可完成配置,至于always zero部分就按照默认配置配置成0即可,起到校验的作用。

至于PNG的设置,有一点不同之处,我们除了需要指定图像的地址,大小,还需要指定里面的cf格式为LV_IMG_CF_UNKNOWN,这样未知的格式就会让LVGL遍历里面的所有解码器尝试解码,这样当解码到lodepng即可正确的解码出对应的PNG图像,并且内部会自动的解码和渲染,使用起来很方便的,开启LVGL内部的Lodepng库只需要再lv_conf.h里面开启LV_USE_PNG选项即可使用LVGL的PNG解码器了。

void Picture_Update(void* Param)
{
    for(;;)
    {
        ESP_LOGI(TAG,"开始进行云图获取");
        Https_Cilent_Get_Weather_Earth();
        Https_Cilent_Get_Weather_Sutable();
        Https_Cilent_Get_Weather_Nanjing();
        img_NanJing.data = (unsigned char *)Weather_Picture.Nanjing.Weather_NanJing;
        img_NanJing.data_size = Weather_Picture.Nanjing.Data_size;
        img_NanJing.header.cf = LV_IMG_CF_UNKNOWN;
        ESP_LOGI("原始数据","%02x,%02x,%02x,%02x,%02x,%02x,%02x,%02x",Weather_Picture.Nanjing.Weather_NanJing[0],Weather_Picture.Nanjing.Weather_NanJing[1],\
        Weather_Picture.Nanjing.Weather_NanJing[2],Weather_Picture.Nanjing.Weather_NanJing[3],Weather_Picture.Nanjing.Weather_NanJing[4],\
    Weather_Picture.Nanjing.Weather_NanJing[5],Weather_Picture.Nanjing.Weather_NanJing[6],Weather_Picture.Nanjing.Weather_NanJing[7]);
        ESP_LOGI("末尾数据","%02x,%02x,%02x,%02x,%02x,%02x,%02x,%02x",Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-8],Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-7],Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-6],\
        Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-5],Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-4],Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-3],\
    Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size-2],Weather_Picture.Nanjing.Weather_NanJing[Weather_Picture.Nanjing.Data_size]);
        
        // 1. 初始化图片描述符(数据源仍指向完整的960*640图片)
        img_dsc.header.always_zero = 0;
        img_dsc.header.w = 960;          // 保持原图宽度(960)
        img_dsc.header.h = 603;          // 保持原图高度(640)
        img_dsc.data_size = 960 * 603 * sizeof(uint16_t); // 原图数据大小
        img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
        img_dsc.data = (const uint8_t *)Weather_Picture.Earth.Weather_Earth;
        img_Suitable.header.w = 960;
        img_Suitable.header.h = 778;
        img_Suitable.data_size = 960 * 778 * sizeof(uint16_t);
        img_Suitable.header.cf = LV_IMG_CF_TRUE_COLOR;
        img_Suitable.data = (const uint8_t *)Weather_Picture.Suitable_weather.Suitable_Weather;
        // 2. 设置图片组件显示源
        
        xSemaphoreTake(lvgl_Update_Handle,portMAX_DELAY);
        // 3. 关键:裁剪显示(0,0)到(240,240)区域
        // 3.1 设置图片显示偏移(从原图(0,0)开始显示)
        lv_img_set_offset_x(guider_ui.screen_img_3, -130);
        lv_img_set_offset_y(guider_ui.screen_img_3, -5);
        
        lv_img_set_zoom(guider_ui.screen_img_3,135); 
        lv_img_set_size_mode(guider_ui.screen_img_3,LV_IMG_SIZE_MODE_REAL);
        lv_img_set_src(guider_ui.screen_img_3, &img_dsc);
        xSemaphoreGive(lvgl_Update_Handle);
        vTaskDelay(pdMS_TO_TICKS(500));
        xSemaphoreTake(lvgl_Update_Handle,portMAX_DELAY);
        lv_img_set_offset_x(guider_ui.screen_img_4, -27);
        lv_img_set_offset_y(guider_ui.screen_img_4, -5);
        lv_img_set_zoom(guider_ui.screen_img_4,100); 
        lv_img_set_size_mode(guider_ui.screen_img_4,LV_IMG_SIZE_MODE_REAL);
        lv_img_set_src(guider_ui.screen_img_4, &img_Suitable);

        xSemaphoreGive(lvgl_Update_Handle);
        vTaskDelay(pdMS_TO_TICKS(500));
        xSemaphoreTake(lvgl_Update_Handle,portMAX_DELAY);
        size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
        ESP_LOGI(TAG, "largest_free_block: %zu bytes", free_heap);
        lv_img_set_offset_x(guider_ui.screen_img_2, -248);
        lv_img_set_offset_y(guider_ui.screen_img_2, -272);
        lv_img_set_zoom(guider_ui.screen_img_2,230); 
        lv_img_set_size_mode(guider_ui.screen_img_2,LV_IMG_SIZE_MODE_REAL);
        lv_img_set_src(guider_ui.screen_img_2,&img_NanJing);
        xSemaphoreGive(lvgl_Update_Handle);
        //free(Weather_Picture.Earth.Weather_Earth);
        vTaskDelete(NULL);

    }
}

通过这样我们就实现了PNG和解码好的RAW数据的渲染了也就实现了我们云端获取数据本地解析显示的效果。

剩下的时间更新和天气更新我都是创建任务进行的,并且使用事件组管理这些任务的动作时机。

进行天气更新的任务,我每隔三分钟进行一次更新:

void Weaher_Get_Data(void* Param)
{
    for(;;)
    {
        Http_Cilent_Get_Weather();
        xEventGroupSetBits(Weather_Update_Down_Handle,(1UL << 0));
        vTaskDelay(pdMS_TO_TICKS(1000*3*60));
    }
}

由于我的天气数据是存在内部的结构体里面的,我还需要一个任务定时的读取结构体进行天气数据的更新:

每隔三秒进行一次更新,但是我加了一个事件组,通过天气更新函数发送,只有当通过网络获取天气以后才会发送信号量被这个任务接收,进行标签的更新,延时三秒可有可无,这里是为了贴合之前写的一些项目。

void LVGL_Weather_Update(void *Param)
{
    char Buffer[300] = {0};
    for(;;)
    {
        if(xEventGroupWaitBits(Weather_Update_Down_Handle,(1UL << 0),pdTRUE,pdFALSE,pdMS_TO_TICKS(20)))
        {
            xSemaphoreTake(lvgl_Update_Handle,portMAX_DELAY);
            sprintf(Buffer,"天气:%s",Weather_Now.Weather);
            lv_label_set_text(guider_ui.screen_label_4,Buffer);
            sprintf(Buffer,"温度:%s",Weather_Now.temp);
            lv_label_set_text(guider_ui.screen_label_5,Buffer);
            sprintf(Buffer,"湿度:%s",Weather_Now.Hyd);
            lv_label_set_text(guider_ui.screen_label_6,Buffer);
            sprintf(Buffer,"风向:%s",Weather_Now.Wind_Dir);
            lv_label_set_text(guider_ui.screen_label_7,Buffer);
            sprintf(Buffer,"风力:%s",Weather_Now.Wind_scale);
            lv_label_set_text(guider_ui.screen_label_8,Buffer);
            sprintf(Buffer,"气压:%s百帕",Weather_Now.Air_Pressure);
            lv_label_set_text(guider_ui.screen_label_9,Buffer);
            if(strncmp(Weather_Now.Weather,"雾",sizeof(Weather_Now.Weather)) == 0)
            {
                lv_img_set_src(guider_ui.screen_animimg_1, screen_animimg_1_imgs[3]);
            }
            else if(strncmp(Weather_Now.Weather,"雨",sizeof(Weather_Now.Weather)) == 0)
            {
                lv_img_set_src(guider_ui.screen_animimg_1, screen_animimg_1_imgs[2]);
            }
            else if(strncmp(Weather_Now.Weather,"晴",sizeof(Weather_Now.Weather)) == 0)
            {
                lv_img_set_src(guider_ui.screen_animimg_1, screen_animimg_1_imgs[1]);
            }
            else if(strncmp(Weather_Now.Weather,"多云",sizeof(Weather_Now.Weather)) == 0)
            {
                lv_img_set_src(guider_ui.screen_animimg_1, screen_animimg_1_imgs[4]);
            }
            else
            {
                lv_img_set_src(guider_ui.screen_animimg_1, screen_animimg_1_imgs[0]);
            }
            xSemaphoreGive(lvgl_Update_Handle);
            
        }
        vTaskDelay(pdMS_TO_TICKS(3000));
    }
}

最后就是左上角的时间显示部分了,每一秒跟新一次:

void LVGL_Time_Update(void *param)
{
    char Buffer[60] = {0};
    for(;;)
    {
        xSemaphoreTake(lvgl_Update_Handle,portMAX_DELAY);
        time_t time_now = time(NULL);
        struct tm *Current_time = localtime(&time_now);
        sprintf(Buffer,"%02d:%02d:%02d",Current_time->tm_hour,Current_time->tm_min,Current_time->tm_sec);
        lv_label_set_text(guider_ui.screen_label_1,Buffer);
        xSemaphoreGive(lvgl_Update_Handle);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

最后就是app_main函数,我们使用二进制信号量和事件组管理任务的动作,我的LVGL更新是使用了一个二进制信号量来管理,确保在一个LVGL操作函数里面不会有其他任务的LVGL操作函数打断正在进行的LVGL函数,防止破坏LVGL的内部结构,毕竟LVGL是非线程安全的。

我当SNTP更新完成以后就会发送信号量给app_main函数,来进行天气相关的任务的创建。

事件组是管理天气和数据刷新的,我的网络天气获取函数会在获取完成以后置位事件组,然后被LVGL刷新天气标签的函数接收到,开始退出阻塞进行天气数据和图标的更新,更新完延时三秒会继续等带下一次事件组的更新到来,如此循环。

void app_main(void)
{
    ESP_LOGI(TAG,"开始准备初始化屏幕");
    Sntp_Update_Down = xSemaphoreCreateBinary();
    lvgl_Update_Handle = xSemaphoreCreateMutex();
    Weather_Update_Down_Handle = xEventGroupCreate();
    lv_init();
    Blk_Set_Duty(50);
    lv_png_init();
    lv_port_disp_init();
    //lv_port_indev_init();
    events_init(&guider_ui);
    setup_ui(&guider_ui);
    
    ESP_LOGI(TAG,"初始化屏幕完成");
    xTaskCreatePinnedToCore(Lv_Timer_Handler,"Lv_Timer_Handler",2048*10,NULL,1,NULL,1);
    setenv("TZ","CST-8",1);
    tzset(); 
    WIFI_Init();
    if(xSemaphoreTake(Sntp_Update_Down,portMAX_DELAY))
    {
        
        xTaskCreatePinnedToCore(LVGL_Time_Update,"LVGL_Time_Update",2048,NULL,3,NULL,1);
        xTaskCreatePinnedToCore(Weaher_Get_Data,"Weaher_Get_Data",4096,NULL,2,NULL,0);
        xTaskCreatePinnedToCore(LVGL_Weather_Update,"LVGL_Weather_Update",4096,NULL,2,NULL,1);
        xTaskCreatePinnedToCore(Picture_Update,"Picture_Update",10240,NULL,2,NULL,0);
    }
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

}

我还更新了屏幕的LEDC调光显示的部分,我们使用LEDC操作其背光引脚实现不同占空比设置其屏幕亮度的效果,这样我们的屏幕就不再那么刺眼了(app_main里面设置为:50%的亮度,LEDC对GPIO20输出的1KHz的方波的占空比为50%)。

//GPIO_NUM_20是LEDC的背光引脚
void LEDC_GPIO_NUM_20_INIT(void)
{
    ledc_channel_config_t LCD_BLK_GPIO_config = {
        .channel = LEDC_CHANNEL_0,
        .duty = 0,
        .gpio_num = GPIO_NUM_20,
        .timer_sel = LEDC_TIMER_0,
        .hpoint = 0,
    };
    ledc_timer_config_t Lcd_Blk_Timer_Config = {
        .clk_cfg = LEDC_USE_PLL_DIV_CLK,
        .freq_hz = 1000,
        .timer_num = LEDC_TIMER_0,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .deconfigure = false,
        .duty_resolution = LEDC_TIMER_10_BIT,
        
    };
    ledc_timer_config(&Lcd_Blk_Timer_Config);
    ledc_channel_config(&LCD_BLK_GPIO_config);
    ledc_set_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,512);
    ledc_update_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0);
}

void Blk_Set_Duty(uint8_t Duty)
{

    ledc_set_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,Duty/10*102);
    ledc_update_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0);
}

这样我们的天气获取+GZIP解析+网络获取图像解码显示的综合案例到此就结束了,我们最后看一下效果,由于ESP32P4自身的性能来说,获取图像需要写入PSRAM,频率不高,所以接受完到显示需要一定的时间,我们需要等待其完成全部图像的接收和解码以后才开始显示,所以更新一次花费的时间也比较长(两分钟左右才可以从获取到解析完成):

我们的项目目前仅实现了从固定的网站获取图像解析操作,至于按照时间自动获取对应的图像,我们需要对中国天气网的网页进行直接的解析和提取到里面的实时API,这部分难度较大,以后再实现,我们目前实现了从固定的API获取图像的操作,这是不完美所在。

点亮屏幕到更新完成耗时总共两分钟左右,耗时较长,主要还是图像体积过于庞大,ESP32的传输速度较慢导致的,实际项目不推荐这套方案,耗时太长。

演示视频如下:

studio_video_1773848726156

这就是我们LVGL+GZIP获取和风天气解压显示+中国天气网的天气图像解析显示的综合项目。

/*作者: 是小枼大人🍡 */

/* zxyyl */

本帖最后由 zxyyl 于 2026-3-18 23:56 编辑
zxyylsy

回复评论 (1)

好贴,楼主写的真详细,点赞 

点个灯吧
点赞  2026-3-20 11:16
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复