3.5寸桌面信息牌 · 2022年2月23日 6

ESP32桌面信息牌_教程

摘要

ESP32桌面信息牌,作品定位桌面创意小摆件,工作之余发发呆、摸摸鱼,盯着屏幕上可爱的动画也别是一般滋味。主要显示的内容及功能如下:时间显示(支持农历)、天气显示(含实时天气 + 未来三天天气 + 空气质量 + 实时气象预警 + 生活指数建议等)、热点新闻、室外温湿度显示等、采用Web配置参数(含wifi信息修改和手动修改城市),交互简单,作品技术路线:ESP32+WIFI+HTTP请求+JSON数据解析+LVGL UI设计

开始制作

1、根据BOM表采购元件,如图1所示

图1

交互式BOM表,对于DIY爱好者非常的友好,鼠标选中元件PCB中即可自动定位到该元件的坐在位置(传送门-一键直达体验),效果如图2所示

图2

2、PCB电路板打样和3D打印外观(资料下载),这一步一般会选择第三方服务,如PCB打样则使用广为人知的嘉立创,外观打印推荐使用光敏树脂材料,如果使用普通PLA材料打印失败率较高且精细度不够导致作品不美观。焊接过程中有条件的,推荐使用加热平台配合钢网,阻容封装采用0603以及还有TYPE-C、40P FPC座后两者引脚间距较密,焊接难度大,使用推荐方法可以大大减小焊接难度,提高焊接体验

3、焊接过程很简单,大致流程:固定PCB板、刷锡膏、手工贴元件、上加热台加热、后焊处理,参考图3-7

图3-7

固件烧录

准备烧录前一定要检查焊接后是否有虚焊和连锡,并使用万用表检测电源端是否有短路,上述问题检查无误一般就可以进入固件烧录环节了,烧录步骤如图8所示,设置bin文件目录及烧录地址,选择COM编号后点击START开始下载,待下方绿色进度头走完提示完成即可断电重启,不出意外,接上屏幕后即能看到显示画面,如图9所示

如插上数据线后无端口,可能是你没有安装ch340的串口驱动或TYPC-C接口和CH340C芯片可能出现虚焊

图8

图9

最后,将屏幕和PCB装入3D打印的外壳中,底板使用B7000胶水密封即可,过程如图10-11所示

图10-11

Web参数配置

固件烧录成功后,屏幕显示配置模式和一大段文字说明,文字很多,其实就是告诉你怎么使用,千万不要偷懒,一定要慢慢看完。

这个时候呢,打开手机的WIFI连接界面,在已搜索到的WIFI列表中找到ESP32桌面信息牌,然后点击连接(无密码),连接成功后不出意外会自动打开Web配置页面(这里采用强制用户认证机制),显示页面内容如图12所示,主要输入WIFI名称和密码即可,城市会根据所在地自动定位,如定位不准可以手动输入,另外授权码目前最新版的固件已经内置,这里留空即可(作者正在进行信息牌点亮中国地图的行动(目前除西北地区外,其他城市基本均已点亮),因此已经内置)

图12

信息输入无误,点击保存并连接,此时信息牌会自动重启,开始连接,并进入显示内容画面。到此,制作过程结束,流程还是比较简单。

电路原理

电源部分(图13),采用DCDC国产方案,DC方案相比LDO效率高、发热低

图13

程序自动下载匹配电路(图14),USB转串口芯片采用国产CH340C,通过2N7002双MOS管配合,巧妙运用流控接口(DTR/RTS)可以实现程序自动下载

图14

主控部分(图15),选用ESP32-WROOM-32模组,内置4M Flash,简化电路设计,只需电源滤波和上电复位电路即可正常工作

图15

外部按钮及光感电路(图16),外部按钮的作用主要用于人机交互,包含双击按钮手动刷新数据,长按按钮设备重启进入配置模式,在配置模式中根据屏幕提示可以更换主屏显示动画(内置6种)以及进入重新配网模式(如需更换WIFI的情况下使用);光感部分主要为自动控制屏幕背光,外部光线增强则屏幕背光亮度增加,外部光线减弱则屏幕背光亮度减小,在全黑环境下屏幕背光保持微微亮,并非全黑。

图16

3.5寸屏幕背光驱动及屏幕接口电路(图17-18),屏幕驱动芯片采用ILI9488,分辨率320*480,通信接口为8位并口

图17-18

程序思路

程序编译环境为VSCode + Platformio,使用Arduino框架,程序总行数超过2000,由于篇幅有限,故而介绍部分主要功能

1.Web配网,ESP32自建热点,手机或PC连接该热点,打开指定HTML页面,配置完成后,将WIFI信息存储在NVS区域中

#include <WiFi.h>
#include <DNSServer.h>
#include <WebServer.h>

const char* AP_NAME = "ESP32桌面信息牌";//Web配网模式下的AP-wifi名字

//暂时存储wifi账号密码、城市代码、NewsKEY
char sta_ssid[32] = {0};
char sta_password[64] = {0};
char sta_citycode[32] = {0};
char sta_NewsKEY[256] = {0};
char blynkTimes[8] = {0};

//配网页面代码 
String page_html = R"(
<!DOCTYPE html>
<html lang='en'>
	<head>
		<meta charset='UTF-8'>
		
		<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
		<title>ESP32桌面小电视</title>
		<style type="text/css">
			* { margin: 0; padding: 0; }
			 html { height: 100%; }
			 h2 {text-align: center;color: #fff;line-height: 2.2;}
			 body { height: 100%; background-color: #1F6F4A; 50% 50% no-repeat; background-size: cover;}
			 .dowebok { position: absolute; left: 50%; top: 30%; width: 380px; height: 440px; margin: -200px 0 0 -200px; border: 3px solid #fff; border-radius: 10px; overflow: hidden;}
			 
			 .form-item { position: relative; width: 360px; margin: 0 auto; padding-bottom: 20px;}
			 .form-item input { width: 288px; height: 48px; padding-left: 10px; border: 1px solid #fff; border-radius: 25px; font-size: 18px; color: #fff; background-color: transparent; outline: none;}
			 .send_button { width: 360px; height: 50px; border: 0; border-radius: 25px; font-size: 18px; color: #1f6f4a; outline: none; cursor: pointer; background-color: #fff; }
			 
			 .tip { display: none; position: absolute; left: 20px; top: 52px; font-size: 14px; color: #f50; }
			 .reg-bar { width: 360px; margin: 20px auto 0; font-size: 14px; overflow: hidden;}
			 .reg-bar a { color: #fff; text-decoration: none; }
			 .reg-bar a:hover { text-decoration: underline; }
			 .reg-bar .reg { float: left; }
			 .reg-bar .forget { float: right; }
			 .dowebok ::-webkit-input-placeholder { font-size: 18px; line-height: 1.4; color: #fff;}
			 .dowebok :-moz-placeholder { font-size: 18px; line-height: 1.4; color: #fff;}
			 .dowebok ::-moz-placeholder { font-size: 18px; line-height: 1.4; color: #fff;}
			 .dowebok :-ms-input-placeholder { font-size: 18px; line-height: 1.4; color: #fff;}
			  
			 @media screen and (max-width: 500px) {
			 * { box-sizing: border-box; }
			 .dowebok { position: static; width: auto; height: auto; margin: 0 30px; border: 0; border-radius: 0; }
			 .logo { margin: 50px auto; }
			 .form-item { width: auto; }
			 .form-item input, .form-item button, .reg-bar { width: 100%; }
			 }
			 
		</style>
	</head>
	
	<body>
		<div class="dowebok">
			<h2>参 数 配 置</h2>
			<form style='text-align: center;padding-top: 20px' name='input' action='/' method='POST'>	 
				 <div class="form-item">
					<input id="username" type="text" name='ssid' autocomplete="off" placeholder="WiFi名称">
				 </div>
				 <div class="form-item">
					<input id="password" type="password" name='password' autocomplete="off" placeholder="WiFi密码">
				 </div>
				 <div class="form-item">
				 	<input id="citycode" type="citycode" name='citycode' autocomplete="off" placeholder="城市代码,留空则定位获取">
				 </div>
				 <div class="form-item">
				 	<input id="NewsKEY" type="NewsKEY" name='NewsKEY' autocomplete="off" placeholder="授权码,系统已内置">
				 </div>
				 <div class="form-item">
					 <div id="">
						<input id="send_button" type='submit' value='保存并连接'>
					 </div>
				</div>
			</form>	
		 </div>
	</body>
</html>
)";

const byte DNS_PORT = 53;//DNS端口号
IPAddress apIP(192, 168, 4, 1);//esp32-AP-IP地址
DNSServer dnsServer;//创建dnsServer实例
WebServer server(80);//创建WebServer

unsigned long getChipId() {
  unsigned long  mcuchipId = 0;
  for(int i=0; i<31; i=i+8) {
    mcuchipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
  }
  return mcuchipId;
}

void handleRoot() {//访问主页回调函数
  long chipid = getChipId();
  server.send(200, "text/html", page_html);
}

void handleRootPost() {//Post回调函数
  Serial.println("handleRootPost");
  if (server.hasArg("ssid")) {//判断是否有账号参数
    Serial.print("got ssid:");
    strcpy(sta_ssid, server.arg("ssid").c_str());//将账号参数拷贝到sta_ssid中
    Serial.println(sta_ssid);
  } else {//没有参数
    Serial.println("error, not found ssid");
    server.send(200, "text/html", "<meta charset='UTF-8'>提示:请输入WiFi名称");//返回错误页面
    return;
  }
  //密码与账号同理
  if (server.hasArg("password")) {
    Serial.print("got password:");
    strcpy(sta_password, server.arg("password").c_str());
    Serial.println(sta_password);
  } else {
    Serial.println("error, not found password");
    server.send(200, "text/html", "<meta charset='UTF-8'>提示:请输入WiFi密码");
    return;
  }


  if (server.hasArg("citycode")) {
    Serial.print("got citycode:");
    strcpy(sta_citycode, server.arg("citycode").c_str());
    Serial.println(sta_citycode);
  } else {
    //Serial.println("error, not found citycode");
    //server.send(200, "text/html", "<meta charset='UTF-8'>提示:请输入城市代码");
    return;
  }

  if (server.hasArg("NewsKEY")) {
    Serial.print("got NewsKEY:");
    strcpy(sta_NewsKEY, server.arg("NewsKEY").c_str());
    Serial.println(sta_NewsKEY);
  } else {
    Serial.println("error, not found NewsKEY");
    server.send(200, "text/html", "<meta charset='UTF-8'>提示:请输入授权码");
    return;
  }

  preferences.begin("wifi", false);
  preferences.putString( "ssid" , sta_ssid);
  preferences.putString( "password", sta_password);
  preferences.putString( "citycode", sta_citycode);
  String nvs_sta_NewsKEY = String(sta_NewsKEY);
  if(nvs_sta_NewsKEY.length() >= 30) { //如果授权码有更新,则做修改,否则无,默认使用上次的记录
    preferences.putString( "NewsKEY", sta_NewsKEY);
  }
  preferences.end();

  server.send(200, "text/html", "<meta charset='UTF-8'><h1>保存成功,ESP32桌面小电视重启中...</h1>");//返回保存成功页面
  delay(2000);
  //连接wifi
  //connectNewWifi();

  ESP.restart(); //重启ESP32
}

void initBasic(void){//初始化基础
  //Serial.begin(115200);
  //WiFi.hostname("Smart-ESP32");//设置ESP32设备名
}

void initSoftAP(void){//初始化AP模式
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  if(WiFi.softAP(AP_NAME)){
    Serial.println("热点已开:ESP32桌面信息牌");
  }
}

void initWebServer(void){//初始化WebServer
  //server.on("/",handleRoot);
  //上面那行必须以下面这种格式去写否则无法强制门户
  server.on("/", HTTP_GET, handleRoot);//设置主页回调函数
  server.onNotFound(handleRoot);//设置无法响应的http请求的回调函数
  server.on("/", HTTP_POST, handleRootPost);//设置Post请求回调函数
  server.begin();//启动WebServer
  Serial.println("Web服务已启动");
}

void initDNS(void){//初始化DNS服务器
  if(dnsServer.start(DNS_PORT, "*", apIP)){//判断将所有地址映射到esp32的ip上是否成功
    Serial.println("DNS服务成功");
  }
  else Serial.println("DNS服务失败");
}

void connectNewWifi(void){
  WiFi.mode(WIFI_STA);//切换为STA模式
 // WiFi.setAutoConnect(true);//设置自动连接
    WiFi.begin(PrefSSID.c_str(), PrefPassword.c_str());//连接上一次连接成功的wifi
  Serial.println("");
  Serial.print("Connect to wifi");
  int count = 0;
   while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    count++;
    if(count > 20){//如果10秒内没有连上,就开启Web配网 可适当调整这个时间
      initSoftAP();
      initWebServer();
      initDNS();
      break;//跳出 防止无限初始化
    }
    Serial.print(".");
  }
  Serial.println("");
  if(WiFi.status() == WL_CONNECTED){
    Serial.println("WIFI Connected!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    server.stop();
  }
}

上述代码为主要函数,具体使用中调用函数setWiFi即可

//强制门户Web配网
bool setWiFi_Flag = false;
void setWiFi()
{
  //配网页面显示
  LV_IMG_DECLARE(Page_Setting);
  lv_obj_t *img_WIFISetting = lv_img_create(lv_scr_act());
  lv_img_set_src(img_WIFISetting, &Page_Setting);
  lv_obj_set_pos(img_WIFISetting, 2, 0);
  for (int i = 0; i <= 100; i++)
  {
    lv_timer_handler();
    delay(5);
  }
  initBasic();
  initSoftAP();
  initWebServer();
  initDNS();
  while (setWiFi_Flag == false)
  {
    server.handleClient();
    dnsServer.processNextRequest();
    if (WiFi.status() == WL_CONNECTED)
    {
      server.stop();
      setWiFi_Flag = true;
    }
  }
}

2.获取新闻数据,使用天行数据的api接口,发起api请求后,接收返回的JSON格式数据,并使用ArduinoJSON库做数据解析,解析后通过LVGL中的lv_label_set_text_fmt函数,将数据推送到屏幕显示,其他的天气数据的数据请求和数据解析与新闻数据的方法一致,由于内容较多就不一一介绍了

void getNews()
{ //获取今日简报
  HTTPClient httpClient;
  //启动内置天行数据APIKEY
  if (TianXingAPIKEY_Flag == true)
  {
    APIKEY = "9307ac0871681b03f6d0ed892caaea83";
  }
  String URL = "http://api.tianapi.com/bulletin/index?key=" + APIKEY;
  //创建 HTTPClient 对象

  httpClient.begin(URL);

  //设置请求头中的User-Agent
  httpClient.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1");
  httpClient.addHeader("Referer", "http://api.tianapi.com/");

  //启动连接并发送HTTP请求
  int httpCode = httpClient.GET();
  Serial.println("正在获取今日简报");
  // Serial.println(URL);

  //如果服务器响应OK则从服务器获取响应体信息并通过串口输出
  if (httpCode == HTTP_CODE_OK)
  {
    String Neswstr = httpClient.getString();
    // Serial.println(Neswstr);
    Serial.println("获取今日简报成功");
    DynamicJsonDocument NeswData(4096);
    deserializeJson(NeswData, Neswstr); //传入指定JSON数据
    const char *Newslist_00_title_1 = NeswData["newslist"][0]["title"].as<char *>();
    const char *Newslist_01_title_1 = NeswData["newslist"][1]["title"].as<char *>();
    const char *Newslist_02_title_1 = NeswData["newslist"][2]["title"].as<char *>();

    lv_label_set_text_fmt(label_message_data,
                          "1、%s\n2、%s\n3、%s",
                          Newslist_00_title_1, Newslist_01_title_1, Newslist_02_title_1);

  }
  httpClient.end();
}

3.上电后首先运行的程序,主要以初始化操作为主(屏幕初始化、人机交互、WIFI联网、LVGL界面),以及配置模式均在该函数中运行,其中还有设备激活部分,该部分目前还在调试阶段

void setup()
{
  Serial.begin(115200);
  pinMode(button, INPUT);
  pinMode(35, INPUT);
  randomSeed(analogRead(35));
  ledcSetup(0, 5000, 8);
  ledcAttachPin(22, 0);
  ledcWrite(0, 150);

  lv_init();

  timer.setInterval(1000L, Simple_timer_disp_Time);

  tft.begin();
  tft.setRotation(0);

  lv_disp_draw_buf_init(&draw_buf, buf, buf2, screenWidth * 10);
  /*Initialize the display*/
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  /*Change the following line to your display resolution*/

  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  lv_disp_drv_register(&disp_drv);

  /*Initialize the (dummy) input device driver*/
  static lv_indev_drv_t indev_drv;
  lv_indev_drv_init(&indev_drv);
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  lv_indev_drv_register(&indev_drv);

  // LV_FONT_DECLARE(LED_Font);
  // vTaskDelay(2000);
  unsigned long enterButtonTime = 0;

  lv_obj_t *setlabel = lv_label_create(lv_scr_act());
  lv_label_set_long_mode(setlabel, LV_LABEL_LONG_SCROLL_CIRCULAR);
  lv_obj_set_size(setlabel, 300, 28);
  lv_obj_set_style_text_align(setlabel, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_align(setlabel, LV_ALIGN_DEFAULT, 0, 0);
  lv_obj_center(setlabel);
  lv_label_set_text(setlabel, " ");
  bool setFlag = false;
  while (digitalRead(button) == HIGH)
  { //上电后,按下按钮自动进入配置页面
    enterButtonTime = millis();
    // Serial.println(enterButtonTime);
    // setWiFi();

    LV_IMG_DECLARE(fengche_00);
    LV_IMG_DECLARE(finsh_00);
    LV_IMG_DECLARE(huojian_00);
    LV_IMG_DECLARE(longmao_00);
    LV_IMG_DECLARE(love_00);
    LV_IMG_DECLARE(taikongren_00);
    LV_IMG_DECLARE(setting_00);
    LV_IMG_DECLARE(wain_00);

    lv_obj_t *img_start = lv_img_create(lv_scr_act());
    lv_obj_set_pos(img_start, 115, 115);

    if (enterButtonTime > 500 && enterButtonTime < 1500)
    {

      lv_img_set_src(img_start, &finsh_00);

      lv_label_set_text(setlabel, "动画:鲤鱼跃龙门");
      setModeValue = 1;
    }
    else if (enterButtonTime > 1500 && enterButtonTime < 2500)
    {
      lv_img_set_src(img_start, &longmao_00);

      lv_label_set_text(setlabel, "动画:龙猫");
      setModeValue = 2;
    }
    else if (enterButtonTime > 2500 && enterButtonTime < 3500)
    {
      lv_img_set_src(img_start, &love_00);

      lv_label_set_text(setlabel, "动画:情人专属版");
      setModeValue = 3;
    }
    else if (enterButtonTime > 3500 && enterButtonTime < 4500)
    {
      lv_img_set_src(img_start, &taikongren_00);

      lv_label_set_text(setlabel, "动画:旋转太空人");
      setModeValue = 4;
    }
    else if (enterButtonTime > 4500 && enterButtonTime < 5500)
    {
      lv_img_set_src(img_start, &huojian_00);

      lv_label_set_text(setlabel, "动画:火箭");
      setModeValue = 5;
    }
    else if (enterButtonTime > 5500 && enterButtonTime < 6500)
    {
      lv_img_set_src(img_start, &fengche_00);

      lv_label_set_text(setlabel, "动画:大风车,转呀转");
      setModeValue = 6;
      
    }
    else if (enterButtonTime > 6500 && enterButtonTime < 7500)
    {
      lv_img_set_src(img_start, &setting_00);

      lv_label_set_text(setlabel, "进入配置模式");
      setModeValue = 7;
    }
    else if (enterButtonTime > 7500 && enterButtonTime < 8500)
    {
      lv_img_set_src(img_start, &wain_00);

      lv_label_set_text(setlabel, "到头了,快放开我吧!");
      setModeValue = 8;
    }

    lv_timer_handler();
    setFlag = true;

  }
  if (setFlag)
  {
    if (setModeValue == 1 || setModeValue == 2 || setModeValue == 3 || setModeValue == 4 || setModeValue == 5 || setModeValue == 6)
    {
      preferences.begin("wifi", false);
      preferences.putInt("GIFNUM", setModeValue);
      preferences.end();
      vTaskDelay(10);
      ESP.restart();
    }
  }
  if (setModeValue == 7)
  {
    setWiFi();
  } //进入配网模式
  if(setModeValue == 8) {
    vTaskDelay(500);
    ESP.restart();
  }

  lv_obj_del(setlabel);

  //首次使用自动进入配网模式,读取NVS存储空间内的ssid、password和citycode
  preferences.begin("wifi", false);
  PrefSSID = preferences.getString("ssid", "none");
  PrefPassword = preferences.getString("password", "none");
  cityCode = preferences.getString("citycode", "none");
  APIKEY = preferences.getString("NewsKEY", "none");
  GIFNUM = preferences.getInt("GIFNUM", 0);
  preferences.end();
  if (PrefSSID == "none")
  {
    setWiFi();
  }
  if (GIFNUM <= 0)
  {
    GIFNUM = 1;
  } //首次使用直接显示鲤鱼跃龙门动画

  static lv_style_t style_bg;
  static lv_style_t style_indic;
  lv_style_init(&style_bg);
  lv_style_set_border_color(&style_bg, lv_palette_main(LV_PALETTE_BLUE));
  lv_style_set_border_width(&style_bg, 2);
  lv_style_set_pad_all(&style_bg, 6);
  lv_style_set_radius(&style_bg, 6);
  lv_style_set_anim_time(&style_bg, 1000);
  lv_style_init(&style_indic);
  lv_style_set_bg_opa(&style_indic, LV_OPA_COVER);
  lv_style_set_bg_color(&style_indic, lv_palette_main(LV_PALETTE_BLUE));
  lv_style_set_radius(&style_indic, 3);
  lv_obj_t *bar = lv_bar_create(lv_scr_act());
  lv_obj_remove_style_all(bar);
  lv_obj_add_style(bar, &style_bg, 0);
  lv_obj_add_style(bar, &style_indic, LV_PART_INDICATOR);
  lv_obj_set_size(bar, 280, 20);
  lv_bar_set_range(bar, 0, 100);
  lv_obj_set_pos(bar, 20, 300);
  lv_bar_set_value(bar, 0, LV_ANIM_ON);

  lv_obj_t *connectText = lv_label_create(lv_scr_act());
  lv_label_set_long_mode(connectText, LV_LABEL_LONG_SCROLL_CIRCULAR);
  // lv_label_set_text(connectText,"星期三");
  lv_obj_set_width(connectText, 220);
  lv_obj_set_height(connectText, 28);
  lv_obj_set_style_text_align(connectText, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_align(connectText, LV_ALIGN_DEFAULT, 0, 0);
  lv_obj_set_pos(connectText, 50, 330);

  //版本号
  lv_obj_t *versionNumberText = lv_label_create(lv_scr_act());
  lv_label_set_long_mode(versionNumberText, LV_LABEL_LONG_SCROLL_CIRCULAR);
  lv_label_set_text_fmt(versionNumberText, "版本号:%s", versionNumber);
  lv_obj_set_width(versionNumberText, 170);
  lv_obj_set_height(versionNumberText, 28);
  lv_obj_set_style_text_align(versionNumberText, LV_TEXT_ALIGN_CENTER, 0);
  lv_obj_set_style_text_color(versionNumberText, lv_color_hex(0xD3D3D3), 0);
  lv_obj_align(versionNumberText, LV_ALIGN_DEFAULT, 0, 0);
  lv_obj_set_pos(versionNumberText, 175, 458);

  //联网时的动画显示
  lv_obj_t *connect_lv_gif = lv_gif_create_from_data(lv_scr_act(), connect_wifi_map);
  lv_obj_set_pos(connect_lv_gif, 120, 135);

  WiFi.begin(PrefSSID.c_str(), PrefPassword.c_str());
  // WiFi.begin("1302","dxzc1302");
  const char *wifiText = PrefSSID.c_str();
  int32_t bar_value = 0;
  // while (WiFi.status() != WL_CONNECTED) {
  // lv_bar_set_value(bar, bar_value, LV_ANIM_ON);
  lv_label_set_text_fmt(connectText, "正在连接:%s...", wifiText);
  while (WiFi.status() != WL_CONNECTED)
  {
    bar_value++;
    // Serial.println(bar_value);
    lv_bar_set_value(bar, bar_value, LV_ANIM_ON);
    lv_timer_handler();
    vTaskDelay(40);
    if (bar_value >= 101)
    { //如果连接超时,自动重启再连接
      // setWiFi();
      ESP.restart(); //重启ESP32
    }
    if (WiFi.status() == WL_CONNECTED)
    { // wifi连接成功后,快速走完进度条
      lv_label_set_text(connectText, "连接成功,正在同步数据...");
      for (int i = bar_value; i <= 100; i++)
      {
        lv_bar_set_value(bar, i, LV_ANIM_ON);
        lv_timer_handler();
        vTaskDelay(5);
      }
    }
  }
  Serial.print("本地IP:");
  Serial.println(WiFi.localIP());

  /*执行激活函数*/
  // EquipmentActivation(true);

  Udp.begin(localPort);
  setSyncProvider(getNtpTime);
  // setSyncInterval(60*60); //NTP网络同步频率,单位秒。
  setSyncInterval(600);

  lv_obj_del(connect_lv_gif);
  lv_obj_del(bar);
  lv_obj_del(connectText); //删除进度条和文字提示
  lv_obj_del(versionNumberText);

  /* Create simple demo */

  lv_draw_line();
  lv_disp_img();
  lv_top_text();
  //lv_COVID_19();

  if (cityCode.length() >= 8)
  {
    // Serial.println("手动设置cityCode");
    getCityWeater(); //获取天气数据
  }
  else
  {
    // Serial.println("自动设置cityCode");
    getCityCode(); //获取城市代码
  }
  getNews();    //获取新闻数据
  //getxinguan(); //获取疫情数据
  getlunar();

  button18.attachDuringLongPress(attachDuringLongPress18); //长按
  button18.attachDoubleClick(attachDoubleClick18);         //双击
  button18.attachClick(attachClick18);                     //短按
}

4.主loop函数中运行的内容不多,主要以LVGL内容刷新为主

unsigned long oldTime = 0;

void loop()
{
  timer.run(); //定时刷新时间任务
  lv_timer_handler(); //LVGL界面刷新
  vTaskDelay(5);
  /*定时设定时间联网刷新一次数据*/
  if (millis() - oldTime >= (updateTime * 60000) && time_Refresh_Flag())
  {
    oldTime = millis();
    getCityWeater();
    getNews();
    //getxinguan();
    getlunar();
    Serial.println("定时更新数据中...");
  }
  /*屏幕背光自动控制*/
  Filter_Value = Filter();
  ledcWrite(0, map(Filter_Value, 0, 4095, 1, 255));
  // Serial.println(Filter_Value);

  button18.tick(); //按键事件 短按、双击、长按
}

结束语

这个作品难度不大,也是作为自学lvgl的第一个实战作品,程序还有很大的优化空间,比如引入FreeRTOS提高执行效率;断网、无网络情况的处理;作品定位也非常明确,就是桌面小摆件,且全程均在WIFI环境下使用;TFT屏幕和ESP32功耗较大,功耗层面做优化意义不大,因此不会考虑增加电池。另外这是一个桌面信息牌,其实是可以融入或嵌入很多的桌面设备中,比如笔筒、台灯等,还可以在此基础上重新设计外壳,增加无线充电单元,PCB预留的接口中增加RGB灯效。