#include "time_manager.h" #include "compressor.h" #include "config.h" #include "rtc_ds3231.h" #include static bool g_time_synced = false; static bool g_tz_set = false; static bool g_rtc_present = false; static uint32_t g_last_sync_utc = 0; static constexpr uint32_t kMinValidEpoch = 1672531200UL; // 2023-01-01 static constexpr uint32_t kMaxValidEpoch = 4102444800UL; // 2100-01-01 static void note_last_sync(uint32_t epoch) { if (epoch == 0) { return; } g_last_sync_utc = epoch; } void time_receiver_init(const char *ntp_server_1, const char *ntp_server_2) { const char *server1 = (ntp_server_1 && ntp_server_1[0] != '\0') ? ntp_server_1 : "pool.ntp.org"; const char *server2 = (ntp_server_2 && ntp_server_2[0] != '\0') ? ntp_server_2 : "time.nist.gov"; configTime(0, 0, server1, server2); if (!g_tz_set) { setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); tzset(); g_tz_set = true; } } uint32_t time_get_utc() { time_t now = time(nullptr); if (now < 1672531200) { return 0; } if (!g_time_synced) { g_time_synced = true; note_last_sync(static_cast(now)); } return static_cast(now); } bool time_is_synced() { return g_time_synced || time_get_utc() > 0; } void time_set_utc(uint32_t epoch) { if (!g_tz_set) { setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); tzset(); g_tz_set = true; } struct timeval tv; tv.tv_sec = epoch; tv.tv_usec = 0; settimeofday(&tv, nullptr); g_time_synced = true; note_last_sync(epoch); if (g_rtc_present) { rtc_ds3231_set_epoch(epoch); } } bool time_send_timesync(uint16_t device_id_short) { uint32_t epoch = time_get_utc(); if (epoch == 0) { return false; } char payload_str[32]; snprintf(payload_str, sizeof(payload_str), "T:%lu", static_cast(epoch)); uint8_t compressed[LORA_MAX_PAYLOAD]; size_t compressed_len = 0; if (!compressBuffer(reinterpret_cast(payload_str), strlen(payload_str), compressed, sizeof(compressed), compressed_len)) { return false; } LoraPacket pkt = {}; pkt.protocol_version = PROTOCOL_VERSION; pkt.role = DeviceRole::Receiver; pkt.device_id_short = device_id_short; pkt.payload_type = PayloadType::TimeSync; pkt.payload_len = compressed_len; memcpy(pkt.payload, compressed, compressed_len); bool ok = lora_send(pkt); if (ok) { lora_receive_continuous(); } return ok; } bool time_handle_timesync_payload(const uint8_t *payload, size_t len) { uint8_t decompressed[64]; size_t decompressed_len = 0; if (!decompressBuffer(payload, len, decompressed, sizeof(decompressed), decompressed_len)) { return false; } if (decompressed_len >= sizeof(decompressed)) { return false; } decompressed[decompressed_len] = '\0'; if (decompressed_len < 3 || decompressed[0] != 'T' || decompressed[1] != ':') { return false; } uint32_t epoch = static_cast(strtoul(reinterpret_cast(decompressed + 2), nullptr, 10)); if (epoch == 0) { return false; } time_set_utc(epoch); return true; } void time_get_local_hhmm(char *out, size_t out_len) { if (!time_is_synced()) { snprintf(out, out_len, "--:--"); return; } time_t now = time(nullptr); struct tm timeinfo; localtime_r(&now, &timeinfo); snprintf(out, out_len, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); } void time_rtc_init() { if (!ENABLE_DS3231) { g_rtc_present = false; return; } g_rtc_present = rtc_ds3231_init(); } bool time_try_load_from_rtc() { if (!g_rtc_present) { return false; } if (time_is_synced()) { return true; } uint32_t epoch = 0; if (!rtc_ds3231_read_epoch(epoch) || epoch == 0) { if (SERIAL_DEBUG_MODE) { Serial.println("rtc: read failed"); } return false; } bool valid = epoch >= kMinValidEpoch && epoch <= kMaxValidEpoch; if (SERIAL_DEBUG_MODE) { Serial.printf("rtc: epoch=%lu %s\n", static_cast(epoch), valid ? "accepted" : "rejected"); } if (!valid) { return false; } time_set_utc(epoch); return true; } bool time_rtc_present() { return g_rtc_present; } uint32_t time_get_last_sync_utc() { return g_last_sync_utc; } uint32_t time_get_last_sync_age_sec() { if (!time_is_synced()) { return 0; } if (g_last_sync_utc == 0) { return 0; } uint32_t now = time_get_utc(); return now > g_last_sync_utc ? now - g_last_sync_utc : 0; }