feat: implement system time synchronization, web server handlers, and clock application components
This commit is contained in:
Vendored
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
"*.c": "c"
|
"*.c": "c"
|
||||||
},
|
},
|
||||||
"idf.currentSetup": "D:\\SDK\\.espressif\\v6.1\\esp-idf",
|
"idf.currentSetup": "D:\\SDK\\.espressif\\v6.1\\esp-idf",
|
||||||
"idf.portWin": "COM4"
|
"idf.port": "COM4"
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
#include "os_cli.h"
|
#include "os_cli.h"
|
||||||
#include "os_app.h"
|
#include "os_app.h"
|
||||||
#include "os_input.h"
|
#include "os_input.h"
|
||||||
#include "os_gfx.h"
|
#include "os_gfx.h"
|
||||||
#include "os_oled.h"
|
#include "os_oled.h"
|
||||||
#include "os_status.h"
|
#include "os_status.h"
|
||||||
#include "sys_uart.h"
|
#include "sys_uart.h"
|
||||||
|
#include "sys_time.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
@@ -16,6 +18,57 @@
|
|||||||
|
|
||||||
static const char *TAG = "SYS_CLI";
|
static const char *TAG = "SYS_CLI";
|
||||||
|
|
||||||
|
static int cmd_time(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc >= 2 && strcmp(argv[1], "sync") == 0) {
|
||||||
|
printf("Triggering SNTP network time sync...\n");
|
||||||
|
sys_time_start_sntp();
|
||||||
|
return 0;
|
||||||
|
} else if (argc >= 3 && strcmp(argv[1], "set") == 0) {
|
||||||
|
time_t epoch = (time_t)strtoull(argv[2], NULL, 10);
|
||||||
|
if (sys_time_set_epoch(epoch) == ESP_OK) {
|
||||||
|
printf("System time successfully set to epoch: %lld\n", (long long)epoch);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
printf("Failed to set time. Usage: time set <epoch_seconds>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct tm tm_info;
|
||||||
|
bool synced = sys_time_get_local(&tm_info);
|
||||||
|
time_t now = sys_time_get_epoch();
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S (%A)", &tm_info);
|
||||||
|
|
||||||
|
printf("\n=== System Wall Clock ===\n");
|
||||||
|
printf("Status: %s\n", synced ? "SYNCHRONIZED" : "UNSYNCHRONIZED (Uptime only)");
|
||||||
|
printf("Local: %s\n", strftime_buf);
|
||||||
|
printf("Epoch: %lld\n", (long long)now);
|
||||||
|
printf("Timezone: %s\n", sys_time_get_timezone());
|
||||||
|
printf("\nCommands: time sync | time set <epoch>\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_tz(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc >= 2) {
|
||||||
|
if (sys_time_set_timezone(argv[1]) == ESP_OK) {
|
||||||
|
printf("Timezone set to '%s'\n", argv[1]);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
printf("Failed to set timezone\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("Current Timezone: %s\n", sys_time_get_timezone());
|
||||||
|
printf("Examples: tz \"GMT0BST,M3.5.0/1,M10.5.0\" (UK)\n");
|
||||||
|
printf(" tz \"EST5EDT,M3.2.0,M11.1.0\" (US Eastern)\n");
|
||||||
|
printf(" tz \"CST-8\" (China / Asia)\n");
|
||||||
|
printf(" tz \"UTC0\" (UTC)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int cmd_uart(int argc, char **argv)
|
static int cmd_uart(int argc, char **argv)
|
||||||
{
|
{
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
@@ -170,6 +223,22 @@ esp_err_t sys_cli_init(void)
|
|||||||
|
|
||||||
esp_console_register_help_command();
|
esp_console_register_help_command();
|
||||||
|
|
||||||
|
const esp_console_cmd_t time_cmd = {
|
||||||
|
.command = "time",
|
||||||
|
.help = "View or configure real-time clock (time | time sync | time set <epoch>)",
|
||||||
|
.hint = "<sync|set <epoch>>",
|
||||||
|
.func = &cmd_time,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&time_cmd);
|
||||||
|
|
||||||
|
const esp_console_cmd_t tz_cmd = {
|
||||||
|
.command = "tz",
|
||||||
|
.help = "View or configure timezone string",
|
||||||
|
.hint = "<tz_string>",
|
||||||
|
.func = &cmd_tz,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&tz_cmd);
|
||||||
|
|
||||||
const esp_console_cmd_t ps_cmd = {
|
const esp_console_cmd_t ps_cmd = {
|
||||||
.command = "ps",
|
.command = "ps",
|
||||||
.help = "List active FreeRTOS tasks and memory",
|
.help = "List active FreeRTOS tasks and memory",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS
|
SRCS
|
||||||
"sys_event.c"
|
"sys_event.c"
|
||||||
"sys_status.c"
|
"sys_status.c"
|
||||||
"sys_uart.c"
|
"sys_uart.c"
|
||||||
|
"sys_time.c"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"include"
|
"include"
|
||||||
REQUIRES
|
REQUIRES
|
||||||
@@ -13,4 +14,7 @@ idf_component_register(
|
|||||||
driver
|
driver
|
||||||
esp_driver_gpio
|
esp_driver_gpio
|
||||||
esp_driver_uart
|
esp_driver_uart
|
||||||
|
nvs_flash
|
||||||
|
lwip
|
||||||
|
esp_netif
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#ifndef SYS_TIME_H
|
||||||
|
#define SYS_TIME_H
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Default Timezone: UK (GMT / BST with Daylight Saving) */
|
||||||
|
#define SYS_TIME_DEFAULT_TZ "GMT0BST,M3.5.0/1,M10.5.0"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize system time service and load timezone from NVS.
|
||||||
|
*/
|
||||||
|
esp_err_t sys_time_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start SNTP background synchronization over active network.
|
||||||
|
*/
|
||||||
|
void sys_time_start_sntp(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether real-world wall clock has been synchronized.
|
||||||
|
*/
|
||||||
|
bool sys_time_is_synced(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current UTC epoch seconds.
|
||||||
|
*/
|
||||||
|
time_t sys_time_get_epoch(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get breakdown of current local time.
|
||||||
|
* @param[out] out_tm Struct tm filled with current local time.
|
||||||
|
* @return true if time is synchronized, false otherwise.
|
||||||
|
*/
|
||||||
|
bool sys_time_get_local(struct tm *out_tm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set epoch time manually.
|
||||||
|
*/
|
||||||
|
esp_err_t sys_time_set_epoch(time_t epoch_sec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set and persist active timezone string in POSIX format.
|
||||||
|
*/
|
||||||
|
esp_err_t sys_time_set_timezone(const char *tz);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get active timezone string.
|
||||||
|
*/
|
||||||
|
const char *sys_time_get_timezone(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SYS_TIME_H
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
#include "sys_time.h"
|
||||||
|
#include "os_event.h"
|
||||||
|
#include "os_status.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_netif_sntp.h"
|
||||||
|
#include "esp_sntp.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static const char *TAG = "SYS_TIME";
|
||||||
|
static char s_tz_str[64] = SYS_TIME_DEFAULT_TZ;
|
||||||
|
static TaskHandle_t s_sntp_task_handle = NULL;
|
||||||
|
|
||||||
|
static const char *const s_ntp_servers[] = {
|
||||||
|
"time.google.com",
|
||||||
|
"time.cloudflare.com",
|
||||||
|
"pool.ntp.org",
|
||||||
|
"time.windows.com",
|
||||||
|
"216.239.35.0" /* Fallback direct IP for time.google.com */
|
||||||
|
};
|
||||||
|
|
||||||
|
static void time_sync_notification_cb(struct timeval *tv)
|
||||||
|
{
|
||||||
|
time_t now = tv ? tv->tv_sec : time(NULL);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
|
||||||
|
ESP_LOGI(TAG, "SNTP packet received & verified: %s", strftime_buf);
|
||||||
|
sys_event_post(SYS_EVENT_TICK_1S, 0, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t sys_time_init(void)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
size_t tz_len = sizeof(s_tz_str);
|
||||||
|
if (nvs_open("system", NVS_READONLY, &nvs) == ESP_OK) {
|
||||||
|
if (nvs_get_str(nvs, "tz", s_tz_str, &tz_len) != ESP_OK) {
|
||||||
|
strncpy(s_tz_str, SYS_TIME_DEFAULT_TZ, sizeof(s_tz_str) - 1);
|
||||||
|
}
|
||||||
|
nvs_close(nvs);
|
||||||
|
} else {
|
||||||
|
strncpy(s_tz_str, SYS_TIME_DEFAULT_TZ, sizeof(s_tz_str) - 1);
|
||||||
|
}
|
||||||
|
s_tz_str[sizeof(s_tz_str) - 1] = '\0';
|
||||||
|
setenv("TZ", s_tz_str, 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "System time service initialized (Timezone: %s)", s_tz_str);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sntp_sync_task(void *pvParameters)
|
||||||
|
{
|
||||||
|
const size_t num_servers = sizeof(s_ntp_servers) / sizeof(s_ntp_servers[0]);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < num_servers && !sys_time_is_synced(); i++) {
|
||||||
|
const char *server = s_ntp_servers[i];
|
||||||
|
ESP_LOGI(TAG, "Attempting SNTP sync with server [%u/%u]: '%s'...", (unsigned int)(i + 1), (unsigned int)num_servers, server);
|
||||||
|
|
||||||
|
esp_netif_sntp_deinit();
|
||||||
|
|
||||||
|
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(server);
|
||||||
|
config.sync_cb = time_sync_notification_cb;
|
||||||
|
config.smooth_sync = false;
|
||||||
|
config.server_from_dhcp = false;
|
||||||
|
config.wait_for_sync = true;
|
||||||
|
config.start = true;
|
||||||
|
|
||||||
|
esp_err_t err = esp_netif_sntp_init(&config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize SNTP for '%s': %s", server, esp_err_to_name(err));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll for sync completion up to 5 seconds per server
|
||||||
|
for (int r = 0; r < 5; r++) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
if (sys_time_is_synced() || sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sys_time_is_synced()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sys_time_is_synced()) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
|
||||||
|
ESP_LOGI(TAG, "SNTP Sync Complete! Current Local Time: %s", strftime_buf);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "SNTP sync could not reach any configured NTP server. Device will retry on reconnect.");
|
||||||
|
}
|
||||||
|
|
||||||
|
s_sntp_task_handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sys_time_start_sntp(void)
|
||||||
|
{
|
||||||
|
if (s_sntp_task_handle != NULL) {
|
||||||
|
ESP_LOGI(TAG, "SNTP sync already running in background");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xTaskCreate(sntp_sync_task, "sntp_task", 4096, NULL, 5, &s_sntp_task_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sys_time_is_synced(void)
|
||||||
|
{
|
||||||
|
time_t now = time(NULL);
|
||||||
|
// Synced if timestamp is after Jan 1, 2024 (1704067200)
|
||||||
|
return (now >= 1704067200ULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t sys_time_get_epoch(void)
|
||||||
|
{
|
||||||
|
return time(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sys_time_get_local(struct tm *out_tm)
|
||||||
|
{
|
||||||
|
if (!out_tm) return false;
|
||||||
|
time_t now = time(NULL);
|
||||||
|
localtime_r(&now, out_tm);
|
||||||
|
return sys_time_is_synced();
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t sys_time_set_epoch(time_t epoch_sec)
|
||||||
|
{
|
||||||
|
if (epoch_sec < 1704067200ULL) {
|
||||||
|
ESP_LOGE(TAG, "Invalid epoch timestamp: %lld", (long long)epoch_sec);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
struct timeval tv = {
|
||||||
|
.tv_sec = epoch_sec,
|
||||||
|
.tv_usec = 0
|
||||||
|
};
|
||||||
|
int ret = settimeofday(&tv, NULL);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set time of day");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&epoch_sec, &timeinfo);
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
|
||||||
|
ESP_LOGI(TAG, "Manual time set: %s", strftime_buf);
|
||||||
|
sys_event_post(SYS_EVENT_TICK_1S, 0, NULL);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t sys_time_set_timezone(const char *tz)
|
||||||
|
{
|
||||||
|
if (!tz || strlen(tz) == 0) return ESP_ERR_INVALID_ARG;
|
||||||
|
strncpy(s_tz_str, tz, sizeof(s_tz_str) - 1);
|
||||||
|
s_tz_str[sizeof(s_tz_str) - 1] = '\0';
|
||||||
|
setenv("TZ", s_tz_str, 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
if (nvs_open("system", NVS_READWRITE, &nvs) == ESP_OK) {
|
||||||
|
nvs_set_str(nvs, "tz", s_tz_str);
|
||||||
|
nvs_commit(nvs);
|
||||||
|
nvs_close(nvs);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Timezone updated to: %s", s_tz_str);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sys_time_get_timezone(void)
|
||||||
|
{
|
||||||
|
return s_tz_str;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "sys_wifi.h"
|
#include "sys_wifi.h"
|
||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
|
#include "sys_time.h"
|
||||||
#include "os_event.h"
|
#include "os_event.h"
|
||||||
#include "os_status.h"
|
#include "os_status.h"
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
@@ -84,6 +85,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t e
|
|||||||
if (esp_wifi_sta_get_ap_info(&record) == ESP_OK) rssi = record.rssi;
|
if (esp_wifi_sta_get_ap_info(&record) == ESP_OK) rssi = record.rssi;
|
||||||
set_status(WIFI_STATE_CONNECTED, rssi);
|
set_status(WIFI_STATE_CONNECTED, rssi);
|
||||||
web_server_start();
|
web_server_start();
|
||||||
|
sys_time_start_sntp();
|
||||||
const ip_event_got_ip_t *got_ip = (const ip_event_got_ip_t *)event_data;
|
const ip_event_got_ip_t *got_ip = (const ip_event_got_ip_t *)event_data;
|
||||||
ESP_LOGI(TAG, "Wi-Fi connected, web server at http://" IPSTR, IP2STR(&got_ip->ip_info.ip));
|
ESP_LOGI(TAG, "Wi-Fi connected, web server at http://" IPSTR, IP2STR(&got_ip->ip_info.ip));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "web_server_pages.h"
|
#include "web_server_pages.h"
|
||||||
#include "sys_uart.h"
|
#include "sys_uart.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
@@ -227,7 +227,7 @@ static const char SERIAL_HTML[] =
|
|||||||
" }"
|
" }"
|
||||||
"});"
|
"});"
|
||||||
"log('[ABrobot Web Serial Connected on TX:GPIO21 / RX:GPIO20 (Internal Pull-Up Active)]\\n', 'log-sys');"
|
"log('[ABrobot Web Serial Connected on TX:GPIO21 / RX:GPIO20 (Internal Pull-Up Active)]\\n', 'log-sys');"
|
||||||
"pollData();"
|
"try{fetch('/time/sync?epoch='+Math.floor(Date.now()/1000));}catch(e){}pollData();"
|
||||||
"</script></body></html>";
|
"</script></body></html>";
|
||||||
|
|
||||||
static esp_err_t serial_page_get(httpd_req_t *request)
|
static esp_err_t serial_page_get(httpd_req_t *request)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "web_server_pages.h"
|
#include "web_server_pages.h"
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
@@ -91,7 +91,7 @@ static const char WIFI_HTML[] =
|
|||||||
" <label for=\"password\">Wi-Fi Password</label>"
|
" <label for=\"password\">Wi-Fi Password</label>"
|
||||||
" <div class=\"pass-wrap\">"
|
" <div class=\"pass-wrap\">"
|
||||||
" <input type=\"password\" id=\"password\" name=\"password\" maxlength=\"64\" placeholder=\"Enter Wi-Fi password (WPA/WPA2)...\" required>"
|
" <input type=\"password\" id=\"password\" name=\"password\" maxlength=\"64\" placeholder=\"Enter Wi-Fi password (WPA/WPA2)...\" required>"
|
||||||
" <button type=\"button\" class=\"pass-toggle\" id=\"eyeBtn\" onclick=\"togglePassword()\" title=\"Show/Hide Password\">👁️</button>"
|
" <button type=\"button\" class=\"pass-toggle\" id=\"eyeBtn\" onclick=\"togglePassword()\" title=\"Show/Hide Password\">ðŸ‘ï¸</button>"
|
||||||
" </div>"
|
" </div>"
|
||||||
" </div>"
|
" </div>"
|
||||||
" <button type=\"submit\" class=\"primary\" id=\"btnConnect\">Connect to Network</button>"
|
" <button type=\"submit\" class=\"primary\" id=\"btnConnect\">Connect to Network</button>"
|
||||||
@@ -112,12 +112,12 @@ static const char WIFI_HTML[] =
|
|||||||
"const list=document.getElementById('networks'),manual=document.getElementById('manual'),manualBox=document.getElementById('manualBox'),hidden=document.getElementById('hidden'),passInput=document.getElementById('password'),msg=document.getElementById('statusMsg'),btnConnect=document.getElementById('btnConnect'),btnScan=document.getElementById('btnScan'),scanCount=document.getElementById('scanCount');"
|
"const list=document.getElementById('networks'),manual=document.getElementById('manual'),manualBox=document.getElementById('manualBox'),hidden=document.getElementById('hidden'),passInput=document.getElementById('password'),msg=document.getElementById('statusMsg'),btnConnect=document.getElementById('btnConnect'),btnScan=document.getElementById('btnScan'),scanCount=document.getElementById('scanCount');"
|
||||||
"function showMsg(text,type){msg.className='msg-box msg-'+(type||'info');msg.textContent=text;msg.style.display='block';}"
|
"function showMsg(text,type){msg.className='msg-box msg-'+(type||'info');msg.textContent=text;msg.style.display='block';}"
|
||||||
"function toggleManual(){let on=hidden.checked;manualBox.style.display=on?'block':'none';manual.required=on;if(on){manual.value='';manual.focus();}else if(list.value)manual.value=list.value;}"
|
"function toggleManual(){let on=hidden.checked;manualBox.style.display=on?'block':'none';manual.required=on;if(on){manual.value='';manual.focus();}else if(list.value)manual.value=list.value;}"
|
||||||
"function togglePassword(){let isPass=passInput.type==='password';passInput.type=isPass?'text':'password';document.getElementById('eyeBtn').textContent=isPass?'🔒':'👁️';}"
|
"function togglePassword(){let isPass=passInput.type==='password';passInput.type=isPass?'text':'password';document.getElementById('eyeBtn').textContent=isPass?'🔒':'ðŸ‘ï¸';}"
|
||||||
"async function scan(){btnScan.disabled=true;btnScan.textContent='Scanning...';scanCount.textContent='Scanning...';showMsg('Scanning for 2.4 GHz Wi-Fi access points...','info');try{const res=await fetch('/scan');if(!res.ok)throw new Error('HTTP '+res.status);const data=await res.json();list.innerHTML='';if(!data.length){list.innerHTML='<option value=\"\">No networks found</option>';scanCount.textContent='0 APs';showMsg('No networks found. Check router or use manual SSID entry.','error');}else{data.forEach(n=>{const opt=document.createElement('option');opt.value=n.ssid;const qual=n.rssi>-60?'Strong':(n.rssi>-75?'Good':'Weak');opt.textContent=n.ssid+' ('+n.rssi+' dBm - '+qual+')';list.appendChild(opt);});scanCount.textContent=data.length+' APs';if(!hidden.checked)manual.value=list.value;showMsg('Found '+data.length+' Wi-Fi network(s). Select your network and enter credentials.','success');}}catch(e){scanCount.textContent='Error';showMsg('Wi-Fi scan failed: '+e.message,'error');}finally{btnScan.disabled=false;btnScan.textContent='Refresh';}}"
|
"async function scan(){btnScan.disabled=true;btnScan.textContent='Scanning...';scanCount.textContent='Scanning...';showMsg('Scanning for 2.4 GHz Wi-Fi access points...','info');try{const res=await fetch('/scan');if(!res.ok)throw new Error('HTTP '+res.status);const data=await res.json();list.innerHTML='';if(!data.length){list.innerHTML='<option value=\"\">No networks found</option>';scanCount.textContent='0 APs';showMsg('No networks found. Check router or use manual SSID entry.','error');}else{data.forEach(n=>{const opt=document.createElement('option');opt.value=n.ssid;const qual=n.rssi>-60?'Strong':(n.rssi>-75?'Good':'Weak');opt.textContent=n.ssid+' ('+n.rssi+' dBm - '+qual+')';list.appendChild(opt);});scanCount.textContent=data.length+' APs';if(!hidden.checked)manual.value=list.value;showMsg('Found '+data.length+' Wi-Fi network(s). Select your network and enter credentials.','success');}}catch(e){scanCount.textContent='Error';showMsg('Wi-Fi scan failed: '+e.message,'error');}finally{btnScan.disabled=false;btnScan.textContent='Refresh';}}"
|
||||||
"list.onchange=()=>{if(!hidden.checked)manual.value=list.value;};"
|
"list.onchange=()=>{if(!hidden.checked)manual.value=list.value;};"
|
||||||
"async function submitWifi(e){e.preventDefault();const ssidVal=manual.value.trim();const passVal=passInput.value;if(!ssidVal){showMsg('Please select or enter an SSID network name.','error');return;}btnConnect.disabled=true;btnConnect.textContent='Connecting...';showMsg('Saving credentials and connecting to \"'+ssidVal+'\"...','info');try{const body='ssid='+encodeURIComponent(ssidVal)+'&password='+encodeURIComponent(passVal);const res=await fetch('/configure',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body});if(res.ok){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is switching to Station mode. Please connect your device to \"'+ssidVal+'\".','success');}else{const err=await res.text();showMsg('Connection failed: '+err,'error');btnConnect.disabled=false;btnConnect.textContent='Connect to Network';}}catch(e){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is connecting to Wi-Fi. Please connect to \"'+ssidVal+'\".','success');}}"
|
"async function submitWifi(e){e.preventDefault();const ssidVal=manual.value.trim();const passVal=passInput.value;if(!ssidVal){showMsg('Please select or enter an SSID network name.','error');return;}btnConnect.disabled=true;btnConnect.textContent='Connecting...';showMsg('Saving credentials and connecting to \"'+ssidVal+'\"...','info');try{const body='ssid='+encodeURIComponent(ssidVal)+'&password='+encodeURIComponent(passVal);const res=await fetch('/configure',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body});if(res.ok){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is switching to Station mode. Please connect your device to \"'+ssidVal+'\".','success');}else{const err=await res.text();showMsg('Connection failed: '+err,'error');btnConnect.disabled=false;btnConnect.textContent='Connect to Network';}}catch(e){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is connecting to Wi-Fi. Please connect to \"'+ssidVal+'\".','success');}}"
|
||||||
"if(!hidden.checked&&list.value)manual.value=list.value;"
|
"if(!hidden.checked&&list.value)manual.value=list.value;"
|
||||||
"scan();"
|
"try{fetch('/time/sync?epoch='+Math.floor(Date.now()/1000));}catch(e){}scan();"
|
||||||
"</script></body></html>";
|
"</script></body></html>";
|
||||||
|
|
||||||
static void copy_string(uint8_t *destination, size_t size, const char *source)
|
static void copy_string(uint8_t *destination, size_t size, const char *source)
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
#include "web_server_pages.h"
|
#include "web_server_pages.h"
|
||||||
|
#include "sys_time.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
static const char *TAG = "SYS_WEB";
|
static const char *TAG = "SYS_WEB";
|
||||||
static httpd_handle_t s_server;
|
static httpd_handle_t s_server;
|
||||||
|
|
||||||
|
static esp_err_t time_sync_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
char query[128] = {0};
|
||||||
|
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
|
||||||
|
char val[32] = {0};
|
||||||
|
if (httpd_query_key_value(query, "epoch", val, sizeof(val)) == ESP_OK) {
|
||||||
|
time_t epoch = (time_t)strtoull(val, NULL, 10);
|
||||||
|
if (epoch >= 1704067200ULL) {
|
||||||
|
sys_time_set_epoch(epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct tm tm_info;
|
||||||
|
bool synced = sys_time_get_local(&tm_info);
|
||||||
|
time_t now = sys_time_get_epoch();
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S", &tm_info);
|
||||||
|
|
||||||
|
char resp[160];
|
||||||
|
snprintf(resp, sizeof(resp), "{\"synced\":%s,\"epoch\":%lld,\"time\":\"%s\",\"tz\":\"%s\"}",
|
||||||
|
synced ? "true" : "false", (long long)now, strftime_buf, sys_time_get_timezone());
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
return httpd_resp_sendstr(req, resp);
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t web_server_start(void)
|
esp_err_t web_server_start(void)
|
||||||
{
|
{
|
||||||
if (s_server) return ESP_OK;
|
if (s_server) return ESP_OK;
|
||||||
@@ -19,6 +50,14 @@ esp_err_t web_server_start(void)
|
|||||||
web_server_register_wifi_pages(s_server);
|
web_server_register_wifi_pages(s_server);
|
||||||
web_server_register_ota_page(s_server);
|
web_server_register_ota_page(s_server);
|
||||||
web_server_register_serial_pages(s_server);
|
web_server_register_serial_pages(s_server);
|
||||||
ESP_LOGI(TAG, "Web server started");
|
|
||||||
|
const httpd_uri_t time_uri = {
|
||||||
|
.uri = "/time/sync",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = time_sync_handler
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(s_server, &time_uri);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Web server started with real-time sync service");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-18
@@ -1,11 +1,14 @@
|
|||||||
#include "apps.h"
|
#include "apps.h"
|
||||||
#include "os_gfx.h"
|
#include "os_gfx.h"
|
||||||
#include "os_status.h"
|
#include "os_status.h"
|
||||||
|
#include "sys_time.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
|
||||||
static uint8_t s_view_mode = 0;
|
static uint8_t s_view_mode = 0;
|
||||||
|
static const char *const s_days[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
|
||||||
|
static const char *const s_months[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
|
||||||
|
|
||||||
static void clock_init(void)
|
static void clock_init(void)
|
||||||
{
|
{
|
||||||
@@ -23,41 +26,86 @@ static void clock_update(uint32_t delta_ms)
|
|||||||
|
|
||||||
sys_gfx_draw_status_bar(&status);
|
sys_gfx_draw_status_bar(&status);
|
||||||
|
|
||||||
uint32_t sec = status.uptime_sec % 60;
|
struct tm tm_info;
|
||||||
uint32_t min = (status.uptime_sec / 60) % 60;
|
bool is_synced = sys_time_get_local(&tm_info);
|
||||||
uint32_t hrs = (status.uptime_sec / 3600);
|
|
||||||
|
|
||||||
char time_str[16];
|
uint32_t sec, min, hrs;
|
||||||
char sub_str[20];
|
if (is_synced) {
|
||||||
|
sec = tm_info.tm_sec;
|
||||||
|
min = tm_info.tm_min;
|
||||||
|
hrs = tm_info.tm_hour;
|
||||||
|
} else {
|
||||||
|
sec = status.uptime_sec % 60;
|
||||||
|
min = (status.uptime_sec / 60) % 60;
|
||||||
|
hrs = (status.uptime_sec / 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
char time_str[32];
|
||||||
|
char sub_str[48];
|
||||||
|
|
||||||
if (s_view_mode == 0) {
|
if (s_view_mode == 0) {
|
||||||
|
// Mode 0: Digital Clock (HH:MM:SS) + Date / Status
|
||||||
snprintf(time_str, sizeof(time_str), "%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec);
|
snprintf(time_str, sizeof(time_str), "%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec);
|
||||||
sys_gfx_draw_str_5x7(12, 11, time_str, COLOR_WHITE);
|
sys_gfx_draw_str_5x7(12, 11, time_str, COLOR_WHITE);
|
||||||
|
|
||||||
snprintf(sub_str, sizeof(sub_str), "RAM:%luK", (unsigned long)(status.free_heap / 1024));
|
if (is_synced) {
|
||||||
sys_gfx_draw_str_3x5(4, 21, sub_str, COLOR_WHITE);
|
int wday = tm_info.tm_wday % 7;
|
||||||
|
int mon = tm_info.tm_mon % 12;
|
||||||
snprintf(sub_str, sizeof(sub_str), "CPU:160M");
|
int mday = tm_info.tm_mday % 100;
|
||||||
sys_gfx_draw_str_3x5(40, 21, sub_str, COLOR_WHITE);
|
int year = (tm_info.tm_year + 1900) % 100;
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "%s %02d-%s-%02d", s_days[wday], mday, s_months[mon], year);
|
||||||
|
sys_gfx_draw_str_3x5(9, 21, sub_str, COLOR_WHITE);
|
||||||
|
} else {
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "NTP: WAITING...");
|
||||||
|
sys_gfx_draw_str_3x5(8, 21, sub_str, COLOR_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
sys_gfx_draw_progress_bar(4, 27, 64, 4, (uint8_t)((sec * 100) / 59));
|
sys_gfx_draw_progress_bar(4, 27, 64, 4, (uint8_t)((sec * 100) / 59));
|
||||||
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
||||||
} else if (s_view_mode == 1) {
|
} else if (s_view_mode == 1) {
|
||||||
snprintf(time_str, sizeof(time_str), "%02lu:%02lu", (unsigned long)min, (unsigned long)sec);
|
// Mode 1: Large Minimalist Time (HH:MM) + Seconds + Date
|
||||||
sys_gfx_draw_str_5x7(18, 14, time_str, COLOR_WHITE);
|
snprintf(time_str, sizeof(time_str), "%02lu:%02lu", (unsigned long)hrs, (unsigned long)min);
|
||||||
sys_gfx_draw_str_5x7(19, 14, time_str, COLOR_WHITE);
|
sys_gfx_draw_str_5x7(16, 13, time_str, COLOR_WHITE);
|
||||||
|
sys_gfx_draw_str_5x7(17, 13, time_str, COLOR_WHITE); // Bold effect
|
||||||
|
|
||||||
snprintf(sub_str, sizeof(sub_str), "UP:%luh %lum", (unsigned long)hrs, (unsigned long)min);
|
char sec_str[16];
|
||||||
sys_gfx_draw_str_3x5(12, 24, sub_str, COLOR_WHITE);
|
snprintf(sec_str, sizeof(sec_str), ":%02lu", (unsigned long)sec);
|
||||||
|
sys_gfx_draw_str_3x5(49, 14, sec_str, COLOR_WHITE);
|
||||||
|
|
||||||
|
if (is_synced) {
|
||||||
|
int wday = tm_info.tm_wday % 7;
|
||||||
|
int mon = tm_info.tm_mon % 12;
|
||||||
|
int mday = tm_info.tm_mday % 100;
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "%s %02d %s", s_days[wday], mday, s_months[mon]);
|
||||||
|
sys_gfx_draw_str_3x5(14, 23, sub_str, COLOR_WHITE);
|
||||||
|
} else {
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "UP:%luh %lum (SYNC..)", (unsigned long)hrs, (unsigned long)min);
|
||||||
|
sys_gfx_draw_str_3x5(4, 23, sub_str, COLOR_WHITE);
|
||||||
|
}
|
||||||
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
||||||
} else {
|
} else {
|
||||||
snprintf(sub_str, sizeof(sub_str), "UP:%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec);
|
// Mode 2: Detailed System Clock & Telemetry Diagnostics
|
||||||
|
if (is_synced) {
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "TIME:%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec);
|
||||||
sys_gfx_draw_str_3x5(3, 10, sub_str, COLOR_WHITE);
|
sys_gfx_draw_str_3x5(3, 10, sub_str, COLOR_WHITE);
|
||||||
|
|
||||||
snprintf(sub_str, sizeof(sub_str), "HEAP:%lu B", (unsigned long)status.free_heap);
|
int year = (tm_info.tm_year + 1900) % 10000;
|
||||||
|
int mon = (tm_info.tm_mon + 1) % 100;
|
||||||
|
int mday = tm_info.tm_mday % 100;
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "DATE:%04d-%02d-%02d", year, mon, mday);
|
||||||
sys_gfx_draw_str_3x5(3, 17, sub_str, COLOR_WHITE);
|
sys_gfx_draw_str_3x5(3, 17, sub_str, COLOR_WHITE);
|
||||||
|
} else {
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "NTP: NOT SYNCED");
|
||||||
|
sys_gfx_draw_str_3x5(3, 10, sub_str, COLOR_WHITE);
|
||||||
|
|
||||||
snprintf(sub_str, sizeof(sub_str), "MINH:%lu B", (unsigned long)status.min_free_heap);
|
snprintf(sub_str, sizeof(sub_str), "HEAP:%lu KB", (unsigned long)(status.free_heap / 1024));
|
||||||
|
sys_gfx_draw_str_3x5(3, 17, sub_str, COLOR_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t up_sec = status.uptime_sec % 60;
|
||||||
|
uint32_t up_min = (status.uptime_sec / 60) % 60;
|
||||||
|
uint32_t up_hrs = (status.uptime_sec / 3600);
|
||||||
|
snprintf(sub_str, sizeof(sub_str), "UP: %02lu:%02lu:%02lu", (unsigned long)up_hrs, (unsigned long)up_min, (unsigned long)up_sec);
|
||||||
sys_gfx_draw_str_3x5(3, 24, sub_str, COLOR_WHITE);
|
sys_gfx_draw_str_3x5(3, 24, sub_str, COLOR_WHITE);
|
||||||
|
|
||||||
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
sys_gfx_draw_action_bar("C:MODE", "H:HOME");
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,4 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
/* OS Subsystems */
|
/* OS Subsystems */
|
||||||
#include "os_event.h"
|
#include "os_event.h"
|
||||||
#include "os_status.h"
|
#include "os_status.h"
|
||||||
|
#include "sys_time.h"
|
||||||
#include "os_gfx.h"
|
#include "os_gfx.h"
|
||||||
#include "os_input.h"
|
#include "os_input.h"
|
||||||
#include "os_app.h"
|
#include "os_app.h"
|
||||||
@@ -87,6 +88,7 @@ void app_main(void)
|
|||||||
// 1. Core Services & Telemetry
|
// 1. Core Services & Telemetry
|
||||||
sys_event_init();
|
sys_event_init();
|
||||||
sys_status_init();
|
sys_status_init();
|
||||||
|
sys_time_init();
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
// 1b. Wi-Fi and provisioning services
|
// 1b. Wi-Fi and provisioning services
|
||||||
|
|||||||
Reference in New Issue
Block a user