/*
|
|
|
|
NTP MODULE
|
|
|
|
Based on esp8266 / esp32 configTime and C date and time functions:
|
|
- https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/NTP-TZ-DST/NTP-TZ-DST.ino
|
|
- https://www.nongnu.org/lwip/2_1_x/group__sntp.html
|
|
- man 3 ctime
|
|
|
|
Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
|
|
|
|
*/
|
|
|
|
#include "espurna.h"
|
|
|
|
#if NTP_SUPPORT
|
|
|
|
#include <Arduino.h>
|
|
#include <coredecls.h>
|
|
|
|
#include <ctime>
|
|
#include <errno.h>
|
|
#include <lwip/apps/sntp.h>
|
|
#include <TZ.h>
|
|
|
|
#include <algorithm>
|
|
#include <forward_list>
|
|
|
|
static_assert(
|
|
(SNTP_SERVER_DNS == 1),
|
|
"lwip must be configured with SNTP_SERVER_DNS"
|
|
);
|
|
|
|
#include "ntp.h"
|
|
#include "ntp_timelib.h"
|
|
#include "utils.h"
|
|
#include "ws.h"
|
|
|
|
namespace espurna {
|
|
namespace ntp {
|
|
namespace {
|
|
|
|
template <typename T>
|
|
T randomizeDuration(T base, T offset) {
|
|
return T(::randomNumber(base.count(), (base + offset).count()));
|
|
}
|
|
|
|
namespace build {
|
|
|
|
// per the lwip recommendations, we delay the actual sntp app start
|
|
// by default, this is expected to be `LWIP_RAND % 5000` (aka [0...5) seconds)
|
|
static constexpr auto StartMin = espurna::duration::Seconds { 1 };
|
|
static constexpr auto StartMax = espurna::duration::Seconds { espurna::duration::Minutes { 5 } };
|
|
static constexpr espurna::duration::Seconds StartDelay { NTP_START_DELAY };
|
|
static_assert((StartMin <= StartDelay) && (StartDelay <= StartMax), "");
|
|
|
|
// per the https://datatracker.ietf.org/doc/html/rfc4330
|
|
// > 10. Best practices
|
|
// > 1. A client MUST NOT under any conditions use a poll interval less
|
|
// > than 15 seconds.
|
|
// (and notice that in case things break, sntp app itself will handle retries)
|
|
static constexpr auto UpdateMin = espurna::duration::Seconds { 15 };
|
|
static constexpr auto UpdateMax = espurna::duration::Seconds { espurna::duration::Days { 30 } };
|
|
static constexpr espurna::duration::Seconds UpdateInterval { NTP_UPDATE_INTERVAL };
|
|
static_assert((UpdateMin <= UpdateInterval) && (UpdateInterval <= UpdateMax), "");
|
|
|
|
// We don't adjust update time(s) more than once, unlike NTP clients such as chrony or timesyncd.
|
|
// These offsets are applied to both values on boot and persist until the device is reset.
|
|
static constexpr auto StartRandomOffset = espurna::duration::Seconds { 10 };
|
|
static constexpr auto UpdateRandomOffset = espurna::duration::Seconds { 300 };
|
|
|
|
const __FlashStringHelper* server() {
|
|
return F(NTP_SERVER);
|
|
}
|
|
|
|
const char* tz() {
|
|
return NTP_TIMEZONE;
|
|
}
|
|
|
|
constexpr bool dhcp() {
|
|
return 1 == (NTP_DHCP_SERVER);
|
|
}
|
|
|
|
} // namespace build
|
|
|
|
namespace settings {
|
|
|
|
espurna::duration::Seconds startDelay() {
|
|
return std::clamp(
|
|
getSetting("ntpStartDelay", build::StartDelay),
|
|
build::StartMin, build::StartMax);
|
|
}
|
|
|
|
espurna::duration::Seconds randomStartDelay() {
|
|
return randomizeDuration(startDelay(), build::StartRandomOffset);
|
|
}
|
|
|
|
espurna::duration::Seconds updateInterval() {
|
|
return std::clamp(
|
|
getSetting("ntpUpdateIntvl", build::UpdateInterval),
|
|
build::UpdateMin, build::UpdateMax);
|
|
}
|
|
|
|
espurna::duration::Seconds randomUpdateInterval() {
|
|
return randomizeDuration(updateInterval(), build::UpdateRandomOffset);
|
|
}
|
|
|
|
// as either DNS name or IP address
|
|
String server() {
|
|
return getSetting("ntpServer", build::server());
|
|
}
|
|
|
|
void server(const String& value) {
|
|
setSetting("ntpServer", value);
|
|
}
|
|
|
|
// POSIX TZ variable, ref.
|
|
// - https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03
|
|
// - https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
|
|
String tz() {
|
|
return getSetting("ntpTZ", build::tz());
|
|
}
|
|
|
|
// in case DHCP packet contains a SNTP option, switch to that server instead of the one from settings
|
|
bool dhcp() {
|
|
return getSetting("ntpDhcp", build::dhcp());
|
|
}
|
|
|
|
void dhcp(bool value) {
|
|
setSetting("ntpDhcp", value);
|
|
}
|
|
|
|
} // namespace settings
|
|
|
|
namespace internal {
|
|
|
|
duration::Seconds start_delay { build::StartDelay };
|
|
duration::Seconds update_interval { build::UpdateInterval };
|
|
|
|
} // namespace internal
|
|
|
|
// With esp8266's lwipopts.h, we are allowed to configure SNTP delays at runtime
|
|
// These are weak, if we don't redefine it will fallback to Core's default ones
|
|
// (notice that our settings are in *seconds*, while SNTP expects *milliseconds*)
|
|
|
|
static_assert(sizeof(decltype(internal::start_delay)::rep) <= sizeof(uint32_t), "");
|
|
static_assert(sizeof(decltype(internal::update_interval)::rep) <= sizeof(uint32_t), "");
|
|
|
|
uint32_t start_delay() {
|
|
return duration::Milliseconds(internal::start_delay).count();
|
|
}
|
|
|
|
uint32_t update_interval() {
|
|
return duration::Milliseconds(internal::update_interval).count();
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace ntp
|
|
} // namespace espurna
|
|
|
|
// aka `SNTP_STARTUP_DELAY_FUNC`
|
|
uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() {
|
|
return espurna::ntp::start_delay();
|
|
}
|
|
|
|
// aka `SNTP_UPDATE_DELAY`
|
|
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {
|
|
return espurna::ntp::update_interval();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
namespace espurna {
|
|
namespace ntp {
|
|
namespace {
|
|
|
|
struct Status {
|
|
Status() = default;
|
|
|
|
void update(time_t timestamp) {
|
|
_synced = true;
|
|
_timestamp = timestamp;
|
|
}
|
|
|
|
bool synced() const {
|
|
return _synced;
|
|
}
|
|
|
|
time_t timestamp() const {
|
|
return _timestamp;
|
|
}
|
|
|
|
private:
|
|
bool _synced { false };
|
|
time_t _timestamp { 0 };
|
|
};
|
|
|
|
|
|
bool setTimestamp(time_t ts) {
|
|
const timeval tv {
|
|
.tv_sec = ts,
|
|
.tv_usec = 0
|
|
};
|
|
|
|
return EINVAL != settimeofday(&tv, nullptr);
|
|
}
|
|
|
|
namespace internal {
|
|
|
|
Status status;
|
|
String server;
|
|
|
|
} // namespace internal
|
|
|
|
bool synced() {
|
|
return internal::status.synced();
|
|
}
|
|
|
|
namespace parse {
|
|
|
|
struct Result {
|
|
Result() = default;
|
|
explicit Result(time_t value) :
|
|
_result(true),
|
|
_timestamp(value)
|
|
{}
|
|
|
|
Result& operator=(time_t value) {
|
|
_result = true;
|
|
_timestamp = value;
|
|
return *this;
|
|
}
|
|
|
|
explicit operator bool() const {
|
|
return _result;
|
|
}
|
|
|
|
time_t timestamp() const {
|
|
return _timestamp;
|
|
}
|
|
|
|
private:
|
|
bool _result { false };
|
|
time_t _timestamp;
|
|
};
|
|
|
|
namespace internal {
|
|
|
|
template <typename T>
|
|
T convert(const char*, char**, int);
|
|
|
|
template <>
|
|
[[gnu::unused]] inline long convert(const char* p, char** endp, int base) {
|
|
return strtol(p, endp, base);
|
|
}
|
|
|
|
template <>
|
|
[[gnu::unused]] inline long long convert(const char* p, char** endp, int base) {
|
|
return strtoll(p, endp, base);
|
|
}
|
|
|
|
} // namespace internal
|
|
|
|
Result timestamp(const String& value) {
|
|
Result out;
|
|
|
|
const char* p { value.c_str() };
|
|
char* endp { nullptr };
|
|
|
|
auto result = internal::convert<time_t>(p, &endp, 10);
|
|
if (!endp || (endp == p)) {
|
|
return out;
|
|
}
|
|
|
|
out = result;
|
|
return out;
|
|
}
|
|
|
|
} // namespace parse
|
|
|
|
namespace timelib {
|
|
namespace internal {
|
|
|
|
time_t last_timestamp { 0 };
|
|
tm local;
|
|
tm utc;
|
|
|
|
void cache(time_t value) {
|
|
if (last_timestamp != value) {
|
|
last_timestamp = value;
|
|
localtime_r(&last_timestamp, &local);
|
|
gmtime_r(&last_timestamp, &utc);
|
|
}
|
|
}
|
|
|
|
} // namespace internal
|
|
|
|
// This is based on the Timelib implementation, which is slightly different from POSIX
|
|
// This remains (mostly) for historical reasons, since we don't want to break existing config for no reason
|
|
|
|
int hour(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_hour;
|
|
}
|
|
|
|
int minute(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_min;
|
|
}
|
|
|
|
int second(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_sec;
|
|
}
|
|
|
|
int day(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_mday;
|
|
}
|
|
|
|
// `tm.tm_wday` range is 0..6, TimeLib is 1..7
|
|
int weekday(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_wday + 1;
|
|
}
|
|
|
|
// `tm.tm_mon` range is 0..11, TimeLib range is 1..12
|
|
int month(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_mon + 1;
|
|
}
|
|
|
|
int year(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::local.tm_year + 1900;
|
|
}
|
|
|
|
int utc_hour(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_hour;
|
|
}
|
|
|
|
int utc_minute(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_min;
|
|
}
|
|
|
|
int utc_second(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_sec;
|
|
}
|
|
|
|
int utc_day(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_mday;
|
|
}
|
|
|
|
int utc_weekday(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_wday + 1;
|
|
}
|
|
|
|
int utc_month(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_mon + 1;
|
|
}
|
|
|
|
int utc_year(time_t ts) {
|
|
internal::cache(ts);
|
|
return internal::utc.tm_year + 1900;
|
|
}
|
|
|
|
time_t now() {
|
|
return ::time(nullptr);
|
|
}
|
|
|
|
} // namespace timelib
|
|
|
|
#if WEB_SUPPORT
|
|
namespace web {
|
|
|
|
bool onKeyCheck(StringView key, const JsonVariant&) {
|
|
return espurna::settings::query::samePrefix(key, STRING_VIEW("ntp"));
|
|
}
|
|
|
|
void onVisible(JsonObject& root) {
|
|
wsPayloadModule(root, PSTR("ntp"));
|
|
}
|
|
|
|
void onData(JsonObject& root) {
|
|
root["ntpStatus"] = synced();
|
|
}
|
|
|
|
void onConnected(JsonObject& root) {
|
|
root["ntpServer"] = settings::server();
|
|
root["ntpTZ"] = settings::tz();
|
|
}
|
|
|
|
} // namespace web
|
|
#endif
|
|
|
|
String activeServer() {
|
|
String server;
|
|
|
|
server = sntp_getservername(0);
|
|
if (!server.length()) {
|
|
auto ip = IPAddress(sntp_getserver(0));
|
|
if (ip) {
|
|
server = ip.toString();
|
|
}
|
|
}
|
|
|
|
return server;
|
|
}
|
|
|
|
String datetime(tm* timestruct) {
|
|
char buffer[32];
|
|
snprintf_P(buffer, sizeof(buffer),
|
|
PSTR("%04d-%02d-%02d %02d:%02d:%02d"),
|
|
timestruct->tm_year + 1900,
|
|
timestruct->tm_mon + 1,
|
|
timestruct->tm_mday,
|
|
timestruct->tm_hour,
|
|
timestruct->tm_min,
|
|
timestruct->tm_sec
|
|
);
|
|
return String(buffer);
|
|
}
|
|
|
|
String datetime(time_t ts) {
|
|
tm timestruct;
|
|
localtime_r(&ts, ×truct);
|
|
return datetime(×truct);
|
|
}
|
|
|
|
String datetime() {
|
|
return synced() ? datetime(timelib::now()) : String();
|
|
}
|
|
|
|
NtpInfo makeInfo() {
|
|
NtpInfo result;
|
|
|
|
const auto sync = internal::status.timestamp();
|
|
tm sync_datetime{};
|
|
gmtime_r(&sync, &sync_datetime);
|
|
result.sync = datetime(&sync_datetime);
|
|
|
|
const auto now = timelib::now();
|
|
result.now = now;
|
|
|
|
tm now_datetime{};
|
|
gmtime_r(&now, &now_datetime);
|
|
result.utc = datetime(&now_datetime);
|
|
|
|
const char* cfg_tz = getenv("TZ");
|
|
if ((cfg_tz != nullptr) && (strcmp(cfg_tz, "UTC0") != 0)) {
|
|
tm local_datetime{};
|
|
localtime_r(&now, &local_datetime);
|
|
result.local = datetime(&local_datetime);
|
|
result.tz = cfg_tz;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#if TERMINAL_SUPPORT
|
|
namespace terminal {
|
|
|
|
void report(Print& out) {
|
|
const auto info = makeInfo();
|
|
out.printf_P(
|
|
PSTR("server: %s\n"
|
|
"update every: %u (s)\n"
|
|
"last synced: %s\n"
|
|
"utc time: %s\n"),
|
|
internal::server.c_str(),
|
|
internal::update_interval.count(),
|
|
info.sync.c_str(),
|
|
info.utc.c_str());
|
|
|
|
if (info.tz.length()) {
|
|
out.printf_P(PSTR("local time: %s (%s)\n"),
|
|
info.local.c_str(),
|
|
info.tz.c_str());
|
|
}
|
|
}
|
|
|
|
namespace commands {
|
|
|
|
PROGMEM_STRING(Ntp, "NTP");
|
|
|
|
void ntp(::terminal::CommandContext&& ctx) {
|
|
if (synced()) {
|
|
report(ctx.output);
|
|
terminalOK(ctx);
|
|
return;
|
|
}
|
|
|
|
terminalError(ctx, F("NTP not synced"));
|
|
}
|
|
|
|
PROGMEM_STRING(Sync, "NTP.SYNC");
|
|
|
|
void sync(::terminal::CommandContext&& ctx) {
|
|
if (synced()) {
|
|
sntp_stop();
|
|
sntp_init();
|
|
terminalOK(ctx);
|
|
return;
|
|
}
|
|
|
|
terminalError(ctx, F("NTP waiting for initial sync"));
|
|
}
|
|
|
|
PROGMEM_STRING(Set, "NTP.SET");
|
|
|
|
[[gnu::unused]]
|
|
void set_simple(::terminal::CommandContext&& ctx) {
|
|
if (ctx.argv.size() != 2) {
|
|
terminalError(ctx, F("NTP.SET <TIME>"));
|
|
return;
|
|
}
|
|
|
|
auto value = parse::timestamp(ctx.argv[1]);
|
|
if (value && setTimestamp(value.timestamp())) {
|
|
internal::status.update(value.timestamp());
|
|
terminalOK(ctx);
|
|
return;
|
|
}
|
|
|
|
terminalError(ctx, F("Invalid timestamp"));
|
|
}
|
|
|
|
// TODO: strptime & mktime is around ~3.7Kb
|
|
[[gnu::unused]]
|
|
void set_strptime(::terminal::CommandContext&& ctx) {
|
|
if (ctx.argv.size() != 2) {
|
|
terminalError(ctx, F("NTP.SET <TIME>"));
|
|
return;
|
|
}
|
|
|
|
const char* const fmt = (ctx.argv[1].endsWith("Z"))
|
|
? "%Y-%m-%dT%H:%M:%SZ" : "%s";
|
|
|
|
tm out{};
|
|
if (strptime(ctx.argv[1].c_str(), fmt, &out) != nullptr) {
|
|
terminalError(ctx, F("Invalid time"));
|
|
return;
|
|
}
|
|
|
|
ctx.output.printf_P(PSTR("Setting time to %s\n"),
|
|
datetime(&out).c_str());
|
|
|
|
auto ts = mktime(&out);
|
|
setTimestamp(ts);
|
|
internal::status.update(ts);
|
|
|
|
terminalOK(ctx);
|
|
}
|
|
|
|
void set_time(::terminal::CommandContext&& ctx) {
|
|
#if 1
|
|
set_simple(std::move(ctx));
|
|
#else
|
|
set_strptime(std::move(ctx));
|
|
#endif
|
|
}
|
|
|
|
static constexpr ::terminal::Command List[] PROGMEM {
|
|
{Ntp, ntp},
|
|
{Sync, sync},
|
|
{Set, set_time},
|
|
};
|
|
|
|
} // namespace commands
|
|
|
|
void setup() {
|
|
espurna::terminal::add(commands::List);
|
|
}
|
|
|
|
} // namespace terminal
|
|
#endif
|
|
|
|
#if DEBUG_SUPPORT
|
|
namespace debug {
|
|
|
|
void report() {
|
|
if (!synced()) {
|
|
DEBUG_MSG_P(PSTR("[NTP] Not synced\n"));
|
|
return;
|
|
}
|
|
|
|
const auto info = makeInfo();
|
|
DEBUG_MSG_P(PSTR("[NTP] Server %s\n"), ntp::internal::server.c_str());
|
|
DEBUG_MSG_P(PSTR("[NTP] Last Sync %s (UTC)\n"), info.sync.c_str());
|
|
DEBUG_MSG_P(PSTR("[NTP] UTC Time %s\n"), info.utc.c_str());
|
|
|
|
if (info.tz.length()) {
|
|
DEBUG_MSG_P(PSTR("[NTP] Local Time %s (%s)\n"),
|
|
info.local.c_str(), info.tz.c_str());
|
|
}
|
|
}
|
|
|
|
void schedule_now() {
|
|
::espurnaRegisterOnceUnique(report);
|
|
}
|
|
|
|
} // namespace debug
|
|
#endif
|
|
|
|
namespace tick {
|
|
|
|
// Never allow delays less than a second, or greater than a minute
|
|
// (ref. Non-OS 3.1.1 os_timer_arm, actual minimal value is 5ms)
|
|
static constexpr espurna::duration::Seconds OffsetMin { 1 };
|
|
static constexpr espurna::duration::Seconds OffsetMax { 60 };
|
|
|
|
using Callbacks = std::forward_list<NtpTickCallback>;
|
|
|
|
namespace internal {
|
|
|
|
Callbacks callbacks;
|
|
timer::SystemTimer timer;
|
|
|
|
} // namespace internal
|
|
|
|
void add(NtpTickCallback callback) {
|
|
internal::callbacks.push_front(callback);
|
|
}
|
|
|
|
void schedule(espurna::duration::Seconds offset);
|
|
|
|
void callback() {
|
|
if (!synced()) {
|
|
schedule(OffsetMax);
|
|
return;
|
|
}
|
|
|
|
const auto now = timelib::now();
|
|
tm local_tm;
|
|
localtime_r(&now, &local_tm);
|
|
|
|
int now_hour = local_tm.tm_hour;
|
|
int now_minute = local_tm.tm_min;
|
|
|
|
static int last_hour { -1 };
|
|
static int last_minute { -1 };
|
|
|
|
// notify subscribers about each tick interval (note that both can happen simultaneously)
|
|
if (last_hour != now_hour) {
|
|
last_hour = now_hour;
|
|
for (auto& callback : internal::callbacks) {
|
|
callback(NtpTick::EveryHour);
|
|
}
|
|
}
|
|
|
|
if (last_minute != now_minute) {
|
|
last_minute = now_minute;
|
|
for (auto& callback : internal::callbacks) {
|
|
callback(NtpTick::EveryMinute);
|
|
}
|
|
}
|
|
|
|
// try to autocorrect each invocation
|
|
schedule(OffsetMax - espurna::duration::Seconds(local_tm.tm_sec));
|
|
}
|
|
|
|
void schedule_now() {
|
|
::espurnaRegisterOnceUnique(callback);
|
|
}
|
|
|
|
void schedule(espurna::duration::Seconds offset) {
|
|
if (!internal::timer) {
|
|
internal::timer.once(
|
|
std::clamp(offset, OffsetMin, OffsetMax),
|
|
schedule_now);
|
|
}
|
|
}
|
|
|
|
} // namespace tick
|
|
|
|
void onSystemTimeSynced() {
|
|
internal::status.update(::time(nullptr));
|
|
tick::schedule_now();
|
|
|
|
#if WEB_SUPPORT
|
|
wsPost(web::onData);
|
|
#endif
|
|
#if DEBUG_SUPPORT
|
|
debug::schedule_now();
|
|
#endif
|
|
}
|
|
|
|
namespace settings {
|
|
|
|
void convertLegacyOffsets() {
|
|
bool save { true };
|
|
bool found { false };
|
|
|
|
bool europe { true };
|
|
bool dst { true };
|
|
int offset { 60 };
|
|
|
|
espurna::settings::foreach(
|
|
[&](espurna::settings::kvs_type::KeyValueResult&& kv) {
|
|
using namespace espurna::settings::internal;
|
|
const auto key = kv.key.read();
|
|
if (key == F("ntpTZ")) {
|
|
save = false;
|
|
} else if (key == F("ntpOffset")) {
|
|
offset = convert<int>(kv.value.read());
|
|
found = true;
|
|
} else if (key == F("ntpDST")) {
|
|
dst = convert<bool>(kv.value.read());
|
|
found = true;
|
|
} else if (key == F("ntpRegion")) {
|
|
europe = (0 == convert<int>(kv.value.read()));
|
|
found = true;
|
|
}
|
|
});
|
|
|
|
if (save && found) {
|
|
// XXX: only expect offsets in hours
|
|
String custom { europe ? F("CET") : F("CST") };
|
|
custom.reserve(32);
|
|
|
|
if (offset > 0) {
|
|
custom += '-';
|
|
}
|
|
custom += abs(offset) / 60;
|
|
|
|
if (dst) {
|
|
custom += europe ? F("CEST") : F("EDT");
|
|
if (europe) {
|
|
custom += F(",M3.5.0,M10.5.0/3");
|
|
} else {
|
|
custom += F(",M3.2.0,M11.1.0");
|
|
}
|
|
}
|
|
|
|
setSetting(F("ntpTZ"), custom);
|
|
}
|
|
|
|
delSetting(F("ntpOffset"));
|
|
delSetting(F("ntpDST"));
|
|
delSetting(F("ntpRegion"));
|
|
}
|
|
|
|
} // namespace settings
|
|
|
|
void configure() {
|
|
// Ignore or accept the DHCP SNTP option
|
|
// When enabled, it is possible that lwip will replace the NTP server pointer from under us
|
|
sntp_servermode_dhcp(espurna::ntp::settings::dhcp());
|
|
|
|
// Note: TZ_... provided by the Core are already wrapped with PSTR(...)
|
|
// but, String() already handles every char pointer as a flash-string
|
|
auto cfg_tz = espurna::ntp::settings::tz();
|
|
const char* active_tz = getenv("TZ");
|
|
|
|
bool changed = cfg_tz != active_tz;
|
|
if (changed) {
|
|
if (cfg_tz.length()) {
|
|
setenv("TZ", cfg_tz.c_str(), 1);
|
|
} else {
|
|
unsetenv("TZ");
|
|
}
|
|
tzset();
|
|
}
|
|
|
|
const auto cfg_server = espurna::ntp::settings::server();
|
|
const auto active_server = activeServer();
|
|
changed = (cfg_server != active_server) || changed;
|
|
|
|
// We skip configTime() API since we already set the TZ just above
|
|
// (and most of the time we expect NTP server to proxy to multiple servers instead of defining more than one here)
|
|
if (changed) {
|
|
sntp_stop();
|
|
internal::server = cfg_server;
|
|
sntp_setservername(0, internal::server.c_str());
|
|
sntp_init();
|
|
DEBUG_MSG_P(PSTR("[NTP] Server: %s, TZ: %s\n"), cfg_server.c_str(),
|
|
cfg_tz.length() ? cfg_tz.c_str() : "UTC0");
|
|
}
|
|
}
|
|
|
|
void onStationModeGotIP(WiFiEventStationModeGotIP) {
|
|
if (!sntp_enabled()) {
|
|
return;
|
|
}
|
|
|
|
const auto server = activeServer();
|
|
if (!server.length()) {
|
|
DEBUG_MSG_P(PSTR("[NTP] Updating `ntpDhcp` to ignore the DHCP values\n"));
|
|
settings::dhcp(false);
|
|
sntp_servermode_dhcp(0);
|
|
::espurnaRegisterOnce(configure);
|
|
return;
|
|
}
|
|
|
|
if (!internal::server.length() || (server != internal::server)) {
|
|
DEBUG_MSG_P(PSTR("[NTP] Updating from DHCP option - \"ntpServer\" => \"%s\"\n"), server.c_str());
|
|
internal::server = server;
|
|
settings::server(server);
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
// Randomize both times to avoid simultaneous requests from multiple devices
|
|
internal::start_delay = settings::randomStartDelay();
|
|
internal::update_interval = settings::randomUpdateInterval();
|
|
DEBUG_MSG_P(PSTR("[NTP] Startup delay: %u (s), Update interval: %u (s)\n"),
|
|
internal::start_delay.count(), internal::update_interval.count());
|
|
|
|
// will be called every time after ntp syncs AND loop() finishes
|
|
settimeofday_cb(onSystemTimeSynced);
|
|
|
|
// make sure our logic does know about the actual server
|
|
// in case dhcp sends out ntp settings
|
|
static auto track_active_server = WiFi.onStationModeGotIP(onStationModeGotIP);
|
|
|
|
// generic configuration, always handled
|
|
::espurnaRegisterReload(configure);
|
|
settings::convertLegacyOffsets();
|
|
configure();
|
|
|
|
// optional modules, depends on the build flags
|
|
#if TERMINAL_SUPPORT
|
|
terminal::setup();
|
|
#endif
|
|
#if WEB_SUPPORT
|
|
wsRegister()
|
|
.onVisible(web::onVisible)
|
|
.onConnected(web::onConnected)
|
|
.onData(web::onData)
|
|
.onKeyCheck(web::onKeyCheck);
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace ntp
|
|
} // namespace espurna
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
int utc_hour(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_hour(ts);
|
|
}
|
|
|
|
int utc_minute(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_minute(ts);
|
|
}
|
|
|
|
int utc_second(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_second(ts);
|
|
}
|
|
|
|
int utc_day(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_day(ts);
|
|
}
|
|
|
|
int utc_weekday(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_weekday(ts);
|
|
}
|
|
|
|
int utc_month(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_month(ts);
|
|
}
|
|
|
|
int utc_year(time_t ts) {
|
|
return ::espurna::ntp::timelib::utc_year(ts);
|
|
}
|
|
|
|
int hour(time_t ts) {
|
|
return ::espurna::ntp::timelib::hour(ts);
|
|
}
|
|
|
|
int minute(time_t ts) {
|
|
return ::espurna::ntp::timelib::minute(ts);
|
|
}
|
|
|
|
int second(time_t ts) {
|
|
return ::espurna::ntp::timelib::second(ts);
|
|
}
|
|
|
|
int day(time_t ts) {
|
|
return ::espurna::ntp::timelib::day(ts);
|
|
}
|
|
|
|
int weekday(time_t ts) {
|
|
return ::espurna::ntp::timelib::weekday(ts);
|
|
}
|
|
|
|
int month(time_t ts) {
|
|
return ::espurna::ntp::timelib::month(ts);
|
|
}
|
|
|
|
int year(time_t ts) {
|
|
return ::espurna::ntp::timelib::year(ts);
|
|
}
|
|
|
|
time_t now() {
|
|
return ::espurna::ntp::timelib::now();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void ntpOnTick(NtpTickCallback callback) {
|
|
::espurna::ntp::tick::add(callback);
|
|
}
|
|
|
|
NtpInfo ntpInfo() {
|
|
return ::espurna::ntp::makeInfo();
|
|
}
|
|
|
|
String ntpDateTime(tm* timestruct) {
|
|
return ::espurna::ntp::datetime(timestruct);
|
|
}
|
|
|
|
String ntpDateTime(time_t ts) {
|
|
return ::espurna::ntp::datetime(ts);
|
|
}
|
|
|
|
String ntpDateTime() {
|
|
return ::espurna::ntp::datetime();
|
|
}
|
|
|
|
bool ntpSynced() {
|
|
return ::espurna::ntp::synced();
|
|
}
|
|
|
|
void ntpSetup() {
|
|
::espurna::ntp::setup();
|
|
}
|
|
|
|
#endif // NTP_SUPPORT
|