new release

This commit is contained in:
2026-05-15 12:20:47 +01:00
parent 286028b6a8
commit d0d431daf2
440 changed files with 2776 additions and 2462 deletions

View File

@@ -0,0 +1,12 @@
set(srcs
"src/time_service.c"
"src/time_service_events.c"
)
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
REQUIRES esp_event network
PRIV_REQUIRES esp_netif lwip freertos
)

View File

@@ -0,0 +1,2 @@
version: "1.0.0"
description: Time synchronization and timezone service

View File

@@ -0,0 +1,63 @@
#pragma once
#include <stdbool.h>
#include <time.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Inicializa o serviço de hora.
*
* Responsabilidades do módulo:
* - definir a timezone local do equipamento
* - registar handlers dos eventos de rede
* - arrancar SNTP quando a STA obtém IP
* - expor o estado atual da sincronização
*/
void time_service_init(void);
/**
* @brief Indica se já houve pelo menos uma sincronização SNTP com sucesso.
*/
bool time_service_is_synced(void);
/**
* @brief Indica se existe uma tentativa de sincronização em curso.
*/
bool time_service_is_sync_in_progress(void);
/**
* @brief Retorna true se a hora atual parece válida.
*
* Critério simples: ano >= 2024.
*/
bool time_service_has_valid_time(void);
/**
* @brief Força uma nova espera pela sincronização atual.
*
* Não reinicializa o SNTP. Apenas lança uma task curta que aguarda pela sync.
*/
esp_err_t time_service_force_resync(void);
/**
* @brief Nome IANA da timezone alvo do produto.
*/
const char *time_service_get_tz_name(void);
/**
* @brief String POSIX usada por setenv("TZ", ...).
*/
const char *time_service_get_tz_posix(void);
/**
* @brief Obtém o epoch atual.
*/
time_t time_service_get_epoch(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,32 @@
#pragma once
#include <stdbool.h>
#include <time.h>
#include "esp_event.h"
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(TIME_SERVICE_EVENTS);
typedef enum {
TIME_SERVICE_EVENT_INIT = 0,
TIME_SERVICE_EVENT_SYNC_STARTED,
TIME_SERVICE_EVENT_SYNC_OK,
TIME_SERVICE_EVENT_SYNC_TIMEOUT,
TIME_SERVICE_EVENT_NETWORK_LOST,
} time_service_event_id_t;
typedef struct {
bool timezone_configured;
bool sntp_initialized;
bool sync_in_progress;
bool synced;
bool valid_time;
time_t now;
} time_service_state_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,267 @@
#include "time_service.h"
#include "time_service_events.h"
#include "network_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif_sntp.h"
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
static const char *TAG = "time_service";
/*
* Portugal continental / Madeira / Canárias partilham esta regra POSIX em ESP-IDF.
* Fonte da equivalência Europe/Lisbon -> POSIX: documentação Espressif RainMaker.
*/
#define TIME_SERVICE_TZ_NAME "Europe/Lisbon"
#define TIME_SERVICE_TZ_POSIX "WET0WEST,M3.5.0/1,M10.5.0"
#define TIME_SERVICE_SYNC_WAIT_MS 15000
#define TIME_SERVICE_MIN_VALID_YEAR 2024
typedef struct {
bool timezone_configured;
bool sntp_initialized;
bool sync_in_progress;
bool synced;
bool inited;
} time_service_runtime_t;
static time_service_runtime_t s_rt = {
.timezone_configured = false,
.sntp_initialized = false,
.sync_in_progress = false,
.synced = false,
.inited = false,
};
static TaskHandle_t s_sync_wait_task = NULL;
static void publish_state(time_service_event_id_t id)
{
time_service_state_t st = {
.timezone_configured = s_rt.timezone_configured,
.sntp_initialized = s_rt.sntp_initialized,
.sync_in_progress = s_rt.sync_in_progress,
.synced = s_rt.synced,
.valid_time = time_service_has_valid_time(),
.now = time(NULL),
};
(void)esp_event_post(TIME_SERVICE_EVENTS, id, &st, sizeof(st), portMAX_DELAY);
}
static void set_portugal_timezone(void)
{
setenv("TZ", TIME_SERVICE_TZ_POSIX, 1);
tzset();
s_rt.timezone_configured = true;
ESP_LOGI(TAG, "Timezone set: %s (%s)", TIME_SERVICE_TZ_NAME, TIME_SERVICE_TZ_POSIX);
}
static void time_sync_notification_cb(struct timeval *tv)
{
(void)tv;
s_rt.synced = true;
s_rt.sync_in_progress = false;
time_t now = time(NULL);
struct tm tm_now = {0};
localtime_r(&now, &tm_now);
ESP_LOGI(TAG, "Time synchronized: %04d-%02d-%02d %02d:%02d:%02d",
tm_now.tm_year + 1900,
tm_now.tm_mon + 1,
tm_now.tm_mday,
tm_now.tm_hour,
tm_now.tm_min,
tm_now.tm_sec);
publish_state(TIME_SERVICE_EVENT_SYNC_OK);
}
static void ensure_sntp_initialized(void)
{
if (s_rt.sntp_initialized)
return;
esp_sntp_config_t cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
cfg.sync_cb = time_sync_notification_cb;
esp_err_t err = esp_netif_sntp_init(&cfg);
if (err == ESP_OK)
{
s_rt.sntp_initialized = true;
ESP_LOGI(TAG, "SNTP initialized");
return;
}
if (err == ESP_ERR_INVALID_STATE)
{
s_rt.sntp_initialized = true;
ESP_LOGW(TAG, "SNTP already initialized elsewhere");
return;
}
ESP_LOGE(TAG, "esp_netif_sntp_init failed: %s", esp_err_to_name(err));
}
static void sync_wait_task(void *arg)
{
(void)arg;
esp_err_t err = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(TIME_SERVICE_SYNC_WAIT_MS));
if (err == ESP_OK)
{
s_rt.synced = true;
s_rt.sync_in_progress = false;
ESP_LOGI(TAG, "Initial SNTP sync complete");
publish_state(TIME_SERVICE_EVENT_SYNC_OK);
}
else
{
s_rt.sync_in_progress = false;
ESP_LOGW(TAG, "SNTP sync wait timeout/error: %s", esp_err_to_name(err));
publish_state(TIME_SERVICE_EVENT_SYNC_TIMEOUT);
}
s_sync_wait_task = NULL;
vTaskDelete(NULL);
}
static esp_err_t start_sync_wait(void)
{
if (!s_rt.sntp_initialized)
return ESP_ERR_INVALID_STATE;
if (s_rt.sync_in_progress)
return ESP_ERR_INVALID_STATE;
s_rt.sync_in_progress = true;
publish_state(TIME_SERVICE_EVENT_SYNC_STARTED);
BaseType_t ok = xTaskCreate(
sync_wait_task,
"time_sync_wait",
4096,
NULL,
4,
&s_sync_wait_task);
if (ok != pdPASS)
{
s_rt.sync_in_progress = false;
s_sync_wait_task = NULL;
return ESP_FAIL;
}
return ESP_OK;
}
static void network_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
(void)base;
(void)data;
if (id == NETWORK_EVENT_STA_GOT_IP)
{
ESP_LOGI(TAG, "NETWORK_EVENT_STA_GOT_IP -> init/check SNTP");
ensure_sntp_initialized();
if (!s_rt.synced && !s_rt.sync_in_progress)
{
esp_err_t err = start_sync_wait();
if (err != ESP_OK)
{
ESP_LOGW(TAG, "start_sync_wait failed: %s", esp_err_to_name(err));
}
}
}
else if (id == NETWORK_EVENT_STA_LOST_IP)
{
ESP_LOGW(TAG, "NETWORK_EVENT_STA_LOST_IP");
publish_state(TIME_SERVICE_EVENT_NETWORK_LOST);
}
}
void time_service_init(void)
{
if (s_rt.inited)
{
ESP_LOGW(TAG, "time_service_init called twice");
return;
}
set_portugal_timezone();
ESP_ERROR_CHECK(esp_event_handler_register(
NETWORK_EVENTS,
NETWORK_EVENT_STA_GOT_IP,
network_event_handler,
NULL));
ESP_ERROR_CHECK(esp_event_handler_register(
NETWORK_EVENTS,
NETWORK_EVENT_STA_LOST_IP,
network_event_handler,
NULL));
s_rt.inited = true;
publish_state(TIME_SERVICE_EVENT_INIT);
}
bool time_service_is_synced(void)
{
return s_rt.synced;
}
bool time_service_is_sync_in_progress(void)
{
return s_rt.sync_in_progress;
}
bool time_service_has_valid_time(void)
{
time_t now = time(NULL);
if (now <= 0)
return false;
struct tm tm_now = {0};
localtime_r(&now, &tm_now);
return (tm_now.tm_year + 1900) >= TIME_SERVICE_MIN_VALID_YEAR;
}
esp_err_t time_service_force_resync(void)
{
if (!s_rt.sntp_initialized)
return ESP_ERR_INVALID_STATE;
if (s_rt.sync_in_progress)
return ESP_ERR_INVALID_STATE;
return start_sync_wait();
}
const char *time_service_get_tz_name(void)
{
return TIME_SERVICE_TZ_NAME;
}
const char *time_service_get_tz_posix(void)
{
return TIME_SERVICE_TZ_POSIX;
}
time_t time_service_get_epoch(void)
{
return time(NULL);
}

View File

@@ -0,0 +1,3 @@
#include "time_service_events.h"
ESP_EVENT_DEFINE_BASE(TIME_SERVICE_EVENTS);