Time Shield Library
C++ library for working with time
Loading...
Searching...
No Matches
ntp_time_service.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2#pragma once
3#ifndef _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED
4#define _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED
5
6#include "config.hpp"
7
8#if TIME_SHIELD_ENABLE_NTP_CLIENT
9
10#include "ntp_client_pool.hpp"
12#include "time_utils.hpp"
13
14#include <atomic>
15#include <chrono>
16#include <condition_variable>
17#include <cstdlib>
18#include <cstdint>
19#include <memory>
20#include <mutex>
21#include <thread>
22#include <utility>
23#include <vector>
24
25namespace time_shield {
26
27 template <class RunnerT>
28 class NtpTimeServiceT;
29
30 namespace detail {
31 template <class RunnerT>
33
34 template <class RunnerT>
36
37#ifdef TIME_SHIELD_TEST_FAKE_NTP
40 public:
42 FakeNtpRunner() = default;
45
46 FakeNtpRunner(const FakeNtpRunner&) = delete;
48
51 stop();
52 }
53
55 bool start(std::chrono::milliseconds interval = std::chrono::seconds(30),
56 bool measure_immediately = true) {
57 if (m_is_running.load()) {
58 return false;
59 }
60 if (interval.count() <= 0) {
61 interval = std::chrono::milliseconds(1);
62 }
63 m_interval = interval;
64 m_is_stop_requested.store(false);
65 m_is_force_requested.store(false);
66 m_is_running.store(true);
67 m_measure_immediately = measure_immediately;
68 try {
69 m_thread = std::thread(&FakeNtpRunner::run_loop, this);
70 } catch (...) {
71 m_is_running.store(false);
72 return false;
73 }
74 return true;
75 }
76
78 bool start(int interval_ms, bool measure_immediately = true) {
79 return start(std::chrono::milliseconds(interval_ms), measure_immediately);
80 }
81
83 void stop() {
84 m_is_stop_requested.store(true);
85 m_cv.notify_all();
86 if (m_thread.joinable()) {
87 m_thread.join();
88 }
89 m_is_running.store(false);
90 }
91
93 bool running() const noexcept { return m_is_running.load(); }
94
97 if (!m_is_running.load()) {
98 return false;
99 }
100 m_is_force_requested.store(true);
101 m_cv.notify_one();
102 return true;
103 }
104
106 bool measure_now() {
107 return do_measure();
108 }
109
111 int64_t offset_us() const noexcept { return m_offset_us.load(); }
113 int64_t utc_time_us() const noexcept { return now_realtime_us() + m_offset_us.load(); }
115 int64_t utc_time_ms() const noexcept { return utc_time_us() / 1000; }
117 int64_t utc_time_sec() const noexcept { return utc_time_us() / 1000000; }
118
120 bool last_measure_ok() const noexcept { return m_last_measure_ok.load(); }
122 uint64_t measure_count() const noexcept { return m_measure_count.load(); }
124 uint64_t fail_count() const noexcept { return m_fail_count.load(); }
126 int64_t last_update_realtime_us() const noexcept { return m_last_update_realtime_us.load(); }
128 int64_t last_success_realtime_us() const noexcept { return m_last_success_realtime_us.load(); }
129
131 std::vector<NtpSample> last_samples() const { return {}; }
132
133 private:
135 void run_loop() {
136 const auto sleep_interval = m_interval;
137 bool is_first = m_measure_immediately;
138 while (!m_is_stop_requested.load()) {
139 if (is_first) {
140 do_measure();
141 is_first = false;
142 } else {
143 std::unique_lock<std::mutex> lk(m_cv_mtx);
144 m_cv.wait_for(lk, sleep_interval, [this]() {
145 return m_is_stop_requested.load() || m_is_force_requested.load();
146 });
147 if (m_is_stop_requested.load()) {
148 break;
149 }
150 m_is_force_requested.store(false);
151 do_measure();
152 }
153 }
154 m_is_running.store(false);
155 }
156
158 bool do_measure() {
159 const uint64_t count = m_measure_count.fetch_add(1) + 1;
160 m_offset_us.store(static_cast<int64_t>(count * 1000));
161 m_last_measure_ok.store(true);
162 const int64_t now = now_realtime_us();
165 return true;
166 }
167
168 private:
169 std::chrono::milliseconds m_interval{std::chrono::seconds(30)};
171
172 std::thread m_thread;
173 std::condition_variable m_cv;
174 std::mutex m_cv_mtx;
175
176 std::atomic<bool> m_is_running{false};
177 std::atomic<bool> m_is_stop_requested{false};
178 std::atomic<bool> m_is_force_requested{false};
179
180 std::atomic<bool> m_last_measure_ok{false};
181 std::atomic<uint64_t> m_measure_count{0};
182 std::atomic<uint64_t> m_fail_count{0};
183 std::atomic<int64_t> m_last_update_realtime_us{0};
184 std::atomic<int64_t> m_last_success_realtime_us{0};
185 std::atomic<int64_t> m_offset_us{0};
186 };
187#endif // _TIME_SHIELD_TEST_FAKE_NTP
188 } // namespace detail
189
198 template <class RunnerT>
200 friend struct detail::NtpTimeServiceSingleton<RunnerT>;
201 friend struct detail::NtpTimeServiceTestAccess<RunnerT>;
202
203 public:
209
212
215 bool init() {
217 }
218
223 bool init(std::chrono::milliseconds interval, bool measure_immediately = true) {
225 return false;
226 }
227
228 std::unique_ptr<RunnerT> local_runner;
229 std::chrono::milliseconds start_interval = interval;
230 bool use_measure_immediately = measure_immediately;
231 {
232 std::unique_lock<std::mutex> lk(m_mtx);
233 while (is_transitioning_locked()) {
234 m_cv.wait(lk);
235 }
237 return false;
238 }
240 return true;
241 }
242 if (start_interval.count() <= 0) {
243 start_interval = std::chrono::milliseconds(1);
244 }
245 m_interval = start_interval;
246 m_measure_immediately = use_measure_immediately;
248
249 local_runner = build_runner_locked();
250 if (!local_runner) {
252 lk.unlock();
253 m_cv.notify_all();
254 return false;
255 }
256 }
257
258 bool has_started = false;
259 bool is_ok = false;
260 try {
261 has_started = local_runner->start(start_interval, use_measure_immediately);
262 if (has_started) {
263 is_ok = local_runner->measure_now();
264 }
265 } catch (...) {
266 is_ok = false;
267 }
268
269 if (!has_started || !is_ok || is_process_shutting_down()) {
270 try {
271 if (has_started) {
272 local_runner->stop();
273 }
274 } catch (...) {
275 // no-throw
276 }
277 local_runner.reset();
278 }
279
280 {
281 std::lock_guard<std::mutex> lk(m_mtx);
282 if (local_runner) {
283 m_last_offset_us.store(local_runner->offset_us(), std::memory_order_relaxed);
284 m_runner = std::move(local_runner);
286 } else {
287 m_runner.reset();
289 }
290 }
291 m_cv.notify_all();
292 return is_ok;
293 }
294
299 bool init(int interval_ms, bool measure_immediately = true) {
300 return init(std::chrono::milliseconds(interval_ms), measure_immediately);
301 }
302
304 void shutdown() {
305 std::unique_ptr<RunnerT> local_runner;
306 {
307 std::unique_lock<std::mutex> lk(m_mtx);
308 while (is_transitioning_locked()) {
309 m_cv.wait(lk);
310 }
311 if (m_state == State::stopped || !m_runner) {
312 return;
313 }
315 local_runner = std::move(m_runner);
316 }
317 try {
318 local_runner->stop();
319 } catch (...) {
320 // no-throw
321 }
322 {
323 std::lock_guard<std::mutex> lk(m_mtx);
325 }
326 m_cv.notify_all();
327 }
328
331 bool running() const noexcept {
332 std::lock_guard<std::mutex> lk(m_mtx);
334 }
335
337 void ensure_started() noexcept {
339 return;
340 }
341 if (running()) {
342 return;
343 }
344 (void)init();
345 }
346
351 int64_t offset_us() noexcept {
353 return m_last_offset_us.load(std::memory_order_relaxed);
354 }
356 std::lock_guard<std::mutex> lk(m_mtx);
357 if (!m_runner) return 0;
358 const int64_t offset = m_runner->offset_us();
359 m_last_offset_us.store(offset, std::memory_order_relaxed);
360 return offset;
361 }
362
367 int64_t utc_time_us() noexcept {
369 return now_realtime_us() + m_last_offset_us.load(std::memory_order_relaxed);
370 }
372 std::lock_guard<std::mutex> lk(m_mtx);
373 if (!m_runner) return now_realtime_us();
374 m_last_offset_us.store(m_runner->offset_us(), std::memory_order_relaxed);
375 return m_runner->utc_time_us();
376 }
377
380 int64_t utc_time_ms() noexcept {
381 return utc_time_us() / 1000;
382 }
383
386 int64_t utc_time_sec() noexcept {
387 return utc_time_us() / 1000000;
388 }
389
392 bool last_measure_ok() const noexcept {
393 std::lock_guard<std::mutex> lk(m_mtx);
394 if (!m_runner) return false;
395 return m_runner->last_measure_ok();
396 }
397
400 uint64_t measure_count() const noexcept {
401 std::lock_guard<std::mutex> lk(m_mtx);
402 if (!m_runner) return 0;
403 return m_runner->measure_count();
404 }
405
408 uint64_t fail_count() const noexcept {
409 std::lock_guard<std::mutex> lk(m_mtx);
410 if (!m_runner) return 0;
411 return m_runner->fail_count();
412 }
413
416 int64_t last_update_realtime_us() const noexcept {
417 std::lock_guard<std::mutex> lk(m_mtx);
418 if (!m_runner) return 0;
419 return m_runner->last_update_realtime_us();
420 }
421
424 int64_t last_success_realtime_us() const noexcept {
425 std::lock_guard<std::mutex> lk(m_mtx);
426 if (!m_runner) return 0;
427 return m_runner->last_success_realtime_us();
428 }
429
433 bool stale(std::chrono::milliseconds max_age) const noexcept {
434 const int64_t last = last_update_realtime_us();
435 if (last == 0) {
436 return true;
437 }
438 const int64_t age = now_realtime_us() - last;
439 return age > static_cast<int64_t>(max_age.count()) * 1000;
440 }
441
445 bool stale(int max_age_ms) const noexcept {
446 return stale(std::chrono::milliseconds(max_age_ms));
447 }
448
452 bool set_servers(std::vector<NtpServerConfig> servers) {
453 std::lock_guard<std::mutex> lk(m_mtx);
455 return false;
456 }
458 m_servers = std::move(servers);
459 return true;
460 }
461
465 std::lock_guard<std::mutex> lk(m_mtx);
467 return false;
468 }
471 return true;
472 }
473
477 std::lock_guard<std::mutex> lk(m_mtx);
479 return false;
480 }
481 m_has_custom_servers = false;
482 m_servers.clear();
483 return true;
484 }
485
490 std::lock_guard<std::mutex> lk(m_mtx);
492 return false;
493 }
495 m_pool_cfg = std::move(cfg);
496 return true;
497 }
498
502 std::lock_guard<std::mutex> lk(m_mtx);
504 return m_pool_cfg;
505 }
506 return NtpPoolConfig{};
507 }
508
511 std::vector<NtpSample> last_samples() const {
512 std::lock_guard<std::mutex> lk(m_mtx);
513 if (!m_runner) return {};
514 return m_runner->last_samples();
515 }
516
518 NtpTimeServiceT() = default;
520 ~NtpTimeServiceT() = default;
521
526 return false;
527 }
528
529 std::unique_ptr<RunnerT> new_runner;
530 std::unique_ptr<RunnerT> old_runner;
531 std::chrono::milliseconds interval;
532 bool measure_immediately = true;
533 bool was_running = false;
534 {
535 std::unique_lock<std::mutex> lk(m_mtx);
536 while (is_transitioning_locked()) {
537 m_cv.wait(lk);
538 }
540 return false;
541 }
542 was_running = m_state == State::running && is_running_locked();
544 new_runner = build_runner_locked();
545 if (!new_runner) {
546 m_state = was_running ? State::running : State::stopped;
547 lk.unlock();
548 m_cv.notify_all();
549 return false;
550 }
551 interval = m_interval;
552 measure_immediately = m_measure_immediately;
553 old_runner = std::move(m_runner);
554 }
555
556 if (old_runner) {
557 try {
558 old_runner->stop();
559 } catch (...) {
560 }
561 }
562
563 bool has_started = false;
564 bool is_ok = false;
565 try {
566 has_started = new_runner->start(interval, measure_immediately);
567 if (has_started) {
568 is_ok = new_runner->measure_now();
569 }
570 } catch (...) {
571 is_ok = false;
572 }
573
574 if (!has_started || !is_ok || is_process_shutting_down()) {
575 try {
576 if (has_started) {
577 new_runner->stop();
578 }
579 } catch (...) {
580 // no-throw
581 }
582 new_runner.reset();
583 }
584
585 {
586 std::lock_guard<std::mutex> lk(m_mtx);
587 if (new_runner) {
588 m_last_offset_us.store(new_runner->offset_us(), std::memory_order_relaxed);
589 m_runner = std::move(new_runner);
591 } else {
592 m_runner.reset();
594 }
595 }
596 m_cv.notify_all();
597 return is_ok;
598 }
599
600 private:
602 enum class ProcessState : uint8_t {
605 };
606
608 enum class State : uint8_t {
613 };
614
618 m_atexit_registration_count.fetch_add(1, std::memory_order_relaxed);
619 }
620 }
621
623 void begin_process_shutdown() noexcept {
624 m_process_state.store(ProcessState::shutting_down, std::memory_order_release);
625
626 std::unique_ptr<RunnerT> local_runner;
627 {
628 std::unique_lock<std::mutex> lk(m_mtx);
629 while (is_transitioning_locked()) {
630 m_cv.wait(lk);
631 }
632 if (m_runner) {
633 m_last_offset_us.store(m_runner->offset_us(), std::memory_order_relaxed);
635 local_runner = std::move(m_runner);
636 } else {
638 }
639 }
640
641 if (local_runner) {
642 try {
643 local_runner->stop();
644 } catch (...) {
645 // no-throw
646 }
647 }
648
649 {
650 std::lock_guard<std::mutex> lk(m_mtx);
652 }
653 m_cv.notify_all();
654 }
655
657 bool is_process_shutting_down() const noexcept {
658 return m_process_state.load(std::memory_order_acquire) == ProcessState::shutting_down;
659 }
660
662 bool is_process_shutting_down_locked() const noexcept {
664 }
665
667 uint32_t atexit_registration_count() const noexcept {
668 return m_atexit_registration_count.load(std::memory_order_relaxed);
669 }
670
672 bool is_running_locked() const noexcept {
673 return m_runner && m_runner->running();
674 }
675
677 bool is_transitioning_locked() const noexcept {
679 }
680
682 bool is_reconfigurable_locked() const noexcept {
684 }
685
687 std::unique_ptr<RunnerT> build_runner_locked() {
688 std::vector<NtpServerConfig> servers;
690 servers = m_servers;
691 } else {
693 }
694
696 NtpClientPool pool(cfg);
697 pool.set_servers(std::move(servers));
698
699 std::unique_ptr<RunnerT> runner;
700 try {
701 runner.reset(new RunnerT(std::move(pool)));
702 } catch (...) {
703 return nullptr;
704 }
705 return runner;
706 }
707
708 private:
709 mutable std::mutex m_mtx;
710 std::condition_variable m_cv;
712 std::atomic<ProcessState> m_process_state{ProcessState::alive};
713 std::atomic<int64_t> m_last_offset_us{0};
714 std::atomic<uint32_t> m_atexit_registration_count{0};
715 std::chrono::milliseconds m_interval{std::chrono::seconds(30)};
717
719 std::vector<NtpServerConfig> m_servers;
720
723
724 std::unique_ptr<RunnerT> m_runner;
725 };
726
727 namespace detail {
728 template <class RunnerT>
731 static NtpTimeServiceT<RunnerT>* p_instance = []() noexcept {
734 return p_service;
735 }();
736 return *p_instance;
737 }
738
739 static void handle_process_exit() noexcept {
740 instance().begin_process_shutdown();
741 }
742 };
743
744 template <class RunnerT>
746 static void begin_process_shutdown() noexcept {
747 NtpTimeServiceSingleton<RunnerT>::instance().begin_process_shutdown();
748 }
749
750 static bool is_process_shutting_down() noexcept {
751 return NtpTimeServiceSingleton<RunnerT>::instance().is_process_shutting_down();
752 }
753
754 static uint32_t atexit_registration_count() noexcept {
755 return NtpTimeServiceSingleton<RunnerT>::instance().atexit_registration_count();
756 }
757 };
758 } // namespace detail
759
760#if defined(TIME_SHIELD_TEST_FAKE_NTP)
764#else
768#endif
769
770namespace ntp {
771
777 inline bool init(std::chrono::milliseconds interval = std::chrono::seconds(30),
778 bool measure_immediately = true) {
779 return NtpTimeService::instance().init(interval, measure_immediately);
780 }
781
787 inline bool init(int interval_ms,
788 bool measure_immediately = true) {
789 return NtpTimeService::instance().init(std::chrono::milliseconds(interval_ms), measure_immediately);
790 }
791
794 inline void shutdown() {
796 }
797
801 inline int64_t offset_us() noexcept {
803 }
804
808 inline int64_t utc_time_us() noexcept {
810 }
811
815 inline int64_t utc_time_ms() noexcept {
817 }
818
822 inline int64_t utc_time_sec() noexcept {
824 }
825
829 inline bool last_measure_ok() noexcept {
831 }
832
836 inline uint64_t measure_count() noexcept {
838 }
839
843 inline uint64_t fail_count() noexcept {
845 }
846
850 inline int64_t last_update_realtime_us() noexcept {
852 }
853
857 inline int64_t last_success_realtime_us() noexcept {
859 }
860
865 inline bool stale(std::chrono::milliseconds max_age) noexcept {
866 return NtpTimeService::instance().stale(max_age);
867 }
868
873 inline bool stale(int max_age_ms) noexcept {
874 return NtpTimeService::instance().stale(max_age_ms);
875 }
876
877} // namespace ntp
878
879} // namespace time_shield
880
881#else // TIME_SHIELD_ENABLE_NTP_CLIENT
882
883namespace time_shield {
885 public:
887 static_assert(sizeof(void*) == 0, "NtpTimeService is disabled by configuration.");
888 return *reinterpret_cast<NtpTimeService*>(0);
889 }
890 };
891} // namespace time_shield
892
893#endif // _TIME_SHIELD_ENABLE_NTP_CLIENT
894
895#endif // _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED
static std::vector< NtpServerConfig > build_default_servers()
Singleton service for background NTP measurements.
std::unique_ptr< RunnerT > build_runner_locked()
Build a runner with current server list and pool config.
NtpTimeServiceT()=default
Construct service.
NtpTimeServiceT(const NtpTimeServiceT &)=delete
uint64_t measure_count() const noexcept
Return total number of measurement attempts.
bool is_running_locked() const noexcept
Check runner status under lock.
bool is_process_shutting_down_locked() const noexcept
Return true when process shutdown has started.
uint32_t atexit_registration_count() const noexcept
Return number of successful atexit registrations.
bool init(int interval_ms, bool measure_immediately=true)
Start background measurements using milliseconds.
int64_t last_success_realtime_us() const noexcept
Return realtime timestamp of last successful measurement.
NtpPoolConfig pool_config() const
Return current pool configuration.
State
Lifecycle state of the singleton runner.
void shutdown()
Stop background measurements and release resources.
bool clear_servers()
Clear custom server list and return to default behavior.
int64_t utc_time_sec() noexcept
Return current UTC time in seconds based on offset.
bool init()
Start background measurements using stored interval.
ProcessState
Global lifetime state of the immortal singleton.
bool is_reconfigurable_locked() const noexcept
Return true when configuration can be changed safely.
bool set_pool_config(NtpPoolConfig cfg)
Override pool configuration for new runner instances.
NtpTimeServiceT & operator=(const NtpTimeServiceT &)=delete
bool running() const noexcept
Return true when background runner is active.
bool stale(std::chrono::milliseconds max_age) const noexcept
Return true when last measurement is older than max_age.
int64_t utc_time_ms() noexcept
Return current UTC time in milliseconds based on offset.
static NtpTimeServiceT & instance() noexcept
Return the singleton instance.
bool last_measure_ok() const noexcept
Return whether last measurement updated the offset.
void register_process_shutdown_handler() noexcept
Register one process-shutdown handler for this specialization.
bool set_servers(std::vector< NtpServerConfig > servers)
Replace server list used for new runner instances.
~NtpTimeServiceT()=default
Immortal singleton is stopped via process-shutdown handler.
bool stale(int max_age_ms) const noexcept
Return true when last measurement is older than max_age_ms.
int64_t offset_us() noexcept
Return last estimated offset in microseconds.
uint64_t fail_count() const noexcept
Return number of failed measurement attempts.
bool is_transitioning_locked() const noexcept
Return true when a start or stop transition is in progress.
bool set_default_servers()
Use conservative default servers for new runner instances.
bool apply_config_now()
Apply current config by rebuilding the runner.
void begin_process_shutdown() noexcept
Mark the singleton as shutting down and stop the runner.
std::unique_ptr< detail::FakeNtpRunner > m_runner
int64_t utc_time_us() noexcept
Return current UTC time in microseconds based on offset.
bool init(std::chrono::milliseconds interval, bool measure_immediately=true)
Start background measurements with interval and immediate flag.
std::vector< NtpSample > last_samples() const
Return copy of last measurement samples.
void ensure_started() noexcept
Ensure background runner is started with current config.
bool is_process_shutting_down() const noexcept
Return true when process shutdown has started.
int64_t last_update_realtime_us() const noexcept
Return realtime timestamp of last measurement attempt.
static NtpTimeService & instance()
int64_t last_update_realtime_us() const noexcept
Return realtime timestamp of last measurement attempt.
int64_t utc_time_sec() const noexcept
Return current UTC time in seconds based on offset.
std::atomic< int64_t > m_last_success_realtime_us
FakeNtpRunner()=default
Construct fake runner.
int64_t utc_time_us() const noexcept
Return current UTC time in microseconds based on offset.
FakeNtpRunner(const FakeNtpRunner &)=delete
bool last_measure_ok() const noexcept
Return whether last measurement updated the offset.
int64_t last_success_realtime_us() const noexcept
Return realtime timestamp of last successful measurement.
bool force_measure()
Wake the worker thread and request a measurement.
bool running() const noexcept
Return true when background thread is running.
uint64_t fail_count() const noexcept
Return number of failed measurement attempts.
std::atomic< int64_t > m_last_update_realtime_us
bool start(std::chrono::milliseconds interval=std::chrono::seconds(30), bool measure_immediately=true)
Start fake measurements on a background thread.
int64_t offset_us() const noexcept
Return last estimated offset in microseconds.
bool do_measure()
Update fake offset and stats.
std::atomic< uint64_t > m_measure_count
void run_loop()
Background loop for fake measurements.
int64_t utc_time_ms() const noexcept
Return current UTC time in milliseconds based on offset.
std::chrono::milliseconds m_interval
bool measure_now()
Perform one measurement immediately.
~FakeNtpRunner()
Stop background thread on destruction.
uint64_t measure_count() const noexcept
Return total number of measurement attempts.
bool start(int interval_ms, bool measure_immediately=true)
Start fake measurements using milliseconds.
void stop()
Stop background measurements.
FakeNtpRunner(NtpClientPool)
Construct fake runner with an unused pool.
FakeNtpRunner & operator=(const FakeNtpRunner &)=delete
std::vector< NtpSample > last_samples() const
Return copy of most recent samples.
Configuration macros for the library.
void init()
Initializes the Time Shield library.
int64_t utc_time_sec() noexcept
Return current UTC time in seconds based on offset.
uint64_t measure_count() noexcept
Return total number of measurement attempts.
bool stale(std::chrono::milliseconds max_age) noexcept
Return true when last measurement is older than max_age.
bool last_measure_ok() noexcept
Return whether last measurement updated the offset.
int64_t offset_us() noexcept
Return last estimated offset in microseconds.
void shutdown()
Stop NTP time service.
int64_t utc_time_ms() noexcept
Return current UTC time in milliseconds based on offset.
int64_t last_success_realtime_us() noexcept
Return realtime timestamp of last successful measurement.
uint64_t fail_count() noexcept
Return number of failed measurement attempts.
int64_t last_update_realtime_us() noexcept
Return realtime timestamp of last measurement attempt.
int64_t utc_time_us() noexcept
Return current UTC time in microseconds based on offset.
ts_ms_t now() noexcept
Get the current UTC timestamp in milliseconds.
int64_t now_realtime_us()
Get current real time in microseconds using a platform-specific method.
Main namespace for the Time Shield library.
static NtpTimeServiceT< RunnerT > & instance() noexcept
Header file with time-related utility functions.