Nhật ký phát triển thứ hai dựa trên Chappie-II

Tiếp tục từ lần viết trước, tôi đã hoàn thiện chức năng trang cài đặt và đồng thời cải tiến kết nối Wi-Fi và đồng bộ hóa thời gian. Bài viết này bao gồm:

  • Tối ưu hóa menu Cài đặt

  • Menu đa cấp và thanh bên

  • Bảng dữ liệu LVGL

  • Văn bản và cuộn

  • Phân tích mạng thông minh của ESP32

  • Đồng bộ hóa thời gian

Menu Cài đặt

Thật ra, đoạn mã đã truyền sai lần trước, nhưng cũng không quan trọng, vì đó chỉ là ví dụ mẫu từ nhà sản xuất và còn cách xa yêu cầu thực tế rất nhiều. Từ lần này, tôi sẽ cố gắng sửa đổi một số thói quen ghi chép, không phải dán toàn bộ mã mà phân tích từng phần, vì toàn bộ mã nguồn có sẵn trên GitHub... Các phương thức được sử dụng chính là:

  • static lv_obj_t * create_text(...);:Tạo nhãn văn bản(lv_label)
  • static lv_obj_t * create_slider(...);:Tạo thanh trượt
  • static lv_obj_t * create_switch(...);:Tạo nút bật/tắt
  • static lv_obj_t * create_WiFiInfo_table(...);:Tạo bảng hiển thị thông tin Wi-Fi
  • void set_sidebar_width(...);:Thay đổi kích thước thanh bên
  • void App_Settings_menu(void);:Tạo giao diện

Đoạn log chủ yếu ghi lại hàm cuối cùng, tham khảo ví dụ mẫu complex_menu từ nhà sản xuất và chỉnh sửa thành phiên bản sơ khai.

Menu đa cấp

Không cần nói gì về điều này, chỉ đơn giản là tạo đối tượng menu cơ bản và thiết lập màu nền và kích thước chiếm màn hình.

Đối tượng menu này là đối tượng cha cho tất cả các trang sau.

        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);

Tiếp theo là tạo các trang con cần thiết, bao gồm:

  • sub_WIFI_page : Hiển thị trạng thái kết nối Wi-Fi và thông tin Wi-Fi đang kết nối
  • sub_BLE_page : Hiển thị thông tin Bluetooth
  • Các trang còn lại đều có ý nghĩa tương tự

Ở đây ta thấy rằng mỗi trang đều không thể thiếu các khái niệm contain và section trong LVGL, contain như một chứa đựng để chứa các thành phần, trong khi section có thể chứa nhiều contain, cấu thành nên cấu trúc "đa cấp" của menu.

Trong mã có thể thấy cont = create_WiFiInfo_table và cont = create_text, họ trả về một đối tượng lv_obj_t, nhưng một là bảng, một là nhãn văn bản; cùng lúc đó sau lv_menu_separator_create(sub_about_page), cho section about chứa hai contain chứa các nhãn văn bản(label).

    lv_obj_t * cont;
    lv_obj_t * section;

    /*Tạo các trang con*/
    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, "Phiên bản 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 được sửa đổi bởi 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, "Thông tin phần mềm", LV_MENU_ITEM_BUILDER_VARIANT_1);
    lv_menu_set_load_page_event(menu, cont, sub_software_info_page);
    cont = create_text(section, NULL, "Thông tin pháp lý", LV_MENU_ITEM_BUILDER_VARIANT_1);
    lv_menu_set_load_page_event(menu, cont, sub_legal_info_page);

Tiếp theo là tạo thanh bên (sidebar), ở đây cũng phản ánh mối quan hệ giữa contain và section trong LVGL. Trong create_text sử dụng như LV_SYMBOL_WIFI、LV_SYMBOL_BLUETOOTH như vậy là các icon mặc định được cung cấp bởi LVGL.

Khi sử dụng thực tế, tôi nhận ra rằng kích thước thanh bên rất kỳ lạ, tìm kiếm trên internet thấy người khác có thanh bên rộng hoặc hẹp, của tôi thậm chí còn hẹp đến không thể nhìn thấy, do đó tôi dùng set_sidebar_width(lv_obj_t* menu, lv_coord_t width); để điều chỉnh trực tiếp kích thước thanh bên.

Lưu ý là trong các phiên bản mới nhất của LVGL, hiệu ứng scroll được kích hoạt theo mặc định khi bố cục vượt quá đối tượng cha (không biết trường hợp khác). Để cảm giác tốt hơn, tôi đã tắt hiệu ứng scroll bằng lv_obj_clear_flag.

    root_page = lv_menu_page_create(menu, "Cài đặt");
    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);

    /*Trang WIFI kế hoạch để hiển thị tình trạng kết nối Wi-Fi*/
    cont = create_text(section, LV_SYMBOL_WIFI, "Wi-Fi", LV_MENU_ITEM_BUILDER_VARIANT_1);
    lv_menu_set_load_page_event(menu, cont, sub_WIFI_page);

    /*Trang BLE kế hoạch để hiển thị tình trạng kết nối Bluetooth*/
    cont = create_text(section, LV_SYMBOL_BLUETOOTH, "Bluetooth", LV_MENU_ITEM_BUILDER_VARIANT_1);
    lv_menu_set_load_page_event(menu, cont, sub_BLE_page);

    create_text(root_page, NULL, "Khác", LV_MENU_ITEM_BUILDER_VARIANT_1);
    section = lv_menu_section_create(root_page);
    cont = create_text(section, NULL, "Về", 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);

Thực tế thay đổi kích thước thanh bên không phải là một vấn đề phức tạp, vì nó cũng chỉ là một đối tượng menu, chỉ cần thay đổi kích thước.

    /**
     * @brief Thiết lập độ rộng thanh bên 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);
    }

Bảng

Tôi vẫn còn là một người mới với LVGL, giờ chỉ học hỏi cái gì thì làm cái ấy, hiểu cái gì thì nói cái ấy, nên việc vẽ bảng là lần đầu tiên gặp phải.

Đã vẽ một bảng có 2 cột và 4 hàng, không có giá trị kỹ thuật cao.

    /**
     * @brief Lấy thông tin hiển thị Wi-Fi
     * @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);
        /*Đổ vào cột đầu tiên*/
        lv_table_set_cell_value(table, 0, 0, "Loại");
        lv_table_set_cell_value(table, 1, 0, "Trạng thái");
        lv_table_set_cell_value(table, 2, 0, "SSID");
        lv_table_set_cell_value(table, 3, 0, "IP");

        /*Đổ vào cột thứ hai*/
        lv_table_set_cell_value(table, 0, 1, "Xâu ký tự");
        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;
    }

Điểm đáng chú ý duy nhất là bảng này sử dụng kích thước màn hình toàn diện, trong khi trang này chỉ cần sử dụng khoảng một nửa màn hình, vì vậy ban đầu chỉ có thể nhìn thấy bên trái của bảng, khi cuộn thì nó lại không hiện lên (thôi). Sau khi biết dùng lv_obj_set_size(..); đặt kích thước cố định, nếu kích thước không đủ để bố cục mở rộng, nó sẽ kích hoạt scroll.

Hàm công nghệ Wi-Fi

Mã nguồn gốc của tác giả đã thực hiện đồng bộ hóa Wi-Fi và thời gian trong hàm onCreate của ứng dụng settings, đó cũng là lý do tại sao tôi bắt đầu thao tác với những chức năng này. Nút Wi-Fi trên màn hình thứ -1 không có ích gì cho chức năng Wi-Fi, vì vậy tôi đã sửa đổi phần mã để nút Wi-Fi có thể kích hoạt quá trình phân tích mạng của ESP32, tách rời chức năng Wi-Fi và đồng bộ hóa thời gian khỏi việc tạo ứng dụng settings.

Xem xét rằng nếu đi vào mạng, mọi thứ khác đều phải đợi thực hiện sau đó thật sự không tiện lợi, vì vậy tôi đã sử dụng FreeRTOS để chạy nhiều nhiệm vụ song song.

static void xTaskOne(void *xTask1){ 
        while (1)
        {
            uint8_t i = 0;

            WiFi.mode(WIFI_STA);
            UI_LOG("[Wi-Fi] Mode Wi-Fi : STA\n");
            UI_LOG("[Wi-Fi] Trying to connect\n");
            WiFi.begin();
            WiFi.beginSmartConfig();
            UI_LOG("[Wi-Fi] Waiting for SmartConfig...\n");
            while (!WiFi.smartConfigDone()) { vTaskDelay(200); }

            UI_LOG("[Wi-Fi] SmartConfig received, connecting Wi-Fi...\n");
            while (WiFi.status() != WL_CONNECTED) { vTaskDelay(200); }

            UI_LOG("[Wi-Fi] Connected. IP: %s\n", WiFi.localIP().toString().c_str());

            vTaskDelete(NULL);
        }
    }
void App_Launcher::WiFi_config()
    {
        UI_LOG("[Wi-Fi] Configuring Wi-Fi start\n");
        xTaskCreatePinnedToCore(xTaskOne, "TaskOne", 4096*10, NULL, 1, NULL, 0);
        UI_LOG("[Wi-Fi] Configuring Wi-Fi done\n");
    }

Mã nguồn phân tích mạng trên mạng có rất nhiều, ví dụ như tạo nhóm sự kiện, thiết lập trạng thái gọi lại, viết hàm gọi lại. Nhưng vì kỹ năng hạn chế, tôi chỉ biết sử dụng cách đơn giản nhất là WiFi.begin(); sau đó WiFi.beginSmartConfig(); và sử dụng ứng dụng open source trên điện thoại di động - EspTouch để kết nối mạng nhanh chóng.

Trong ESP32, Wi-Fi có ba chế độ chính:

  • Chế độ STA: STA là viết tắt của Station, giống như một thiết bị kết nối không dây, STA không chấp nhận kết nối không dây, nó có thể kết nối đến AP, nói cách khác là trạng thái kết nối với điểm truy cập Wi-Fi giống như khi kết nối với điểm truy cập Wi-Fi trên điện thoại di động, có thể kết nối đến các điểm truy cập khác.
  • Chế độ AP: AP, viết tắt của Access Point, là một điểm tạo ra mạng không dây, là nút trung tâm của mạng. Thông thường, router gia đình hoặc văn phòng sử dụng là một AP.
  • Chế độ AP, STA hỗ trợ lẫn nhau: có thể kết nối đến người khác và người khác cũng có thể kết nối đến mình.

Trong khi đó, smartconfig ít nhất cần chế độ STA (có lẽ).

Trong khi đó, chạy nhiều nhiệm vụ song song bằng FreeRTOS nhờ dòng lệnh xTaskCreatePinnedToCore(xTaskOne, "TaskOne", 4096*10, NULL, 1, NULL, 0);. Cần lưu ý rằng tham số cuối cùng quyết định nhiệm vụ chạy ở core nào. FreeRTOS trên ESP32 được thiết kế để chạy trên một core. Nhưng ESP32 là một chip dual-core, bao gồm Protocol CPU (gọi là CPU 0 hoặc PRO_CPU) và Application CPU (gọi là CPU 1 hoặc APP_CPU). Hai core này thực tế là giống nhau và chia sẻ cùng bộ nhớ. Điều này cũng cung cấp cơ sở vật lý cho việc các nhiệm vụ chạy trên hai core chuyển đổi.

Đừng quên, luôn nhớ giải phóng nhiệm vụvTaskDelete().

Đồng bộ hóa thời gian qua mạng

Tôi原本打算通过SNTP thực hiện đồng bộ hóa thời gian, nhưng tôi phát hiện rằng cuộc gọi API thực sự không phản hồi gì, và cách đồng bộ hóa thời gian của tác giả nếu không đặt trong hàm onCreate sẽ khiến hệ thống khởi động lại. Dựa trên nguyên tắc "nếu chạy được đừng thay đổi", tôi quyết định dành thời gian sau để thử nghiệm sử dụng SNTP để thực hiện đồng bộ hóa thời gian.

    void App_Settings_onCreate()
    {

        UI_LOG("[%s] onCreate1\n", App_Settings_appName().c_str());
        UI_LOG("[Wi-Fi] Attempting to synchronize time\n");
        http.begin("http://quan.suning.com/getSysTime.do"); //Bắt đầu HTTP
        int httpCode = http.GET();

        if (httpCode > 0)
        {
            // httpCode sẽ là âm nếu có lỗi
            if (httpCode == HTTP_CODE_OK) // Nhận được nội dung đúng
            {
            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;//Phản ánh thời gian xử lý dữ liệu            
            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 for Applauncher restart\n");
            }
        }
        http.end();
        UI_LOG("[HTTP] Disconnected\n");
        device->Rtc.setDate(&rtc_date);
        device->Rtc.setTime(&rtc_time);
        UI_LOG("[SYS] Time synchronization done\n");
        App_Settings_menu();

    }

Đây là cách lấy một chuỗi {"sysTime2":"2024-11-05 02:16:45","sysTime1":"20241105021645"} từ quan.suning.com/getSysTime.do để lấy thông số thời gian hiện tại, sau đó đọc từng phần và ghi vào RTC.

Cảm giác SNTP vẫn đẹp mắt hơn

Tóm tắt

Khi viết ban đầu, tôi sẽ dần nhận ra những thiếu sót của mình. Viết mã theo nhu cầu sẽ giúp tôi học cách sử dụng nhanh chóng, nhưng khi viết lại để ghi chép lại thì tôi sẽ dần nhận ra rằng còn rất nhiều thứ mà chỉ biết dùng mà chưa hiểu rõ nguyên nhân, sau đó tìm hiểu thêm sẽ giúp tôi học được nhiều kiến thức hơn nữa.

Theo thời gian hiểu sâu hơn về dự án, tôi cũng dần hiểu rõ hơn về quy trình vận hành của hệ thống, hy vọng sau này khi viết ứng dụng chức năng sẽ không gặp vấn đề gì.

Thẻ: ESP32 lvgl SmartConfig Networking Time Synchronization

Đăng vào ngày 8 tháng 10 lúc 19:31