diff --git a/deployment/cpu/ec/BUILD b/deployment/cpu/ec/BUILD index 3e950d73..c23e6e69 100644 --- a/deployment/cpu/ec/BUILD +++ b/deployment/cpu/ec/BUILD @@ -14,6 +14,7 @@ cpu_def( etcs = [ "diag_config.json", "logger_config.json", + "ntp_config.json", ], platform_etcs = [ "//deployment/cpu/ec:sm_config", diff --git a/deployment/cpu/ec/ntp_config.json b/deployment/cpu/ec/ntp_config.json new file mode 100644 index 00000000..8c013625 --- /dev/null +++ b/deployment/cpu/ec/ntp_config.json @@ -0,0 +1,5 @@ +{ + "ip": "192.168.10.51", + "ntp_class": 4, + "T_hb_ms": 1000 +} \ No newline at end of file diff --git a/deployment/cpu/fc/BUILD b/deployment/cpu/fc/BUILD index ba830d4d..b1266eb5 100644 --- a/deployment/cpu/fc/BUILD +++ b/deployment/cpu/fc/BUILD @@ -13,6 +13,7 @@ cpu_def( etcs = [ "diag_config.json", "logger_config.json", + "ntp_config.json", ], platform_etcs = [ "//deployment/cpu/fc:sm_config", diff --git a/deployment/cpu/fc/ntp_config.json b/deployment/cpu/fc/ntp_config.json new file mode 100644 index 00000000..75546782 --- /dev/null +++ b/deployment/cpu/fc/ntp_config.json @@ -0,0 +1,5 @@ +{ + "ip": "192.168.10.52", + "ntp_class": 3, + "T_hb_ms": 1000 +} \ No newline at end of file diff --git a/mw/timestamp_mw/ntp/config/BUILD b/mw/timestamp_mw/ntp/config/BUILD new file mode 100644 index 00000000..4dc9e795 --- /dev/null +++ b/mw/timestamp_mw/ntp/config/BUILD @@ -0,0 +1,14 @@ +cc_library( + name = "ntp_config_manager", + srcs = [ + "config_manager.cpp", + ], + hdrs = [ + "config_manager.hpp", + ], + deps = [ + "//core/json:simba_json", + "@srp_platform//ara/log", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/mw/timestamp_mw/ntp/config/config_manager.cpp b/mw/timestamp_mw/ntp/config/config_manager.cpp new file mode 100644 index 00000000..6cef3822 --- /dev/null +++ b/mw/timestamp_mw/ntp/config/config_manager.cpp @@ -0,0 +1,56 @@ +/** + * @file config_manager.hpp + * @brief Loads device configuration + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2025 + * + */ + +#include "mw/timestamp_mw/ntp/config/config_manager.hpp" +#include "core/json/json_parser.h" +#include "ara/log/log.h" + +namespace srp { +namespace tinyNTP { +namespace { + static constexpr auto kDefault_ip = "127.0.0.1"; + static constexpr auto kDefault_device_class = 7; + static constexpr auto kDefault_announce_interval = 1000; +} // namespace + +NtpConfig ConfigManager::LoadConfig(const std::string& filepath) { + NtpConfig config; + // Podstawowe dane do fallbacku + config.ip = "127.0.0.1"; + config.ntp_class = 7; + config.t_hb_ms = 1000; + + auto parser_opt = srp::core::json::JsonParser::Parser(filepath); + if (!parser_opt.has_value()) { + ara::log::LogError() << "Cannot open or parse config file: " << filepath + << ". Using fallback values."; + return config; + } + const auto& parser = parser_opt.value(); + + auto ip = parser.GetString("ip"); + config.ip = ip.value_or(kDefault_ip); + + auto ntp_class = parser.GetNumber("ntp_class"); + config.ntp_class = ntp_class.value_or(kDefault_device_class); + if (config.ntp_class > 7) { + ara::log::LogWarn() << "Config ntp_class > 7. Using fallback class 7."; + config.ntp_class = kDefault_device_class; + } + + auto t_hb_ms = parser.GetNumber("T_hb_ms"); + config.t_hb_ms = t_hb_ms.value_or(kDefault_announce_interval); + + return config; +} + +} // namespace tinyNTP +} // namespace srp diff --git a/mw/timestamp_mw/ntp/config/config_manager.hpp b/mw/timestamp_mw/ntp/config/config_manager.hpp new file mode 100644 index 00000000..6d2d70f9 --- /dev/null +++ b/mw/timestamp_mw/ntp/config/config_manager.hpp @@ -0,0 +1,43 @@ +/** + * @file config_manager.hpp + * @brief Loads device configuration + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2025 + * + */ + +#ifndef MW_TIMESTAMP_MW_NTP_CONFIG_CONFIG_MANAGER_HPP_ +#define MW_TIMESTAMP_MW_NTP_CONFIG_CONFIG_MANAGER_HPP_ + +#include +#include + +namespace srp { +namespace tinyNTP { + +constexpr auto kConfig_file_path = "/srp/opt/cpu_srp/ntp_config.json"; + +struct NtpConfig { + std::string ip; + uint8_t ntp_class; + uint32_t t_hb_ms; +}; + +class ConfigManager { + public: + /** + * @brief Wczytuje konfiguracje z pliku JSON + * + * @param filepath Ścieżka do pliku konfiguracyjnego + * @return NtpConfig Struktura z wczytanymi parametrami + */ + static NtpConfig LoadConfig(const std::string& filepath = kConfig_file_path); +}; + +} // namespace tinyNTP +} // namespace srp + +#endif // MW_TIMESTAMP_MW_NTP_CONFIG_CONFIG_MANAGER_HPP_ diff --git a/mw/timestamp_mw/ntp/controller/BUILD b/mw/timestamp_mw/ntp/controller/BUILD index 1a776591..f07ebafb 100644 --- a/mw/timestamp_mw/ntp/controller/BUILD +++ b/mw/timestamp_mw/ntp/controller/BUILD @@ -2,10 +2,14 @@ load("@srp_platform//tools/model_generator/ara:data_structure_generator.bzl", "d cc_library( name = "ntp_controller", deps = [ - "//communication-core/sockets:socket_tcp", - "//core/json:simba_json", + "//communication-core/sockets:socket_udp", + "//communication-core/sockets:socket_udp_multicast", "//core/timestamp:timestamp_controller", "//mw/timestamp_mw/ntp/controller:ntp_com_data", + "//mw/timestamp_mw/ntp/config:ntp_config_manager", + "//mw/timestamp_mw/ntp/discovery:ntp_discovery", + "//core/common:condition_lib", + "@srp_platform//ara/log", ], srcs = ["ntp_controller.cpp"], hdrs = ["ntp_controller.hpp"], diff --git a/mw/timestamp_mw/ntp/controller/ntp_controller.cpp b/mw/timestamp_mw/ntp/controller/ntp_controller.cpp index b7f3bc89..f3a187c9 100644 --- a/mw/timestamp_mw/ntp/controller/ntp_controller.cpp +++ b/mw/timestamp_mw/ntp/controller/ntp_controller.cpp @@ -12,108 +12,232 @@ #include "mw/timestamp_mw/ntp/controller/ntp_controller.hpp" #include #include +#include "mw/timestamp_mw/ntp/config/config_manager.hpp" #include "core/common/condition.h" -#include "core/json/json_parser.h" #include "ara/log/log.h" + namespace srp { namespace tinyNTP { namespace { - constexpr auto kIp_file_path = "/srp/opt/cpu_srp/logger_config.json"; - constexpr auto kRX_port = 9999; - constexpr auto kTx_port = 9998; - constexpr auto masterIP = "192.168.10.102"; + constexpr auto kRX_Tx_udp_port = 9998; + constexpr auto kRX_Tx_multicast_port = 9999; + constexpr auto kMulticastIP = "231.255.42.99"; constexpr auto kHeader_size = 33; - constexpr auto kDelay_time = 4000; } -int64_t NtpController::CalculateOffset(const int64_t T0, const int64_t T1, - const int64_t T2, const int64_t T3) { +bool NtpController::Init(const NtpConfig& config) { + myIP = config.ip; + ntp_class_ = config.ntp_class; + t_hb_ms_ = config.t_hb_ms; + + timestamp_.Init(); + discovery_manager_.Init(myIP, ntp_class_, false); + + if (udp_sock_.Init(myIP, kRX_Tx_udp_port) != srp::core::ErrorCode::kOk) { + ara::log::LogError() << "Failed to initialize udp socket!"; + } else { + ara::log::LogInfo() << "Udp socket initialized!"; + } + if (multicast_sock_.Init(myIP, kMulticastIP, kRX_Tx_multicast_port) != srp::core::ErrorCode::kOk) { + ara::log::LogError() << "Failed to initialize multicast socket!"; + } else { + ara::log::LogInfo() << "Multicast socket initialized!"; + } + + this->udp_sock_.SetRXCallback(std::bind(&NtpController::udp_socket_callback, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3)); + this->multicast_sock_.SetRXCallback(std::bind(&NtpController::multicast_socket_callback, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3)); + + udp_sock_.StartRXThread(); + multicast_sock_.StartRXThread(); + + ntp_thread = std::jthread([this](std::stop_token token){ + thread_loop(token); + }); + + ara::log::LogInfo() << "NtpController initialized with IP: " << myIP + << ", NTP Class: " << static_cast(ntp_class_) + << ", interval [ms]: " << t_hb_ms_; + + return true; +} + +int64_t NtpController::CalculateOffset(const int64_t& T0, const int64_t& T1, + const int64_t& T2, const int64_t& T3) { return ((T1 - T0) + (T2 - T3)) / 2; } -uint64_t NtpController::CalculateRoundTripDelay(const int64_t T0, const int64_t T1, - const int64_t T2, const int64_t T3) { +uint64_t NtpController::CalculateRoundTripDelay(const int64_t& T0, const int64_t& T1, + const int64_t& T2, const int64_t& T3) { return static_cast((T3 - T0) - (T2 - T1)); } +uint8_t NtpController::EncodeSettings(uint8_t device_class, bool is_holdover, uint8_t msg_type) { + uint8_t settings = 0; + + // Bity 0-2: Klasa urządzenia + settings |= (device_class & 0x07); + + // Bit 3: Holdover + if (is_holdover) { + settings |= (1 << 3); + } + + // Bity 4-5: Version + // Bit 6: msg_type (0 dla Unicast, 1 dla Announce) + if (msg_type == 1) { + settings |= (1 << 6); + } + + // Bit 7: Reserved + + return settings; +} + +void NtpController::SendAnnounce() { + srp::mw::tinyNTP::ntpStruct frame; -std::vector NtpController::socket_callback(const std::string& ip, const std::uint16_t& port, - const std::vector payload) { - auto now_ms = GetTimestamp(); - ara::log::LogDebug() << "Receive socket callback"; - auto val = srp::data::Convert::Conv(payload); - if (!val.has_value()) { - return {}; + /** + * @todo: Implement holdover + */ + frame.settings = EncodeSettings(ntp_class_, false, 1); + frame.t0 = 0; frame.t1 = 0; frame.t2 = 0; frame.t3 = 0; + + auto buf = srp::data::Convert2Vector::Conv(frame); + + if (multicast_sock_.Transmit(buf) != srp::core::ErrorCode::kOk) { + ara::log::LogError() << "Failed to send Announce multicast frame!"; + } else { + ara::log::LogDebug() << "Announce multicast frame sent correctly!"; } - mw::tinyNTP::ntpStruct hdr = val.value(); - hdr.t1 = now_ms; - ara::log::LogDebug() << "Success send response for callback"; - hdr.t2 = GetTimestamp(); - return srp::data::Convert2Vector::Conv(hdr); } +void NtpController::SendSyncRequest(const std::string& current_master_ip) { + srp::mw::tinyNTP::ntpStruct header; + header.settings = EncodeSettings(ntp_class_, false, 0); + header.t0 = GetTimestamp(); + header.t1 = 0; header.t2 = 0; header.t3 = 0; -int64_t NtpController::GetTimestamp() { - return this->timestamp_.GetNewTimeStamp(); + last_t0_ = header.t0; + + auto buf = srp::data::Convert2Vector::Conv(header); + + this->udp_sock_.Transmit(current_master_ip, kRX_Tx_udp_port, buf); } -void NtpController::thread_loop(std::stop_token token) { - while (!token.stop_requested()) { - ara::log::LogDebug() << "Start NTP SYNC"; - srp::mw::tinyNTP::ntpStruct header; - header.t0 = GetTimestamp(); +std::optional NtpController::ParseAndValidatePayload( + const std::vector& payload, const std::string& ip) { + if (payload.size() != kHeader_size) { + ara::log::LogError() << "Invalid payload size! Rejecting the packet."; + return std::nullopt; + } + + if (ip == myIP) { + ara::log::LogDebug() << "Rejecting own packet."; + return std::nullopt; + } + + return srp::data::Convert::Conv(payload); +} + +void NtpController::udp_socket_callback(const std::string& ip, + const uint16_t& port, + const std::vector& payload) { + int64_t now_ms = GetTimestamp(); + + auto val = ParseAndValidatePayload(payload, ip); + if (!val.has_value()) return; + srp::mw::tinyNTP::ntpStruct header = val.value(); + + uint8_t msg_type = (header.settings >> 6) & 0x01; + + if (msg_type != 0) { + ara::log::LogWarn() << "Received non-Sync message on unicast socket from: " << ip; + return; + } + + auto master_opt = discovery_manager_.GetBestMaster(); + bool is_server = (!master_opt.has_value() || master_opt.value().ip == myIP); + + if (is_server) { + if (header.t1 != 0 || header.t2 != 0) { + ara::log::LogWarn() << "Received Sync response, but I am the server now. Dropping."; + return; + } + + header.t1 = now_ms; + header.t2 = GetTimestamp(); + auto buf = srp::data::Convert2Vector::Conv(header); - auto res = this->sock_.Transmit(masterIP, kRX_port, buf); - auto t3 = GetTimestamp(); - if (!res.has_value()) { - continue; + + udp_sock_.Transmit(ip, kRX_Tx_udp_port, buf); + + ara::log::LogDebug() << "Sent sync response to " << ip; + } else { + if (header.t1 == 0 || header.t2 == 0) { + ara::log::LogWarn() << "Received Sync request, but I am a client now. Dropping."; + return; } - auto val = srp::data::Convert::Conv(res.value()); - if (!val.has_value()) { - continue; + + if (header.t0 != last_t0_) { + ara::log::LogWarn() << "Received Sync response for an old or unknown request. Dropping."; + return; } - srp::mw::tinyNTP::ntpStruct hdr = val.value(); - auto offset = CalculateOffset(hdr.t0, hdr.t1, hdr.t2, t3); - auto round_trip_time = CalculateRoundTripDelay(hdr.t0, hdr.t1, hdr.t2, t3); + + int64_t t3 = now_ms; + + auto offset = CalculateOffset(header.t0, header.t1, header.t2, t3); + auto round_trip_time = CalculateRoundTripDelay(header.t0, header.t1, header.t2, t3); + this->timestamp_.CorrectStartPoint(offset); + ara::log::LogDebug() << "Round trip time [ms]: " << round_trip_time - << " ,offset value [ms]: " << offset; - core::condition::wait_for(std::chrono::milliseconds(kDelay_time), token); + << " ,offset value [ms]: " << offset; } } -bool NtpController::Init() { - auto ip = readMyIP(); - if (!ip.has_value()) { - return false; - } - myIP = ip.value(); - timestamp_.Init(); - if (myIP == masterIP) { - sock_.Init(com::soc::SocketConfig{myIP, kRX_port, kTx_port}); - this->sock_.SetRXCallback(std::bind(&NtpController::socket_callback, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3)); - sock_.StartRXThread(); - } else { - ntp_thread = std::jthread([this](std::stop_token token){ - thread_loop(token); - }); +void NtpController::multicast_socket_callback(const std::string& ip, + const uint16_t& port, + const std::vector& payload) { + auto val = ParseAndValidatePayload(payload, ip); + if (!val.has_value()) return; + srp::mw::tinyNTP::ntpStruct header = val.value(); + + uint8_t msg_type = (header.settings >> 6) & 0x01; + + if (msg_type != 1) { + ara::log::LogWarn() << "Received non-Announce message on multicast socket from: " << ip; + return; } - return true; + + uint8_t sender_class = header.settings & 0x07; + bool holdover = (header.settings >> 3) & 0x01; + + ara::log::LogDebug() << "Updating node with ip: " << ip; + discovery_manager_.UpdateNode(ip, sender_class, holdover); } -// TODO(matikrajek42@gmail.com) fix this shit func -std::optional NtpController::readMyIP() { - const std::string path = kIp_file_path; - auto parser = core::json::JsonParser::Parser(path); - if (!parser.has_value()) { - return std::nullopt; - } - auto ip = parser.value().GetString("ip"); - if (!ip.has_value()) { - return std::nullopt; +int64_t NtpController::GetTimestamp() { + return this->timestamp_.GetNewTimeStamp(); +} + +void NtpController::thread_loop(std::stop_token token) { + ara::log::LogInfo() << "Start NTP Sync."; + + while (!token.stop_requested()) { + auto master_opt = discovery_manager_.GetBestMaster(); + + if (!master_opt.has_value() || master_opt.value().ip == myIP) { + ara::log::LogDebug() << "Working as a server. Broadcasting Announce."; + SendAnnounce(); + } else { + ara::log::LogDebug() << "Sending Sync to Master: " << master_opt.value().ip; + SendSyncRequest(master_opt.value().ip); + } + + core::condition::wait_for(std::chrono::milliseconds(t_hb_ms_), token); } - return ip.value(); } } // namespace tinyNTP diff --git a/mw/timestamp_mw/ntp/controller/ntp_controller.hpp b/mw/timestamp_mw/ntp/controller/ntp_controller.hpp index 7ac55988..33f68336 100644 --- a/mw/timestamp_mw/ntp/controller/ntp_controller.hpp +++ b/mw/timestamp_mw/ntp/controller/ntp_controller.hpp @@ -15,26 +15,44 @@ #include #include #include // NOLINT -#include "communication-core/sockets/tcp_socket.h" +#include "communication-core/sockets/udp_socket.h" +#include "communication-core/sockets/udp_multicast_socket.h" #include "core/timestamp/timestamp_driver.hpp" #include "srp/mw/tinyNTP/ntpStruct.h" +#include "mw/timestamp_mw/ntp/config/config_manager.hpp" +#include "mw/timestamp_mw/ntp/discovery/discovery_manager.hpp" namespace srp { namespace tinyNTP { class NtpController { private: - com::soc::StreamTCPSocket sock_; + com::soc::UdpSocket udp_sock_; + com::soc::UdpMulticastSocket multicast_sock_; + core::timestamp::TimestampMaster timestamp_; std::jthread ntp_thread; std::string myIP; + uint8_t ntp_class_; + uint32_t t_hb_ms_; + int64_t last_t0_; + DiscoveryManager discovery_manager_; + + void SendAnnounce(); + void SendSyncRequest(const std::string& current_master_ip); + std::optional ParseAndValidatePayload(const std::vector& payload, const std::string& ip); + public: - std::optional readMyIP(); - std::vector socket_callback(const std::string& ip, const std::uint16_t& port, - const std::vector payload); + bool Init(const NtpConfig& config); + + void udp_socket_callback(const std::string& ip, const std::uint16_t& port, + const std::vector& payload); + void multicast_socket_callback(const std::string& ip, const std::uint16_t& port, + const std::vector& payload); + void thread_loop(std::stop_token token); - int64_t CalculateOffset(const int64_t T0, const int64_t T1, const int64_t T2, const int64_t T3); - uint64_t CalculateRoundTripDelay(const int64_t T0, const int64_t T1, const int64_t T2, const int64_t T3); - bool Init(); + uint8_t EncodeSettings(uint8_t device_class, bool is_holdover, uint8_t msg_type); + int64_t CalculateOffset(const int64_t& T0, const int64_t& T1, const int64_t& T2, const int64_t& T3); + uint64_t CalculateRoundTripDelay(const int64_t& T0, const int64_t& T1, const int64_t& T2, const int64_t& T3); int64_t GetTimestamp(); }; diff --git a/mw/timestamp_mw/ntp/discovery/BUILD b/mw/timestamp_mw/ntp/discovery/BUILD new file mode 100644 index 00000000..67018cee --- /dev/null +++ b/mw/timestamp_mw/ntp/discovery/BUILD @@ -0,0 +1,14 @@ +cc_library( + name = "ntp_discovery", + srcs = [ + "discovery_manager.cpp", + ], + hdrs = [ + "discovery_manager.hpp", + ], + deps = [ + "//core/common:condition_lib", + "@srp_platform//ara/log", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/mw/timestamp_mw/ntp/discovery/discovery_manager.cpp b/mw/timestamp_mw/ntp/discovery/discovery_manager.cpp new file mode 100644 index 00000000..0fd39894 --- /dev/null +++ b/mw/timestamp_mw/ntp/discovery/discovery_manager.cpp @@ -0,0 +1,102 @@ +/** + * @file discovery_manager.cpp + * @brief Manages the selection of the best time provider + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2025 + * + */ + +#include "mw/timestamp_mw/ntp/discovery/discovery_manager.hpp" +#include "core/common/condition.h" +#include "ara/log/log.h" + +namespace srp { +namespace tinyNTP { +namespace { + constexpr auto kTimeout_seconds = 15; +} + +void DiscoveryManager::Init(const std::string& ip, const uint8_t ntp_class, const bool holdover) { + local_node_ = NodeInfo{ip, ntp_class, holdover}; + + cleanup_thread_ = std::jthread([this](std::stop_token token) { + cleanup_thread_loop(token); + }); +} + +/** + * @brief Usuwa nieaktywne węzły z mapy urządzeń sieciowych + */ +void DiscoveryManager::RemoveExpiredNodes() { + std::lock_guard lock(map_mutex_); + + const auto now = std::chrono::steady_clock::now(); + + for (auto it = neighbors_.begin(); it != neighbors_.end(); ) { + const auto delta = std::chrono::duration_cast(now - it->second.last_seen).count(); + + if (delta > kTimeout_seconds) { + ara::log::LogDebug() << "Removing expired node with IP: " << it->first; + it = neighbors_.erase(it); + } else { + ++it; + } + } +} + +void DiscoveryManager::cleanup_thread_loop(std::stop_token token) { + ara::log::LogInfo() << "Start cleanup thread"; + const auto check_interval = std::chrono::seconds(1); + + while (!token.stop_requested()) { + RemoveExpiredNodes(); + core::condition::wait_for(check_interval, token); + } +} + +void DiscoveryManager::UpdateNode(const std::string& ip, const uint8_t ntp_class, const bool holdover) { + std::lock_guard lock(map_mutex_); + + auto result = neighbors_.insert({ip, NodeInfo{}}); + NodeInfo& node = result.first->second; + + node.ip = ip; + node.ntp_class = ntp_class; + node.holdover = holdover; + node.last_seen = std::chrono::steady_clock::now(); +} + +/** + * @brief Zwraca najlepszy węzeł w sieci + * + * @param local_node + * @return std::optional - W przypadku gdy lokalny node jest najlepszym w sieci zwrócony optional jest pusty + */ +std::optional DiscoveryManager::GetBestMaster() { + std::lock_guard lock(map_mutex_); + + NodeInfo best_neighbor = local_node_; + + for (const auto& [ip, node] : neighbors_) { + if (node.ntp_class != best_neighbor.ntp_class) { + if (node.ntp_class < best_neighbor.ntp_class) { + best_neighbor = node; + } + } else if (node.holdover != best_neighbor.holdover) { + if (!node.holdover) { + best_neighbor = node; + } + } else if (node.ip < best_neighbor.ip) { + best_neighbor = node; + } + } + + if (best_neighbor.ip == local_node_.ip) return std::nullopt; + return best_neighbor; +} + +} // namespace tinyNTP +} // namespace srp diff --git a/mw/timestamp_mw/ntp/discovery/discovery_manager.hpp b/mw/timestamp_mw/ntp/discovery/discovery_manager.hpp new file mode 100644 index 00000000..4451f5c9 --- /dev/null +++ b/mw/timestamp_mw/ntp/discovery/discovery_manager.hpp @@ -0,0 +1,64 @@ +/** + * @file discovery_manager.hpp + * @brief Manages the selection of the best time provider + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2025 + * + */ + +#ifndef MW_TIMESTAMP_MW_NTP_DISCOVERY_DISCOVERY_MANAGER_HPP_ +#define MW_TIMESTAMP_MW_NTP_DISCOVERY_DISCOVERY_MANAGER_HPP_ + +#include +#include +#include // NOLINT +#include // NOLINT +#include // NOLINT +#include +#include + +namespace srp { +namespace tinyNTP { + +/** + * @brief Pojedynczy węzeł sieci + * + * ip - adres ip + * ntp_class - klasa urządzenia (0-7), im niższa wartość, tym wyższy priorytet. + * holdover - 1 = brak aktualnej referencji (czas niepewny); 0 = zsynchronizowany. + * last_seen - czas otrzymania ostatniej wiadomości od danego urządzenia + */ +struct NodeInfo { + std::string ip; + uint8_t ntp_class; + bool holdover; + std::chrono::steady_clock::time_point last_seen; +}; + +class DiscoveryManager { + private: + NodeInfo local_node_; + std::unordered_map neighbors_; // ip -> NodeInfo + + void RemoveExpiredNodes(); + void cleanup_thread_loop(std::stop_token token); + + std::jthread cleanup_thread_; + mutable std::mutex map_mutex_; + public: + DiscoveryManager() = default; + ~DiscoveryManager() = default; + + void Init(const std::string& ip, const uint8_t ntp_class, const bool holdover); + + void UpdateNode(const std::string& ip, const uint8_t ntp_class, const bool holdover); + std::optional GetBestMaster(); +}; + +} // namespace tinyNTP +} // namespace srp + +#endif // MW_TIMESTAMP_MW_NTP_DISCOVERY_DISCOVERY_MANAGER_HPP_ diff --git a/mw/timestamp_mw/ntp/ut/BUILD b/mw/timestamp_mw/ntp/ut/BUILD index 96f11916..37afbd1a 100644 --- a/mw/timestamp_mw/ntp/ut/BUILD +++ b/mw/timestamp_mw/ntp/ut/BUILD @@ -8,4 +8,26 @@ cc_test( "@com_google_googletest//:gtest_main", "//mw/timestamp_mw/ntp/controller:ntp_controller", ], +) + +cc_test( + name = "discovery_manager_test", + size = "small", + srcs = ["discovery_manager_test.cc"], + visibility = ["//visibility:public"], + deps = [ + "@com_google_googletest//:gtest_main", + "//mw/timestamp_mw/ntp/discovery:ntp_discovery", + ], +) + +cc_test( + name = "config_manager_test", + size = "small", + srcs = ["config_manager_test.cc"], + visibility = ["//visibility:public"], + deps = [ + "@com_google_googletest//:gtest_main", + "//mw/timestamp_mw/ntp/config:ntp_config_manager", + ], ) \ No newline at end of file diff --git a/mw/timestamp_mw/ntp/ut/config_manager_test.cc b/mw/timestamp_mw/ntp/ut/config_manager_test.cc new file mode 100644 index 00000000..0d97f56e --- /dev/null +++ b/mw/timestamp_mw/ntp/ut/config_manager_test.cc @@ -0,0 +1,95 @@ +/** + * @file config_manager_test.cc + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @brief + * @version 0.1 + * @date 2026-05-04 + * + * @copyright Copyright (c) 2025 + * + */ + +#include +#include +#include + +#include "mw/timestamp_mw/ntp/config/config_manager.hpp" + +using srp::tinyNTP::ConfigManager; + +class ConfigManagerTest : public ::testing::Test { + protected: + const std::string temp_filepath = "test_config.json"; + + void CreateJsonFile(const std::string& content) { + std::ofstream file(temp_filepath); + file << content; + file.close(); + } + + void TearDown() override { + std::remove(temp_filepath.c_str()); // Usuwamy plik testowy + } +}; + +TEST_F(ConfigManagerTest, LoadsValidConfigCorrectly) { + CreateJsonFile(R"({ + "ip": "192.168.1.100", + "ntp_class": 3, + "T_hb_ms": 500 + })"); + + auto config = ConfigManager::LoadConfig(temp_filepath); + + EXPECT_EQ(config.ip, "192.168.1.100"); + EXPECT_EQ(config.ntp_class, 3); + EXPECT_EQ(config.t_hb_ms, 500); +} + +TEST_F(ConfigManagerTest, UsesFallbackWhenFileDoesNotExist) { + auto config = ConfigManager::LoadConfig("fake_path_that_doesnt_exist.json"); + + EXPECT_EQ(config.ip, "127.0.0.1"); + EXPECT_EQ(config.ntp_class, 7); + EXPECT_EQ(config.t_hb_ms, 1000); +} + +TEST_F(ConfigManagerTest, UsesFallbackOnInvalidJsonSyntax) { + CreateJsonFile(R"({ + "ip": "10.0.0.1", + "ntp_class": 2 + "T_hb_ms": 500 + )"); + + auto config = ConfigManager::LoadConfig(temp_filepath); + + EXPECT_EQ(config.ip, "127.0.0.1"); + EXPECT_EQ(config.ntp_class, 7); + EXPECT_EQ(config.t_hb_ms, 1000); +} + +TEST_F(ConfigManagerTest, ForcesFallbackClassIfOutOfBounds) { + CreateJsonFile(R"({ + "ip": "10.0.0.2", + "ntp_class": 15, + "T_hb_ms": 200 + })"); + + auto config = ConfigManager::LoadConfig(temp_filepath); + + EXPECT_EQ(config.ip, "10.0.0.2"); + EXPECT_EQ(config.t_hb_ms, 200); + EXPECT_EQ(config.ntp_class, 7); +} + +TEST_F(ConfigManagerTest, HandlesMissingFieldsGracefully) { + CreateJsonFile(R"({ + "ip": "172.16.0.5" + })"); // Brak ntp_class i T_hb_ms + + auto config = ConfigManager::LoadConfig(temp_filepath); + + EXPECT_EQ(config.ip, "172.16.0.5"); + EXPECT_EQ(config.ntp_class, 7); + EXPECT_EQ(config.t_hb_ms, 1000); +} diff --git a/mw/timestamp_mw/ntp/ut/controler_test.cc b/mw/timestamp_mw/ntp/ut/controler_test.cc index f762a111..dec65ef1 100644 --- a/mw/timestamp_mw/ntp/ut/controler_test.cc +++ b/mw/timestamp_mw/ntp/ut/controler_test.cc @@ -36,3 +36,12 @@ TEST_F(NtpControllerTest, CalculateRoundTripDelayTest) { int64_t expected_delay = 1500; // (3000 - 1000) - (2000 - 1500) = 1500 EXPECT_EQ(ntpController.CalculateRoundTripDelay(T0, T1, T2, T3), expected_delay); } + +TEST_F(NtpControllerTest, EncodeSettingsTest) { + uint8_t device_class = 3; + bool is_holdover = true; + uint8_t msg_type = 1; + + uint8_t expected_encode = 0b01001011; + EXPECT_EQ(ntpController.EncodeSettings(device_class, is_holdover, msg_type), expected_encode); +} diff --git a/mw/timestamp_mw/ntp/ut/discovery_manager_test.cc b/mw/timestamp_mw/ntp/ut/discovery_manager_test.cc new file mode 100644 index 00000000..2b8951e1 --- /dev/null +++ b/mw/timestamp_mw/ntp/ut/discovery_manager_test.cc @@ -0,0 +1,89 @@ +/** + * @file discovery_manager_test.cc + * @author Wiktor Müller (wiktor.muller8@gmail.com) + * @brief + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2025 + * + */ + +#include +#include +#include +#include // NOLINT +#include "mw/timestamp_mw/ntp/discovery/discovery_manager.hpp" + +using srp::tinyNTP::DiscoveryManager; +using srp::tinyNTP::NodeInfo; + +class DiscoveryManagerTest : public ::testing::Test { + protected: + DiscoveryManager discoveryManager; + + NodeInfo default_local_node; + + void SetUp() override { + default_local_node.ip = "192.168.0.50"; + default_local_node.ntp_class = 5; + default_local_node.holdover = true; + default_local_node.last_seen = std::chrono::steady_clock::now(); + + discoveryManager.Init(default_local_node.ip, default_local_node.ntp_class, default_local_node.holdover); + } +}; + +TEST_F(DiscoveryManagerTest, LocalNodeIsBestWhenNetworkEmpty) { + auto best_master = discoveryManager.GetBestMaster(); + + EXPECT_FALSE(best_master.has_value()); +} + +TEST_F(DiscoveryManagerTest, ExternalNodeWinsByClass) { + discoveryManager.UpdateNode("192.168.0.100", 2, true); + + auto best_master = discoveryManager.GetBestMaster(); + + ASSERT_TRUE(best_master.has_value()); + EXPECT_EQ(best_master->ip, "192.168.0.100"); +} + +TEST_F(DiscoveryManagerTest, LocalNodeWinsByClass) { + discoveryManager.UpdateNode("192.168.0.100", 7, true); + + auto best_master = discoveryManager.GetBestMaster(); + + ASSERT_FALSE(best_master.has_value()); +} + +TEST_F(DiscoveryManagerTest, ExternalNodeWinsByHoldover) { + discoveryManager.UpdateNode("192.168.0.100", 5, false); + + auto best_master = discoveryManager.GetBestMaster(); + + ASSERT_TRUE(best_master.has_value()); + EXPECT_EQ(best_master->ip, "192.168.0.100"); +} + +TEST_F(DiscoveryManagerTest, ExternalNodeWinsByIpTieBreaker) { + discoveryManager.UpdateNode("192.168.0.10", 5, true); + auto best_master = discoveryManager.GetBestMaster(); + + ASSERT_TRUE(best_master.has_value()); + EXPECT_EQ(best_master->ip, "192.168.0.10"); + + discoveryManager.UpdateNode("10.168.0.10", 5, true); + best_master = discoveryManager.GetBestMaster(); + + ASSERT_TRUE(best_master.has_value()); + EXPECT_EQ(best_master->ip, "10.168.0.10"); +} + +TEST_F(DiscoveryManagerTest, LocalNodeDefeatsLowerIpWithBetterClass) { + discoveryManager.UpdateNode("10.0.0.1", 7, false); + + auto best_master = discoveryManager.GetBestMaster(); + + EXPECT_FALSE(best_master.has_value()); +} diff --git a/mw/timestamp_mw/service/timestamp_service.cpp b/mw/timestamp_mw/service/timestamp_service.cpp index b84387eb..a80c49ec 100644 --- a/mw/timestamp_mw/service/timestamp_service.cpp +++ b/mw/timestamp_mw/service/timestamp_service.cpp @@ -20,12 +20,16 @@ int TimestampService::Run(const std::stop_token& token) { core::condition::wait(token); return 0; } + int TimestampService::Initialize(const std::map parms) { - if (!this->ntp_controller.Init()) { + auto config = srp::tinyNTP::ConfigManager::LoadConfig(); + + if (!this->ntp_controller.Init(config)) { ara::log::LogError() << "NTP controller initialization failed."; return -1; } + ara::log::LogInfo() << "Init completed"; return 0; }