feat: implement system time synchronization, web server handlers, and clock application components

This commit is contained in:
dark98
2026-09-25 20:57:19 +01:00
parent 4bf31f908a
commit 59b0318579
11 changed files with 442 additions and 33 deletions
+70 -1
View File
@@ -1,13 +1,15 @@
#include "os_cli.h"
#include "os_cli.h"
#include "os_app.h"
#include "os_input.h"
#include "os_gfx.h"
#include "os_oled.h"
#include "os_status.h"
#include "sys_uart.h"
#include "sys_time.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "esp_console.h"
#include "esp_system.h"
#include "esp_log.h"
@@ -16,6 +18,57 @@
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)
{
if (argc < 2) {
@@ -170,6 +223,22 @@ esp_err_t sys_cli_init(void)
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 = {
.command = "ps",
.help = "List active FreeRTOS tasks and memory",
+5 -1
View File
@@ -1,8 +1,9 @@
idf_component_register(
idf_component_register(
SRCS
"sys_event.c"
"sys_status.c"
"sys_uart.c"
"sys_time.c"
INCLUDE_DIRS
"include"
REQUIRES
@@ -13,4 +14,7 @@ idf_component_register(
driver
esp_driver_gpio
esp_driver_uart
nvs_flash
lwip
esp_netif
)
+61
View File
@@ -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
+184
View File
@@ -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;
}
+3 -1
View File
@@ -1,5 +1,6 @@
#include "sys_wifi.h"
#include "sys_wifi.h"
#include "web_server.h"
#include "sys_time.h"
#include "os_event.h"
#include "os_status.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;
set_status(WIFI_STATE_CONNECTED, rssi);
web_server_start();
sys_time_start_sntp();
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));
}
+2 -2
View File
@@ -1,4 +1,4 @@
#include "web_server_pages.h"
#include "web_server_pages.h"
#include "sys_uart.h"
#include "esp_log.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');"
"pollData();"
"try{fetch('/time/sync?epoch='+Math.floor(Date.now()/1000));}catch(e){}pollData();"
"</script></body></html>";
static esp_err_t serial_page_get(httpd_req_t *request)
+4 -4
View File
@@ -1,4 +1,4 @@
#include "web_server_pages.h"
#include "web_server_pages.h"
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_wifi.h"
@@ -91,7 +91,7 @@ static const char WIFI_HTML[] =
" <label for=\"password\">Wi-Fi Password</label>"
" <div class=\"pass-wrap\">"
" <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>"
" <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');"
"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 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';}}"
"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');}}"
"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>";
static void copy_string(uint8_t *destination, size_t size, const char *source)
+41 -2
View File
@@ -1,11 +1,42 @@
#include "web_server.h"
#include "web_server.h"
#include "web_server_pages.h"
#include "sys_time.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
static const char *TAG = "SYS_WEB";
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)
{
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_ota_page(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;
}