diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f59ce18..9d81e9b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,7 +87,9 @@ if(EMULATOR_BUILD) else() list(APPEND SRC_FILES ${SRC_FILES_PLAYER}) endif() -add_executable(${PROJECT_NAME} ${SRC_FILES}) +add_executable(${PROJECT_NAME} ${SRC_FILES} + src/util/wifi_status.c +) target_include_directories(${PROJECT_NAME} PRIVATE src/ diff --git a/src/core/settings.c b/src/core/settings.c index 04985b01..c7aa9b1c 100644 --- a/src/core/settings.c +++ b/src/core/settings.c @@ -178,6 +178,7 @@ const setting_t g_setting_defaults = { }, }, }, + // En la inicialización de g_setting_defaults en settings.c .clock = { .year = 2023, .month = 3, @@ -186,6 +187,7 @@ const setting_t g_setting_defaults = { .min = 30, .sec = 30, .format = 0, + .utc_offset = 0, // UTC±00:00 por defecto }, // Refer to `page_input.c`'s arrays `rollerFunctionPointers` and `btnFunctionPointers` .inputs = { @@ -321,6 +323,8 @@ void settings_init(void) { int file_version = ini_getl("settings", "file_version", SETTINGS_INI_VERSION_UNKNOWN, SETTING_INI); if (file_version != SETTING_INI_VERSION) settings_reset(); + + g_setting.clock.auto_sync = ini_getl("clock", "auto_sync", 0, SETTING_INI); } void settings_load(void) { @@ -468,6 +472,9 @@ void settings_load(void) { g_setting.language.lang = ini_getl("language", "lang", g_setting_defaults.language.lang, SETTING_INI); } + // UTC + g_setting.clock.utc_offset = ini_getl("clock", "utc_offset", 0, SETTING_INI); // Por defecto UTC±00:00 + // Check if (fs_file_exists(SELF_TEST_FILE)) { unlink(SELF_TEST_FILE); diff --git a/src/core/settings.h b/src/core/settings.h index 27da5f5c..6ad2b752 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -198,6 +198,8 @@ typedef struct { int min; int sec; int format; + int utc_offset; + int auto_sync; // 0: Disabled, 1: Enabled } setting_clock_t; #define WIFI_RF_CHANNELS 14 // World Channels diff --git a/src/ui/page_clock.c b/src/ui/page_clock.c index 8d334047..595318ad 100644 --- a/src/ui/page_clock.c +++ b/src/ui/page_clock.c @@ -14,6 +14,8 @@ #include "ui/page_common.h" #include "ui/ui_attribute.h" #include "ui/ui_style.h" +#include "util/ntp_client.h" +#include "ui/page_wifi.h" /** * Various data types used to @@ -45,6 +47,9 @@ typedef enum { ITEM_SECOND, ITEM_FORMAT, ITEM_SET_CLOCK, + ITEM_UTC, + ITEM_SYNC_NTP, + ITEM_AUTO_SYNC, // New item ITEM_BACK, ITEM_LIST_TOTAL @@ -66,7 +71,7 @@ typedef struct { */ static const int MAX_YEARS_DROPDOWN = 300; // 2023 + 300 == 2323 static lv_coord_t col_dsc[] = {160, 160, 160, 160, 160, 160, LV_GRID_TEMPLATE_LAST}; -static lv_coord_t row_dsc[] = {60, 60, 60, 60, 60, 15, 10, 60, 60, 60, LV_GRID_TEMPLATE_LAST}; +static lv_coord_t row_dsc[] = {60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, LV_GRID_TEMPLATE_LAST}; static item_t page_clock_items[ITEM_LIST_TOTAL] = {0}; static int page_clock_item_selected = ITEM_YEAR; static int page_clock_item_focused = 0; @@ -79,6 +84,90 @@ static int page_clock_set_clock_confirm = 0; static int page_clock_is_dirty = 0; static struct rtc_date page_clock_rtc_date = {0}; +// UTC timezone options +static char* utc_options[] = { + "UTC-12:00", "UTC-11:00", "UTC-10:00", "UTC-09:00", "UTC-08:00", + "UTC-07:00", "UTC-06:00", "UTC-05:00", "UTC-04:00", "UTC-03:00", + "UTC-02:00", "UTC-01:00", "UTC 00:00", "UTC+01:00", "UTC+02:00", + "UTC+03:00", "UTC+04:00", "UTC+05:00", "UTC+06:00", "UTC+07:00", + "UTC+08:00", "UTC+09:00", "UTC+10:00", "UTC+11:00", "UTC+12:00", + "UTC+13:00", "UTC+14:00" +}; + +// Offset values in seconds corresponding to the options +static const int utc_seconds[] = { +-43200, // UTC−12:00 +-39600, // UTC−11:00 +-36000, // UTC−10:00 +-32400, // UTC−09:00 +-28800, // UTC−08:00 +-25200, // UTC−07:00 +-21600, // UTC−06:00 +-18000, // UTC−05:00 +-14400, // UTC−04:00 +-10800, // UTC−03:00 + -7200, // UTC−02:00 + -3600, // UTC−01:00 + 0, // UTC±00:00 + 3600, // UTC+01:00 + 7200, // UTC+02:00 + 10800, // UTC+03:00 + 14400, // UTC+04:00 + 18000, // UTC+05:00 + 21600, // UTC+06:00 + 25200, // UTC+07:00 + 28800, // UTC+08:00 + 32400, // UTC+09:00 + 36000, // UTC+10:00 + 39600, // UTC+11:00 + 43200, // UTC+12:00 + 46800, // UTC+13:00 + 50400 // UTC+14:00 +}; + +// Convert seconds offset to array index +int utc_offset_to_index(int offset_seconds) { + // Find closest offset + int index = 12; // Default UTC±00:00 + int min_diff = abs(offset_seconds); + + for (int i = 0; i < sizeof(utc_seconds)/sizeof(utc_seconds[0]); i++) { + int diff = abs(offset_seconds - utc_seconds[i]); + if (diff < min_diff) { + min_diff = diff; + index = i; + } + } + + return index; +} + +// Convert index to seconds offset +int index_to_utc_offset(int index) { + if (index >= 0 && index < sizeof(utc_seconds)/sizeof(utc_seconds[0])) { + return utc_seconds[index]; + } + + return 0; // Default UTC±00:00 +} + +/** + * Callback para sincronización NTP asíncrona + */ +static void ntp_sync_callback(int result, void* user_data) { + if (result == 0) { + // Éxito, actualizar la interfaz en el hilo principal + // Usamos lv_async_call o enviamos un mensaje al hilo principal + // Esto dependerá de la implementación específica del sistema + + // En este caso, el UI se actualizará automáticamente en el próximo ciclo + // porque refrescamos la fecha/hora cada 250ms + } + + // El mensaje de éxito/error se mostrará directamente en la interfaz + // cuando el usuario inicia la sincronización +} + /** * Acquire index from selected dropdown option as a string. */ @@ -243,6 +332,13 @@ static void page_clock_set_clock_reset() { page_clock_set_clock_confirm = 0; } +/** + * Reset the 'Sync from Internet' text back to its initial state. + */ +static void page_clock_sync_reset_cb(struct _lv_timer_t *timer) { + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, _lang("Sync from Internet")); +} + /** * Determine if the type of object selected is a lv_obj_t. */ @@ -254,6 +350,7 @@ static int page_clock_selected_item_is_obj(int selected_item) { case ITEM_HOUR: case ITEM_MINUTE: case ITEM_SECOND: + case ITEM_UTC: return 1; default: return 0; @@ -306,14 +403,22 @@ static void page_clock_set_clock_pending_cb(struct _lv_timer_t *timer) { */ static void page_clock_refresh_ui_timer_cb(struct _lv_timer_t *timer) { page_clock_refresh_datetime(); + + // Actualizar estado del botón Sync from Internet + bool wifi_connected = page_wifi_is_sta_connected(); + if (wifi_connected) { + lv_obj_clear_state(page_clock_items[ITEM_SYNC_NTP].data.obj, LV_STATE_DISABLED); + lv_obj_set_style_text_color(page_clock_items[ITEM_SYNC_NTP].data.obj, lv_color_make(255, 255, 255), 0); + } else { + lv_obj_add_state(page_clock_items[ITEM_SYNC_NTP].data.obj, LV_STATE_DISABLED); + lv_obj_set_style_text_color(page_clock_items[ITEM_SYNC_NTP].data.obj, lv_color_make(128, 128, 128), 0); + } } /** * Callback invoked once `Set Clock` is triggered and confirmed via the menu. */ static void page_clock_set_clock_timer_cb(struct _lv_timer_t *timer) { - char text[512]; - page_clock_build_date_from_selected(&page_clock_rtc_date); g_setting.clock.year = page_clock_rtc_date.year; @@ -330,7 +435,6 @@ static void page_clock_set_clock_timer_cb(struct _lv_timer_t *timer) { ini_putl("clock", "hour", g_setting.clock.hour, SETTING_INI); ini_putl("clock", "min", g_setting.clock.min, SETTING_INI); ini_putl("clock", "sec", g_setting.clock.sec, SETTING_INI); - ini_putl("clock", "sec", g_setting.clock.sec, SETTING_INI); rtc_set_clock(&page_clock_rtc_date); page_clock_refresh_datetime(); @@ -342,25 +446,35 @@ static void page_clock_set_clock_timer_cb(struct _lv_timer_t *timer) { * Main allocation routine for this page. */ static lv_obj_t *page_clock_create(lv_obj_t *parent, panel_arr_t *arr) { - char buf[256]; + char buf[288]; + int contentHeight = 0; + for (size_t i = 0; i < (ARRAY_SIZE(row_dsc) - 1); i++) { + contentHeight += row_dsc[i]; + } + contentHeight += row_dsc[1]; + int contentWidth = 0; + for (size_t i = 0; i < (ARRAY_SIZE(col_dsc) - 1); i++) { + contentWidth += col_dsc[i]; + } + rtc_get_clock(&page_clock_rtc_date); page_clock_build_options_from_date(&page_clock_rtc_date); lv_obj_t *page = lv_menu_page_create(parent, NULL); lv_obj_clear_flag(page, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(page, 1053, 900); + lv_obj_set_size(page, contentWidth + 93, contentHeight + 300); lv_obj_add_style(page, &style_subpage, LV_PART_MAIN); lv_obj_set_style_pad_top(page, 94, 0); lv_obj_t *section = lv_menu_section_create(page); lv_obj_add_style(section, &style_submenu, LV_PART_MAIN); - lv_obj_set_size(section, 1053, 894); + lv_obj_set_size(section, contentWidth + 93, contentHeight + 294); snprintf(buf, sizeof(buf), "%s:", _lang("Clock")); create_text(NULL, section, false, buf, LV_MENU_ITEM_BUILDER_VARIANT_2); lv_obj_t *cont = lv_obj_create(section); - lv_obj_set_size(cont, 1280, 800); + lv_obj_set_size(cont, contentWidth, contentHeight); lv_obj_set_pos(cont, 0, 0); lv_obj_set_layout(cont, LV_LAYOUT_GRID); lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE); @@ -371,30 +485,60 @@ static lv_obj_t *page_clock_create(lv_obj_t *parent, panel_arr_t *arr) { create_select_item(arr, cont); - // Current date/time or last saved setting. + // Row 0: Year/Month/Day dropdowns page_clock_create_dropdown(cont, ITEM_YEAR, page_clock_rtc_date.year, 1, 0); page_clock_create_dropdown(cont, ITEM_MONTH, page_clock_rtc_date.month, 2, 0); page_clock_create_dropdown(cont, ITEM_DAY, page_clock_rtc_date.day, 3, 0); + + // Row 1: Hour/Minute/Second dropdowns page_clock_create_dropdown(cont, ITEM_HOUR, page_clock_rtc_date.hour, 1, 1); page_clock_create_dropdown(cont, ITEM_MINUTE, page_clock_rtc_date.min, 2, 1); page_clock_create_dropdown(cont, ITEM_SECOND, page_clock_rtc_date.sec, 3, 1); + // Row 2: Format selection (AM/PM vs 24 Hour) snprintf(buf, sizeof(buf), "%s/%s", _lang("AM"), _lang("PM")); create_btn_group_item(&page_clock_items[ITEM_FORMAT].data.btn, cont, 2, _lang("Format"), buf, _lang("24 Hour"), "", "", 2); page_clock_items[ITEM_FORMAT].type = ITEM_TYPE_BTN; page_clock_items[ITEM_FORMAT].panel = arr->panel[2]; btn_group_set_sel(&page_clock_items[ITEM_FORMAT].data.btn, g_setting.clock.format); + // Row 3: Set Clock page_clock_items[ITEM_SET_CLOCK].data.obj = create_label_item(cont, _lang("Set Clock"), 1, 3, 3); page_clock_items[ITEM_SET_CLOCK].type = ITEM_TYPE_BTN; page_clock_items[ITEM_SET_CLOCK].panel = arr->panel[3]; + // Row 4: Time Zone + create_label_item(cont, _lang("Time Zone"), 1, 4, 1); + lv_obj_t* utc_dropdown = create_dropdown_item(cont,"",2,4,200,row_dsc[4],2,10,LV_GRID_ALIGN_START,&lv_font_montserrat_26); + lv_dropdown_clear_options(utc_dropdown); + for (int i = 0; i < sizeof(utc_options)/sizeof(utc_options[0]); i++) { + lv_dropdown_add_option(utc_dropdown, utc_options[i], LV_DROPDOWN_POS_LAST); + } + lv_dropdown_set_selected(utc_dropdown, utc_offset_to_index(g_setting.clock.utc_offset)); + page_clock_items[ITEM_UTC].data.obj = utc_dropdown; + page_clock_items[ITEM_UTC].type = ITEM_TYPE_OBJ; + page_clock_items[ITEM_UTC].panel = arr->panel[4]; + + // Row 5: Sync from Internet + page_clock_items[ITEM_SYNC_NTP].data.obj = create_label_item(cont, _lang("Sync from Internet"), 1, 5, 3); + page_clock_items[ITEM_SYNC_NTP].type = ITEM_TYPE_BTN; + page_clock_items[ITEM_SYNC_NTP].panel = arr->panel[5]; + + // Row 6: Auto Sync + create_btn_group_item(&page_clock_items[ITEM_AUTO_SYNC].data.btn, cont, 2, _lang("Auto Sync"), + _lang("Enable"), _lang("Disable"), "", "", 6); + page_clock_items[ITEM_AUTO_SYNC].type = ITEM_TYPE_BTN; + page_clock_items[ITEM_AUTO_SYNC].panel = arr->panel[6]; + btn_group_set_sel(&page_clock_items[ITEM_AUTO_SYNC].data.btn, !g_setting.clock.auto_sync); + + // Row 7: Back snprintf(buf, sizeof(buf), "< %s", _lang("Back")); - page_clock_items[ITEM_BACK].data.obj = create_label_item(cont, buf, 1, 4, 1); + page_clock_items[ITEM_BACK].data.obj = create_label_item(cont, buf, 1, 7, 1); page_clock_items[ITEM_BACK].type = ITEM_TYPE_BTN; - page_clock_items[ITEM_BACK].panel = arr->panel[4]; + page_clock_items[ITEM_BACK].panel = arr->panel[7]; - page_clock_create_datetime_item(cont, 5); + // Row 8: Current Date/Time Display + page_clock_create_datetime_item(cont, 8); if (rtc_has_battery() != 0) { lv_obj_t *note = lv_label_create(cont); @@ -405,7 +549,7 @@ static lv_obj_t *page_clock_create(lv_obj_t *parent, panel_arr_t *arr) { lv_obj_set_style_text_color(note, lv_color_make(255, 255, 255), 0); lv_obj_set_style_pad_top(note, 12, 0); lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP); - lv_obj_set_grid_cell(note, LV_GRID_ALIGN_START, 1, 4, LV_GRID_ALIGN_START, 6, 2); + lv_obj_set_grid_cell(note, LV_GRID_ALIGN_START, 1, 4, LV_GRID_ALIGN_START, 9, 2); } page_clock_clear_datetime(); @@ -505,6 +649,59 @@ static void page_clock_on_roller(uint8_t key) { /** * Main input selection routine for this page. */ +static void ntp_sync_check_timer_cb(lv_timer_t* timer) { + if (!clock_is_syncing_from_ntp()) { + clock_sync_status_t sync_status = clock_get_last_sync_status(); + + switch(sync_status) { + case CLOCK_SYNC_SUCCESS: { + // Get updated time after successful sync + struct rtc_date date; + rtc_get_clock(&date); + + // Update screen date + page_clock_rtc_date = date; + page_clock_build_options_from_date(&page_clock_rtc_date); + page_clock_refresh_datetime(); + + // Save to settings + g_setting.clock.year = date.year; + g_setting.clock.month = date.month; + g_setting.clock.day = date.day; + g_setting.clock.hour = date.hour; + g_setting.clock.min = date.min; + g_setting.clock.sec = date.sec; + + // Update configuration file + ini_putl("clock", "year", g_setting.clock.year, SETTING_INI); + ini_putl("clock", "month", g_setting.clock.month, SETTING_INI); + ini_putl("clock", "day", g_setting.clock.day, SETTING_INI); + ini_putl("clock", "hour", g_setting.clock.hour, SETTING_INI); + ini_putl("clock", "min", g_setting.clock.min, SETTING_INI); + ini_putl("clock", "sec", g_setting.clock.sec, SETTING_INI); + + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, "#00FF00 Sync Complete#"); + break; + } + + case CLOCK_SYNC_FAILED: + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, "#FF0000 Sync Failed#"); + break; + + default: + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, "#FF0000 Sync Error#"); + break; + } + + // Create timer to restore text + lv_timer_t *reset_timer = lv_timer_create(page_clock_sync_reset_cb, 2000, NULL); + lv_timer_set_repeat_count(reset_timer, 1); + + // Delete this timer + lv_timer_del(timer); + } +} + static void page_clock_on_click(uint8_t key, int sel) { char buf[128]; // Ignore commands until timer has expired before allowing user to proceed. @@ -518,6 +715,40 @@ static void page_clock_on_click(uint8_t key, int sel) { g_setting.clock.format = btn_group_get_sel(&page_clock_items[ITEM_FORMAT].data.btn); ini_putl("clock", "format", g_setting.clock.format, SETTING_INI); break; + case ITEM_UTC: + if (!page_clock_item_focused) { + page_clock_items[ITEM_UTC].last_option = + lv_dropdown_get_selected(page_clock_items[ITEM_UTC].data.obj); + + lv_obj_t *list = lv_dropdown_get_list(page_clock_items[ITEM_UTC].data.obj); + lv_dropdown_open(page_clock_items[ITEM_UTC].data.obj); + lv_obj_add_style(list, &style_dropdown, LV_PART_MAIN); + lv_obj_set_style_text_color(list, lv_color_make(0, 0, 0), LV_PART_SELECTED | LV_STATE_CHECKED); + page_clock_item_focused = 1; + } else { + lv_event_send(page_clock_items[ITEM_UTC].data.obj, LV_EVENT_RELEASED, NULL); + page_clock_item_focused = 0; + int option = lv_dropdown_get_selected(page_clock_items[ITEM_UTC].data.obj); + + if (page_clock_items[ITEM_UTC].last_option != option) { + page_clock_items[ITEM_UTC].last_option = option; + g_setting.clock.utc_offset = index_to_utc_offset(option); + ini_putl("clock", "utc_offset", g_setting.clock.utc_offset, SETTING_INI); + page_clock_is_dirty = 1; + + if (!page_clock_set_clock_pending_timer) { + page_clock_set_clock_pending_timer = lv_timer_create(page_clock_set_clock_pending_cb, 50, NULL); + lv_timer_set_repeat_count(page_clock_set_clock_pending_timer, -1); + } + } + } + break; + case ITEM_AUTO_SYNC: + page_clock_is_dirty = 1; + btn_group_toggle_sel(&page_clock_items[ITEM_AUTO_SYNC].data.btn); + g_setting.clock.auto_sync = !btn_group_get_sel(&page_clock_items[ITEM_AUTO_SYNC].data.btn); + ini_putl("clock", "auto_sync", g_setting.clock.auto_sync, SETTING_INI); + break; case ITEM_SET_CLOCK: if (page_clock_set_clock_confirm) { snprintf(buf, sizeof(buf), "#FF0000 %s %s...#", _lang("Updating"), _lang("Clock")); @@ -531,6 +762,35 @@ static void page_clock_on_click(uint8_t key, int sel) { page_clock_set_clock_confirm = 1; } break; + case ITEM_SYNC_NTP: + // Verificar si el WiFi está habilitado y conectado + if (!page_wifi_is_sta_connected()) { + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, "#FF0000 WiFi Client Mode Required#"); + lv_timer_t *reset_timer = lv_timer_create(page_clock_sync_reset_cb, 2000, NULL); + lv_timer_set_repeat_count(reset_timer, 1); + return; + } + + if (page_clock_set_clock_confirm) { + snprintf(buf, sizeof(buf), "#FF0000 %s %s...#", _lang("Syncing"), _lang("Clock")); + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, buf); + + if (clock_sync_from_ntp_async(ntp_sync_callback, NULL) == 0) { + lv_timer_t *check_timer = lv_timer_create(ntp_sync_check_timer_cb, 500, NULL); + lv_timer_set_repeat_count(check_timer, 20); // Timeout after 10 seconds + } else { + snprintf(buf, sizeof(buf), "#FF0000 %s#", _lang("Sync Failed")); + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, buf); + lv_timer_t *reset_timer = lv_timer_create(page_clock_sync_reset_cb, 2000, NULL); + lv_timer_set_repeat_count(reset_timer, 1); + } + page_clock_set_clock_confirm = 0; + } else { + snprintf(buf, sizeof(buf), "#FFFF00 %s...#", _lang("Click to confirm or Scroll to cancel")); + lv_label_set_text(page_clock_items[ITEM_SYNC_NTP].data.obj, buf); + page_clock_set_clock_confirm = 1; + } + break; case ITEM_BACK: submenu_exit(); break; @@ -570,21 +830,21 @@ static void page_clock_on_click(uint8_t key, int sel) { } /** - * Main Menu page data structure, notice max is set to zero - * in order to allow us to override default user input logic. - */ +* Main Menu page data structure, notice max is set to zero +* in order to allow us to override default user input logic. +*/ page_pack_t pp_clock = { - .p_arr = { - .cur = 0, - .max = 0, - }, - .name = "Clock", - .create = page_clock_create, - .enter = page_clock_enter, - .exit = page_clock_exit, - .on_created = NULL, - .on_update = NULL, - .on_roller = page_clock_on_roller, - .on_click = page_clock_on_click, - .on_right_button = NULL, -}; + .p_arr = { + .cur = 0, + .max = 0, + }, + .name = "Clock", + .create = page_clock_create, + .enter = page_clock_enter, + .exit = page_clock_exit, + .on_created = NULL, + .on_update = NULL, + .on_roller = page_clock_on_roller, + .on_click = page_clock_on_click, + .on_right_button = NULL, +}; \ No newline at end of file diff --git a/src/ui/page_clock.h b/src/ui/page_clock.h index e9d83799..9578669b 100644 --- a/src/ui/page_clock.h +++ b/src/ui/page_clock.h @@ -5,9 +5,10 @@ extern "C" { #endif #include "ui/ui_main_menu.h" +#include "util/ntp_client.h" // Incluir la cabecera en lugar de declarar la función extern page_pack_t pp_clock; #ifdef __cplusplus } -#endif +#endif \ No newline at end of file diff --git a/src/ui/page_wifi.c b/src/ui/page_wifi.c index c268e361..ed928c7f 100644 --- a/src/ui/page_wifi.c +++ b/src/ui/page_wifi.c @@ -22,6 +22,9 @@ #include "ui/ui_style.h" #include "util/filesystem.h" #include "util/system.h" +#include "driver/rtc.h" +#include "util/ntp_client.h" +#include "util/wifi_status.h" /** * Types @@ -337,6 +340,16 @@ static void page_wifi_update_settings() { system_exec("dropbear"); } } + + // Check if we should sync time when connected as client + if (g_setting.wifi.mode == WIFI_MODE_STA && g_setting.clock.auto_sync) { + WIFI_STA_CONNECT_STATUS_S status; + if (wifi_sta_get_connect_status("wlan0", &status) == 0 && + status.state == WIFI_STA_STATUS_CONNECTED) { + // Trigger NTP sync + clock_sync_from_ntp(); + } + } } /** @@ -815,13 +828,60 @@ static void page_wifi_exit() { page_wifi.item_select = 0; } +/** + * Callback function for background NTP synchronization + */ +static void wifi_ntp_sync_callback(int result, void* user_data) { + switch (result) { + case NTP_SUCCESS: + LOGI("Background NTP time sync successful"); + // Settings are already updated inside ntp_sync_thread + break; + + case NTP_ERR_CONNECT: + LOGE("Background NTP time sync failed - Connection error"); + break; + + case NTP_ERR_TIMEOUT: + LOGE("Background NTP time sync failed - Timeout"); + break; + + case NTP_ERR_INVALID: + LOGE("Background NTP time sync failed - Invalid data received"); + break; + + default: + LOGE("Background NTP time sync failed with error: %d", result); + break; + } +} + /** * Invoked periodically. */ static void page_wifi_on_update(uint32_t delta_ms) { static uint32_t elapsed = -1; + static bool was_connected = false; + bool is_connected = false; + + if (g_setting.wifi.enable && g_setting.wifi.mode == WIFI_MODE_STA) { + const char *current_ip = page_wifi_get_real_address(); + is_connected = (current_ip != NULL); + + if (is_connected && !was_connected) { + LOGI("WiFi client connection established"); + + if (!clock_is_syncing_from_ntp() && + clock_get_last_sync_status() != NTP_SUCCESS) { + LOGI("Starting background NTP sync after connection established"); + clock_sync_from_ntp_async(wifi_ntp_sync_callback, NULL); + } + } + + was_connected = is_connected; + } - // Check immediately after running, then every 5 minutes. + // Check immediately after executing, then every 5 minutes for updates. if (g_setting.wifi.enable && (elapsed == -1 || (elapsed += delta_ms) > 300000)) { switch (g_setting.wifi.mode) { case WIFI_MODE_STA: @@ -836,7 +896,8 @@ static void page_wifi_on_update(uint32_t delta_ms) { elapsed = 0; } -} + + } /** * Main navigation routine for this page. @@ -1221,3 +1282,11 @@ void page_wifi_get_statusbar_text(char *buffer, int size) { } } } + +bool page_wifi_is_sta_connected(void) { + WIFI_STA_CONNECT_STATUS_S connect_status; + if (wifi_sta_get_connect_status("wlan0", &connect_status) == 0) { + return connect_status.state == WIFI_STA_STATUS_CONNECTED; + } + return false; +} diff --git a/src/ui/page_wifi.h b/src/ui/page_wifi.h index 69cad202..330ecdce 100644 --- a/src/ui/page_wifi.h +++ b/src/ui/page_wifi.h @@ -9,7 +9,17 @@ extern "C" { extern page_pack_t pp_wifi; extern void page_wifi_get_statusbar_text(char *buffer, int size); +extern bool page_wifi_is_sta_connected(void); // Nueva función #ifdef __cplusplus } #endif + +#ifndef PAGE_WIFI_H +#define PAGE_WIFI_H + +#include + +bool page_wifi_is_sta_connected(void); + +#endif // PAGE_WIFI_H diff --git a/src/util/ntp_client.c b/src/util/ntp_client.c new file mode 100644 index 00000000..e0a9bb1b --- /dev/null +++ b/src/util/ntp_client.c @@ -0,0 +1,487 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "driver/rtc.h" +#include "util/system.h" +#include "core/settings.h" +#include "ui/page_clock.h" +#include "util/ntp_client.h" + +#define NTP_TIMESTAMP_DELTA 2208988800ull // Seconds between 1900 (NTP) and 1970 (epoch) +#define NTP_PORT 123 +#define NTP_TIMEOUT_SEC 3 // Timeout reduced to 3 seconds + +// NTP synchronization states +typedef enum { + NTP_SYNC_IDLE = 0, + NTP_SYNC_IN_PROGRESS, + NTP_SYNC_SUCCESS, + NTP_SYNC_FAILED +} ntp_sync_state_t; + +// NTP packet structure according to RFC 5905 +typedef struct { + uint8_t li_vn_mode; /* leap indicator, version and mode */ + uint8_t stratum; /* stratum level */ + uint8_t poll; /* poll interval */ + uint8_t precision; /* precision */ + uint32_t rootdelay; /* root delay */ + uint32_t rootdispersion; /* root dispersion */ + uint32_t refid; /* reference ID */ + uint32_t refts_sec; /* reference timestamp seconds */ + uint32_t refts_frac; /* reference timestamp fraction */ + uint32_t origts_sec; /* originate timestamp seconds */ + uint32_t origts_frac; /* originate timestamp fraction */ + uint32_t recvts_sec; /* receive timestamp seconds */ + uint32_t recvts_frac; /* receive timestamp fraction */ + uint32_t xmitts_sec; /* transmit timestamp seconds */ + uint32_t xmitts_frac; /* transmit timestamp fraction */ +} ntp_packet_t; + +// Callback structure +typedef struct { + ntp_callback_t callback_fn; + void* user_data; +} ntp_callback_data_t; + +// Global variables +static volatile ntp_sync_state_t g_ntp_sync_state = NTP_SYNC_IDLE; +static pthread_mutex_t g_ntp_mutex = PTHREAD_MUTEX_INITIALIZER; +static char* g_ntp_servers[] = { + "pool.ntp.org", + "time.google.com", + "time.cloudflare.com", + "time.apple.com", + "time.windows.com" +}; +static const int g_ntp_server_count = sizeof(g_ntp_servers) / sizeof(g_ntp_servers[0]); + +// List of static IP addresses as backup +static char* g_ntp_fallback_ips[] = { + "162.159.200.1", // time.cloudflare.com + "216.239.35.4", // time.google.com + "17.253.114.125", // time.apple.com + "13.65.245.138" // time.windows.com +}; +static const int g_ntp_fallback_count = sizeof(g_ntp_fallback_ips) / sizeof(g_ntp_fallback_ips[0]); + +// Function to convert NTP time to unix time +static time_t ntp_time_to_unix_time(uint32_t ntp_time) { + return (time_t)(ntp_time - NTP_TIMESTAMP_DELTA); +} + +// Function to set non-blocking socket +static int set_socket_nonblocking(int sockfd) { + int flags = fcntl(sockfd, F_GETFL, 0); + if (flags == -1) { + return -1; + } + return fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); +} + +// Function to attempt resolving and connecting to an NTP server +static int connect_to_ntp_server(const char* server, struct sockaddr_in* serv_addr) { + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sockfd < 0) { + LOGE("Error creating socket for NTP"); + return -1; + } + + // Set timeout + struct timeval tv; + tv.tv_sec = NTP_TIMEOUT_SEC; + tv.tv_usec = 0; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 || + setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { + LOGE("Error setting socket timeout"); + close(sockfd); + return -1; + } + + // Attempt to resolve server name + struct hostent *host_entry = gethostbyname(server); + if (host_entry == NULL) { + LOGI("Could not resolve %s", server); + close(sockfd); + return -1; + } + + // Set server address + memset(serv_addr, 0, sizeof(struct sockaddr_in)); + serv_addr->sin_family = AF_INET; + memcpy(&serv_addr->sin_addr.s_addr, host_entry->h_addr, host_entry->h_length); + serv_addr->sin_port = htons(NTP_PORT); + + return sockfd; +} + +// Function to attempt connecting to a specific NTP server IP +static int connect_to_ntp_ip(const char* ip_address, struct sockaddr_in* serv_addr) { + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sockfd < 0) { + LOGE("Error creating socket for NTP"); + return -1; + } + + // Set timeout + struct timeval tv; + tv.tv_sec = NTP_TIMEOUT_SEC; + tv.tv_usec = 0; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 || + setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { + LOGE("Error setting socket timeout"); + close(sockfd); + return -1; + } + + // Set server address + memset(serv_addr, 0, sizeof(struct sockaddr_in)); + serv_addr->sin_family = AF_INET; + serv_addr->sin_port = htons(NTP_PORT); + + if (inet_pton(AF_INET, ip_address, &serv_addr->sin_addr) <= 0) { + LOGE("Invalid IP address: %s", ip_address); + close(sockfd); + return -1; + } + + return sockfd; +} + +// Function to send and receive NTP packet (with retries) +static int send_receive_ntp_packet(int sockfd, struct sockaddr_in* serv_addr, ntp_packet_t* packet) { + int retries = 3; + socklen_t addr_len = sizeof(struct sockaddr_in); + + // Initialize NTP packet + memset(packet, 0, sizeof(ntp_packet_t)); + packet->li_vn_mode = 0x1b; // LI = 0, Version = 3, Mode = 3 (client) + + while (retries--) { + // Send packet + if (sendto(sockfd, packet, sizeof(ntp_packet_t), 0, + (struct sockaddr *)serv_addr, addr_len) < 0) { + LOGE("Error sending NTP packet (retry %d): %s", 2-retries, strerror(errno)); + continue; + } + + // Set select for timeout + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(sockfd, &readfds); + + struct timeval timeout; + timeout.tv_sec = 1; // 1 second per attempt + timeout.tv_usec = 0; + + int select_result = select(sockfd + 1, &readfds, NULL, NULL, &timeout); + + if (select_result <= 0) { + if (select_result == 0) { + LOGI("NTP response timeout (retry %d)", 2-retries); + } else { + LOGE("Select error (retry %d): %s", 2-retries, strerror(errno)); + } + continue; + } + + // Receive response + if (recvfrom(sockfd, packet, sizeof(ntp_packet_t), 0, + (struct sockaddr *)serv_addr, &addr_len) < 0) { + LOGE("Error receiving NTP packet (retry %d): %s", 2-retries, strerror(errno)); + continue; + } + + // Success + return 0; + } + + // Attempts exhausted + return -1; +} + +// Function to correctly calculate timezone difference +static time_t apply_timezone_offset(time_t utc_time, int offset_seconds) { + // Simply add the offset in seconds + return utc_time + offset_seconds; +} + +// Function that performs NTP synchronization (executed in a thread) +static void* ntp_sync_thread(void* arg) { + ntp_callback_data_t* callback_data = (ntp_callback_data_t*)arg; + int result = -1; + int sockfd = -1; + ntp_packet_t packet; + struct sockaddr_in serv_addr; + + LOGI("NTP sync thread started"); + + // Check if WiFi is enabled + if (!g_setting.wifi.enable) { + LOGE("WiFi disabled, cannot sync time from NTP"); + goto cleanup; + } + + // Ensure WiFi is active + system_exec("ifconfig wlan0 up"); + usleep(500000); // Wait reduced to 0.5 seconds + + // Attempt multiple NTP servers + for (int i = 0; i < g_ntp_server_count; i++) { + LOGI("Trying NTP server: %s", g_ntp_servers[i]); + + sockfd = connect_to_ntp_server(g_ntp_servers[i], &serv_addr); + if (sockfd < 0) { + continue; + } + + if (send_receive_ntp_packet(sockfd, &serv_addr, &packet) == 0) { + result = 0; + break; + } + + close(sockfd); + sockfd = -1; + } + + // If no server works, try the backup IPs + if (result != 0) { + LOGI("Trying fallback NTP server IPs"); + for (int i = 0; i < g_ntp_fallback_count; i++) { + LOGI("Trying NTP fallback IP: %s", g_ntp_fallback_ips[i]); + + sockfd = connect_to_ntp_ip(g_ntp_fallback_ips[i], &serv_addr); + if (sockfd < 0) { + continue; + } + + if (send_receive_ntp_packet(sockfd, &serv_addr, &packet) == 0) { + result = 0; + break; + } + + close(sockfd); + sockfd = -1; + } + } + + if (result == 0) { + // Convert received time + uint32_t txTm = ntohl(packet.xmitts_sec); + time_t utc_time = ntp_time_to_unix_time(txTm); + + LOGI("NTP time received (UTC): %u", (unsigned int)utc_time); + + // Use the user-configured offset + int timezone_offset = g_setting.clock.utc_offset; + LOGI("Using configured timezone offset: %d seconds", timezone_offset); + + // Apply the timezone to get local time + time_t local_time = apply_timezone_offset(utc_time, timezone_offset); + LOGI("Local time after timezone adjustment: %u", (unsigned int)local_time); + + // Update RTC with the received time adjusted to local timezone + struct timeval tv_now; + tv_now.tv_sec = local_time; + tv_now.tv_usec = 0; + + struct rtc_date rd; + rtc_tv2rd(&tv_now, &rd); + + // Validate the date + if (rtc_has_valid_date(&rd) != 0) { + LOGE("NTP returned invalid date: %04d-%02d-%02d %02d:%02d:%02d", + rd.year, rd.month, rd.day, rd.hour, rd.min, rd.sec); + result = -1; + } else if (rd.year < 2023) { // Additional validation to ensure reasonable date + LOGE("NTP returned unreasonable date: %04d", rd.year); + result = -1; + } else { + // Update the RTC + rtc_set_clock(&rd); + LOGI("NTP time sync successful: %04d-%02d-%02d %02d:%02d:%02d", + rd.year, rd.month, rd.day, rd.hour, rd.min, rd.sec); + + // Save to settings + g_setting.clock.year = rd.year; + g_setting.clock.month = rd.month; + g_setting.clock.day = rd.day; + g_setting.clock.hour = rd.hour; + g_setting.clock.min = rd.min; + g_setting.clock.sec = rd.sec; + + // Update configuration file + ini_putl("clock", "year", g_setting.clock.year, SETTING_INI); + ini_putl("clock", "month", g_setting.clock.month, SETTING_INI); + ini_putl("clock", "day", g_setting.clock.day, SETTING_INI); + ini_putl("clock", "hour", g_setting.clock.hour, SETTING_INI); + ini_putl("clock", "min", g_setting.clock.min, SETTING_INI); + ini_putl("clock", "sec", g_setting.clock.sec, SETTING_INI); + } + } + +cleanup: + if (sockfd >= 0) { + close(sockfd); + } + + // Update state + pthread_mutex_lock(&g_ntp_mutex); + g_ntp_sync_state = (result == 0) ? NTP_SYNC_SUCCESS : NTP_SYNC_FAILED; + pthread_mutex_unlock(&g_ntp_mutex); + + // Call the callback if it exists + if (callback_data != NULL) { + if (callback_data->callback_fn != NULL) { + callback_data->callback_fn(result, callback_data->user_data); + } + free(callback_data); + } + + LOGI("NTP sync thread finished with result: %d", result); + return NULL; +} + +// Public function to check if synchronization is in progress +int clock_is_syncing_from_ntp(void) { + int is_syncing = 0; + + pthread_mutex_lock(&g_ntp_mutex); + is_syncing = (g_ntp_sync_state == NTP_SYNC_IN_PROGRESS); + pthread_mutex_unlock(&g_ntp_mutex); + + return is_syncing; +} + +// Public function to start NTP synchronization (asynchronous with callback) +int clock_sync_from_ntp_async(ntp_callback_t callback_fn, void* user_data) { + int ret = -1; + + pthread_mutex_lock(&g_ntp_mutex); + + // Check if synchronization is already in progress + if (g_ntp_sync_state == NTP_SYNC_IN_PROGRESS) { + LOGI("NTP sync already in progress"); + pthread_mutex_unlock(&g_ntp_mutex); + return -1; + } + + // Update state + g_ntp_sync_state = NTP_SYNC_IN_PROGRESS; + + // Prepare data for the callback + ntp_callback_data_t* callback_data = malloc(sizeof(ntp_callback_data_t)); + if (callback_data == NULL) { + LOGE("Failed to allocate memory for callback data"); + g_ntp_sync_state = NTP_SYNC_IDLE; + pthread_mutex_unlock(&g_ntp_mutex); + return -1; + } + + callback_data->callback_fn = callback_fn; + callback_data->user_data = user_data; + + // Create thread for synchronization + pthread_t thread_id; + if (pthread_create(&thread_id, NULL, ntp_sync_thread, callback_data) != 0) { + LOGE("Error creating NTP thread"); + g_ntp_sync_state = NTP_SYNC_IDLE; + free(callback_data); + ret = -1; + } else { + pthread_detach(thread_id); + ret = 0; + } + + pthread_mutex_unlock(&g_ntp_mutex); + return ret; +} + +// Public function for compatibility with existing code (blocking) +int clock_sync_from_ntp(void) { + // Check if synchronization is already in progress + if (clock_is_syncing_from_ntp()) { + LOGI("NTP sync already in progress"); + return -1; + } + + // Attempt to start asynchronous synchronization without callback + if (clock_sync_from_ntp_async(NULL, NULL) != 0) { + return -1; + } + + // Wait for result (but with timeout) + int result = -1; + int timeout_count = 0; + const int max_timeout = 10; // 10 * 100ms = 1 second max + + while (timeout_count < max_timeout) { + usleep(100000); // 100ms + + pthread_mutex_lock(&g_ntp_mutex); + if (g_ntp_sync_state == NTP_SYNC_SUCCESS) { + result = 0; + g_ntp_sync_state = NTP_SYNC_IDLE; + pthread_mutex_unlock(&g_ntp_mutex); + break; + } else if (g_ntp_sync_state == NTP_SYNC_FAILED) { + result = -1; + g_ntp_sync_state = NTP_SYNC_IDLE; + pthread_mutex_unlock(&g_ntp_mutex); + break; + } + pthread_mutex_unlock(&g_ntp_mutex); + + timeout_count++; + } + + // If still in progress after timeout, assume it will continue in background + if (timeout_count >= max_timeout) { + LOGI("NTP sync still in progress, continuing in background"); + } + + return result; +} + +// Public function to get last synchronization status +clock_sync_status_t clock_get_last_sync_status(void) { + clock_sync_status_t status; + + pthread_mutex_lock(&g_ntp_mutex); + + switch (g_ntp_sync_state) { + case NTP_SYNC_IDLE: + status = CLOCK_SYNC_NONE; + break; + case NTP_SYNC_IN_PROGRESS: + status = CLOCK_SYNC_IN_PROGRESS; + break; + case NTP_SYNC_SUCCESS: + status = CLOCK_SYNC_SUCCESS; + break; + case NTP_SYNC_FAILED: + status = CLOCK_SYNC_FAILED; + break; + default: + status = CLOCK_SYNC_NONE; + break; + } + + pthread_mutex_unlock(&g_ntp_mutex); + + return status; +} \ No newline at end of file diff --git a/src/util/ntp_client.h b/src/util/ntp_client.h new file mode 100644 index 00000000..760ae66e --- /dev/null +++ b/src/util/ntp_client.h @@ -0,0 +1,72 @@ +#ifndef NTP_CLIENT_H +#define NTP_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Error codes +#define NTP_SUCCESS 0 +#define NTP_ERR_CONNECT -1 +#define NTP_ERR_TIMEOUT -2 +#define NTP_ERR_INVALID -3 + +/** + * @brief Callback function type for asynchronous NTP synchronization + * + * @param result Synchronization result (0 = success, -1 = error) + * @param user_data User data provided in initial call + */ +typedef void (*ntp_callback_t)(int result, void* user_data); + +/** + * @brief Clock sync status values + */ +typedef enum { + CLOCK_SYNC_NONE = 0, + CLOCK_SYNC_IN_PROGRESS, + CLOCK_SYNC_SUCCESS, + CLOCK_SYNC_FAILED +} clock_sync_status_t; + +/** + * @brief Inicia la sincronización del reloj con un servidor NTP (bloqueante) + * + * Esta función intenta sincronizar el reloj con un servidor NTP. + * Es bloqueante pero tiene un timeout interno de 1 segundo. + * + * @return 0 en caso de éxito, -1 en caso de error + */ +int clock_sync_from_ntp(void); + +/** + * @brief Inicia la sincronización del reloj con un servidor NTP (asíncrona) + * + * Esta función inicia la sincronización en un hilo separado y retorna inmediatamente. + * El resultado se comunicará a través del callback proporcionado. + * + * @param callback_fn Función que será llamada al completar la sincronización (puede ser NULL) + * @param user_data Datos que serán pasados al callback (puede ser NULL) + * @return 0 si se inició correctamente, -1 si hubo un error al iniciar + */ +int clock_sync_from_ntp_async(ntp_callback_t callback_fn, void* user_data); + +/** + * @brief Verifica si hay una sincronización NTP en progreso + * + * @return 1 si hay una sincronización en progreso, 0 en caso contrario + */ +int clock_is_syncing_from_ntp(void); + +/** + * @brief Obtiene el estado de la última sincronización + * + * @return Estado de la última sincronización usando clock_sync_status_t + */ +clock_sync_status_t clock_get_last_sync_status(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NTP_CLIENT_H */ \ No newline at end of file diff --git a/src/util/wifi_status.c b/src/util/wifi_status.c new file mode 100644 index 00000000..2fe2cf1d --- /dev/null +++ b/src/util/wifi_status.c @@ -0,0 +1,51 @@ +#include "util/wifi_status.h" +#include +#include +#include +#include +#include +#include +#include + +int wifi_sta_get_connect_status(const char *interface, WIFI_STA_CONNECT_STATUS_S *status) { + struct iwreq wreq; + int sockfd; + + if (!interface || !status) { + return -1; + } + + // Initialize status + memset(status, 0, sizeof(WIFI_STA_CONNECT_STATUS_S)); + + // Create socket + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0) { + return -1; + } + + // Prepare request + memset(&wreq, 0, sizeof(struct iwreq)); + strncpy(wreq.ifr_name, interface, IFNAMSIZ-1); + + // Get connection status + if (ioctl(sockfd, SIOCGIWNAME, &wreq) >= 0) { + // Check if interface is up + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, interface, IFNAMSIZ-1); + + if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) >= 0) { + if (ifr.ifr_flags & IFF_UP) { + status->state = WIFI_STA_STATUS_CONNECTED; + } else { + status->state = WIFI_STA_STATUS_DISCONNECTED; + } + } + } else { + status->state = WIFI_STA_STATUS_FAILED; + } + + close(sockfd); + return 0; +} diff --git a/src/util/wifi_status.h b/src/util/wifi_status.h new file mode 100644 index 00000000..7617828b --- /dev/null +++ b/src/util/wifi_status.h @@ -0,0 +1,19 @@ +#ifndef WIFI_STATUS_H +#define WIFI_STATUS_H + +typedef enum { + WIFI_STA_STATUS_DISCONNECTED = 0, + WIFI_STA_STATUS_CONNECTING, + WIFI_STA_STATUS_CONNECTED, + WIFI_STA_STATUS_FAILED +} wifi_sta_status_t; + +typedef struct { + wifi_sta_status_t state; + char ssid[32]; + int signal_strength; +} WIFI_STA_CONNECT_STATUS_S; + +int wifi_sta_get_connect_status(const char *interface, WIFI_STA_CONNECT_STATUS_S *status); + +#endif // WIFI_STATUS_H