From 94d81c0c6bef5cb0cd7252c4897d7a8f73de2d65 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 9 Aug 2025 01:19:51 -0500 Subject: [PATCH] Initial NTP protocol functionality --- test/source/scwx/network/ntp_client.test.cpp | 21 +++ test/test.cmake | 3 +- wxdata/include/scwx/network/ntp_client.hpp | 32 +++++ wxdata/include/scwx/types/ntp_types.hpp | 61 ++++++++ wxdata/source/scwx/network/ntp_client.cpp | 143 +++++++++++++++++++ wxdata/source/scwx/types/ntp_types.cpp | 51 +++++++ wxdata/wxdata.cmake | 12 +- 7 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 test/source/scwx/network/ntp_client.test.cpp create mode 100644 wxdata/include/scwx/network/ntp_client.hpp create mode 100644 wxdata/include/scwx/types/ntp_types.hpp create mode 100644 wxdata/source/scwx/network/ntp_client.cpp create mode 100644 wxdata/source/scwx/types/ntp_types.cpp diff --git a/test/source/scwx/network/ntp_client.test.cpp b/test/source/scwx/network/ntp_client.test.cpp new file mode 100644 index 00000000..cebd8cc2 --- /dev/null +++ b/test/source/scwx/network/ntp_client.test.cpp @@ -0,0 +1,21 @@ +#include + +#include + +namespace scwx +{ +namespace network +{ + +TEST(NtpClient, Poll) +{ + NtpClient client {}; + + client.Open("time.nist.gov", "123"); + //client.Open("pool.ntp.org", "123"); + //client.Open("time.windows.com", "123"); + client.Poll(); +} + +} // namespace network +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index 0ae26b53..57fbc37e 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -17,7 +17,8 @@ set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp source/scwx/common/products.test.cpp) set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) -set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) +set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp + source/scwx/network/ntp_client.test.cpp) set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp source/scwx/provider/aws_level3_data_provider.test.cpp source/scwx/provider/iem_api_provider.test.cpp diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp new file mode 100644 index 00000000..55ef4204 --- /dev/null +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace scwx::network +{ + +/** + * @brief NTP Client + */ +class NtpClient +{ +public: + explicit NtpClient(); + ~NtpClient(); + + NtpClient(const NtpClient&) = delete; + NtpClient& operator=(const NtpClient&) = delete; + + NtpClient(NtpClient&&) noexcept; + NtpClient& operator=(NtpClient&&) noexcept; + + void Open(std::string_view host, std::string_view service); + void Poll(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::network diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp new file mode 100644 index 00000000..39c1f12f --- /dev/null +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +namespace scwx::types::ntp +{ + +/* Adapted from: + * https://github.com/lettier/ntpclient/blob/master/source/c/main.c + * + * Copyright (c) 2014 David Lettier + * Copyright (c) 2020 Krystian Stasiowski + * Distributed under the BSD 3-Clause License (See + * https://github.com/lettier/ntpclient/blob/master/LICENSE) + */ + +#pragma pack(push, 1) + +struct NtpPacket +{ + union + { + std::uint8_t li_vn_mode; + struct + { + std::uint8_t mode : 3; // Client will pick mode 3 for client. + std::uint8_t vn : 3; // Version number of the protocol. + std::uint8_t li : 2; // Leap indicator. + } fields; + }; + + std::uint8_t stratum; // Stratum level of the local clock. + std::uint8_t poll; // Maximum interval between successive messages. + std::uint8_t precision; // Precision of the local clock. + + std::uint32_t rootDelay; // Total round trip delay time. + std::uint32_t rootDispersion; // Max error aloud from primary clock source. + std::uint32_t refId; // Reference clock identifier. + + std::uint32_t refTm_s; // Reference time-stamp seconds. + std::uint32_t refTm_f; // Reference time-stamp fraction of a second. + + std::uint32_t origTm_s; // Originate time-stamp seconds. + std::uint32_t origTm_f; // Originate time-stamp fraction of a second. + + std::uint32_t rxTm_s; // Received time-stamp seconds. + std::uint32_t rxTm_f; // Received time-stamp fraction of a second. + + std::uint32_t txTm_s; // The most important field the client cares about. + // Transmit time-stamp seconds. + std::uint32_t txTm_f; // Transmit time-stamp fraction of a second. + + static NtpPacket Parse(const std::span data); +}; +// Total: 48 bytes. + +#pragma pack(pop) + +} // namespace scwx::types::ntp diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp new file mode 100644 index 00000000..7315eeec --- /dev/null +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include + +#include +#include +#include + +namespace scwx::network +{ + +static const std::string logPrefix_ = "scwx::network::ntp_client"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kReceiveBufferSize_ {48u}; + +class NtpClient::Impl +{ +public: + explicit Impl(); + ~Impl(); + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + void Open(std::string_view host, std::string_view service); + void Poll(); + void ReceivePacket(std::size_t length); + + boost::asio::thread_pool threadPool_ {2u}; + + types::ntp::NtpPacket transmitPacket_ {}; + + boost::asio::ip::udp::socket socket_; + std::optional serverEndpoint_ {}; + std::array receiveBuffer_ {}; + + std::vector serverList_ { + "time.nist.gov", "ntp.pool.org", "time.windows.com"}; +}; + +NtpClient::NtpClient() : p(std::make_unique()) {} +NtpClient::~NtpClient() = default; + +NtpClient::NtpClient(NtpClient&&) noexcept = default; +NtpClient& NtpClient::operator=(NtpClient&&) noexcept = default; + +void NtpClient::Open(std::string_view host, std::string_view service) +{ + p->Open(host, service); +} + +void NtpClient::Poll() +{ + p->Poll(); +} + +NtpClient::Impl::Impl() : socket_ {threadPool_} +{ + transmitPacket_.fields.vn = 3; // Version + transmitPacket_.fields.mode = 3; // Client (3) +} + +NtpClient::Impl::~Impl() +{ + threadPool_.join(); +} + +void NtpClient::Impl::Open(std::string_view host, std::string_view service) +{ + boost::asio::ip::udp::resolver resolver(threadPool_); + boost::system::error_code ec; + + auto results = resolver.resolve(host, service, ec); + if (ec.value() == boost::system::errc::success && !results.empty()) + { + logger_->info("Using NTP server: {}", host); + serverEndpoint_ = *results.begin(); + socket_.open(serverEndpoint_->protocol()); + } + else + { + serverEndpoint_ = std::nullopt; + logger_->warn("Could not resolve host {}: {}", host, ec.message()); + } +} + +void NtpClient::Impl::Poll() +{ + using namespace std::chrono_literals; + + static constexpr auto kTimeout_ = 15s; + + try + { + std::size_t transmitPacketSize = sizeof(transmitPacket_); + // Send NTP request + socket_.send_to(boost::asio::buffer(&transmitPacket_, transmitPacketSize), + *serverEndpoint_); + + // Receive NTP response + auto future = + socket_.async_receive_from(boost::asio::buffer(receiveBuffer_), + *serverEndpoint_, + boost::asio::use_future); + std::size_t bytesReceived = 0; + + switch (future.wait_for(kTimeout_)) + { + case std::future_status::ready: + bytesReceived = future.get(); + ReceivePacket(bytesReceived); + break; + + case std::future_status::timeout: + case std::future_status::deferred: + logger_->warn("Timeout waiting for NTP response"); + socket_.cancel(); + break; + } + } + catch (const std::exception& ex) + { + logger_->error("Error polling: {}", ex.what()); + } +} + +void NtpClient::Impl::ReceivePacket(std::size_t length) +{ + if (length >= sizeof(types::ntp::NtpPacket)) + { + auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); + (void) packet; + } + else + { + logger_->warn("Received too few bytes: {}", length); + } +} + +} // namespace scwx::network diff --git a/wxdata/source/scwx/types/ntp_types.cpp b/wxdata/source/scwx/types/ntp_types.cpp new file mode 100644 index 00000000..cca0c422 --- /dev/null +++ b/wxdata/source/scwx/types/ntp_types.cpp @@ -0,0 +1,51 @@ +#include + +#include +#include + +#ifdef _WIN32 +# include +#else +# include +#endif + +namespace scwx::types::ntp +{ + +NtpPacket NtpPacket::Parse(const std::span data) +{ + NtpPacket packet; + + assert(data.size() >= sizeof(NtpPacket)); + + packet = *reinterpret_cast(data.data()); + + // Detect Kiss-o'-Death (KoD) packet + if (packet.stratum == 0) + { + // TODO + std::string kissCode = + std::string(reinterpret_cast(&packet.refId), 4); + (void) kissCode; + } + + packet.rootDelay = ntohl(packet.rootDelay); + packet.rootDispersion = ntohl(packet.rootDispersion); + packet.refId = ntohl(packet.refId); + + packet.refTm_s = ntohl(packet.refTm_s); + packet.refTm_f = ntohl(packet.refTm_f); + + packet.origTm_s = ntohl(packet.origTm_s); + packet.origTm_f = ntohl(packet.origTm_f); + + packet.rxTm_s = ntohl(packet.rxTm_s); + packet.rxTm_f = ntohl(packet.rxTm_f); + + packet.txTm_s = ntohl(packet.txTm_s); + packet.txTm_f = ntohl(packet.txTm_f); + + return packet; +} + +} // namespace scwx::types::ntp diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index ab23a4e7..bc489cb3 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -60,9 +60,11 @@ set(HDR_GR include/scwx/gr/color.hpp set(SRC_GR source/scwx/gr/color.cpp source/scwx/gr/placefile.cpp) set(HDR_NETWORK include/scwx/network/cpr.hpp - include/scwx/network/dir_list.hpp) + include/scwx/network/dir_list.hpp + include/scwx/network/ntp_client.hpp) set(SRC_NETWORK source/scwx/network/cpr.cpp - source/scwx/network/dir_list.cpp) + source/scwx/network/dir_list.cpp + source/scwx/network/ntp_client.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level2_chunks_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp @@ -80,8 +82,10 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) -set(HDR_TYPES include/scwx/types/iem_types.hpp) -set(SRC_TYPES source/scwx/types/iem_types.cpp) +set(HDR_TYPES include/scwx/types/iem_types.hpp + include/scwx/types/ntp_types.hpp) +set(SRC_TYPES source/scwx/types/iem_types.cpp + source/scwx/types/ntp_types.cpp) set(HDR_UTIL include/scwx/util/digest.hpp include/scwx/util/enum.hpp include/scwx/util/environment.hpp