緊接著上一次沒做完的繼續,完善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 begin
int httpCode = http.GET();
if (httpCode > 0)
{
// httpCode will be negative on error
if (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的時候不要翻車。