紧接着上一次没做完的继续,完善settings页面功能,实现过程中顺便完善了WiFi连接和时间同步。
本文主要包括:
-
Settings菜单完善
- 多级菜单和sidebar
- lvgl表格
- 文本和滚动
-
Esp32的smartconfig配网
-
时间同步
Setting菜单
说实话其实上一次代码传错了,不过也无所谓,那本身就是官方例程,和实际需求差的还是比较远的。顺便从这次开始更正一些记录习惯,不要大段大段的贴代码,要一块一块的分析,毕竟完整代码GitHub里就有..
用到的主要方法有: -
static lv_obj_t * create_text(...);
:创建文本标签(lv_label) -
static lv_obj_t * create_slider(...);
:创建滑动条 -
static lv_obj_t * create_switch(...);
:创建开关 -
static lv_obj_t * create_WiFiInfo_table(...);
:创建用于显示WiFi信息的表(table) -
void set_sidebar_width(...);
:更改sidebar大小 -
void App_Settings_menu(void);
:界面生成
主要记录的是最后一个,参考官方的complex_menu例程魔改的半成品。
多级菜单
这一段没什么好说的,就是创建最基础的menu对象并设置背景颜色和占据屏幕的大小。
这里的menu对象是接下来所有页面的父对象。
lv_obj_t * menu = lv_menu_create(lv_scr_act());lv_color_t bg_color = lv_obj_get_style_bg_color(menu, 0);if(lv_color_brightness(bg_color) > 127) {lv_obj_set_style_bg_color(menu, lv_color_darken(lv_obj_get_style_bg_color(menu, 0), 10), 0);}else {lv_obj_set_style_bg_color(menu, lv_color_darken(lv_obj_get_style_bg_color(menu, 0), 50), 0);}lv_obj_set_size(menu, lv_disp_get_hor_res(NULL), lv_disp_get_ver_res(NULL));lv_obj_center(menu);
接下来是创建需要的子页面,包括有:
-
sub_WIFI_page : 显示WiFi连接状态和连接的WiFi的信息
-
sub_BLE_page :显示蓝牙相关信息
-
剩下的都是字面意思
在这里可以看到整个页面离不开lvgl中的contain和section概念,contain作为容纳各种组件的容器存在,而section则可以容纳多个contain,这也构成了“多级”菜单的基础。
代码中可以看到的就有cont = create_WiFiInfo_table
和cont = create_text
他们都返回了lv_obj_t对象,但一个是表一个是文本标签;同时在lv_menu_separator_create(sub_about_page)
后,对于about这个section容纳了两个包含着文本标签(label)的cont。
lv_obj_t * cont;lv_obj_t * section;/*Create sub pages*/lv_obj_t * sub_WIFI_page = lv_menu_page_create(menu, NULL);lv_obj_set_style_pad_hor(sub_WIFI_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);section = lv_menu_section_create(sub_WIFI_page);//cont = create_WiFiInfo_table(section,WiFi.SSID().c_str(),WiFi.localIP().toString().c_str(),(const char *)WiFi.status());cont = create_WiFiInfo_table(section,"123","123","123");lv_menu_set_load_page_event(menu, cont, sub_WIFI_page);lv_obj_t * sub_BLE_page = lv_menu_page_create(menu, NULL);lv_obj_set_style_pad_hor(sub_BLE_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);lv_menu_separator_create(sub_BLE_page);section = lv_menu_section_create(sub_BLE_page);lv_obj_t * sub_software_info_page = lv_menu_page_create(menu, NULL);lv_obj_set_style_pad_hor(sub_software_info_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);section = lv_menu_section_create(sub_software_info_page);create_text(section, NULL, "Version 1.1", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_obj_t * sub_legal_info_page = lv_menu_page_create(menu, NULL);lv_obj_set_style_pad_hor(sub_legal_info_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);section = lv_menu_section_create(sub_legal_info_page);create_text(section, NULL,"Chappie-II modified by K0maru3",LV_MENU_ITEM_BUILDER_VARIANT_1);lv_obj_t * sub_about_page = lv_menu_page_create(menu, NULL);lv_obj_set_style_pad_hor(sub_about_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);lv_menu_separator_create(sub_about_page);section = lv_menu_section_create(sub_about_page);cont = create_text(section, NULL, "Software information", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_menu_set_load_page_event(menu, cont, sub_software_info_page);cont = create_text(section, NULL, "Legal information", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_menu_set_load_page_event(menu, cont, sub_legal_info_page);
接下来是创建侧边栏(sidebar),这里同样体现了lvgl中section和cont的关系。create_text
中使用的如LV_SYMBOL_WIFI、LV_SYMBOL_BLUETOOTH这样的参数是lvgl提供的一些默认的icon。
在实际使用的时候发现侧边栏的大小很奇怪,在网上搜索发现有的人很宽有的人很窄,我的出现了窄到自都看不见的情况,所以用到了set_sidebar_width(lv_obj_t* menu, lv_coord_t width);
来直接调整侧边栏的大小。
值得注意的是在较新版本的lvgl中scroll效果是默认启用的,只要布局超过父对象就会启用(不清楚其他情况),为了手感考虑使用lv_obj_clear_flag
关掉了滚动。
root_page = lv_menu_page_create(menu, "Settings");lv_obj_set_style_pad_hor(root_page, lv_obj_get_style_pad_left(lv_menu_get_main_header(menu), 0), 0);section = lv_menu_section_create(root_page);/*WIFI页面计划用于显示Wifi连接情况*/cont = create_text(section, LV_SYMBOL_WIFI, "WIFI", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_menu_set_load_page_event(menu, cont, sub_WIFI_page);/*BLE页面计划用于显示蓝牙连接情况*/cont = create_text(section, LV_SYMBOL_BLUETOOTH, "BLE", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_menu_set_load_page_event(menu, cont, sub_BLE_page);create_text(root_page, NULL, "Others", LV_MENU_ITEM_BUILDER_VARIANT_1);section = lv_menu_section_create(root_page);cont = create_text(section, NULL, "About", LV_MENU_ITEM_BUILDER_VARIANT_1);lv_menu_set_load_page_event(menu, cont, sub_about_page);lv_menu_set_sidebar_page(menu, root_page);set_sidebar_width(menu, 100);lv_obj_clear_flag(menu, LV_OBJ_FLAG_SCROLLABLE);lv_event_send(lv_obj_get_child(lv_obj_get_child(lv_menu_get_cur_sidebar_page(menu), 0), 0), LV_EVENT_CLICKED,NULL);
其实更改sidebar的宽度不是一个非常复杂的事情,毕竟sidebar也是一个menu对象,直接修改宽度就行。
/*** @brief Set the sidebar width object* @param menu * @param width */void set_sidebar_width(lv_obj_t* menu, lv_coord_t width) {lv_menu_t* obj = (lv_menu_t*)menu;lv_obj_set_width(obj->sidebar, width);}
表格
其实我对lvgl还是一个纯萌新的状态,现在纯是碰到什么学什么,学到什么用什么,理解了什么说什么的一个状态,这个表格的绘制也是第一次接触。绘制了一个两列四行的表格,没什么技术含量。
/*** @brief 获取显示WIFI信息* @param parent * @param ssid * @param ip_adress * @param wifi_state * @return lv_obj_t* */static lv_obj_t * create_WiFiInfo_table(lv_obj_t * parent, const char * ssid,const char * ip_adress,const char * wifi_status){lv_obj_t * obj = lv_menu_cont_create(parent);lv_obj_t * table = lv_table_create(obj);lv_table_set_col_cnt(table, 2);/*Fill the first column*/lv_table_set_cell_value(table, 0, 0, "Type");lv_table_set_cell_value(table, 1, 0, "Status");lv_table_set_cell_value(table, 2, 0, "SSID");lv_table_set_cell_value(table, 3, 0, "IP");/*Fill the second column*/lv_table_set_cell_value(table, 0, 1, "Strings");lv_table_set_cell_value(table, 1, 1, wifi_status);lv_table_set_cell_value(table, 2, 1, ssid);lv_table_set_cell_value(table, 3, 1, ip_adress);lv_obj_set_size(table, 140, 160);return obj;}
唯一值得说道的就是这个表格默认使用的尺寸是全屏尺寸,而我的这页表其实最多只能用到屏幕的一版左右,所以最开始直接就是只能看见表的左边,要scroll的时候它偏偏不来了(恼。后面知道了用lv_obj_set_size(..);
设定固定的值的时候,若大小不足以对象的布局展开,就会启用scroll。
WiFi功能
原作者的代码中,wifi和时间的同步都在settings app执行oncreate时候完成,这也是为什么我写着写着开始折腾这两个功能。而-1屏上的wifi按钮对于wifi功能一点用都没有,因此我修改了部分代码使得wifi按钮可以触发esp32的配网,把wifi连接和时间同步和settings app的创建分开了。
考虑到如果进入联网那其他什么事都要放到后面做实在是不人性化,所以用到了FreeRTOS的多任务并行。
static void xTaskOne(void *xTask1){ while (1){uint8_t i = 0;WiFi.mode(WIFI_STA);UI_LOG("[WiFi] WiFi mode : STA\n");UI_LOG("[WiFi] try connect\n");WiFi.begin();WiFi.beginSmartConfig();UI_LOG("[WiFi] Waiting for SmartConfig...\n");while (!WiFi.smartConfigDone()) { vTaskDelay(200); }UI_LOG("[WiFi] SmartConfig received, connecting WiFi...\n");while (WiFi.status() != WL_CONNECTED) { vTaskDelay(200); }UI_LOG("[WiFi] Connected. IP: %s\n", WiFi.localIP().toString().c_str());vTaskDelete(NULL);}}
void App_Launcher::WiFi_config(){UI_LOG("[WiFi] WiFi config start\n");xTaskCreatePinnedToCore(xTaskOne, "TaskOne", 4096*10, NULL, 1, NULL, 0);UI_LOG("[WiFi] WiFi config done\n");}
配网的参考代码网上有很多,比如创建事件组,设定回调状态,编写回调函数。但是我学艺不精只能用最最简单的直接WiFi.begin();
然后WiFi.beginSmartConfig();
然后使用官方开源的手机app——EspTouch就能快速配网了。
Esp32中WiFi主要是三种模式:
-
STA模式:STA是Station的简称,类似于无线终端,STA本身并不接受无线的接入,它可以连接到AP,简单来说就是和手机连接WIFI热点的工作状态相同,可以连接其它的热点。
-
AP模式:AP,也就是无线接入点,是一个无线网络的创建者,是网络的中心节点。一般家庭或办公室使用的无线路由器就一个AP。
-
AP,STA混合模式:既可以连别人,也可以别人连自己。
而smartconfig至少需要STA模式(好像是)。
而FreeRTOS的多任务并行,则是依靠xTaskCreatePinnedToCore(xTaskOne, "TaskOne", 4096*10, NULL, 1, NULL, 0);
这一行实现的。需要注意的是最后一个参数,决定了任务在哪个核心上运行。Esp32的FreeRTOS是设计运行在单核上。但ESP32是双核的,包含Protocol CPU(称为CPU 0或PRO_CPU)和Application CPU(称为CPU 1或APP_CPU)。这两个核实际上是相同的,并且共享相同的内存。这也为任务在两个核心上交替运行提供了硬件基础。
对了一定要记住,及时释放任务vTaskDelete()
。
网络时间同步
本来想通过SNTP实现网络时间同步的,但是我发现正常调用接口居然一点反应都没有,而原作者的同步方式如果不放在onCreate里面又会导致系统重启。秉持着能跑就不动原则,决定之后有空再去折腾通过sntp来实现时间同步
void App_Settings_onCreate(){UI_LOG("[%s] onCreate1\n", App_Settings_appName().c_str());UI_LOG("[WiFi] try to sync time\n");http.begin("http://quan.suning.com/getSysTime.do"); //HTTP beginint httpCode = http.GET();if (httpCode > 0){// httpCode will be negative on errorif (httpCode == HTTP_CODE_OK) // 收到正确的内容{UI_LOG("[HTTP] Connected\n");String resBuff = http.getString();UI_LOG("[HTTP] Getting string\n");char *pEnd;char charArray[100];char str [30];resBuff.toCharArray(charArray, 61);for(uint8_t i = 0;i<=15;i++){str[i]=charArray[i+46];}UI_LOG("[HTTP] string to charArray\n");int nums[6];extract_ints(str, nums, 6);rtc_date.year = nums[0];rtc_date.month = nums[1];rtc_date.date = nums[2];rtc_time.hours = nums[3];rtc_time.minutes = nums[4];rtc_time.seconds = nums[5]+1;//补偿数据处理的时间 uint16_t y = nums[0];uint8_t m = nums[1];uint8_t d = nums[2];if(m<3){m+=12;y-=1;}rtc_date.weekDay=(d+2*m+3*(m+1)/5+y+y/4-y/100+y/400+1)%7;UI_LOG("[SYS] rtc_date struct already\n");UI_LOG("[SYS] waiting applauncher restart\n");}}http.end();UI_LOG("[HTTP] disConnected\n");device->Rtc.setDate(&rtc_date);device->Rtc.setTime(&rtc_time);UI_LOG("[SYS] time sync done\n");App_Settings_menu();}
这里是通过访问quan.suning.com/getSysTime.do获得一串{"sysTime2":"2024-11-05 02:16:45","sysTime1":"20241105021645"}
来获得当前时间参数,然后分部分读出来以后写入RTC。
感觉还是SNTP比较优雅
总结
一开始写东西就会逐渐发现自己的不足,以编写代码为需求驱动会让我快速学会怎么用,而写下来记录下来的时候就会逐步发现很多东西还是只知其然不知其所以然,再去逐步查找资料,让我学到了更多的东西。
随着对项目理解的加深,我也在逐渐理解这个系统的运行逻辑,希望之后编写功能APP的时候不要翻车。