From e1ece4a33b42bde657f26d4f559ebe2860e448e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 27 Mar 2026 11:15:35 +0800 Subject: [PATCH 01/12] feat: add TimeSlave process and libTSClient shm IPC channel --- score/TimeDaemon/code/common/BUILD | 2 +- score/TimeDaemon/code/common/data_types/BUILD | 2 +- score/TimeDaemon/code/ptp_machine/BUILD | 1 + score/TimeDaemon/code/ptp_machine/real/BUILD | 56 ++ .../code/ptp_machine/real/details/BUILD | 54 ++ .../real/details/real_ptp_engine.cpp | 99 ++++ .../real/details/real_ptp_engine.h | 70 +++ .../real/details/real_ptp_engine_test.cpp | 281 +++++++++ .../code/ptp_machine/real/factory.cpp | 29 + .../code/ptp_machine/real/factory.h | 45 ++ .../code/ptp_machine/real/gptp_real_machine.h | 38 ++ .../real/gptp_real_machine_test.cpp | 127 ++++ score/TimeSlave/BUILD | 12 + score/TimeSlave/code/BUILD | 12 + score/TimeSlave/code/application/BUILD | 33 ++ score/TimeSlave/code/application/main.cpp | 20 + .../TimeSlave/code/application/time_slave.cpp | 88 +++ score/TimeSlave/code/application/time_slave.h | 59 ++ score/TimeSlave/code/gptp/BUILD | 60 ++ score/TimeSlave/code/gptp/details/BUILD | 199 +++++++ .../code/gptp/details/frame_codec.cpp | 102 ++++ .../TimeSlave/code/gptp/details/frame_codec.h | 68 +++ .../code/gptp/details/frame_codec_test.cpp | 149 +++++ .../code/gptp/details/i_network_identity.h | 44 ++ .../code/gptp/details/i_raw_socket.h | 60 ++ .../code/gptp/details/message_parser.cpp | 118 ++++ .../code/gptp/details/message_parser.h | 57 ++ .../code/gptp/details/message_parser_test.cpp | 210 +++++++ .../code/gptp/details/network_identity.h | 53 ++ .../code/gptp/details/pdelay_measurer.cpp | 146 +++++ .../code/gptp/details/pdelay_measurer.h | 85 +++ .../gptp/details/pdelay_measurer_test.cpp | 165 ++++++ score/TimeSlave/code/gptp/details/ptp_types.h | 223 +++++++ .../TimeSlave/code/gptp/details/raw_socket.h | 88 +++ .../code/gptp/details/sync_state_machine.cpp | 172 ++++++ .../code/gptp/details/sync_state_machine.h | 100 ++++ .../gptp/details/sync_state_machine_test.cpp | 240 ++++++++ score/TimeSlave/code/gptp/gptp_engine.cpp | 338 +++++++++++ score/TimeSlave/code/gptp/gptp_engine.h | 127 ++++ .../TimeSlave/code/gptp/gptp_engine_test.cpp | 498 ++++++++++++++++ score/TimeSlave/code/gptp/instrument/BUILD | 48 ++ .../TimeSlave/code/gptp/instrument/probe.cpp | 64 ++ score/TimeSlave/code/gptp/instrument/probe.h | 93 +++ .../code/gptp/instrument/probe_test.cpp | 170 ++++++ score/TimeSlave/code/gptp/phc/BUILD | 30 + score/TimeSlave/code/gptp/phc/phc_adjuster.h | 72 +++ .../TimeSlave/code/gptp/platform/linux/BUILD | 30 + .../gptp/platform/linux/network_identity.cpp | 86 +++ .../code/gptp/platform/linux/phc_adjuster.cpp | 111 ++++ .../code/gptp/platform/linux/raw_socket.cpp | 206 +++++++ score/TimeSlave/code/gptp/platform/qnx/BUILD | 33 ++ .../gptp/platform/qnx/network_identity.cpp | 98 +++ .../code/gptp/platform/qnx/phc_adjuster.cpp | 69 +++ .../code/gptp/platform/qnx/qnx_raw_shim.cpp | 561 ++++++++++++++++++ .../code/gptp/platform/qnx/raw_socket.cpp | 90 +++ score/TimeSlave/code/gptp/record/BUILD | 42 ++ score/TimeSlave/code/gptp/record/recorder.cpp | 56 ++ score/TimeSlave/code/gptp/record/recorder.h | 86 +++ .../code/gptp/record/recorder_test.cpp | 176 ++++++ score/libTSClient/BUILD | 54 ++ score/libTSClient/gptp_ipc.h | 20 + score/libTSClient/gptp_ipc_channel.h | 56 ++ score/libTSClient/gptp_ipc_publisher.cpp | 97 +++ score/libTSClient/gptp_ipc_publisher.h | 62 ++ score/libTSClient/gptp_ipc_receiver.cpp | 102 ++++ score/libTSClient/gptp_ipc_receiver.h | 63 ++ score/libTSClient/gptp_ipc_test.cpp | 339 +++++++++++ 67 files changed, 7242 insertions(+), 2 deletions(-) create mode 100644 score/TimeDaemon/code/ptp_machine/real/BUILD create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/BUILD create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp create mode 100644 score/TimeDaemon/code/ptp_machine/real/factory.cpp create mode 100644 score/TimeDaemon/code/ptp_machine/real/factory.h create mode 100644 score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h create mode 100644 score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp create mode 100644 score/TimeSlave/BUILD create mode 100644 score/TimeSlave/code/BUILD create mode 100644 score/TimeSlave/code/application/BUILD create mode 100644 score/TimeSlave/code/application/main.cpp create mode 100644 score/TimeSlave/code/application/time_slave.cpp create mode 100644 score/TimeSlave/code/application/time_slave.h create mode 100644 score/TimeSlave/code/gptp/BUILD create mode 100644 score/TimeSlave/code/gptp/details/BUILD create mode 100644 score/TimeSlave/code/gptp/details/frame_codec.cpp create mode 100644 score/TimeSlave/code/gptp/details/frame_codec.h create mode 100644 score/TimeSlave/code/gptp/details/frame_codec_test.cpp create mode 100644 score/TimeSlave/code/gptp/details/i_network_identity.h create mode 100644 score/TimeSlave/code/gptp/details/i_raw_socket.h create mode 100644 score/TimeSlave/code/gptp/details/message_parser.cpp create mode 100644 score/TimeSlave/code/gptp/details/message_parser.h create mode 100644 score/TimeSlave/code/gptp/details/message_parser_test.cpp create mode 100644 score/TimeSlave/code/gptp/details/network_identity.h create mode 100644 score/TimeSlave/code/gptp/details/pdelay_measurer.cpp create mode 100644 score/TimeSlave/code/gptp/details/pdelay_measurer.h create mode 100644 score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp create mode 100644 score/TimeSlave/code/gptp/details/ptp_types.h create mode 100644 score/TimeSlave/code/gptp/details/raw_socket.h create mode 100644 score/TimeSlave/code/gptp/details/sync_state_machine.cpp create mode 100644 score/TimeSlave/code/gptp/details/sync_state_machine.h create mode 100644 score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp create mode 100644 score/TimeSlave/code/gptp/gptp_engine.cpp create mode 100644 score/TimeSlave/code/gptp/gptp_engine.h create mode 100644 score/TimeSlave/code/gptp/gptp_engine_test.cpp create mode 100644 score/TimeSlave/code/gptp/instrument/BUILD create mode 100644 score/TimeSlave/code/gptp/instrument/probe.cpp create mode 100644 score/TimeSlave/code/gptp/instrument/probe.h create mode 100644 score/TimeSlave/code/gptp/instrument/probe_test.cpp create mode 100644 score/TimeSlave/code/gptp/phc/BUILD create mode 100644 score/TimeSlave/code/gptp/phc/phc_adjuster.h create mode 100644 score/TimeSlave/code/gptp/platform/linux/BUILD create mode 100644 score/TimeSlave/code/gptp/platform/linux/network_identity.cpp create mode 100644 score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp create mode 100644 score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp create mode 100644 score/TimeSlave/code/gptp/platform/qnx/BUILD create mode 100644 score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp create mode 100644 score/TimeSlave/code/gptp/platform/qnx/phc_adjuster.cpp create mode 100644 score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp create mode 100644 score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp create mode 100644 score/TimeSlave/code/gptp/record/BUILD create mode 100644 score/TimeSlave/code/gptp/record/recorder.cpp create mode 100644 score/TimeSlave/code/gptp/record/recorder.h create mode 100644 score/TimeSlave/code/gptp/record/recorder_test.cpp create mode 100644 score/libTSClient/BUILD create mode 100644 score/libTSClient/gptp_ipc.h create mode 100644 score/libTSClient/gptp_ipc_channel.h create mode 100644 score/libTSClient/gptp_ipc_publisher.cpp create mode 100644 score/libTSClient/gptp_ipc_publisher.h create mode 100644 score/libTSClient/gptp_ipc_receiver.cpp create mode 100644 score/libTSClient/gptp_ipc_receiver.h create mode 100644 score/libTSClient/gptp_ipc_test.cpp diff --git a/score/TimeDaemon/code/common/BUILD b/score/TimeDaemon/code/common/BUILD index ea4d4fd..fe5fa19 100644 --- a/score/TimeDaemon/code/common/BUILD +++ b/score/TimeDaemon/code/common/BUILD @@ -22,7 +22,7 @@ cc_library( ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], - visibility = ["//score/TimeDaemon:__subpackages__"], + visibility = ["//score:__subpackages__"], deps = [], ) diff --git a/score/TimeDaemon/code/common/data_types/BUILD b/score/TimeDaemon/code/common/data_types/BUILD index e6b718d..d7a7468 100644 --- a/score/TimeDaemon/code/common/data_types/BUILD +++ b/score/TimeDaemon/code/common/data_types/BUILD @@ -22,7 +22,7 @@ cc_library( ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], - visibility = ["//score/TimeDaemon:__subpackages__"], + visibility = ["//score:__subpackages__"], deps = ["//score/time/HighPrecisionLocalSteadyClock:interface"], ) diff --git a/score/TimeDaemon/code/ptp_machine/BUILD b/score/TimeDaemon/code/ptp_machine/BUILD index d596cfc..2e05898 100644 --- a/score/TimeDaemon/code/ptp_machine/BUILD +++ b/score/TimeDaemon/code/ptp_machine/BUILD @@ -23,6 +23,7 @@ cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", test_suites_from_sub_packages = [ "//score/TimeDaemon/code/ptp_machine/core:unit_test_suite", + "//score/TimeDaemon/code/ptp_machine/real:unit_test_suite", "//score/TimeDaemon/code/ptp_machine/stub:unit_test_suite", ], visibility = ["//score/TimeDaemon:__subpackages__"], diff --git a/score/TimeDaemon/code/ptp_machine/real/BUILD b/score/TimeDaemon/code/ptp_machine/real/BUILD new file mode 100644 index 0000000..55ec5df --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/BUILD @@ -0,0 +1,56 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "gptp_real_machine", + srcs = [ + "factory.cpp", + ], + hdrs = [ + "factory.h", + "gptp_real_machine.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score/TimeDaemon:__subpackages__"], + deps = [ + "//score/TimeDaemon/code/ptp_machine/core:ptp_machine", + "//score/TimeDaemon/code/ptp_machine/real/details:real_ptp_engine", + "//score/libTSClient:gptp_ipc", + ], +) + +cc_test( + name = "gptp_real_machine_test", + srcs = ["gptp_real_machine_test.cpp"], + tags = ["unit"], + deps = [ + ":gptp_real_machine", + "//score/libTSClient:gptp_ipc", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@score_baselibs//score/mw/log:console_only_backend", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":gptp_real_machine_test"], + test_suites_from_sub_packages = [ + "//score/TimeDaemon/code/ptp_machine/real/details:unit_test_suite", + ], + visibility = ["//score/TimeDaemon:__subpackages__"], +) diff --git a/score/TimeDaemon/code/ptp_machine/real/details/BUILD b/score/TimeDaemon/code/ptp_machine/real/details/BUILD new file mode 100644 index 0000000..71588d2 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/BUILD @@ -0,0 +1,54 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "real_ptp_engine", + srcs = [ + "real_ptp_engine.cpp", + ], + hdrs = [ + "real_ptp_engine.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score/TimeDaemon:__subpackages__"], + deps = [ + "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeDaemon/code/common/data_types:ptp_time_info", + "//score/libTSClient:gptp_ipc", + "@score_baselibs//score/mw/log:frontend", + ], +) + +cc_test( + name = "real_ptp_engine_test", + srcs = ["real_ptp_engine_test.cpp"], + tags = ["unit"], + deps = [ + ":real_ptp_engine", + "//score/libTSClient:gptp_ipc", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@score_baselibs//score/mw/log:console_only_backend", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":real_ptp_engine_test"], + test_suites_from_sub_packages = [], + visibility = ["//score/TimeDaemon:__subpackages__"], +) diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp new file mode 100644 index 0000000..6e46287 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp @@ -0,0 +1,99 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" + +#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/mw/log/logging.h" + +namespace score +{ +namespace td +{ +namespace details +{ + +RealPTPEngine::RealPTPEngine(std::string ipc_name) noexcept + : ipc_name_{std::move(ipc_name)} +{ +} + +bool RealPTPEngine::Initialize() +{ + if (initialized_) + return true; + + initialized_ = receiver_.Init(ipc_name_); + if (initialized_) + { + score::mw::log::LogInfo(kGPtpMachineContext) + << "RealPTPEngine: connected to IPC channel " << ipc_name_; + } + else + { + score::mw::log::LogError(kGPtpMachineContext) + << "RealPTPEngine: failed to open IPC channel " << ipc_name_; + } + return initialized_; +} + +bool RealPTPEngine::Deinitialize() +{ + if (initialized_) + { + receiver_.Close(); + initialized_ = false; + } + return true; +} + +bool RealPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) +{ + if (!initialized_) + return false; + + auto result = receiver_.Receive(); + if (!result.has_value()) + return false; + + cached_ = result.value(); + + const bool time_ok = ReadTimeValueAndStatus(info); + const bool pdelay_ok = ReadPDelayMeasurementData(info); + const bool sync_ok = ReadSyncMeasurementData(info); + return time_ok && pdelay_ok && sync_ok; +} + +bool RealPTPEngine::ReadTimeValueAndStatus(PtpTimeInfo& info) noexcept +{ + info.local_time = cached_.local_time; + info.ptp_assumed_time = cached_.ptp_assumed_time; + info.rate_deviation = cached_.rate_deviation; + info.status = cached_.status; + return true; +} + +bool RealPTPEngine::ReadPDelayMeasurementData(PtpTimeInfo& info) const noexcept +{ + info.pdelay_data = cached_.pdelay_data; + return true; +} + +bool RealPTPEngine::ReadSyncMeasurementData(PtpTimeInfo& info) const noexcept +{ + info.sync_fup_data = cached_.sync_fup_data; + return true; +} + +} // namespace details +} // namespace td +} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h new file mode 100644 index 0000000..a7215d7 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h @@ -0,0 +1,70 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H + +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_receiver.h" + +#include + +namespace score +{ +namespace td +{ +namespace details +{ + +/** + * @brief PTP engine that reads time data from the IPC channel written by TimeSlave. + */ +class RealPTPEngine final +{ + public: + explicit RealPTPEngine(std::string ipc_name = score::ts::details::kGptpIpcName) noexcept; + ~RealPTPEngine() noexcept = default; + + RealPTPEngine(const RealPTPEngine&) = delete; + RealPTPEngine& operator=(const RealPTPEngine&) = delete; + RealPTPEngine(RealPTPEngine&&) = delete; + RealPTPEngine& operator=(RealPTPEngine&&) = delete; + + /// Open and map the IPC channel. + /// @return true on success. + bool Initialize(); + + /// Unmap the IPC channel. + /// @return true (always succeeds). + bool Deinitialize(); + + /// Read a fresh snapshot from the IPC channel and populate @p info. + /// Delegates to ReadTimeValueAndStatus, ReadPDelayMeasurementData, + /// and ReadSyncMeasurementData. + bool ReadPTPSnapshot(PtpTimeInfo& info); + + bool ReadTimeValueAndStatus(PtpTimeInfo& info) noexcept; + bool ReadPDelayMeasurementData(PtpTimeInfo& info) const noexcept; + bool ReadSyncMeasurementData(PtpTimeInfo& info) const noexcept; + + private: + std::string ipc_name_; + score::ts::details::GptpIpcReceiver receiver_; + bool initialized_{false}; + PtpTimeInfo cached_{}; +}; + +} // namespace details +} // namespace td +} // namespace score + +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp new file mode 100644 index 0000000..0677b91 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp @@ -0,0 +1,281 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" + +#include "score/libTSClient/gptp_ipc_publisher.h" + +#include + +#include +#include + +namespace score +{ +namespace td +{ +namespace details +{ + +namespace +{ + +std::string UniqueShmName() +{ + static std::atomic counter{0}; + return "/gptp_rpe_ut_" + std::to_string(::getpid()) + "_" + + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +// Build a fully-populated PtpTimeInfo for roundtrip verification. +PtpTimeInfo MakeTestInfo() +{ + PtpTimeInfo info{}; + info.ptp_assumed_time = std::chrono::nanoseconds{9'876'543'210LL}; + info.rate_deviation = -0.25; + + info.status.is_synchronized = true; + info.status.is_correct = true; + info.status.is_timeout = false; + info.status.is_time_jump_future = false; + info.status.is_time_jump_past = false; + + info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + info.sync_fup_data.reference_global_timestamp = 100'000'000'500ULL; + info.sync_fup_data.reference_local_timestamp = 100'000'001'000ULL; + info.sync_fup_data.sync_ingress_timestamp = 100'000'001'000ULL; + info.sync_fup_data.correction_field = 8U; + info.sync_fup_data.sequence_id = 55; + info.sync_fup_data.pdelay = 4'000U; + info.sync_fup_data.port_number = 1; + info.sync_fup_data.clock_identity = 0xCAFEBABEDEAD0001ULL; + + info.pdelay_data.request_origin_timestamp = 200'000'000'000ULL; + info.pdelay_data.request_receipt_timestamp = 200'000'001'000ULL; + info.pdelay_data.response_origin_timestamp = 200'000'001'000ULL; + info.pdelay_data.response_receipt_timestamp = 200'000'002'000ULL; + info.pdelay_data.pdelay = 1'000U; + info.pdelay_data.req_port_number = 2; + info.pdelay_data.resp_port_number = 3; + info.pdelay_data.req_clock_identity = 0x0102030405060708ULL; + info.pdelay_data.resp_clock_identity = 0x0807060504030201ULL; + return info; +} + +} // namespace + +class RealPTPEngineTest : public ::testing::Test +{ + protected: + void SetUp() override + { + name_ = UniqueShmName(); + engine_ = std::make_unique(name_); + } + + void TearDown() override + { + engine_->Deinitialize(); + pub_.Destroy(); + } + + std::string name_; + score::ts::details::GptpIpcPublisher pub_; + std::unique_ptr engine_; +}; + +// ── Lifecycle ──────────────────────────────────────────────────────────────── + +TEST_F(RealPTPEngineTest, Initialize_WhenShmNotExist_ReturnsFalse) +{ + // No publisher → shm doesn't exist. + EXPECT_FALSE(engine_->Initialize()); +} + +TEST_F(RealPTPEngineTest, Initialize_WhenShmExists_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + EXPECT_TRUE(engine_->Initialize()); +} + +TEST_F(RealPTPEngineTest, Initialize_CalledTwiceWhenInitialized_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Initialize()); // idempotent +} + +TEST_F(RealPTPEngineTest, Deinitialize_WhenNotInitialized_ReturnsTrue) +{ + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(RealPTPEngineTest, Deinitialize_AfterInitialize_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(RealPTPEngineTest, Deinitialize_CalledTwice_BothReturnTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Deinitialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(RealPTPEngineTest, ReInitialize_AfterDeinitialize_Succeeds) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + ASSERT_TRUE(engine_->Deinitialize()); + EXPECT_TRUE(engine_->Initialize()); +} + +// ── ReadPTPSnapshot ─────────────────────────────────────────────────────────── + +TEST_F(RealPTPEngineTest, ReadPTPSnapshot_WhenNotInitialized_ReturnsFalse) +{ + PtpTimeInfo info{}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); +} + +TEST_F(RealPTPEngineTest, ReadPTPSnapshot_WithPublishedData_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakeTestInfo()); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadPTPSnapshot(result)); +} + +TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesTimeAndStatusCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const PtpTimeInfo expected = MakeTestInfo(); + pub_.Publish(expected); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.ptp_assumed_time, expected.ptp_assumed_time); + EXPECT_DOUBLE_EQ(result.rate_deviation, expected.rate_deviation); + EXPECT_EQ(result.status.is_synchronized, expected.status.is_synchronized); + EXPECT_EQ(result.status.is_correct, expected.status.is_correct); + EXPECT_EQ(result.status.is_timeout, expected.status.is_timeout); +} + +TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesSyncFupDataCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const PtpTimeInfo expected = MakeTestInfo(); + pub_.Publish(expected); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.sync_fup_data.precise_origin_timestamp, + expected.sync_fup_data.precise_origin_timestamp); + EXPECT_EQ(result.sync_fup_data.reference_global_timestamp, + expected.sync_fup_data.reference_global_timestamp); + EXPECT_EQ(result.sync_fup_data.sequence_id, expected.sync_fup_data.sequence_id); + EXPECT_EQ(result.sync_fup_data.pdelay, expected.sync_fup_data.pdelay); + EXPECT_EQ(result.sync_fup_data.clock_identity, expected.sync_fup_data.clock_identity); +} + +TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesPDelayDataCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const PtpTimeInfo expected = MakeTestInfo(); + pub_.Publish(expected); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.pdelay_data.pdelay, expected.pdelay_data.pdelay); + EXPECT_EQ(result.pdelay_data.req_port_number, expected.pdelay_data.req_port_number); + EXPECT_EQ(result.pdelay_data.resp_port_number, expected.pdelay_data.resp_port_number); + EXPECT_EQ(result.pdelay_data.req_clock_identity, + expected.pdelay_data.req_clock_identity); + EXPECT_EQ(result.pdelay_data.resp_clock_identity, + expected.pdelay_data.resp_clock_identity); +} + +// ── Individual sub-methods (called after ReadPTPSnapshot populates cache) ───── + +TEST_F(RealPTPEngineTest, ReadTimeValueAndStatus_FromCachedData_AlwaysReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakeTestInfo()); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo snap{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); + + // Call again on a fresh struct — should use the cached data. + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadTimeValueAndStatus(result)); + EXPECT_EQ(result.ptp_assumed_time, snap.ptp_assumed_time); + EXPECT_DOUBLE_EQ(result.rate_deviation, snap.rate_deviation); + EXPECT_EQ(result.status.is_synchronized, snap.status.is_synchronized); +} + +TEST_F(RealPTPEngineTest, ReadPDelayMeasurementData_FromCachedData_AlwaysReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakeTestInfo()); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo snap{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); + + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadPDelayMeasurementData(result)); + EXPECT_EQ(result.pdelay_data.pdelay, snap.pdelay_data.pdelay); +} + +TEST_F(RealPTPEngineTest, ReadSyncMeasurementData_FromCachedData_AlwaysReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakeTestInfo()); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo snap{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); + + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadSyncMeasurementData(result)); + EXPECT_EQ(result.sync_fup_data.sequence_id, snap.sync_fup_data.sequence_id); +} + +// Sub-methods on default-constructed cache (before any snapshot) return true +// with zeroed data. +TEST_F(RealPTPEngineTest, SubMethods_BeforeSnapshot_ReturnTrueWithZeroData) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadTimeValueAndStatus(result)); + EXPECT_TRUE(engine_->ReadPDelayMeasurementData(result)); + EXPECT_TRUE(engine_->ReadSyncMeasurementData(result)); + EXPECT_EQ(result.ptp_assumed_time, std::chrono::nanoseconds{0}); +} + +} // namespace details +} // namespace td +} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.cpp b/score/TimeDaemon/code/ptp_machine/real/factory.cpp new file mode 100644 index 0000000..5d53a87 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/factory.cpp @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/factory.h" + +namespace score +{ +namespace td +{ + +std::shared_ptr CreateGPTPRealMachine( + const std::string& name, + const std::string& ipc_name) +{ + constexpr std::chrono::milliseconds updateInterval(50); + return std::make_shared(name, updateInterval, ipc_name); +} + +} // namespace td +} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.h b/score/TimeDaemon/code/ptp_machine/real/factory.h new file mode 100644 index 0000000..de324c9 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/factory.h @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H + +#include "score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h" +#include "score/libTSClient/gptp_ipc_channel.h" + +#include +#include + +namespace score +{ +namespace td +{ + +/** + * @brief Factory function to create a configured GPTPRealMachine. + * + * Creates a GPTPRealMachine backed by the real gPTP engine. + * The engine reads PtpTimeInfo snapshots published by TimeSlave via + * the IPC channel named @p ipc_name. + * + * @param name Logical name for the machine instance. + * @param ipc_name IPC channel name (default: kGptpIpcName). + * @return A fully configured GPTPRealMachine instance. + */ +std::shared_ptr CreateGPTPRealMachine( + const std::string& name, + const std::string& ipc_name = score::ts::details::kGptpIpcName); + +} // namespace td +} // namespace score + +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h new file mode 100644 index 0000000..3860ba0 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H + +#include "score/TimeDaemon/code/ptp_machine/core/ptp_machine.h" +#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" + +namespace score +{ +namespace td +{ + +/// @brief PTPMachine instantiated with the real gPTP engine. +/// +/// Reads PtpTimeInfo snapshots written by TimeSlave via the IPC channel. +/// Construct via CreateGPTPRealMachine() (see factory.h) or directly: +/// +/// @code +/// auto machine = std::make_shared( +/// "real", std::chrono::milliseconds{50}, "/gptp_ptp_info"); +/// @endcode +using GPTPRealMachine = PTPMachine; + +} // namespace td +} // namespace score + +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp new file mode 100644 index 0000000..71291d8 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp @@ -0,0 +1,127 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/factory.h" +#include "score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h" +#include "score/libTSClient/gptp_ipc_publisher.h" + +#include + +#include +#include +#include +#include + +namespace score +{ +namespace td +{ + +namespace +{ + +std::string UniqueShmName() +{ + static std::atomic counter{0}; + return "/gptp_rm_it_" + std::to_string(::getpid()) + "_" + + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +score::td::PtpTimeInfo MakePublishedInfo() +{ + score::td::PtpTimeInfo info{}; + info.ptp_assumed_time = std::chrono::nanoseconds{5'000'000'000LL}; + info.rate_deviation = 0.5; + info.status.is_synchronized = true; + info.status.is_correct = true; + info.sync_fup_data.sequence_id = 7U; + info.sync_fup_data.pdelay = 1'000U; + return info; +} + +} // namespace + +class GPTPRealMachineIntegrationTest : public ::testing::Test +{ + protected: + void SetUp() override + { + name_ = UniqueShmName(); + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakePublishedInfo()); + + machine_ = CreateGPTPRealMachine("RealPTPMachine", name_); + machine_->SetPublishCallback([this](const PtpTimeInfo& data) { + { + std::lock_guard lk(mu_); + published_ = data; + } + promise_.set_value(); + }); + } + + void TearDown() override + { + machine_->Stop(); + machine_.reset(); + pub_.Destroy(); + } + + std::string name_; + score::ts::details::GptpIpcPublisher pub_; + std::shared_ptr machine_; + std::promise promise_; + PtpTimeInfo published_{}; + std::mutex mu_; +}; + +TEST_F(GPTPRealMachineIntegrationTest, GetName_ReturnsConstructionName) +{ + EXPECT_EQ(machine_->GetName(), "RealPTPMachine"); +} + +TEST_F(GPTPRealMachineIntegrationTest, Init_WhenShmExists_ReturnsTrue) +{ + EXPECT_TRUE(machine_->Init()); +} + +TEST_F(GPTPRealMachineIntegrationTest, Init_WhenShmMissing_ReturnsFalse) +{ + auto m = CreateGPTPRealMachine("NoShm", "/gptp_nosuchshm_xyz"); + EXPECT_FALSE(m->Init()); +} + +TEST_F(GPTPRealMachineIntegrationTest, Start_DeliversPublishedData_ViaCallback) +{ + ASSERT_TRUE(machine_->Init()); + machine_->Start(); + + auto fut = promise_.get_future(); + ASSERT_EQ(fut.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + std::lock_guard lk(mu_); + EXPECT_EQ(published_.ptp_assumed_time, std::chrono::nanoseconds{5'000'000'000LL}); + EXPECT_DOUBLE_EQ(published_.rate_deviation, 0.5); + EXPECT_TRUE(published_.status.is_synchronized); + EXPECT_TRUE(published_.status.is_correct); + EXPECT_EQ(published_.sync_fup_data.sequence_id, 7U); + EXPECT_EQ(published_.sync_fup_data.pdelay, 1'000U); +} + +TEST_F(GPTPRealMachineIntegrationTest, Init_CalledTwice_SecondCallReturnsSameResult) +{ + ASSERT_TRUE(machine_->Init()); + EXPECT_TRUE(machine_->Init()); +} + +} // namespace td +} // namespace score diff --git a/score/TimeSlave/BUILD b/score/TimeSlave/BUILD new file mode 100644 index 0000000..ca5de74 --- /dev/null +++ b/score/TimeSlave/BUILD @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* diff --git a/score/TimeSlave/code/BUILD b/score/TimeSlave/code/BUILD new file mode 100644 index 0000000..ca5de74 --- /dev/null +++ b/score/TimeSlave/code/BUILD @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* diff --git a/score/TimeSlave/code/application/BUILD b/score/TimeSlave/code/application/BUILD new file mode 100644 index 0000000..d83c578 --- /dev/null +++ b/score/TimeSlave/code/application/BUILD @@ -0,0 +1,33 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_binary( + name = "TimeSlave", + srcs = [ + "main.cpp", + "time_slave.cpp", + "time_slave.h", + ], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + deps = [ + "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeSlave/code/gptp:gptp_engine", + "//score/libTSClient:gptp_ipc", + "//score/time/HighPrecisionLocalSteadyClock", + "@score_baselibs//score/mw/log:console_only_backend", + "@score_lifecycle_health//src/lifecycle_client_lib", + ], +) diff --git a/score/TimeSlave/code/application/main.cpp b/score/TimeSlave/code/application/main.cpp new file mode 100644 index 0000000..29f2478 --- /dev/null +++ b/score/TimeSlave/code/application/main.cpp @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/application/time_slave.h" + +#include "src/lifecycle_client_lib/include/runapplication.h" + +int main(int argc, const char* argv[]) +{ + return score::mw::lifecycle::run_application(argc, argv); +} diff --git a/score/TimeSlave/code/application/time_slave.cpp b/score/TimeSlave/code/application/time_slave.cpp new file mode 100644 index 0000000..23d97e6 --- /dev/null +++ b/score/TimeSlave/code/application/time_slave.cpp @@ -0,0 +1,88 @@ +/* + * @Author: chenhao.gao chenhao.gao@ecarxgroup.com + * @Date: 2026-03-25 10:20:36 + * @LastEditors: chenhao.gao chenhao.gao@ecarxgroup.com + * @LastEditTime: 2026-03-25 16:03:13 + * @FilePath: /score_inc_time/score/TimeSlave/code/application/time_slave.cpp + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/application/time_slave.h" + +#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/mw/log/logging.h" +#include "score/time/HighPrecisionLocalSteadyClock/details/factory_impl.h" + +#include + +namespace score +{ +namespace ts +{ + +TimeSlave::TimeSlave() = default; + +std::int32_t TimeSlave::Initialize( + const score::mw::lifecycle::ApplicationContext& /*context*/) +{ + // Create the high-precision local clock for the gPTP engine + score::time::HighPrecisionLocalSteadyClock::FactoryImpl clock_factory{}; + auto clock = clock_factory.CreateHighPrecisionLocalSteadyClock(); + + engine_ = std::make_unique(opts_, std::move(clock)); + + if (!engine_->Initialize()) + { + score::mw::log::LogError(kGPtpMachineContext) + << "TimeSlave: GptpEngine initialization failed"; + return -1; + } + + if (!publisher_.Init()) + { + score::mw::log::LogError(kGPtpMachineContext) + << "TimeSlave: shared memory publisher initialization failed"; + return -1; + } + + score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave initialized"; + return 0; +} + +std::int32_t TimeSlave::Run(const score::cpp::stop_token& token) +{ + constexpr auto kPublishInterval = std::chrono::milliseconds{50}; + + score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave running"; + + while (!token.stop_requested()) + { + PtpTimeInfo info{}; + if (engine_->ReadPTPSnapshot(info)) + { + publisher_.Publish(info); + } + + std::this_thread::sleep_for(kPublishInterval); + } + + engine_->Deinitialize(); + publisher_.Destroy(); + + score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave stopped"; + return 0; +} + +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/application/time_slave.h b/score/TimeSlave/code/application/time_slave.h new file mode 100644 index 0000000..9f3795c --- /dev/null +++ b/score/TimeSlave/code/application/time_slave.h @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_APPLICATION_TIME_SLAVE_H +#define SCORE_TIMESLAVE_CODE_APPLICATION_TIME_SLAVE_H + +#include "score/TimeSlave/code/gptp/gptp_engine.h" +#include "score/libTSClient/gptp_ipc_publisher.h" + +#include "src/lifecycle_client_lib/include/application.h" + +#include + +namespace score +{ +namespace ts +{ + +/** + * @brief Standalone TimeSlave process that runs the gPTP engine + * and publishes time data to shared memory. + * + * TimeSlave is the gPTP protocol endpoint. It runs GptpEngine internally + * (with RxThread + PdelayThread) and periodically writes PtpTimeInfo + * to shared memory for consumption by TimeDaemon via ShmPTPEngine. + */ +class TimeSlave final : public score::mw::lifecycle::Application +{ + public: + explicit TimeSlave(); + ~TimeSlave() noexcept override = default; + + TimeSlave(TimeSlave&&) noexcept = delete; + TimeSlave(const TimeSlave&) noexcept = delete; + TimeSlave& operator=(TimeSlave&&) & noexcept = delete; + TimeSlave& operator=(const TimeSlave&) & noexcept = delete; + + std::int32_t Initialize(const score::mw::lifecycle::ApplicationContext& context) override; + std::int32_t Run(const score::cpp::stop_token& token) override; + + private: + details::GptpEngineOptions opts_; + std::unique_ptr engine_; + details::GptpIpcPublisher publisher_; +}; + +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_APPLICATION_TIME_SLAVE_H diff --git a/score/TimeSlave/code/gptp/BUILD b/score/TimeSlave/code/gptp/BUILD new file mode 100644 index 0000000..98dcbfa --- /dev/null +++ b/score/TimeSlave/code/gptp/BUILD @@ -0,0 +1,60 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "gptp_engine", + srcs = ["gptp_engine.cpp"], + hdrs = ["gptp_engine.h"], + features = COMPILER_WARNING_FEATURES, + linkopts = select({ + "@platforms//os:qnx": ["-lsocket", "-lc"], + "//conditions:default": ["-lpthread"], + }), + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeDaemon/code/common/data_types:ptp_time_info", + "//score/TimeSlave/code/gptp/details:gptp_details", + "//score/time/HighPrecisionLocalSteadyClock:interface", + "@score_baselibs//score/mw/log:frontend", + ], +) + +cc_test( + name = "gptp_engine_test", + srcs = ["gptp_engine_test.cpp"], + tags = ["unit"], + deps = [ + ":gptp_engine", + "//score/TimeSlave/code/gptp/details:i_network_identity", + "//score/TimeSlave/code/gptp/details:i_raw_socket", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@score_baselibs//score/mw/log:console_only_backend", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":gptp_engine_test"], + test_suites_from_sub_packages = [ + "//score/TimeSlave/code/gptp/details:unit_test_suite", + "//score/TimeSlave/code/gptp/instrument:unit_test_suite", + "//score/TimeSlave/code/gptp/record:unit_test_suite", + ], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/details/BUILD b/score/TimeSlave/code/gptp/details/BUILD new file mode 100644 index 0000000..e1f1c20 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/BUILD @@ -0,0 +1,199 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "ptp_types", + hdrs = ["ptp_types.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], +) + +cc_library( + name = "i_raw_socket", + hdrs = ["i_raw_socket.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [], +) + +cc_library( + name = "i_network_identity", + hdrs = ["i_network_identity.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [":ptp_types"], +) + +cc_library( + name = "raw_socket", + srcs = select({ + "@platforms//os:qnx": ["//score/TimeSlave/code/gptp/platform/qnx:raw_socket_src"], + "//conditions:default": ["//score/TimeSlave/code/gptp/platform/linux:raw_socket_src"], + }), + hdrs = ["raw_socket.h"], + features = COMPILER_WARNING_FEATURES, + linkopts = select({ + "@platforms//os:qnx": ["-lsocket"], + "//conditions:default": [], + }), + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + ":i_raw_socket", + ":ptp_types", + ], +) + +cc_library( + name = "frame_codec", + srcs = ["frame_codec.cpp"], + hdrs = ["frame_codec.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [":ptp_types"], +) + +cc_library( + name = "message_parser", + srcs = ["message_parser.cpp"], + hdrs = ["message_parser.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [":ptp_types"], +) + +cc_library( + name = "sync_state_machine", + srcs = ["sync_state_machine.cpp"], + hdrs = ["sync_state_machine.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + ":ptp_types", + "//score/TimeDaemon/code/common/data_types:ptp_time_info", + ], +) + +cc_library( + name = "pdelay_measurer", + srcs = ["pdelay_measurer.cpp"], + hdrs = ["pdelay_measurer.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + ":frame_codec", + ":i_raw_socket", + ":ptp_types", + "//score/TimeDaemon/code/common/data_types:ptp_time_info", + ], +) + +cc_library( + name = "network_identity", + srcs = select({ + "@platforms//os:qnx": ["//score/TimeSlave/code/gptp/platform/qnx:network_identity_src"], + "//conditions:default": ["//score/TimeSlave/code/gptp/platform/linux:network_identity_src"], + }), + hdrs = ["network_identity.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + ":i_network_identity", + ":ptp_types", + ], +) + +cc_library( + name = "gptp_details", + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + ":frame_codec", + ":i_network_identity", + ":i_raw_socket", + ":message_parser", + ":network_identity", + ":pdelay_measurer", + ":ptp_types", + ":raw_socket", + ":sync_state_machine", + ], +) + +cc_test( + name = "pdelay_measurer_test", + srcs = ["pdelay_measurer_test.cpp"], + tags = ["unit"], + deps = [ + ":pdelay_measurer", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "frame_codec_test", + srcs = ["frame_codec_test.cpp"], + tags = ["unit"], + deps = [ + ":frame_codec", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "message_parser_test", + srcs = ["message_parser_test.cpp"], + tags = ["unit"], + deps = [ + ":message_parser", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "sync_state_machine_test", + srcs = ["sync_state_machine_test.cpp"], + tags = ["unit"], + deps = [ + ":sync_state_machine", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [ + ":frame_codec_test", + ":message_parser_test", + ":pdelay_measurer_test", + ":sync_state_machine_test", + ], + test_suites_from_sub_packages = [], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/details/frame_codec.cpp b/score/TimeSlave/code/gptp/details/frame_codec.cpp new file mode 100644 index 0000000..d12dd41 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/frame_codec.cpp @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/frame_codec.h" + +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +int Str2Mac(const char* s, unsigned char mac[kMacAddrLen]) noexcept +{ + unsigned int b[kMacAddrLen]{}; + if (std::sscanf(s, "%x:%x:%x:%x:%x:%x", + &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != kMacAddrLen) + { + return -1; + } + for (int i = 0; i < kMacAddrLen; ++i) + mac[i] = static_cast(b[i]); + return 0; +} + +} // namespace + +bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, + int frame_len, + int& ptp_offset) const +{ + const int kEthHdrLen = static_cast(sizeof(ethhdr)); + if (frame_len < kEthHdrLen) + return false; + + ethhdr hdr{}; + std::memcpy(&hdr, frame, sizeof(hdr)); + + const auto etype = static_cast(ntohs(hdr.h_proto)); + + if (etype == static_cast(kEthP8021Q)) + { + // Skip 4-byte VLAN tag; re-read EtherType + if (frame_len < kEthHdrLen + kVlanTagLen + 2) + return false; + const uint16_t inner_etype_be = + *reinterpret_cast(frame + kEthHdrLen + kVlanTagLen); + if (ntohs(inner_etype_be) != static_cast(kEthP1588)) + return false; + ptp_offset = kEthHdrLen + kVlanTagLen; + return true; + } + + if (etype != static_cast(kEthP1588)) + return false; + + ptp_offset = kEthHdrLen; + return true; +} + +bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, + unsigned int& buf_len) const +{ + constexpr unsigned int kMaxFrameSize = 2048U; + const unsigned int kHdrLen = static_cast(sizeof(ethhdr)); + + if (buf_len + kHdrLen > kMaxFrameSize) + return false; + + std::memmove(buf + kHdrLen, buf, buf_len); + + auto* hdr = reinterpret_cast(buf); + if (Str2Mac(kPtpSrcMac, hdr->h_source) != 0 || + Str2Mac(kPtpDstMac, hdr->h_dest) != 0) + { + return false; + } + hdr->h_proto = htons(static_cast(kEthP1588)); + + buf_len += kHdrLen; + return true; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/frame_codec.h b/score/TimeSlave/code/gptp/details/frame_codec.h new file mode 100644 index 0000000..6a49687 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/frame_codec.h @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_FRAME_CODEC_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_FRAME_CODEC_H + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief Ethernet frame encode/decode for PTP-over-L2. + * + * Uses the standard PTP multicast destination MAC 01:80:C2:00:00:0E and + * EtherType 0x88F7. VLAN-tagged frames are accepted on receive. + */ +class FrameCodec final +{ + public: + /** + * @brief Locate the PTP payload inside a raw Ethernet frame. + * + * Handles 802.1Q VLAN-tagged frames transparently. + * + * @param frame Raw frame bytes as received from the socket. + * @param frame_len Total length of @p frame in bytes. + * @param ptp_offset Output: byte offset where the PTP message starts. + * @return true if @p frame contains a PTP/1588 Ethertype, false otherwise. + */ + bool ParseEthernetHeader(const std::uint8_t* frame, + int frame_len, + int& ptp_offset) const; + + /** + * @brief Prepend an Ethernet header for PTP multicast transmission. + * + * Modifies @p buf in-place (shifts payload to make room) and increments + * @p buf_len by the size of the added header. + * + * @param buf Buffer large enough to hold existing payload plus header. + * @param buf_len In/out: payload length → frame length after prepend. + * @return true on success, false if the buffer would overflow. + */ + bool AddEthernetHeader(std::uint8_t* buf, unsigned int& buf_len) const; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_FRAME_CODEC_H diff --git a/score/TimeSlave/code/gptp/details/frame_codec_test.cpp b/score/TimeSlave/code/gptp/details/frame_codec_test.cpp new file mode 100644 index 0000000..ca5c486 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/frame_codec_test.cpp @@ -0,0 +1,149 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/frame_codec.h" + +#include + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// Build a minimal raw Ethernet frame with the given EtherType in the ethhdr. +// The buffer is zero-initialized; callers fill in anything extra. +std::vector MakeEthFrame(std::uint16_t etype, int total_len) +{ + std::vector buf(static_cast(total_len), 0); + // h_proto at bytes 12-13 (big-endian) + const std::uint16_t etype_be = htons(etype); + std::memcpy(&buf[12], &etype_be, 2); + return buf; +} + +} // namespace + +class FrameCodecParseTest : public ::testing::Test +{ + protected: + FrameCodec codec_; +}; + +// ── ParseEthernetHeader ─────────────────────────────────────────────────────── + +TEST_F(FrameCodecParseTest, TooShort_ReturnsFalse) +{ + std::uint8_t tiny[10] = {}; + int offset = -1; + EXPECT_FALSE(codec_.ParseEthernetHeader(tiny, 10, offset)); +} + +TEST_F(FrameCodecParseTest, ExactlyEthHdrLength_NonPtp_ReturnsFalse) +{ + // 14 bytes, EtherType = 0x0800 (IPv4) — not PTP and not VLAN + auto buf = MakeEthFrame(0x0800, 14); + int offset = -1; + EXPECT_FALSE(codec_.ParseEthernetHeader(buf.data(), 14, offset)); +} + +TEST_F(FrameCodecParseTest, Eth1588_Valid_ReturnsTrueAndOffset14) +{ + // Plain PTP frame: ethhdr(14) + PTP payload + auto buf = MakeEthFrame(static_cast(kEthP1588), 80); + int offset = -1; + ASSERT_TRUE(codec_.ParseEthernetHeader(buf.data(), 80, offset)); + EXPECT_EQ(offset, 14); // PTP payload immediately after ethhdr +} + +TEST_F(FrameCodecParseTest, Vlan8021Q_ValidPtpInner_ReturnsTrueAndOffset18) +{ + // VLAN-tagged: ethhdr(14) + VLAN tag(4) + inner EtherType(2) + payload + // Minimum valid length = 20; inner EtherType is at bytes [18..19] + auto buf = MakeEthFrame(static_cast(kEthP8021Q), 60); + // Inner EtherType = kEthP1588 at offset 14 + kVlanTagLen = 18 + const std::uint16_t inner_be = htons(static_cast(kEthP1588)); + std::memcpy(&buf[14 + kVlanTagLen], &inner_be, 2); + int offset = -1; + ASSERT_TRUE(codec_.ParseEthernetHeader(buf.data(), 60, offset)); + EXPECT_EQ(offset, 14 + kVlanTagLen); +} + +TEST_F(FrameCodecParseTest, Vlan8021Q_TooShortForInnerType_ReturnsFalse) +{ + // kEthHdrLen(14) + kVlanTagLen(4) + 2 = 20; provide only 19 bytes + auto buf = MakeEthFrame(static_cast(kEthP8021Q), 19); + int offset = -1; + EXPECT_FALSE(codec_.ParseEthernetHeader(buf.data(), 19, offset)); +} + +TEST_F(FrameCodecParseTest, Vlan8021Q_NonPtpInnerType_ReturnsFalse) +{ + auto buf = MakeEthFrame(static_cast(kEthP8021Q), 30); + // Inner EtherType = IPv4 (non-PTP) + const std::uint16_t inner_be = htons(0x0800U); + std::memcpy(&buf[14 + kVlanTagLen], &inner_be, 2); + int offset = -1; + EXPECT_FALSE(codec_.ParseEthernetHeader(buf.data(), 30, offset)); +} + +TEST_F(FrameCodecParseTest, UnknownEtherType_ReturnsFalse) +{ + auto buf = MakeEthFrame(0xABCDU, 60); + int offset = -1; + EXPECT_FALSE(codec_.ParseEthernetHeader(buf.data(), 60, offset)); +} + +// ── AddEthernetHeader ───────────────────────────────────────────────────────── + +TEST_F(FrameCodecParseTest, AddEthernetHeader_NormalPayload_ReturnsTrueAndIncrementsLen) +{ + // Buffer large enough for payload + 14-byte header + constexpr unsigned int kPayloadLen = 44U; + std::uint8_t buf[256] = {}; + // Put a sentinel in the payload area so we can verify the shift + buf[0] = 0xDE; + buf[1] = 0xAD; + + unsigned int len = kPayloadLen; + ASSERT_TRUE(codec_.AddEthernetHeader(buf, len)); + EXPECT_EQ(len, kPayloadLen + 14U); + + // Payload was shifted right by 14 bytes + EXPECT_EQ(buf[14], 0xDE); + EXPECT_EQ(buf[15], 0xAD); + + // h_proto at bytes 12-13 should be kEthP1588 in network byte order + const std::uint16_t h_proto_be = htons(static_cast(kEthP1588)); + std::uint16_t actual{}; + std::memcpy(&actual, &buf[12], 2); + EXPECT_EQ(actual, h_proto_be); +} + +TEST_F(FrameCodecParseTest, AddEthernetHeader_PayloadTooLarge_ReturnsFalse) +{ + constexpr unsigned int kTooBig = 2048U; // buf_len + 14 > 2048 + std::uint8_t buf[4096] = {}; + unsigned int len = kTooBig; + EXPECT_FALSE(codec_.AddEthernetHeader(buf, len)); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/i_network_identity.h b/score/TimeSlave/code/gptp/details/i_network_identity.h new file mode 100644 index 0000000..92a4b1e --- /dev/null +++ b/score/TimeSlave/code/gptp/details/i_network_identity.h @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_NETWORK_IDENTITY_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_NETWORK_IDENTITY_H + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Interface for resolving the IEEE 1588 ClockIdentity from a network interface. +class INetworkIdentity +{ + public: + virtual ~INetworkIdentity() noexcept = default; + + /// Resolve the ClockIdentity for @p iface_name. Returns true on success. + virtual bool Resolve(const std::string& iface_name) = 0; + + /// Return the resolved identity. Valid only after a successful Resolve(). + virtual ClockIdentity GetClockIdentity() const = 0; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_NETWORK_IDENTITY_H diff --git a/score/TimeSlave/code/gptp/details/i_raw_socket.h b/score/TimeSlave/code/gptp/details/i_raw_socket.h new file mode 100644 index 0000000..4ac3fd9 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/i_raw_socket.h @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_RAW_SOCKET_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_RAW_SOCKET_H + +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Interface for a platform raw socket used by GptpEngine and PeerDelayMeasurer. +class IRawSocket +{ + public: + virtual ~IRawSocket() noexcept = default; + + /// Open the socket bound to @p iface. Returns false on failure. + virtual bool Open(const std::string& iface) = 0; + + /// Configure hardware TX/RX timestamping. Returns false on failure. + virtual bool EnableHwTimestamping() = 0; + + /// Close the socket and release the file descriptor. + virtual void Close() = 0; + + /// Receive one frame. + /// @return Number of bytes received, 0 on timeout, -1 on error. + virtual int Recv(std::uint8_t* buf, std::size_t buf_len, + ::timespec& hwts, int timeout_ms) = 0; + + /// Send one frame. + /// @return Number of bytes sent, or -1 on error. + virtual int Send(const void* buf, int len, ::timespec& hwts) = 0; + + /// Return the underlying file descriptor. + virtual int GetFd() const = 0; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_RAW_SOCKET_H diff --git a/score/TimeSlave/code/gptp/details/message_parser.cpp b/score/TimeSlave/code/gptp/details/message_parser.cpp new file mode 100644 index 0000000..b3ac7e0 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/message_parser.cpp @@ -0,0 +1,118 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/message_parser.h" + +#include +#include + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define BSWAP64(x) __builtin_bswap64(x) +#else +#define BSWAP64(x) (x) +#endif + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +std::uint16_t LoadU16(const std::uint8_t* p) noexcept +{ + std::uint16_t v{}; + std::memcpy(&v, p, sizeof(v)); + return ntohs(v); +} + +std::uint32_t LoadU32(const std::uint8_t* p) noexcept +{ + std::uint32_t v{}; + std::memcpy(&v, p, sizeof(v)); + return ntohl(v); +} + +std::uint64_t LoadBe64(const std::uint8_t* p) noexcept +{ + std::uint64_t v{}; + std::memcpy(&v, p, sizeof(v)); + return BSWAP64(v); +} + +Timestamp LoadTimestamp(const std::uint8_t* p) noexcept +{ + Timestamp ts{}; + ts.seconds_msb = LoadU16(p); + ts.seconds_lsb = LoadU32(p + 2); + ts.nanoseconds = LoadU32(p + 6); + return ts; +} + +} // namespace + +bool GptpMessageParser::Parse(const std::uint8_t* payload, + std::size_t payload_len, + PTPMessage& msg) const +{ + if (payload == nullptr || payload_len < sizeof(PTPHeader)) + return false; + + msg.ptpHdr.tsmt = payload[0]; + msg.ptpHdr.version = payload[1]; + msg.ptpHdr.messageLength = LoadU16(payload + 2); + msg.ptpHdr.domainNumber = payload[4]; + msg.ptpHdr.reserved1 = payload[5]; + std::memcpy(msg.ptpHdr.flagField, payload + 6, 2); + msg.ptpHdr.correctionField = static_cast(LoadBe64(payload + 8)); + msg.ptpHdr.reserved2 = LoadU32(payload + 16); + std::memcpy(msg.ptpHdr.sourcePortIdentity.clockIdentity.id, payload + 20, 8); + msg.ptpHdr.sourcePortIdentity.portNumber = LoadU16(payload + 28); + msg.ptpHdr.sequenceId = LoadU16(payload + 30); + msg.ptpHdr.controlField = payload[32]; + msg.ptpHdr.logMessageInterval = static_cast(payload[33]); + + msg.msgtype = msg.ptpHdr.tsmt & 0x0FU; + + constexpr std::size_t kBodyOffset = 34U; + + switch (msg.msgtype) + { + case kPtpMsgtypeFollowUp: + if (payload_len >= kBodyOffset + sizeof(Timestamp)) + msg.follow_up.preciseOriginTimestamp = LoadTimestamp(payload + kBodyOffset); + break; + + case kPtpMsgtypePdelayResp: + if (payload_len >= kBodyOffset + sizeof(Timestamp)) + msg.pdelay_resp.responseOriginTimestamp = LoadTimestamp(payload + kBodyOffset); + break; + + case kPtpMsgtypePdelayRespFollowUp: + if (payload_len >= kBodyOffset + sizeof(Timestamp)) + msg.pdelay_resp_fup.responseOriginReceiptTimestamp = + LoadTimestamp(payload + kBodyOffset); + break; + + default: + break; + } + + return true; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/message_parser.h b/score/TimeSlave/code/gptp/details/message_parser.h new file mode 100644 index 0000000..a1bd4c3 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/message_parser.h @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_MESSAGE_PARSER_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_MESSAGE_PARSER_H + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief IEEE 802.1AS / 1588-v2 message parser. + * + * Decoupled from the socket layer: callers feed the PTP payload (post + * Ethernet-header stripping) as a byte buffer and receive a fully populated + * PTPMessage. + */ +class GptpMessageParser final +{ + public: + /** + * @brief Parse @p payload_len bytes at @p payload into @p msg. + * + * Populates the PTPHeader union fields and the message-type-specific body + * fields (Timestamps, PortIdentity, correctionField). Does NOT touch the + * hardware-timestamp fields (recvHardwareTS, sendHardwareTS) — those are + * filled by the caller after the socket recv. + * + * @return true if the payload contains a valid IEEE 1588 / 802.1AS header. + */ + bool Parse(const std::uint8_t* payload, + std::size_t payload_len, + PTPMessage& msg) const; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_MESSAGE_PARSER_H diff --git a/score/TimeSlave/code/gptp/details/message_parser_test.cpp b/score/TimeSlave/code/gptp/details/message_parser_test.cpp new file mode 100644 index 0000000..d5ed88a --- /dev/null +++ b/score/TimeSlave/code/gptp/details/message_parser_test.cpp @@ -0,0 +1,210 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/message_parser.h" + +#include + +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// PTP header occupies exactly 34 bytes on the wire. +constexpr std::size_t kHdrSize = 34U; +// Timestamp body = 10 bytes (u16 + u32 + u32). +constexpr std::size_t kTsSize = 10U; + +// Store a 16-bit big-endian value at buf[off]. +void PutU16Be(std::uint8_t* buf, std::size_t off, std::uint16_t val) +{ + const std::uint16_t v = htons(val); + std::memcpy(buf + off, &v, 2); +} + +// Store a 32-bit big-endian value at buf[off]. +void PutU32Be(std::uint8_t* buf, std::size_t off, std::uint32_t val) +{ + const std::uint32_t v = htonl(val); + std::memcpy(buf + off, &v, 4); +} + +// Store a 64-bit big-endian value at buf[off]. +void PutU64Be(std::uint8_t* buf, std::size_t off, std::uint64_t val) +{ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + val = __builtin_bswap64(val); +#endif + std::memcpy(buf + off, &val, 8); +} + +// Build a minimal PTP payload of type `msgtype` with the given header fields. +// Optionally appends a 10-byte Timestamp body (seconds_lsb + nanoseconds). +std::vector BuildPayload(std::uint8_t msgtype, + std::uint16_t seqId, + std::int64_t correction = 0, + std::uint16_t port_number = 0, + std::uint64_t clock_id = 0, + std::uint32_t ts_sec_lsb = 0, + std::uint32_t ts_ns = 0) +{ + const std::size_t total = kHdrSize + kTsSize; + std::vector buf(total, 0); + + buf[0] = static_cast((kPtpTransportSpecific) | (msgtype & 0x0FU)); + buf[1] = kPtpVersion; + PutU16Be(buf.data(), 2, static_cast(total)); // messageLength + // domainNumber = 0 (default) + PutU64Be(buf.data(), 8, static_cast(correction)); // correctionField + // Clock identity is a raw byte array; store in native order so ClockIdentityToU64 roundtrips. + std::memcpy(buf.data() + 20, &clock_id, 8); + PutU16Be(buf.data(), 28, port_number); + PutU16Be(buf.data(), 30, seqId); + buf[32] = kCtlFollowUp; + + // Timestamp body at offset 34: seconds_msb(u16) + seconds_lsb(u32) + nanoseconds(u32) + PutU16Be(buf.data(), kHdrSize, 0U); // seconds_msb = 0 + PutU32Be(buf.data(), kHdrSize + 2, ts_sec_lsb); + PutU32Be(buf.data(), kHdrSize + 6, ts_ns); + + return buf; +} + +} // namespace + +class MessageParserTest : public ::testing::Test +{ + protected: + GptpMessageParser parser_; +}; + +// ── Rejection cases ─────────────────────────────────────────────────────────── + +TEST_F(MessageParserTest, NullPayload_ReturnsFalse) +{ + PTPMessage msg{}; + EXPECT_FALSE(parser_.Parse(nullptr, 64U, msg)); +} + +TEST_F(MessageParserTest, TooShortPayload_ReturnsFalse) +{ + std::uint8_t tiny[10] = {}; + PTPMessage msg{}; + EXPECT_FALSE(parser_.Parse(tiny, 10U, msg)); +} + +// ── Sync (no body decoded, only header) ─────────────────────────────────────── + +TEST_F(MessageParserTest, SyncMessage_ReturnsTrue_MsgtypeIsSync) +{ + auto buf = BuildPayload(kPtpMsgtypeSync, 7U); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.msgtype, kPtpMsgtypeSync); +} + +TEST_F(MessageParserTest, Header_SequenceId_DecodedCorrectly) +{ + const std::uint16_t kSeq = 0x1234U; + auto buf = BuildPayload(kPtpMsgtypeSync, kSeq); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.ptpHdr.sequenceId, kSeq); +} + +TEST_F(MessageParserTest, Header_CorrectionField_DecodedCorrectly) +{ + // correctionField = 65536 (0x10000) → CorrectionToTmv would give 1 ns + const std::int64_t kCorr = 65536LL; + auto buf = BuildPayload(kPtpMsgtypeSync, 1U, kCorr); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.ptpHdr.correctionField, kCorr); +} + +TEST_F(MessageParserTest, Header_SourcePortIdentity_DecodedCorrectly) +{ + const std::uint64_t kClockId = 0xCAFEBABEDEAD0001ULL; + const std::uint16_t kPort = 3U; + auto buf = BuildPayload(kPtpMsgtypeSync, 1U, 0, kPort, kClockId); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.ptpHdr.sourcePortIdentity.portNumber, kPort); + EXPECT_EQ(ClockIdentityToU64(msg.ptpHdr.sourcePortIdentity.clockIdentity), kClockId); +} + +// ── FollowUp body ───────────────────────────────────────────────────────────── + +TEST_F(MessageParserTest, FollowUp_Body_TimestampDecodedCorrectly) +{ + // precise_origin = 2 seconds + 500_000_000 ns + const std::uint32_t kSecLsb = 2U; + const std::uint32_t kNs = 500'000'000U; + auto buf = BuildPayload(kPtpMsgtypeFollowUp, 99U, 0, 0, 0, kSecLsb, kNs); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.msgtype, kPtpMsgtypeFollowUp); + EXPECT_EQ(msg.follow_up.preciseOriginTimestamp.seconds_lsb, kSecLsb); + EXPECT_EQ(msg.follow_up.preciseOriginTimestamp.nanoseconds, kNs); +} + +// ── PdelayResp body ─────────────────────────────────────────────────────────── + +TEST_F(MessageParserTest, PdelayResp_Body_TimestampDecodedCorrectly) +{ + const std::uint32_t kSecLsb = 3U; + const std::uint32_t kNs = 123'456'789U; + auto buf = BuildPayload(kPtpMsgtypePdelayResp, 5U, 0, 0, 0, kSecLsb, kNs); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.msgtype, kPtpMsgtypePdelayResp); + EXPECT_EQ(msg.pdelay_resp.responseOriginTimestamp.seconds_lsb, kSecLsb); + EXPECT_EQ(msg.pdelay_resp.responseOriginTimestamp.nanoseconds, kNs); +} + +// ── PdelayRespFollowUp body ─────────────────────────────────────────────────── + +TEST_F(MessageParserTest, PdelayRespFollowUp_Body_TimestampDecodedCorrectly) +{ + const std::uint32_t kSecLsb = 7U; + const std::uint32_t kNs = 999'000'000U; + auto buf = BuildPayload(kPtpMsgtypePdelayRespFollowUp, 11U, 0, 0, 0, kSecLsb, kNs); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.msgtype, kPtpMsgtypePdelayRespFollowUp); + EXPECT_EQ(msg.pdelay_resp_fup.responseOriginReceiptTimestamp.seconds_lsb, kSecLsb); + EXPECT_EQ(msg.pdelay_resp_fup.responseOriginReceiptTimestamp.nanoseconds, kNs); +} + +// ── Unknown type: header parsed, no body crash ──────────────────────────────── + +TEST_F(MessageParserTest, UnknownMsgtype_ReturnsTrue_HeaderParsed) +{ + // Use PdelayReq (type 0x2) which has no special body decoding branch. + auto buf = BuildPayload(kPtpMsgtypePdelayReq, 20U); + PTPMessage msg{}; + ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); + EXPECT_EQ(msg.msgtype, kPtpMsgtypePdelayReq); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/network_identity.h b/score/TimeSlave/code/gptp/details/network_identity.h new file mode 100644 index 0000000..b334488 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/network_identity.h @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_NETWORK_IDENTITY_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_NETWORK_IDENTITY_H + +#include "score/TimeSlave/code/gptp/details/i_network_identity.h" +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief Derive the IEEE 1588 ClockIdentity from a network interface. + * + * The identity is built from the interface's EUI-48 MAC address by inserting + * 0xFF 0xFE at positions 3–4 to form an EUI-64 (per IEEE 1588-2019 §7.5.2.2). + * Platform implementation: Linux + QNX via #ifdef. + */ +class NetworkIdentity : public INetworkIdentity +{ + public: + /// Resolve the ClockIdentity for @p iface_name. + /// @return true on success. + bool Resolve(const std::string& iface_name) override; + + /// Return the resolved identity. Valid only after a successful Resolve(). + ClockIdentity GetClockIdentity() const override { return identity_; } + + private: + ClockIdentity identity_{}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_NETWORK_IDENTITY_H diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp new file mode 100644 index 0000000..9f5f596 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -0,0 +1,146 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/pdelay_measurer.h" +#include "score/TimeSlave/code/gptp/details/frame_codec.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +PeerDelayMeasurer::PeerDelayMeasurer( + const ClockIdentity& local_identity) noexcept + : local_identity_{local_identity} +{ +} + +int PeerDelayMeasurer::SendRequest(IRawSocket& socket) +{ + PTPMessage req{}; + req.ptpHdr.tsmt = kPtpMsgtypePdelayReq | kPtpTransportSpecific; + req.ptpHdr.version = kPtpVersion; + req.ptpHdr.domainNumber = 0; + req.ptpHdr.messageLength = htons(sizeof(PdelayReqBody)); + req.ptpHdr.flagField[0] = 0; + req.ptpHdr.flagField[1] = 0; + req.ptpHdr.correctionField = 0; + req.ptpHdr.reserved2 = 0; + req.ptpHdr.sourcePortIdentity.clockIdentity = local_identity_; + req.ptpHdr.sourcePortIdentity.portNumber = htons(0x0001U); + req.ptpHdr.sequenceId = htons(static_cast(seqnum_)); + req.ptpHdr.controlField = kCtlOther; + req.ptpHdr.logMessageInterval = 0x7F; + + // Save a copy with host-byte-order sequence ID for later matching + { + std::lock_guard lk(mutex_); + req_ = req; + req_.ptpHdr.sequenceId = static_cast(seqnum_); + } + ++seqnum_; + + auto buf = reinterpret_cast(&req); + unsigned int len = sizeof(PdelayReqBody); + FrameCodec codec; + if (!codec.AddEthernetHeader(buf, len)) + return -1; + + ::timespec hwts{}; + const int r = socket.Send(buf, static_cast(len), hwts); + if (r > 0) + { + std::lock_guard lk(mutex_); + req_.sendHardwareTS = TmvT{ + static_cast(hwts.tv_sec) * kNsPerSec + hwts.tv_nsec}; + } + return r; +} + +void PeerDelayMeasurer::OnResponse(const PTPMessage& msg) +{ + std::lock_guard lk(mutex_); + resp_ = msg; +} + +void PeerDelayMeasurer::OnResponseFollowUp(const PTPMessage& msg) +{ + { + std::lock_guard lk(mutex_); + resp_fup_ = msg; + } + ComputeAndStore(); +} + +void PeerDelayMeasurer::ComputeAndStore() noexcept +{ + std::lock_guard lk(mutex_); + + // All three messages must share the same sequence ID + if (req_.ptpHdr.sequenceId != resp_.ptpHdr.sequenceId) + return; + if (resp_.ptpHdr.sequenceId != resp_fup_.ptpHdr.sequenceId) + return; + + // t1 = HW send timestamp of our Pdelay_Req + const TmvT t1 = req_.sendHardwareTS; + // t2 = remote receipt time (from Pdelay_Resp body: requestReceiptTimestamp) + const TmvT t2 = resp_.parseMessageTs; + // t3 = remote send time (from Pdelay_Resp_FUP body) + corrections + const TmvT t3 = resp_fup_.parseMessageTs; + const TmvT c1 = CorrectionToTmv(resp_.ptpHdr.correctionField); + const TmvT c2 = CorrectionToTmv(resp_fup_.ptpHdr.correctionField); + const TmvT t3c = TmvT{t3.ns + c1.ns + c2.ns}; + // t4 = local HW receive timestamp of Pdelay_Resp + const TmvT t4 = resp_.recvHardwareTS; + + const std::int64_t delay = ((t2.ns - t1.ns) + (t4.ns - t3c.ns)) / 2LL; + + PDelayResult r{}; + r.path_delay_ns = delay; + r.valid = true; + + score::td::PDelayData& d = r.pdelay_data; + d.request_origin_timestamp = static_cast(t1.ns); + d.request_receipt_timestamp = static_cast(t2.ns); + d.response_origin_timestamp = static_cast(t3.ns); + d.response_receipt_timestamp = static_cast(t4.ns); + d.reference_global_timestamp = static_cast(t3c.ns); + d.reference_local_timestamp = static_cast(t4.ns); + d.sequence_id = resp_.ptpHdr.sequenceId; + d.pdelay = static_cast(delay); + d.req_port_number = + req_.ptpHdr.sourcePortIdentity.portNumber; + d.req_clock_identity = + ClockIdentityToU64(req_.ptpHdr.sourcePortIdentity.clockIdentity); + d.resp_port_number = + resp_.ptpHdr.sourcePortIdentity.portNumber; + d.resp_clock_identity = + ClockIdentityToU64(resp_.ptpHdr.sourcePortIdentity.clockIdentity); + + result_ = r; +} + +PDelayResult PeerDelayMeasurer::GetResult() const +{ + std::lock_guard lk(mutex_); + return result_; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.h b/score/TimeSlave/code/gptp/details/pdelay_measurer.h new file mode 100644 index 0000000..f8ce97b --- /dev/null +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.h @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H + +#include "score/TimeSlave/code/gptp/details/i_raw_socket.h" +#include "score/TimeSlave/code/gptp/details/ptp_types.h" +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Result produced by a completed Pdelay measurement cycle. +struct PDelayResult +{ + std::int64_t path_delay_ns{0}; + score::td::PDelayData pdelay_data{}; + bool valid{false}; +}; + +/** + * @brief Measures one-way peer delay using the IEEE 802.1AS Pdelay mechanism. + * + * Implements the IEEE 802.1AS two-step peer-delay measurement: + * path_delay = ((t2 − t1) + (t4 − t3c)) / 2 + * + * Thread-safety: @c SendRequest() is called from the PdelayThread. + * @c OnResponse() / @c OnResponseFollowUp() / @c GetResult() + * are called from the RxThread. An internal mutex makes the + * class safe for this two-thread usage pattern. + */ +class PeerDelayMeasurer final +{ + public: + explicit PeerDelayMeasurer(const ClockIdentity& local_identity) noexcept; + + /// Build and transmit a Pdelay_Req frame. @p socket must be open. + /// @return 0 on success, negative on error. + int SendRequest(IRawSocket& socket); + + /// Process an incoming Pdelay_Resp message. + void OnResponse(const PTPMessage& msg); + + /// Process an incoming Pdelay_Resp_Follow_Up message; triggers computation. + void OnResponseFollowUp(const PTPMessage& msg); + + /// Return the latest computed measurement (or invalid if none yet). + PDelayResult GetResult() const; + + private: + void ComputeAndStore() noexcept; + + ClockIdentity local_identity_{}; + + mutable std::mutex mutex_; + + int seqnum_{0}; + PTPMessage req_{}; + PTPMessage resp_{}; + PTPMessage resp_fup_{}; + PDelayResult result_{}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp new file mode 100644 index 0000000..21d37a3 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp @@ -0,0 +1,165 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/pdelay_measurer.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// Build a PTPMessage suitable for OnResponse / OnResponseFollowUp. +// seqId must be 0 to match the default-constructed req_ inside PeerDelayMeasurer +// (req_.ptpHdr.sequenceId == 0 before SendRequest is ever called). +PTPMessage MakeResp(std::uint16_t seqId, + std::int64_t parse_ts_ns, // t2 or t3 + std::int64_t recv_hw_ns = 0, // t4 (only used in Resp, not FUP) + std::int64_t corr_ns = 0) noexcept +{ + PTPMessage msg{}; + msg.ptpHdr.sequenceId = seqId; + msg.ptpHdr.correctionField = corr_ns << 16; // CorrectionToTmv does >> 16 + msg.parseMessageTs.ns = parse_ts_ns; + msg.recvHardwareTS.ns = recv_hw_ns; + return msg; +} + +} // namespace + +class PeerDelayMeasurerTest : public ::testing::Test +{ + protected: + // ClockIdentity is all-zeros; sufficient for the delay computation tests. + PeerDelayMeasurer measurer_{ClockIdentity{}}; +}; + +// ── Default state ───────────────────────────────────────────────────────────── + +TEST_F(PeerDelayMeasurerTest, GetResult_BeforeAnyMessage_IsInvalid) +{ + EXPECT_FALSE(measurer_.GetResult().valid); + EXPECT_EQ(measurer_.GetResult().path_delay_ns, 0LL); +} + +// ── Sequence-ID mismatch guards ─────────────────────────────────────────────── + +TEST_F(PeerDelayMeasurerTest, SeqIdMismatch_BetweenReqAndResp_NoResult) +{ + // Default req_.ptpHdr.sequenceId == 0; resp has seqId == 1 → mismatch. + measurer_.OnResponse(MakeResp(1U, 100LL, 180LL)); + measurer_.OnResponseFollowUp(MakeResp(1U, 80LL)); + EXPECT_FALSE(measurer_.GetResult().valid); +} + +TEST_F(PeerDelayMeasurerTest, SeqIdMismatch_BetweenRespAndFup_NoResult) +{ + // resp seqId == 0 (matches default req_), resp_fup seqId == 1 → mismatch. + measurer_.OnResponse(MakeResp(0U, 100LL, 180LL)); + measurer_.OnResponseFollowUp(MakeResp(1U, 80LL)); + EXPECT_FALSE(measurer_.GetResult().valid); +} + +// ── Delay computation (symmetric link) ─────────────────────────────────────── +// +// Default req_ gives: t1 = 0 ns (sendHardwareTS == 0) +// +// Chosen timestamps: +// t2 (resp.parseMessageTs) = 100 ns (remote receipt time) +// t3 (resp_fup.parseMessageTs) = 80 ns (remote send time) +// t4 (resp.recvHardwareTS) = 180 ns (local receive time) +// +// delay = ((t2 − t1) + (t4 − t3)) / 2 +// = ((100 − 0) + (180 − 80)) / 2 +// = (100 + 100) / 2 +// = 100 ns + +TEST_F(PeerDelayMeasurerTest, Computation_SymmetricLink_CorrectDelay) +{ + measurer_.OnResponse(MakeResp(0U, /*t2=*/100LL, /*t4=*/180LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, /*t3=*/80LL)); + + const PDelayResult r = measurer_.GetResult(); + ASSERT_TRUE(r.valid); + EXPECT_EQ(r.path_delay_ns, 100LL); +} + +TEST_F(PeerDelayMeasurerTest, Computation_AsymmetricLink_CorrectDelay) +{ + // t1=0, t2=200, t3=150, t4=400 → ((200-0) + (400-150)) / 2 = (200+250)/2 = 225 + measurer_.OnResponse(MakeResp(0U, 200LL, 400LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, 150LL)); + + const PDelayResult r = measurer_.GetResult(); + ASSERT_TRUE(r.valid); + EXPECT_EQ(r.path_delay_ns, 225LL); +} + +// ── Correction field applied to t3 ─────────────────────────────────────────── +// +// t1=0, t2=100, t4=180 +// t3=80 ns, correction_resp = 2 ns (stored as 2<<16), correction_fup = 0 +// t3c = t3 + c1 + c2 = 80 + 2 + 0 = 82 +// delay = ((100-0) + (180-82)) / 2 = (100+98) / 2 = 99 + +TEST_F(PeerDelayMeasurerTest, Computation_CorrectionField_AppliedToT3) +{ + measurer_.OnResponse(MakeResp(0U, 100LL, 180LL, /*corr_ns=*/2LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, 80LL)); + + const PDelayResult r = measurer_.GetResult(); + ASSERT_TRUE(r.valid); + EXPECT_EQ(r.path_delay_ns, 99LL); +} + +// ── PDelayData fields ───────────────────────────────────────────────────────── + +TEST_F(PeerDelayMeasurerTest, PDelayData_TimestampFields_PopulatedCorrectly) +{ + measurer_.OnResponse(MakeResp(0U, /*t2=*/100LL, /*t4=*/180LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, /*t3=*/80LL)); + + const score::td::PDelayData& d = measurer_.GetResult().pdelay_data; + EXPECT_EQ(d.request_origin_timestamp, 0ULL); // t1 + EXPECT_EQ(d.request_receipt_timestamp, 100ULL); // t2 + EXPECT_EQ(d.response_origin_timestamp, 80ULL); // t3 + EXPECT_EQ(d.response_receipt_timestamp, 180ULL); // t4 + EXPECT_EQ(d.pdelay, 100ULL); // computed delay +} + +// ── Multiple cycles: result updated on each valid completion ────────────────── + +TEST_F(PeerDelayMeasurerTest, SecondCycle_OverwritesPreviousResult) +{ + // First measurement: delay = 100 ns + measurer_.OnResponse(MakeResp(0U, 100LL, 180LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, 80LL)); + ASSERT_TRUE(measurer_.GetResult().valid); + + // Second measurement with same seqId (still 0): delay = 50 ns + // t1=0, t2=50, t3=25, t4=100 → ((50+75)/2=62 ... let me recalculate) + // t1=0, t2=50, t4=100, t3=50 → ((50-0)+(100-50))/2 = (50+50)/2 = 50 + measurer_.OnResponse(MakeResp(0U, 50LL, 100LL)); + measurer_.OnResponseFollowUp(MakeResp(0U, 50LL)); + + EXPECT_EQ(measurer_.GetResult().path_delay_ns, 50LL); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/ptp_types.h b/score/TimeSlave/code/gptp/details/ptp_types.h new file mode 100644 index 0000000..2828c16 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/ptp_types.h @@ -0,0 +1,223 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PTP_TYPES_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PTP_TYPES_H + +#include +#include +#include + +#ifndef _QNX_PLAT +#include +#else +// Minimal ethhdr definition for QNX +struct ethhdr +{ + unsigned char h_dest[6]; + unsigned char h_source[6]; + uint16_t h_proto; +}; +#endif + +#ifndef PACKED +#define PACKED __attribute__((packed)) +#endif + +namespace score +{ +namespace ts +{ +namespace details +{ + +// ─── EtherType constants ──────────────────────────────────────────────────── +constexpr int kEthP1588 = 0x88F7; +constexpr int kEthP8021Q = 0x8100; + +// ─── MAC / buffer sizes ───────────────────────────────────────────────────── +constexpr int kMacAddrLen = 6; +constexpr int kVlanTagLen = 4; + +// ─── PTP message-type codes ───────────────────────────────────────────────── +constexpr std::uint8_t kPtpMsgtypeSync = 0x0; +constexpr std::uint8_t kPtpMsgtypePdelayReq = 0x2; +constexpr std::uint8_t kPtpMsgtypePdelayResp = 0x3; +constexpr std::uint8_t kPtpMsgtypeFollowUp = 0x8; +constexpr std::uint8_t kPtpMsgtypePdelayRespFollowUp = 0xA; + +// ─── PTP header constants ──────────────────────────────────────────────────── +constexpr std::uint8_t kPtpTransportSpecific = (1U << 4U); +constexpr std::uint8_t kPtpVersion = 2U; + +constexpr std::int64_t kNsPerSec = 1'000'000'000LL; + +// ─── MAC addresses ─────────────────────────────────────────────────────────── +constexpr const char* kPtpSrcMac = "02:00:00:FF:00:11"; +constexpr const char* kPtpDstMac = "01:80:C2:00:00:0E"; + +// ─── Control field ─────────────────────────────────────────────────────────── +enum ControlField : std::uint8_t +{ + kCtlSync = 0, + kCtlDelayReq = 1, + kCtlFollowUp = 2, + kCtlDelayResp = 3, + kCtlManagement = 4, + kCtlOther = 5 +}; + +// ─── State machine states ──────────────────────────────────────────────────── +enum class SyncState : std::uint8_t +{ + kEmpty, + kHaveSync, + kHaveFup +}; + +// ─── Time value type ───────────────────────────────────────────────────────── +struct TmvT +{ + std::int64_t ns{0}; +}; + +// ─── PTP wire structures (all PACKED) ──────────────────────────────────────── +struct PACKED ClockIdentity +{ + std::uint8_t id[8]{}; +}; + +struct PACKED PortIdentity +{ + ClockIdentity clockIdentity; + std::uint16_t portNumber{0}; +}; + +struct PACKED Timestamp +{ + std::uint16_t seconds_msb{0}; + std::uint32_t seconds_lsb{0}; + std::uint32_t nanoseconds{0}; +}; + +struct PACKED PTPHeader +{ + std::uint8_t tsmt{0}; + std::uint8_t version{0}; + std::uint16_t messageLength{0}; + std::uint8_t domainNumber{0}; + std::uint8_t reserved1{0}; + std::uint8_t flagField[2]{}; + std::int64_t correctionField{0}; + std::uint32_t reserved2{0}; + PortIdentity sourcePortIdentity{}; + std::uint16_t sequenceId{0}; + std::uint8_t controlField{0}; + std::int8_t logMessageInterval{0}; +}; + +struct PACKED SyncBody +{ + PTPHeader ptpHdr{}; + Timestamp originTimestamp{}; +}; + +struct PACKED FollowUpBody +{ + PTPHeader ptpHdr{}; + Timestamp preciseOriginTimestamp{}; +}; + +struct PACKED PdelayReqBody +{ + PTPHeader ptpHdr{}; + Timestamp requestReceiptTimestamp{}; + PortIdentity reserved{}; +}; + +struct PACKED PdelayRespBody +{ + PTPHeader ptpHdr{}; + Timestamp responseOriginTimestamp{}; + PortIdentity requestingPortIdentity{}; +}; + +struct PACKED PdelayRespFollowUpBody +{ + PTPHeader ptpHdr{}; + Timestamp responseOriginReceiptTimestamp{}; + PortIdentity requestingPortIdentity{}; +}; + +struct PACKED RawMessageData +{ + std::uint8_t buffer[1500]{}; +}; + +struct PTPMessage +{ + union PACKED + { + PTPHeader ptpHdr; + SyncBody sync; + FollowUpBody follow_up; + PdelayReqBody pdelay_req; + PdelayRespBody pdelay_resp; + PdelayRespFollowUpBody pdelay_resp_fup; + RawMessageData data; + }; + + std::uint8_t msgtype{0}; + TmvT sendHardwareTS{}; + TmvT parseMessageTs{}; + TmvT recvHardwareTS{}; +}; + +static_assert(sizeof(PTPMessage) <= 1600, "PTPMessage too large"); + +// ─── Timestamp conversion helpers ──────────────────────────────────────────── +inline TmvT TimestampToTmv(const Timestamp& ts) noexcept +{ + const std::uint64_t sec = (static_cast(ts.seconds_msb) << 32U) | + static_cast(ts.seconds_lsb); + return TmvT{static_cast(sec * static_cast(kNsPerSec) + + ts.nanoseconds)}; +} + +inline Timestamp TmvToTimestamp(const TmvT& x) noexcept +{ + Timestamp t{}; + const std::uint64_t sec = static_cast(x.ns) / 1'000'000'000ULL; + const std::uint64_t nsec = static_cast(x.ns) % 1'000'000'000ULL; + t.seconds_lsb = static_cast(sec & 0xFFFFFFFFULL); + t.seconds_msb = static_cast((sec >> 32U) & 0xFFFFULL); + t.nanoseconds = static_cast(nsec); + return t; +} + +inline TmvT CorrectionToTmv(std::int64_t corr) noexcept +{ + return TmvT{corr >> 16}; +} + +inline std::uint64_t ClockIdentityToU64(const ClockIdentity& ci) noexcept +{ + std::uint64_t v{0}; + std::memcpy(&v, ci.id, sizeof(v)); + return v; +} + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PTP_TYPES_H diff --git a/score/TimeSlave/code/gptp/details/raw_socket.h b/score/TimeSlave/code/gptp/details/raw_socket.h new file mode 100644 index 0000000..36ea437 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/raw_socket.h @@ -0,0 +1,88 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_RAW_SOCKET_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_RAW_SOCKET_H + +#include "score/TimeSlave/code/gptp/details/i_raw_socket.h" + +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief Platform raw socket for Ethernet I/O with hardware timestamping. + * + * On Linux uses AF_PACKET / SO_TIMESTAMPING. + * On QNX uses the QNX raw-socket shim. + */ +class RawSocket : public IRawSocket +{ + public: + RawSocket() noexcept = default; + ~RawSocket() override; + + RawSocket(const RawSocket&) = delete; + RawSocket& operator=(const RawSocket&) = delete; + RawSocket(RawSocket&&) = delete; + RawSocket& operator=(RawSocket&&) = delete; + + /// Open the socket bound to @p iface. Returns false on failure. + bool Open(const std::string& iface) override; + + /// Configure hardware TX/RX timestamping on the already-opened socket. + /// Returns false on failure. A no-op on platforms that don't support it. + bool EnableHwTimestamping() override; + + /// Close the socket and release the file descriptor. + void Close() override; + + /// Receive one frame. + /// + /// @param buf Output buffer. + /// @param buf_len Capacity of @p buf. + /// @param hwts Output: hardware receive timestamp (zeroed if unavailable). + /// @param timeout_ms <0 block indefinitely, 0 non-blocking, >0 timeout in ms. + /// @return Number of bytes received, 0 on timeout, -1 on error. + int Recv(std::uint8_t* buf, std::size_t buf_len, + ::timespec& hwts, int timeout_ms) override; + + /// Send one frame. + /// + /// @param buf Frame data including Ethernet header. + /// @param len Number of bytes to send. + /// @param hwts Output: hardware transmit timestamp (zeroed if unavailable). + /// @return Number of bytes sent, or -1 on error. + int Send(const void* buf, int len, ::timespec& hwts) override; + + /// Return the underlying file descriptor (for advanced use / polling). + int GetFd() const override { return fd_; } + + private: + int fd_{-1}; + std::string iface_{}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_RAW_SOCKET_H diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp new file mode 100644 index 0000000..d97afc8 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp @@ -0,0 +1,172 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/sync_state_machine.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +std::int64_t MonoNs() noexcept +{ + ::timespec ts{}; + ::clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * kNsPerSec + ts.tv_nsec; +} + +} // namespace + +SyncStateMachine::SyncStateMachine( + std::int64_t jump_future_threshold_ns) noexcept + : jump_future_threshold_ns_{jump_future_threshold_ns} +{ +} + +void SyncStateMachine::OnSync(const PTPMessage& msg) +{ + switch (state_) + { + case SyncState::kEmpty: + last_sync_ = msg; + state_ = SyncState::kHaveSync; + break; + + case SyncState::kHaveSync: + // Newer Sync replaces the stale one (master sends faster than FUP arrives) + last_sync_ = msg; + break; + + case SyncState::kHaveFup: + // Buffered FUP is now stale; start fresh with the new Sync + last_sync_ = msg; + state_ = SyncState::kHaveSync; + break; + } +} + +std::optional SyncStateMachine::OnFollowUp( + const PTPMessage& msg) +{ + switch (state_) + { + case SyncState::kEmpty: + // FUP arrived before its Sync — buffer it and wait + last_fup_ = msg; + state_ = SyncState::kHaveFup; + return std::nullopt; + + case SyncState::kHaveFup: + // Another FUP without a matching Sync — replace buffer + last_fup_ = msg; + return std::nullopt; + + case SyncState::kHaveSync: + if (last_sync_.ptpHdr.sequenceId != msg.ptpHdr.sequenceId) + { + // Sequence-ID mismatch: buffer the FUP and wait for matching Sync + last_fup_ = msg; + state_ = SyncState::kHaveFup; + return std::nullopt; + } + + { + SyncResult result = BuildResult(last_sync_, msg); + state_ = SyncState::kEmpty; + last_sync_mono_ns_.store(MonoNs(), std::memory_order_release); + return result; + } + } + return std::nullopt; +} + +bool SyncStateMachine::IsTimeout(std::int64_t mono_now_ns, + std::int64_t timeout_ns) const +{ + if (timeout_ns <= 0) + return false; + const std::int64_t last = last_sync_mono_ns_.load(std::memory_order_acquire); + if (last == 0) + return false; // never synchronized yet — not a "timeout" + return (mono_now_ns - last) > timeout_ns; +} + +SyncResult SyncStateMachine::BuildResult( + const PTPMessage& sync, + const PTPMessage& fup) noexcept +{ + const TmvT sync_corr = CorrectionToTmv(sync.ptpHdr.correctionField); + const TmvT fup_corr = CorrectionToTmv(fup.ptpHdr.correctionField); + const TmvT fup_ts = TimestampToTmv(fup.follow_up.preciseOriginTimestamp); + + const std::int64_t master_ns = fup_ts.ns + sync_corr.ns + fup_corr.ns; + const std::int64_t offset_ns = sync.recvHardwareTS.ns - master_ns; + + SyncResult r{}; + r.master_ns = master_ns; + r.offset_ns = offset_ns; + + if (last_master_ns_ != 0) + { + const std::int64_t delta = master_ns - last_master_ns_; + if (delta < 0) + r.is_time_jump_past = true; + else if (jump_future_threshold_ns_ > 0 && delta > jump_future_threshold_ns_) + r.is_time_jump_future = true; + } + + score::td::SyncFupData& d = r.sync_fup_data; + d.precise_origin_timestamp = + static_cast(fup_ts.ns); + d.reference_global_timestamp = + static_cast(master_ns); + d.reference_local_timestamp = + static_cast(sync.recvHardwareTS.ns); + d.sync_ingress_timestamp = + static_cast(sync.recvHardwareTS.ns); + d.correction_field = + static_cast(sync.ptpHdr.correctionField); + d.sequence_id = fup.ptpHdr.sequenceId; + d.pdelay = 0U; // filled by GptpEngine from IPeerDelayMeasurer + d.port_number = sync.ptpHdr.sourcePortIdentity.portNumber; + d.clock_identity = + ClockIdentityToU64(sync.ptpHdr.sourcePortIdentity.clockIdentity); + + // IEEE 802.1AS Clause 11.4.1 + if (prev_slave_rx_ns_ != 0 && prev_master_origin_ns_ != 0) + { + const std::int64_t slave_interval = sync.recvHardwareTS.ns - prev_slave_rx_ns_; + const std::int64_t master_interval = master_ns - prev_master_origin_ns_; + if (master_interval > 0) + { + neighbor_rate_ratio_ = + static_cast(slave_interval) / static_cast(master_interval); + } + } + prev_slave_rx_ns_ = sync.recvHardwareTS.ns; + prev_master_origin_ns_ = master_ns; + + last_master_ns_ = master_ns; + + return r; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.h b/score/TimeSlave/code/gptp/details/sync_state_machine.h new file mode 100644 index 0000000..73340b1 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.h @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" + +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Output produced by a successful Sync+FollowUp pairing. +struct SyncResult +{ + std::int64_t master_ns{0}; ///< Grandmaster time (ns since epoch) + std::int64_t offset_ns{0}; ///< local hw_ts − master_ns + score::td::SyncFupData sync_fup_data{}; ///< Ready to copy into PtpTimeInfo (pdelay field filled by engine) + bool is_time_jump_future{false}; + bool is_time_jump_past{false}; +}; + +/** + * @brief Two-step Sync / Follow_Up correlation state machine + * (IEEE 802.1AS slave port). + * + * Detects forward time jumps (> @p jump_future_threshold_ns) and backward + * jumps. Computes neighborRateRatio from successive Sync intervals. + * Does NOT adjust any hardware clock; offset computation is purely + * informational for the upstream consumer. + * + * Thread-safety: NOT thread-safe. All calls must come from the same thread + * (the RxLoop thread in GptpEngine), except IsTimeout() which is atomic. + */ +class SyncStateMachine final +{ + public: + /// @param jump_future_threshold_ns Offset delta above which the state is + /// flagged as a future time jump. Set to 0 to disable detection. + explicit SyncStateMachine( + std::int64_t jump_future_threshold_ns = 500'000'000LL) noexcept; + + /// Called when a Sync message is received (with its HW receive timestamp + /// already stored in @p msg.recvHardwareTS). + void OnSync(const PTPMessage& msg); + + /// Called when a FollowUp message is received. + /// @return A SyncResult on a successful Sync+FUP pairing, std::nullopt otherwise. + std::optional OnFollowUp(const PTPMessage& msg); + + /// @return true if no valid Sync+FUP has been received for longer than + /// @p timeout_ns nanoseconds (monotonic). + bool IsTimeout(std::int64_t mono_now_ns, + std::int64_t timeout_ns) const; + + /// @return The latest computed neighborRateRatio (1.0 until first pair). + double GetNeighborRateRatio() const { return neighbor_rate_ratio_; } + + private: + SyncResult BuildResult(const PTPMessage& sync, + const PTPMessage& fup) noexcept; + + SyncState state_{SyncState::kEmpty}; + PTPMessage last_sync_{}; + PTPMessage last_fup_{}; + std::int64_t last_master_ns_{0}; + std::int64_t jump_future_threshold_ns_; + + // neighborRateRatio computation (IEEE 802.1AS Clause 11.4.1) + std::int64_t prev_slave_rx_ns_{0}; + std::int64_t prev_master_origin_ns_{0}; + double neighbor_rate_ratio_{1.0}; + + /// Monotonic timestamp of the last successful Sync+FUP pair (ns). + /// Atomic so that IsTimeout() can be called from a different thread. + std::atomic last_sync_mono_ns_{0}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp new file mode 100644 index 0000000..1b78754 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp @@ -0,0 +1,240 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/sync_state_machine.h" + +#include + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// Build a Sync PTPMessage with the given sequence ID and hardware RX timestamp. +// The correctionField encodes correction in sub-ns units (<<16 so >>16 == 0). +PTPMessage MakeSync(std::uint16_t seqId, + std::int64_t recv_hw_ns, + std::int64_t corr_ns = 0LL) noexcept +{ + PTPMessage msg{}; + msg.msgtype = kPtpMsgtypeSync; + msg.ptpHdr.sequenceId = seqId; + msg.ptpHdr.correctionField = corr_ns << 16; // CorrectionToTmv does >> 16 + msg.recvHardwareTS.ns = recv_hw_ns; + return msg; +} + +// Build a FollowUp PTPMessage with the given sequence ID and precise origin +// timestamp (in nanoseconds since epoch). +PTPMessage MakeFollowUp(std::uint16_t seqId, + std::int64_t origin_ns, + std::int64_t corr_ns = 0LL) noexcept +{ + PTPMessage msg{}; + msg.msgtype = kPtpMsgtypeFollowUp; + msg.ptpHdr.sequenceId = seqId; + msg.ptpHdr.correctionField = corr_ns << 16; + // Encode origin_ns into the preciseOriginTimestamp wire field. + msg.follow_up.preciseOriginTimestamp = TmvToTimestamp(TmvT{origin_ns}); + return msg; +} + +// Helper: deliver a matching Sync+FollowUp pair and return the SyncResult. +// Aborts the test if the pair does not produce a result. +SyncResult DeliverPair(SyncStateMachine& ssm, + std::uint16_t seqId, + std::int64_t recv_hw_ns, + std::int64_t origin_ns) +{ + ssm.OnSync(MakeSync(seqId, recv_hw_ns)); + auto result = ssm.OnFollowUp(MakeFollowUp(seqId, origin_ns)); + if (!result.has_value()) + ADD_FAILURE() << "Expected SyncResult but got nullopt"; + return result.value_or(SyncResult{}); +} + +} // namespace + +class SyncStateMachineTest : public ::testing::Test +{ + protected: + // threshold = 500 ms + SyncStateMachine ssm_{500'000'000LL}; +}; + +// ── Basic pairing ───────────────────────────────────────────────────────────── + +TEST_F(SyncStateMachineTest, SyncThenFollowUp_MatchingSeq_ReturnsSyncResult) +{ + ssm_.OnSync(MakeSync(1U, 1'000'000'000LL)); + auto result = ssm_.OnFollowUp(MakeFollowUp(1U, 900'000'000LL)); + ASSERT_TRUE(result.has_value()); + // master_ns = origin_ns (no correction) + EXPECT_EQ(result->master_ns, 900'000'000LL); + // offset = recv_hw - master + EXPECT_EQ(result->offset_ns, 1'000'000'000LL - 900'000'000LL); +} + +TEST_F(SyncStateMachineTest, FollowUpBeforeSync_ReturnsNullopt) +{ + // kEmpty state: FUP arrives first → buffered, no result yet + auto result = ssm_.OnFollowUp(MakeFollowUp(1U, 0LL)); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(SyncStateMachineTest, MultipleSyncs_ThenFollowUp_UsesLatestSync) +{ + // Two Syncs without a FUP between them — newer Sync should be used + ssm_.OnSync(MakeSync(1U, 1'000'000'000LL)); + ssm_.OnSync(MakeSync(2U, 2'000'000'000LL)); + // FUP with seqId == 2 (matches the newer Sync) + auto result = ssm_.OnFollowUp(MakeFollowUp(2U, 1'800'000'000LL)); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->master_ns, 1'800'000'000LL); +} + +TEST_F(SyncStateMachineTest, SeqIdMismatch_ReturnsNullopt_ThenMatchesOnNext) +{ + ssm_.OnSync(MakeSync(10U, 1'000'000'000LL)); + // FUP for a different seqId → no result; state becomes kHaveFup + auto r1 = ssm_.OnFollowUp(MakeFollowUp(99U, 0LL)); + EXPECT_FALSE(r1.has_value()); + + // Now deliver a Sync that matches the buffered FUP + ssm_.OnSync(MakeSync(99U, 2'000'000'000LL)); + auto r2 = ssm_.OnFollowUp(MakeFollowUp(99U, 1'900'000'000LL)); + ASSERT_TRUE(r2.has_value()); + EXPECT_EQ(r2->master_ns, 1'900'000'000LL); +} + +// ── SyncFupData fields ──────────────────────────────────────────────────────── + +TEST_F(SyncStateMachineTest, SyncFupData_SequenceId_SetFromFollowUp) +{ + const std::uint16_t kSeq = 42U; + ssm_.OnSync(MakeSync(kSeq, 1'000'000'000LL)); + auto result = ssm_.OnFollowUp(MakeFollowUp(kSeq, 900'000'000LL)); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->sync_fup_data.sequence_id, kSeq); +} + +TEST_F(SyncStateMachineTest, SyncFupData_PreciseOriginTimestamp_MatchesInput) +{ + const std::int64_t kOrigin = 5'000'000'000LL; // 5 s + ssm_.OnSync(MakeSync(1U, 6'000'000'000LL)); + auto result = ssm_.OnFollowUp(MakeFollowUp(1U, kOrigin)); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(static_cast(result->sync_fup_data.precise_origin_timestamp), + kOrigin); +} + +// ── Jump detection ──────────────────────────────────────────────────────────── + +TEST_F(SyncStateMachineTest, JumpPast_Detected_OnSecondPair) +{ + // First pair establishes baseline master_ns = 2 s + DeliverPair(ssm_, 1U, 2'100'000'000LL, 2'000'000'000LL); + + // Second pair: master_ns goes backward → is_time_jump_past + auto result = ssm_.OnFollowUp( + MakeFollowUp(2U, 1'000'000'000LL)); // no Sync preceding this on new seqId + + ssm_.OnSync(MakeSync(2U, 3'000'000'000LL)); + auto r2 = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); + ASSERT_TRUE(r2.has_value()); + EXPECT_TRUE(r2->is_time_jump_past); + EXPECT_FALSE(r2->is_time_jump_future); +} + +TEST_F(SyncStateMachineTest, JumpFuture_Detected_WhenDeltaExceedsThreshold) +{ + // First pair: master_ns = 1 s + DeliverPair(ssm_, 1U, 1'100'000'000LL, 1'000'000'000LL); + + // Second pair: master_ns jumps by 2 s > threshold (500 ms) + ssm_.OnSync(MakeSync(2U, 3'100'000'000LL)); + auto r2 = ssm_.OnFollowUp(MakeFollowUp(2U, 3'000'000'000LL)); + ASSERT_TRUE(r2.has_value()); + EXPECT_TRUE(r2->is_time_jump_future); + EXPECT_FALSE(r2->is_time_jump_past); +} + +TEST_F(SyncStateMachineTest, NoJump_WhenFirstPair) +{ + // First pair — no previous baseline; no jump should be flagged + ssm_.OnSync(MakeSync(1U, 1'000'000'000LL)); + auto result = ssm_.OnFollowUp(MakeFollowUp(1U, 900'000'000LL)); + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(result->is_time_jump_past); + EXPECT_FALSE(result->is_time_jump_future); +} + +// ── neighborRateRatio ───────────────────────────────────────────────────────── + +TEST_F(SyncStateMachineTest, NeighborRateRatio_Default_IsOne) +{ + EXPECT_DOUBLE_EQ(ssm_.GetNeighborRateRatio(), 1.0); +} + +TEST_F(SyncStateMachineTest, NeighborRateRatio_AfterTwoPairs_Computed) +{ + // Pair 1: slave_rx = 1000 ms, master_origin = 1000 ms + DeliverPair(ssm_, 1U, 1'000'000'000LL, 1'000'000'000LL); + + // Pair 2: slave_rx = 2000 ms (+1000 ms), master_origin = 2010 ms (+1010 ms) + // ratio = 1000_000_000 / 1010_000_000 ≈ 0.99009... + DeliverPair(ssm_, 2U, 2'000'000'000LL, 2'010'000'000LL); + + const double expected = 1'000'000'000.0 / 1'010'000'000.0; + EXPECT_NEAR(ssm_.GetNeighborRateRatio(), expected, 1e-9); +} + +// ── IsTimeout ───────────────────────────────────────────────────────────────── + +TEST_F(SyncStateMachineTest, IsTimeout_BeforeFirstSync_ReturnsFalse) +{ + // last_sync_mono_ns_ == 0; should never be considered a timeout + EXPECT_FALSE(ssm_.IsTimeout(std::numeric_limits::max(), 1LL)); +} + +TEST_F(SyncStateMachineTest, IsTimeout_AfterSuccessfulPair_WithLargeNow_ReturnsTrue) +{ + DeliverPair(ssm_, 1U, 1'000'000'000LL, 900'000'000LL); + // Provide a mono_now far in the future; timeout = 1 s + EXPECT_TRUE(ssm_.IsTimeout(std::numeric_limits::max(), + 1'000'000'000LL)); +} + +TEST_F(SyncStateMachineTest, IsTimeout_AfterSuccessfulPair_WithSmallDelta_ReturnsFalse) +{ + DeliverPair(ssm_, 1U, 1'000'000'000LL, 900'000'000LL); + // Provide mono_now = 0, which is before the recorded timestamp → not timed out + EXPECT_FALSE(ssm_.IsTimeout(0LL, 1'000'000'000LL)); +} + +TEST_F(SyncStateMachineTest, IsTimeout_ZeroTimeout_AlwaysReturnsFalse) +{ + DeliverPair(ssm_, 1U, 1'000'000'000LL, 900'000'000LL); + EXPECT_FALSE(ssm_.IsTimeout(std::numeric_limits::max(), 0LL)); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp new file mode 100644 index 0000000..c827962 --- /dev/null +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -0,0 +1,338 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/gptp_engine.h" +#include "score/TimeSlave/code/gptp/details/network_identity.h" +#include "score/TimeSlave/code/gptp/details/raw_socket.h" + +#include "score/mw/log/logging.h" +#include "score/TimeDaemon/code/common/logging_contexts.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +constexpr int kRxTimeoutMs = 100; // poll timeout; keeps RxLoop responsive to shutdown +constexpr int kRxBufferSize = 2048; + +std::int64_t MonoNs() noexcept +{ + ::timespec ts{}; + ::clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; +} + +} // namespace + +GptpEngine::GptpEngine( + GptpEngineOptions opts, + std::unique_ptr local_clock) noexcept + : opts_{std::move(opts)}, + local_clock_{std::move(local_clock)}, + socket_{std::make_unique()}, + identity_{std::make_unique()}, + codec_{}, + parser_{}, + sync_sm_{opts_.jump_future_threshold_ns}, + pdelay_{nullptr} +{ +} + +GptpEngine::GptpEngine( + GptpEngineOptions opts, + std::unique_ptr local_clock, + std::unique_ptr socket, + std::unique_ptr identity) noexcept + : opts_{std::move(opts)}, + local_clock_{std::move(local_clock)}, + socket_{std::move(socket)}, + identity_{std::move(identity)}, + codec_{}, + parser_{}, + sync_sm_{opts_.jump_future_threshold_ns}, + pdelay_{nullptr} +{ +} + +GptpEngine::~GptpEngine() noexcept +{ + (void)Deinitialize(); +} + +bool GptpEngine::Initialize() +{ + if (running_.load(std::memory_order_acquire)) + return true; + + if (!identity_->Resolve(opts_.iface_name)) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) + << "GptpEngine: failed to resolve ClockIdentity for " + << opts_.iface_name; + return false; + } + + pdelay_ = std::make_unique( + identity_->GetClockIdentity()); + + if (!socket_->Open(opts_.iface_name)) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) + << "GptpEngine: failed to open raw socket on " << opts_.iface_name; + return false; + } + + if (!socket_->EnableHwTimestamping()) + { + score::mw::log::LogWarn(score::td::kGPtpMachineContext) + << "GptpEngine: HW timestamping not available on " + << opts_.iface_name << ", falling back to SW timestamps"; + } + + running_.store(true, std::memory_order_release); + + if (::pthread_create(&rx_thread_, nullptr, &RxThreadEntry, this) != 0) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) + << "GptpEngine: failed to create RxThread"; + running_.store(false, std::memory_order_release); + socket_->Close(); + return false; + } + rx_started_ = true; + + if (::pthread_create(&pdelay_thread_, nullptr, &PdelayThreadEntry, this) != 0) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) + << "GptpEngine: failed to create PdelayThread"; + (void)Deinitialize(); + return false; + } + pdelay_started_ = true; + + score::mw::log::LogInfo(score::td::kGPtpMachineContext) + << "GptpEngine initialized on " << opts_.iface_name; + return true; +} + +bool GptpEngine::Deinitialize() +{ + running_.store(false, std::memory_order_release); + + // Close the socket first so that the RxThread's poll() unblocks + socket_->Close(); + + if (rx_started_) + { + ::pthread_join(rx_thread_, nullptr); + rx_started_ = false; + } + if (pdelay_started_) + { + ::pthread_join(pdelay_thread_, nullptr); + pdelay_started_ = false; + } + + score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine deinitialized"; + return true; +} + +bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) +{ + if (!running_.load(std::memory_order_acquire)) + return false; + + const std::int64_t mono_now = MonoNs(); + const std::int64_t timeout_ns = + static_cast(opts_.sync_timeout_ms) * 1'000'000LL; + + const bool timed_out = sync_sm_.IsTimeout(mono_now, timeout_ns); + + std::lock_guard lk(snapshot_mutex_); + snapshot_.local_time = local_clock_->Now(); + if (timed_out) + { + snapshot_.status.is_synchronized = false; + snapshot_.status.is_timeout = true; + snapshot_.status.is_correct = false; + } + info = snapshot_; + return true; +} + +void* GptpEngine::RxThreadEntry(void* arg) noexcept +{ + if (arg != nullptr) + static_cast(arg)->RxLoop(); + return nullptr; +} + +void* GptpEngine::PdelayThreadEntry(void* arg) noexcept +{ + if (arg != nullptr) + static_cast(arg)->PdelayLoop(); + return nullptr; +} + +void GptpEngine::RxLoop() noexcept +{ + std::uint8_t buf[kRxBufferSize]; + ::timespec hwts{}; + + while (running_.load(std::memory_order_acquire)) + { + std::memset(&hwts, 0, sizeof(hwts)); + const int n = socket_->Recv(buf, sizeof(buf), hwts, kRxTimeoutMs); + if (n <= 0) + continue; + HandlePacket(buf, n, hwts); + } +} + +void GptpEngine::PdelayLoop() noexcept +{ + ::timespec next{}; + ::clock_gettime(CLOCK_MONOTONIC, &next); + // Configurable warm-up before first Pdelay_Req (default 2 s) + const std::int64_t warmup_ns = + static_cast(opts_.pdelay_warmup_ms) * 1'000'000LL; + const std::int64_t next_warmup_ns = + static_cast(next.tv_sec) * 1'000'000'000LL + + next.tv_nsec + warmup_ns; + next.tv_sec = static_cast(next_warmup_ns / 1'000'000'000LL); + next.tv_nsec = static_cast(next_warmup_ns % 1'000'000'000LL); + + const std::int64_t interval_ns = + static_cast( + opts_.pdelay_interval_ms > 0 ? opts_.pdelay_interval_ms : 1000) + * 1'000'000LL; + + while (running_.load(std::memory_order_acquire)) + { + ::clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, nullptr); + if (!running_.load(std::memory_order_acquire)) + break; + + if (pdelay_) + { + (void)pdelay_->SendRequest(*socket_); + } + + const std::int64_t next_ns = + static_cast(next.tv_sec) * 1'000'000'000LL + + next.tv_nsec + interval_ns; + next.tv_sec = static_cast(next_ns / 1'000'000'000LL); + next.tv_nsec = static_cast(next_ns % 1'000'000'000LL); + } +} + +void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, + const ::timespec& hwts) noexcept +{ + int ptp_offset = 0; + if (!codec_.ParseEthernetHeader(frame, len, ptp_offset)) + return; + + const auto* payload = frame + ptp_offset; + const std::size_t payload_len = static_cast(len - ptp_offset); + + PTPMessage msg{}; + if (!parser_.Parse(payload, payload_len, msg)) + return; + + const TmvT hw_ts{ + static_cast(hwts.tv_sec) * 1'000'000'000LL + hwts.tv_nsec}; + + switch (msg.msgtype) + { + case kPtpMsgtypeSync: + msg.recvHardwareTS = hw_ts; + sync_sm_.OnSync(msg); + break; + + case kPtpMsgtypeFollowUp: + msg.parseMessageTs = TimestampToTmv(msg.follow_up.preciseOriginTimestamp); + { + auto result = sync_sm_.OnFollowUp(msg); + if (result.has_value() && pdelay_) + { + const PDelayResult pdr = pdelay_->GetResult(); + // IEEE 802.1AS: subtract peer link delay from offset + if (pdr.valid) + { + result->offset_ns -= pdr.path_delay_ns; + result->sync_fup_data.pdelay = + static_cast(pdr.path_delay_ns); + } + else + { + result->sync_fup_data.pdelay = 0U; + } + UpdateSnapshot(*result, pdr); + } + } + break; + + case kPtpMsgtypePdelayResp: + msg.recvHardwareTS = hw_ts; + msg.parseMessageTs = + TimestampToTmv(msg.pdelay_resp.responseOriginTimestamp); + if (pdelay_) + pdelay_->OnResponse(msg); + break; + + case kPtpMsgtypePdelayRespFollowUp: + msg.parseMessageTs = + TimestampToTmv(msg.pdelay_resp_fup.responseOriginReceiptTimestamp); + if (pdelay_) + pdelay_->OnResponseFollowUp(msg); + break; + + default: + break; + } +} + +void GptpEngine::UpdateSnapshot(const SyncResult& sync, + const PDelayResult& pdelay) noexcept +{ + std::lock_guard lk(snapshot_mutex_); + + const std::int64_t local_rx_ns = + static_cast(sync.sync_fup_data.reference_local_timestamp); + snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; + snapshot_.local_time = local_clock_->Now(); + snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); + + snapshot_.status.is_synchronized = true; + snapshot_.status.is_timeout = false; + snapshot_.status.is_time_jump_future = sync.is_time_jump_future; + snapshot_.status.is_time_jump_past = sync.is_time_jump_past; + snapshot_.status.is_correct = + !sync.is_time_jump_future && !sync.is_time_jump_past; + + snapshot_.sync_fup_data = sync.sync_fup_data; + snapshot_.pdelay_data = pdelay.pdelay_data; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/gptp_engine.h b/score/TimeSlave/code/gptp/gptp_engine.h new file mode 100644 index 0000000..1c09e9c --- /dev/null +++ b/score/TimeSlave/code/gptp/gptp_engine.h @@ -0,0 +1,127 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_GPTP_ENGINE_H +#define SCORE_TIMESLAVE_CODE_GPTP_GPTP_ENGINE_H + +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/TimeSlave/code/gptp/details/frame_codec.h" +#include "score/TimeSlave/code/gptp/details/i_network_identity.h" +#include "score/TimeSlave/code/gptp/details/i_raw_socket.h" +#include "score/TimeSlave/code/gptp/details/message_parser.h" +#include "score/TimeSlave/code/gptp/details/pdelay_measurer.h" +#include "score/TimeSlave/code/gptp/details/ptp_types.h" +#include "score/TimeSlave/code/gptp/details/sync_state_machine.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Configuration for GptpEngine. +struct GptpEngineOptions +{ + std::string iface_name = "eth0"; ///< Network interface for gPTP + int pdelay_interval_ms = 1000; ///< Period between Pdelay_Req transmissions (ms) + int pdelay_warmup_ms = 2000; ///< Delay before first Pdelay_Req (ms) + int sync_timeout_ms = 3300; ///< Declare timeout after this many ms without Sync + std::int64_t jump_future_threshold_ns = 500'000'000LL; ///< 500 ms +}; + +/** + * @brief gPTP engine for the TimeSlave process. + * + * Runs two POSIX threads: RxThread (receive/parse PTP frames) and + * PdelayThread (periodic Pdelay_Req transmission). + * + * ReadPTPSnapshot() is thread-safe once Initialize() returns true. + */ +class GptpEngine final +{ + public: + explicit GptpEngine( + GptpEngineOptions opts, + std::unique_ptr local_clock) noexcept; + + /// Constructor for testing: inject fake socket and identity. + GptpEngine( + GptpEngineOptions opts, + std::unique_ptr local_clock, + std::unique_ptr socket, + std::unique_ptr identity) noexcept; + + ~GptpEngine() noexcept; + + GptpEngine(const GptpEngine&) = delete; + GptpEngine& operator=(const GptpEngine&) = delete; + GptpEngine(GptpEngine&&) = delete; + GptpEngine& operator=(GptpEngine&&) = delete; + + /// Open the raw socket, enable HW timestamping, resolve the ClockIdentity, + /// and start the Rx and Pdelay background threads. + /// @return true on success. + bool Initialize(); + + /// Stop background threads and close the socket. + /// @return true (always succeeds). + bool Deinitialize(); + + /// Copy the latest measurement snapshot into @p info. + /// Non-blocking; returns false only if the engine is not initialized. + bool ReadPTPSnapshot(score::td::PtpTimeInfo& info); + + private: + static void* RxThreadEntry(void* arg) noexcept; + static void* PdelayThreadEntry(void* arg) noexcept; + void RxLoop() noexcept; + void PdelayLoop() noexcept; + + void HandlePacket(const std::uint8_t* frame, int len, + const ::timespec& hwts) noexcept; + void UpdateSnapshot(const SyncResult& sync, + const PDelayResult& pdelay) noexcept; + + GptpEngineOptions opts_; + + std::unique_ptr local_clock_; + std::unique_ptr socket_; + std::unique_ptr identity_; + FrameCodec codec_; + GptpMessageParser parser_; + SyncStateMachine sync_sm_; + std::unique_ptr pdelay_; + + mutable std::mutex snapshot_mutex_; + score::td::PtpTimeInfo snapshot_{}; + + std::atomic running_{false}; + pthread_t rx_thread_{}; + pthread_t pdelay_thread_{}; + bool rx_started_{false}; + bool pdelay_started_{false}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_GPTP_ENGINE_H diff --git a/score/TimeSlave/code/gptp/gptp_engine_test.cpp b/score/TimeSlave/code/gptp/gptp_engine_test.cpp new file mode 100644 index 0000000..90d7b30 --- /dev/null +++ b/score/TimeSlave/code/gptp/gptp_engine_test.cpp @@ -0,0 +1,498 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/gptp_engine.h" +#include "score/TimeSlave/code/gptp/details/i_network_identity.h" +#include "score/TimeSlave/code/gptp/details/i_raw_socket.h" +#include "score/time/HighPrecisionLocalSteadyClock/high_precision_local_steady_clock.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// ── FakeClock ───────────────────────────────────────────────────────────────── + +class FakeClock final : public score::time::HighPrecisionLocalSteadyClock +{ + public: + score::time::HighPrecisionLocalSteadyClock::time_point Now() noexcept override + { + return score::time::HighPrecisionLocalSteadyClock::time_point{ + std::chrono::nanoseconds{42'000'000'000LL}}; + } +}; + +// ── FakeSocket ──────────────────────────────────────────────────────────────── + +class FakeSocket final : public IRawSocket +{ + public: + void Push(std::vector data, ::timespec hwts = {}) + { + { + std::lock_guard lk(mtx_); + frames_.push_back({std::move(data), hwts}); + } + cv_.notify_one(); + } + + bool Open(const std::string&) override { return true; } + bool EnableHwTimestamping() override { return hw_ts_ok_; } + + void Close() override + { + { + std::lock_guard lk(mtx_); + closed_ = true; + } + cv_.notify_all(); + } + + int Recv(std::uint8_t* buf, std::size_t buf_len, + ::timespec& hwts, int timeout_ms) override + { + std::unique_lock lk(mtx_); + const auto timeout = std::chrono::milliseconds(timeout_ms > 0 ? timeout_ms : 100); + cv_.wait_for(lk, timeout, + [this] { return closed_ || !frames_.empty(); }); + if (closed_) + return -1; + if (frames_.empty()) + return 0; + auto& [data, ts] = frames_.front(); + const std::size_t n = std::min(data.size(), buf_len); + std::memcpy(buf, data.data(), n); + hwts = ts; + frames_.pop_front(); + return static_cast(n); + } + + int Send(const void*, int len, ::timespec&) override { return len; } + int GetFd() const override { return -1; } + + void SetHwTsOk(bool v) { hw_ts_ok_ = v; } + + private: + std::deque, ::timespec>> frames_; + std::mutex mtx_; + std::condition_variable cv_; + bool closed_{false}; + bool hw_ts_ok_{true}; +}; + +// ── FakeIdentity ────────────────────────────────────────────────────────────── + +class FakeIdentity final : public INetworkIdentity +{ + public: + explicit FakeIdentity(bool resolve_ok = true) : resolve_ok_{resolve_ok} {} + + bool Resolve(const std::string&) override { return resolve_ok_; } + + ClockIdentity GetClockIdentity() const override + { + ClockIdentity ci{}; + ci.id[0] = 0xAA; + ci.id[7] = 0xBB; + return ci; + } + + private: + bool resolve_ok_; +}; + +// ── Frame builders ──────────────────────────────────────────────────────────── + +// 14-byte Ethernet header with EtherType 0x88F7 (IEEE 1588) +void AppendEthHeader(std::vector& buf) +{ + // dst: 01:80:c2:00:00:0e + const std::uint8_t dst[6] = {0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E}; + // src: 02:00:00:ff:00:11 + const std::uint8_t src[6] = {0x02, 0x00, 0x00, 0xFF, 0x00, 0x11}; + buf.insert(buf.end(), dst, dst + 6); + buf.insert(buf.end(), src, src + 6); + buf.push_back(0x88); + buf.push_back(0xF7); +} + +// Build a 34-byte PTP header at the back of buf. +void AppendPtpHeader(std::vector& buf, + std::uint8_t msgtype, std::uint16_t seqId, + std::uint8_t ctlField = 0) +{ + const std::size_t start = buf.size(); + buf.resize(start + 34, 0); + std::uint8_t* p = buf.data() + start; + p[0] = static_cast(0x10U | (msgtype & 0x0FU)); // tsmt + p[1] = 0x02; // version + const std::uint16_t len = htons(static_cast(buf.size() - 14)); + std::memcpy(p + 2, &len, 2); + const std::uint16_t seq = htons(seqId); + std::memcpy(p + 30, &seq, 2); + p[32] = ctlField; +} + +// Append a 10-byte Timestamp body (sec_msb=0, sec_lsb, ns). +void AppendTimestamp(std::vector& buf, + std::uint32_t sec_lsb, std::uint32_t ns) +{ + const std::uint16_t msb = htons(0U); + const std::uint32_t sl = htonl(sec_lsb); + const std::uint32_t n = htonl(ns); + const std::uint8_t* p; + p = reinterpret_cast(&msb); + buf.insert(buf.end(), p, p + 2); + p = reinterpret_cast(&sl); + buf.insert(buf.end(), p, p + 4); + p = reinterpret_cast(&n); + buf.insert(buf.end(), p, p + 4); +} + +std::vector MakeSyncFrame(std::uint16_t seqId) +{ + std::vector f; + AppendEthHeader(f); + AppendPtpHeader(f, kPtpMsgtypeSync, seqId, /*ctl=*/0); + AppendTimestamp(f, 0, 0); // Sync body (origin timestamp, unused) + return f; +} + +std::vector MakeFollowUpFrame(std::uint16_t seqId, + std::uint32_t sec_lsb, + std::uint32_t ns) +{ + std::vector f; + AppendEthHeader(f); + AppendPtpHeader(f, kPtpMsgtypeFollowUp, seqId, /*ctl=*/2); + AppendTimestamp(f, sec_lsb, ns); + return f; +} + +std::vector MakePdelayRespFrame(std::uint16_t seqId) +{ + std::vector f; + AppendEthHeader(f); + AppendPtpHeader(f, kPtpMsgtypePdelayResp, seqId, /*ctl=*/5); + AppendTimestamp(f, 1, 0); // responseOriginTimestamp + // requesting port identity (10 bytes) + f.resize(f.size() + 10, 0); + return f; +} + +std::vector MakePdelayRespFupFrame(std::uint16_t seqId) +{ + std::vector f; + AppendEthHeader(f); + AppendPtpHeader(f, kPtpMsgtypePdelayRespFollowUp, seqId, /*ctl=*/5); + AppendTimestamp(f, 2, 0); // responseOriginReceiptTimestamp + f.resize(f.size() + 10, 0); // requesting port identity + return f; +} + +std::vector MakeUnknownFrame() +{ + std::vector f; + AppendEthHeader(f); + AppendPtpHeader(f, kPtpMsgtypePdelayReq, 0, /*ctl=*/5); + return f; +} + +// ── Test helpers ────────────────────────────────────────────────────────────── + +GptpEngineOptions FastOptions() +{ + GptpEngineOptions o; + o.iface_name = "lo"; + o.pdelay_warmup_ms = 0; // no warmup — first Pdelay_Req fires immediately + o.pdelay_interval_ms = 10; // 10 ms cycle + o.sync_timeout_ms = 3300; + o.jump_future_threshold_ns = 500'000'000LL; + return o; +} + +// Wait up to @p max_ms for snapshot.status.is_synchronized to become true. +bool WaitForSync(GptpEngine& eng, int max_ms = 500) +{ + for (int i = 0; i < max_ms / 10; ++i) + { + score::td::PtpTimeInfo info{}; + eng.ReadPTPSnapshot(info); + if (info.status.is_synchronized) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return false; +} + +// ── Fixtures ────────────────────────────────────────────────────────────────── + +// Fixture for tests that use real socket+identity paths (no injection). +class GptpEngineTest : public ::testing::Test +{ + protected: + void SetUp() override + { + engine_ = std::make_unique( + FastOptions(), std::make_unique()); + } + + void TearDown() override { engine_->Deinitialize(); } + + std::unique_ptr engine_; +}; + +// Fixture for tests that inject FakeSocket + FakeIdentity. +class GptpEngineFakeTest : public ::testing::Test +{ + protected: + void SetUp() override + { + auto sock = std::make_unique(); + auto identity = std::make_unique(); + socket_raw_ = sock.get(); + engine_ = std::make_unique( + FastOptions(), + std::make_unique(), + std::move(sock), + std::move(identity)); + } + + void TearDown() override { engine_->Deinitialize(); } + + FakeSocket* socket_raw_{nullptr}; + std::unique_ptr engine_; +}; + +} // namespace + +// ── GptpEngineTest — uninitialised paths ────────────────────────────────────── + +TEST_F(GptpEngineTest, Deinitialize_WhenNotInitialized_ReturnsTrue) +{ + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(GptpEngineTest, Deinitialize_CalledTwice_BothReturnTrue) +{ + EXPECT_TRUE(engine_->Deinitialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(GptpEngineTest, ReadPTPSnapshot_WhenNotInitialized_ReturnsFalse) +{ + score::td::PtpTimeInfo info{}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); +} + +TEST_F(GptpEngineTest, ReadPTPSnapshot_InfoUnchanged_WhenNotInitialized) +{ + score::td::PtpTimeInfo info{}; + info.ptp_assumed_time = std::chrono::nanoseconds{999LL}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); + EXPECT_EQ(info.ptp_assumed_time, std::chrono::nanoseconds{999LL}); +} + +// ── GptpEngineFakeTest — Initialize / Deinitialize ─────────────────────────── + +TEST_F(GptpEngineFakeTest, Initialize_WithFakeSocket_ReturnsTrue) +{ + EXPECT_TRUE(engine_->Initialize()); +} + +TEST_F(GptpEngineFakeTest, Initialize_CalledTwice_ReturnsTrueOnSecondCall) +{ + EXPECT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Initialize()); // already running → returns true +} + +TEST_F(GptpEngineFakeTest, Deinitialize_AfterInitialize_ReturnsTrue) +{ + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(GptpEngineFakeTest, ReadPTPSnapshot_AfterInitialize_ReturnsTrue) +{ + ASSERT_TRUE(engine_->Initialize()); + score::td::PtpTimeInfo info{}; + EXPECT_TRUE(engine_->ReadPTPSnapshot(info)); +} + +TEST_F(GptpEngineFakeTest, ReadPTPSnapshot_NotSynchronized_BeforeAnySync) +{ + ASSERT_TRUE(engine_->Initialize()); + score::td::PtpTimeInfo info{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); + EXPECT_FALSE(info.status.is_synchronized); +} + +// ── GptpEngineFakeTest — identity failure ───────────────────────────────────── + +TEST(GptpEngineIdentityFailTest, Initialize_IdentityResolveFails_ReturnsFalse) +{ + auto sock = std::make_unique(); + auto identity = std::make_unique(/*resolve_ok=*/false); + GptpEngine eng{FastOptions(), std::make_unique(), + std::move(sock), std::move(identity)}; + EXPECT_FALSE(eng.Initialize()); + EXPECT_TRUE(eng.Deinitialize()); +} + +// ── GptpEngineFakeTest — HW timestamp unavailable (warning path) ────────────── + +TEST_F(GptpEngineFakeTest, Initialize_HwTsUnavailable_StillReturnsTrue) +{ + socket_raw_->SetHwTsOk(false); + EXPECT_TRUE(engine_->Initialize()); +} + +// ── GptpEngineFakeTest — Sync + FollowUp → UpdateSnapshot ──────────────────── + +TEST_F(GptpEngineFakeTest, HandlePacket_SyncFollowUp_SnapshotBecomesSync) +{ + ASSERT_TRUE(engine_->Initialize()); + + // Send Sync then FollowUp with the same seqId. + ::timespec hwts{}; + hwts.tv_sec = 1; + hwts.tv_nsec = 500'000'000L; + socket_raw_->Push(MakeSyncFrame(1U), hwts); + socket_raw_->Push(MakeFollowUpFrame(1U, /*sec=*/2, /*ns=*/0)); + + EXPECT_TRUE(WaitForSync(*engine_)); + score::td::PtpTimeInfo info{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); + EXPECT_TRUE(info.status.is_synchronized); + EXPECT_FALSE(info.status.is_timeout); +} + +TEST_F(GptpEngineFakeTest, HandlePacket_MultipleSyncFup_SnapshotUpdated) +{ + ASSERT_TRUE(engine_->Initialize()); + + for (std::uint16_t seq = 1U; seq <= 3U; ++seq) + { + socket_raw_->Push(MakeSyncFrame(seq)); + socket_raw_->Push(MakeFollowUpFrame(seq, seq, 0U)); + } + + EXPECT_TRUE(WaitForSync(*engine_)); +} + +// ── GptpEngineFakeTest — PdelayResp + PdelayRespFollowUp ───────────────────── + +TEST_F(GptpEngineFakeTest, HandlePacket_PdelayRespSequence_DoesNotCrash) +{ + ASSERT_TRUE(engine_->Initialize()); + + socket_raw_->Push(MakePdelayRespFrame(0U)); + socket_raw_->Push(MakePdelayRespFupFrame(0U)); + + // Just verify no crash; sleep briefly to let the RxThread process. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +// ── GptpEngineFakeTest — unknown msgtype (default branch) ──────────────────── + +TEST_F(GptpEngineFakeTest, HandlePacket_UnknownMsgtype_DefaultBranchNocrash) +{ + ASSERT_TRUE(engine_->Initialize()); + socket_raw_->Push(MakeUnknownFrame()); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); +} + +// ── GptpEngineFakeTest — bad Ethernet header ───────────────────────────────── + +TEST_F(GptpEngineFakeTest, HandlePacket_TooShortFrame_EarlyReturn) +{ + ASSERT_TRUE(engine_->Initialize()); + socket_raw_->Push({0x01, 0x02, 0x03}); // < 14 bytes, ParseEthernetHeader returns false + std::this_thread::sleep_for(std::chrono::milliseconds(30)); +} + +// ── GptpEngineFakeTest — Sync+FUP then timeout path ────────────────────────── + +TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) +{ + // Use a very short timeout (50 ms) so we can trigger it quickly. + GptpEngineOptions opts = FastOptions(); + opts.sync_timeout_ms = 50; + + auto sock = std::make_unique(); + auto identity = std::make_unique(); + FakeSocket* raw_sock = sock.get(); + + GptpEngine eng{opts, std::make_unique(), + std::move(sock), std::move(identity)}; + ASSERT_TRUE(eng.Initialize()); + + // First receive a Sync+FUP so the state machine records a timestamp. + ::timespec hwts{}; + hwts.tv_sec = 1; + raw_sock->Push(MakeSyncFrame(1U), hwts); + raw_sock->Push(MakeFollowUpFrame(1U, 2U, 0U)); + + // Wait for it to be processed and become synchronized. + bool got_sync = false; + for (int i = 0; i < 50; ++i) + { + score::td::PtpTimeInfo tmp{}; + eng.ReadPTPSnapshot(tmp); + if (tmp.status.is_synchronized) { got_sync = true; break; } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + ASSERT_TRUE(got_sync) << "engine never became synchronized"; + + // Now wait longer than sync_timeout_ms for the timeout to trigger. + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + + score::td::PtpTimeInfo info{}; + ASSERT_TRUE(eng.ReadPTPSnapshot(info)); + EXPECT_TRUE(info.status.is_timeout); + EXPECT_FALSE(info.status.is_synchronized); + EXPECT_TRUE(eng.Deinitialize()); +} + +// ── Non-injectable path — nonexistent interface ─────────────────────────────── + +TEST(GptpEngineRealSocketTest, Initialize_NonExistentInterface_ReturnsFalse) +{ + GptpEngineOptions opts; + opts.iface_name = "nonexistent_iface_xyz"; + opts.pdelay_warmup_ms = 0; + GptpEngine eng{opts, std::make_unique()}; + EXPECT_FALSE(eng.Initialize()); + EXPECT_TRUE(eng.Deinitialize()); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/instrument/BUILD b/score/TimeSlave/code/gptp/instrument/BUILD new file mode 100644 index 0000000..48ca897 --- /dev/null +++ b/score/TimeSlave/code/gptp/instrument/BUILD @@ -0,0 +1,48 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "probe", + srcs = ["probe.cpp"], + hdrs = ["probe.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeSlave/code/gptp/record:recorder", + "@score_baselibs//score/mw/log:frontend", + ], +) + +cc_test( + name = "probe_test", + srcs = ["probe_test.cpp"], + tags = ["unit"], + deps = [ + ":probe", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@score_baselibs//score/mw/log:console_only_backend", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":probe_test"], + test_suites_from_sub_packages = [], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/instrument/probe.cpp b/score/TimeSlave/code/gptp/instrument/probe.cpp new file mode 100644 index 0000000..c9b9087 --- /dev/null +++ b/score/TimeSlave/code/gptp/instrument/probe.cpp @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/instrument/probe.h" + +#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/mw/log/logging.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + + +ProbeManager& ProbeManager::Instance() +{ + static ProbeManager instance; + return instance; +} + +void ProbeManager::Trace(ProbePoint point, const ProbeData& data) +{ + score::mw::log::LogDebug(score::td::kGPtpMachineContext) + << "PROBE point=" << static_cast(point) + << " ts=" << data.ts_mono_ns + << " val=" << data.value_ns + << " seq=" << data.seq_id; + + if (recorder_ != nullptr && recorder_->IsEnabled()) + { + recorder_->Record(RecordEntry{ + data.ts_mono_ns, + RecordEvent::kProbe, + data.value_ns, + 0, + static_cast(data.seq_id), + static_cast(point), + }); + } +} + +std::int64_t ProbeMonoNs() noexcept +{ + ::timespec ts{}; + ::clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1'000'000'000LL + ts.tv_nsec; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/instrument/probe.h b/score/TimeSlave/code/gptp/instrument/probe.h new file mode 100644 index 0000000..d740d6d --- /dev/null +++ b/score/TimeSlave/code/gptp/instrument/probe.h @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_INSTRUMENT_PROBE_H +#define SCORE_TIMESLAVE_CODE_GPTP_INSTRUMENT_PROBE_H + +#include "score/TimeSlave/code/gptp/record/recorder.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Measurement probe points within the gPTP pipeline. +enum class ProbePoint : std::uint8_t +{ + kRxPacketReceived = 0, + kSyncFrameParsed = 1, + kFollowUpProcessed = 2, + kOffsetComputed = 3, + kPdelayReqSent = 4, + kPdelayCompleted = 5, + kPhcAdjusted = 6, +}; + +/// Data payload for a single probe event. +struct ProbeData +{ + std::int64_t ts_mono_ns{0}; + std::int64_t value_ns{0}; + std::uint32_t seq_id{0}; +}; + +/** + * @brief Singleton manager for runtime measurement probes. + * + * When enabled, traces probe events to the logger and optionally to a Recorder. + * Controlled at runtime via SetEnabled(). + */ +class ProbeManager final +{ + public: + static ProbeManager& Instance(); + + void SetEnabled(bool enabled) { enabled_.store(enabled, std::memory_order_release); } + bool IsEnabled() const { return enabled_.load(std::memory_order_acquire); } + + /// Optional: link to a Recorder for persistent probe output. + void SetRecorder(Recorder* recorder) { recorder_ = recorder; } + + /// Record a probe event. Thread-safe. + void Trace(ProbePoint point, const ProbeData& data); + + private: + ProbeManager() = default; + std::atomic enabled_{false}; + Recorder* recorder_{nullptr}; +}; + +/// Returns the current monotonic timestamp in nanoseconds. +std::int64_t ProbeMonoNs() noexcept; + +} // namespace details +} // namespace ts +} // namespace score + +// Convenience macro: zero overhead when probing is disabled. +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define GPTP_PROBE(point, ...) \ + do \ + { \ + if (::score::ts::details::ProbeManager::Instance().IsEnabled()) \ + { \ + ::score::ts::details::ProbeManager::Instance().Trace( \ + point, {__VA_ARGS__}); \ + } \ + } while (0) + +#endif // SCORE_TIMESLAVE_CODE_GPTP_INSTRUMENT_PROBE_H diff --git a/score/TimeSlave/code/gptp/instrument/probe_test.cpp b/score/TimeSlave/code/gptp/instrument/probe_test.cpp new file mode 100644 index 0000000..cf0854c --- /dev/null +++ b/score/TimeSlave/code/gptp/instrument/probe_test.cpp @@ -0,0 +1,170 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/instrument/probe.h" + +#include + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +// ProbeManager is a singleton; reset it between tests. +class ProbeManagerTest : public ::testing::Test +{ + protected: + void TearDown() override + { + ProbeManager::Instance().SetEnabled(false); + ProbeManager::Instance().SetRecorder(nullptr); + } +}; + +// ── Enable / disable ────────────────────────────────────────────────────────── + +TEST_F(ProbeManagerTest, DefaultState_IsDisabled) +{ + EXPECT_FALSE(ProbeManager::Instance().IsEnabled()); +} + +TEST_F(ProbeManagerTest, SetEnabled_True_IsEnabledReturnsTrue) +{ + ProbeManager::Instance().SetEnabled(true); + EXPECT_TRUE(ProbeManager::Instance().IsEnabled()); +} + +TEST_F(ProbeManagerTest, SetEnabled_FalseThenTrue_TogglesCorrectly) +{ + ProbeManager::Instance().SetEnabled(true); + ProbeManager::Instance().SetEnabled(false); + EXPECT_FALSE(ProbeManager::Instance().IsEnabled()); +} + +TEST_F(ProbeManagerTest, Instance_ReturnsSameSingleton) +{ + EXPECT_EQ(&ProbeManager::Instance(), &ProbeManager::Instance()); +} + +// ── Trace when disabled ─────────────────────────────────────────────────────── + +TEST_F(ProbeManagerTest, Trace_WhenDisabled_DoesNotCrash) +{ + ProbeData d{}; + d.ts_mono_ns = 1'000'000LL; + d.value_ns = 500LL; + d.seq_id = 1U; + EXPECT_NO_THROW( + ProbeManager::Instance().Trace(ProbePoint::kSyncFrameParsed, d)); +} + +// ── Trace when enabled without recorder ─────────────────────────────────────── + +TEST_F(ProbeManagerTest, Trace_WhenEnabled_NoRecorder_DoesNotCrash) +{ + ProbeManager::Instance().SetEnabled(true); + ProbeData d{}; + d.ts_mono_ns = 2'000'000LL; + d.value_ns = -100LL; + d.seq_id = 2U; + EXPECT_NO_THROW( + ProbeManager::Instance().Trace(ProbePoint::kFollowUpProcessed, d)); +} + +// ── Trace with recorder attached ───────────────────────────────────────────── + +class ProbeManagerWithRecorderTest : public ::testing::Test +{ + protected: + void SetUp() override + { + path_ = "/tmp/probe_test_" + std::to_string(::getpid()) + ".csv"; + Recorder::Config cfg; + cfg.enabled = true; + cfg.file_path = path_; + recorder_ = std::make_unique(cfg); + + ProbeManager::Instance().SetEnabled(true); + ProbeManager::Instance().SetRecorder(recorder_.get()); + } + + void TearDown() override + { + ProbeManager::Instance().SetEnabled(false); + ProbeManager::Instance().SetRecorder(nullptr); + std::remove(path_.c_str()); + } + + std::string path_; + std::unique_ptr recorder_; +}; + +TEST_F(ProbeManagerWithRecorderTest, Trace_WritesToRecorder) +{ + ProbeData d{}; + d.ts_mono_ns = 3'000'000LL; + d.value_ns = 42LL; + d.seq_id = 3U; + ProbeManager::Instance().Trace(ProbePoint::kPdelayCompleted, d); + + // Flush by replacing recorder (which closes file in destructor) + ProbeManager::Instance().SetRecorder(nullptr); + recorder_.reset(); + + // File should have header + 1 data line + std::ifstream f(path_); + int lines = 0; + std::string line; + while (std::getline(f, line)) + ++lines; + EXPECT_EQ(lines, 2); +} + +TEST_F(ProbeManagerWithRecorderTest, Trace_AllProbePoints_DoNotCrash) +{ + const ProbePoint points[] = { + ProbePoint::kRxPacketReceived, + ProbePoint::kSyncFrameParsed, + ProbePoint::kFollowUpProcessed, + ProbePoint::kOffsetComputed, + ProbePoint::kPdelayReqSent, + ProbePoint::kPdelayCompleted, + ProbePoint::kPhcAdjusted, + }; + for (auto p : points) + { + EXPECT_NO_THROW(ProbeManager::Instance().Trace(p, ProbeData{})); + } +} + +// ── ProbeMonoNs ─────────────────────────────────────────────────────────────── + +TEST(ProbeMonoNsTest, ReturnsPositiveValue) +{ + EXPECT_GT(ProbeMonoNs(), 0LL); +} + +TEST(ProbeMonoNsTest, MonotonicallyIncreasing) +{ + const std::int64_t t1 = ProbeMonoNs(); + const std::int64_t t2 = ProbeMonoNs(); + EXPECT_GE(t2, t1); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/phc/BUILD b/score/TimeSlave/code/gptp/phc/BUILD new file mode 100644 index 0000000..a0c2a53 --- /dev/null +++ b/score/TimeSlave/code/gptp/phc/BUILD @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "phc_adjuster", + srcs = select({ + "@platforms//os:qnx": ["//score/TimeSlave/code/gptp/platform/qnx:phc_adjuster_src"], + "//conditions:default": ["//score/TimeSlave/code/gptp/platform/linux:phc_adjuster_src"], + }), + hdrs = ["phc_adjuster.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = select({ + "@platforms//os:qnx": ["//score/TimeSlave/code/gptp/details:raw_socket"], + "//conditions:default": [], + }), +) diff --git a/score/TimeSlave/code/gptp/phc/phc_adjuster.h b/score/TimeSlave/code/gptp/phc/phc_adjuster.h new file mode 100644 index 0000000..eaf544b --- /dev/null +++ b/score/TimeSlave/code/gptp/phc/phc_adjuster.h @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_PHC_PHC_ADJUSTER_H +#define SCORE_TIMESLAVE_CODE_GPTP_PHC_PHC_ADJUSTER_H + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Configuration for PHC hardware clock synchronization. +struct PhcConfig +{ + bool enabled = false; + std::string device = ""; ///< QNX: "emac0", Linux: "/dev/ptp0" + std::int64_t step_threshold_ns = 100'000'000LL; ///< >100ms = step, else slew +}; + +/** + * @brief Adjusts the PTP Hardware Clock (PHC) based on gPTP offset and rate. + * + * When enabled, applies step corrections for large offsets and frequency + * slew for continuous tracking. When disabled, all methods are no-ops. + * + * Platform-specific: Linux uses clock_adjtime(), QNX uses EMAC PTP ioctls. + */ +class PhcAdjuster final +{ + public: + explicit PhcAdjuster(PhcConfig cfg); + ~PhcAdjuster(); + + PhcAdjuster(const PhcAdjuster&) = delete; + PhcAdjuster& operator=(const PhcAdjuster&) = delete; + + /// @return true if hardware clock adjustment is enabled. + bool IsEnabled() const { return cfg_.enabled; } + + /// Apply a time step or slew based on offset magnitude. + /// If |offset_ns| > step_threshold_ns, a step correction is applied; + /// otherwise the offset is ignored (frequency slew handles drift). + void AdjustOffset(std::int64_t offset_ns); + + /// Adjust the PHC frequency to track the master clock rate. + /// @param rate_ratio neighborRateRatio (1.0 = no drift). + void AdjustFrequency(double rate_ratio); + + private: + PhcConfig cfg_; + int phc_fd_{-1}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_PHC_PHC_ADJUSTER_H diff --git a/score/TimeSlave/code/gptp/platform/linux/BUILD b/score/TimeSlave/code/gptp/platform/linux/BUILD new file mode 100644 index 0000000..a29f395 --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/linux/BUILD @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +filegroup( + name = "raw_socket_src", + srcs = ["raw_socket.cpp"], + visibility = ["//score:__subpackages__"], +) + +filegroup( + name = "network_identity_src", + srcs = ["network_identity.cpp"], + visibility = ["//score:__subpackages__"], +) + +filegroup( + name = "phc_adjuster_src", + srcs = ["phc_adjuster.cpp"], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp b/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp new file mode 100644 index 0000000..a3860bb --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp @@ -0,0 +1,86 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/network_identity.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +/// Read the MAC address of @p iface_name into @p out_mac (6 bytes). +/// @return Number of MAC bytes written, or -1 on failure. +int ReadMac(const char* iface_name, unsigned char out_mac[8]) noexcept +{ + if (!iface_name || !out_mac) + return -1; + + ::ifreq ifr{}; + std::strncpy(ifr.ifr_name, iface_name, IFNAMSIZ - 1); + + const int fd = ::socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) + return -1; + + const int rc = ::ioctl(fd, SIOCGIFHWADDR, &ifr); + ::close(fd); + if (rc < 0) + return -1; + + std::memcpy(out_mac, ifr.ifr_hwaddr.sa_data, 6); + return 6; +} + +} // namespace + +bool NetworkIdentity::Resolve(const std::string& iface_name) +{ + unsigned char mac[8]{}; + const int len = ReadMac(iface_name.c_str(), mac); + + if (len == 6) + { + // EUI-48 → EUI-64: insert 0xFF 0xFE after the OUI (octets 0-2) + identity_.id[0] = mac[0]; + identity_.id[1] = mac[1]; + identity_.id[2] = mac[2]; + identity_.id[3] = 0xFFU; + identity_.id[4] = 0xFEU; + identity_.id[5] = mac[3]; + identity_.id[6] = mac[4]; + identity_.id[7] = mac[5]; + return true; + } + if (len == 8) + { + std::memcpy(identity_.id, mac, 8); + return true; + } + return false; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp new file mode 100644 index 0000000..3cad558 --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/phc/phc_adjuster.h" + +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// clock_adjtime is not always exposed via glibc headers in cross-compilers. +// Use the syscall directly. +int phc_clock_adjtime(clockid_t clk_id, struct timex* tx) +{ + return static_cast(::syscall(SYS_clock_adjtime, clk_id, tx)); +} + +// Construct a clockid from a PHC file descriptor (kernel convention). +// See linux/include/uapi/linux/time.h +clockid_t phc_fd_to_clockid(int fd) +{ + // NOLINTNEXTLINE(hicpp-signed-bitwise) + return static_cast(~fd << 3 | 3); +} + +} // namespace + +PhcAdjuster::PhcAdjuster(PhcConfig cfg) : cfg_{std::move(cfg)} +{ + if (cfg_.enabled && !cfg_.device.empty()) + { + phc_fd_ = ::open(cfg_.device.c_str(), O_RDWR); + } +} + +PhcAdjuster::~PhcAdjuster() +{ + if (phc_fd_ >= 0) + { + ::close(phc_fd_); + phc_fd_ = -1; + } +} + +void PhcAdjuster::AdjustOffset(std::int64_t offset_ns) +{ + if (!cfg_.enabled || phc_fd_ < 0) + return; + + // Only step-correct large offsets; small ones are handled by frequency slew + if (std::abs(offset_ns) < cfg_.step_threshold_ns) + return; + + struct timex tx{}; + tx.modes = ADJ_SETOFFSET | ADJ_NANO; + tx.time.tv_sec = static_cast(offset_ns / 1'000'000'000LL); + tx.time.tv_usec = static_cast(offset_ns % 1'000'000'000LL); + + // Handle negative sub-second values + if (tx.time.tv_usec < 0) + { + tx.time.tv_sec -= 1; + tx.time.tv_usec += 1'000'000'000L; + } + + (void)phc_clock_adjtime(phc_fd_to_clockid(phc_fd_), &tx); +} + +void PhcAdjuster::AdjustFrequency(double rate_ratio) +{ + if (!cfg_.enabled || phc_fd_ < 0) + return; + + // Convert rate_ratio to ppb offset from 1.0, then to scaled ppm for kernel + // rate_ratio = slave_interval / master_interval + // ppb = (rate_ratio - 1.0) * 1e9 + // kernel expects freq in units of 2^-16 ppm = (ppb / 1000) * 65536 + const double ppb = (rate_ratio - 1.0) * 1e9; + const long scaled_ppm = static_cast(ppb / 1000.0 * 65536.0); + + struct timex tx{}; + tx.modes = ADJ_FREQUENCY; + tx.freq = scaled_ppm; + + (void)phc_clock_adjtime(phc_fd_to_clockid(phc_fd_), &tx); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp new file mode 100644 index 0000000..587d2db --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp @@ -0,0 +1,206 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/raw_socket.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +void DrainErrQueue(int fd) noexcept +{ + char buf[2048]; + ::iovec iov{buf, sizeof(buf)}; + char ctrl[2048]; + ::msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + + while (::recvmsg(fd, &msg, MSG_ERRQUEUE) > 0) + { + } +} + +} // namespace + +RawSocket::~RawSocket() +{ + Close(); +} + +bool RawSocket::Open(const std::string& iface) +{ + Close(); + + const int fd = ::socket(AF_PACKET, SOCK_RAW, htons(ETH_P_1588)); + if (fd < 0) + return false; + + ::ifreq ifr{}; + std::strncpy(ifr.ifr_name, iface.c_str(), IFNAMSIZ - 1); + if (::ioctl(fd, SIOCGIFINDEX, &ifr) < 0) + { + ::close(fd); + return false; + } + + ::sockaddr_ll sa{}; + sa.sll_family = AF_PACKET; + sa.sll_protocol = htons(ETH_P_1588); + sa.sll_ifindex = ifr.ifr_ifindex; + if (::bind(fd, reinterpret_cast<::sockaddr*>(&sa), sizeof(sa)) < 0) + { + ::close(fd); + return false; + } + + // SO_BINDTODEVICE: best-effort, don't fail if it doesn't work + (void)::setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, + iface.c_str(), static_cast(iface.size())); + + fd_ = fd; + iface_ = iface; + return true; +} + +bool RawSocket::EnableHwTimestamping() +{ + if (fd_ < 0) + return false; + + ::ifreq ifr{}; + ::hwtstamp_config cfg{}; + std::strncpy(ifr.ifr_name, iface_.c_str(), IFNAMSIZ - 1); + ifr.ifr_data = reinterpret_cast(&cfg); + + cfg.tx_type = HWTSTAMP_TX_ON; + cfg.rx_filter = HWTSTAMP_FILTER_ALL; + + if (::ioctl(fd_, SIOCSHWTSTAMP, &ifr) < 0) + { + // Fall back to PTP-only filter + cfg.rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT; + (void)::ioctl(fd_, SIOCSHWTSTAMP, &ifr); + } + + const int ts_opts = SOF_TIMESTAMPING_TX_HARDWARE | + SOF_TIMESTAMPING_RX_HARDWARE | + SOF_TIMESTAMPING_RAW_HARDWARE; + if (::setsockopt(fd_, SOL_SOCKET, SO_TIMESTAMPING, + &ts_opts, sizeof(ts_opts)) < 0) + { + return false; + } + return true; +} + +void RawSocket::Close() +{ + if (fd_ >= 0) + { + ::close(fd_); + fd_ = -1; + } + iface_.clear(); +} + +int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, + ::timespec& hwts, int timeout_ms) +{ + if (fd_ < 0 || buf == nullptr || buf_len == 0) + return -1; + + // Poll with caller-specified timeout + ::pollfd pfd{fd_, POLLIN, 0}; + const int pr = ::poll(&pfd, 1, timeout_ms); + if (pr == 0) + return 0; // timeout + if (pr < 0) + return -1; + + char ctrl[1024]; + ::iovec iov{buf, buf_len}; + ::msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + + const int len = static_cast(::recvmsg(fd_, &msg, 0)); + if (len < 0) + return -1; + + std::memset(&hwts, 0, sizeof(hwts)); + for (::cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; + cm = CMSG_NXTHDR(&msg, cm)) + { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMPING) + { + const auto* ts = reinterpret_cast(CMSG_DATA(cm)); + if (ts[2].tv_sec != 0 || ts[2].tv_nsec != 0) + hwts = ts[2]; + } + } + return len; +} + +int RawSocket::Send(const void* buf, int len, ::timespec& hwts) +{ + if (fd_ < 0 || buf == nullptr || len <= 0) + return -1; + + DrainErrQueue(fd_); + + const int sent = static_cast(::send(fd_, buf, static_cast(len), 0)); + if (sent < 0) + return -1; + + // Retrieve TX hardware timestamp from error queue + ::pollfd pfd{fd_, POLLERR, 0}; + if (::poll(&pfd, 1, -1) > 0 && (pfd.revents & POLLERR) != 0) + { + std::uint8_t tmp[2048]; + ::timespec tx_hwts{}; + (void)Recv(tmp, sizeof(tmp), tx_hwts, 0); + hwts = tx_hwts; + } + else + { + std::memset(&hwts, 0, sizeof(hwts)); + } + return sent; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/platform/qnx/BUILD b/score/TimeSlave/code/gptp/platform/qnx/BUILD new file mode 100644 index 0000000..4bba537 --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/qnx/BUILD @@ -0,0 +1,33 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +filegroup( + name = "raw_socket_src", + srcs = [ + "qnx_raw_shim.cpp", + "raw_socket.cpp", + ], + visibility = ["//score:__subpackages__"], +) + +filegroup( + name = "network_identity_src", + srcs = ["network_identity.cpp"], + visibility = ["//score:__subpackages__"], +) + +filegroup( + name = "phc_adjuster_src", + srcs = ["phc_adjuster.cpp"], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp b/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp new file mode 100644 index 0000000..1140167 --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp @@ -0,0 +1,98 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/network_identity.h" + +#include +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +/// Read the MAC address of @p iface_name into @p out_mac (6 or 8 bytes). +/// @return Number of MAC bytes written, or -1 on failure. +int ReadMac(const char* iface_name, unsigned char out_mac[8]) noexcept +{ + if (!iface_name || !out_mac) + return -1; + + ::ifaddrs* ifaddr = nullptr; + if (::getifaddrs(&ifaddr) != 0 || ifaddr == nullptr) + return -1; + + int result = -1; + for (::ifaddrs* ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) + { + if (!ifa->ifa_name || !ifa->ifa_addr) + continue; + if (std::strcmp(ifa->ifa_name, iface_name) != 0) + continue; + if (ifa->ifa_addr->sa_family != AF_LINK) + continue; + + const auto* sdl = reinterpret_cast(ifa->ifa_addr); + const auto* mac = reinterpret_cast(LLADDR(sdl)); + const int len = static_cast(sdl->sdl_alen); + if (len == 6 || len == 8) + { + std::memcpy(out_mac, mac, static_cast(len)); + result = len; + break; + } + } + + ::freeifaddrs(ifaddr); + return result; +} + +} // namespace + +bool NetworkIdentity::Resolve(const std::string& iface_name) +{ + unsigned char mac[8]{}; + const int len = ReadMac(iface_name.c_str(), mac); + + if (len == 6) + { + // EUI-48 → EUI-64: insert 0xFF 0xFE after the OUI (octets 0-2) + identity_.id[0] = mac[0]; + identity_.id[1] = mac[1]; + identity_.id[2] = mac[2]; + identity_.id[3] = 0xFFU; + identity_.id[4] = 0xFEU; + identity_.id[5] = mac[3]; + identity_.id[6] = mac[4]; + identity_.id[7] = mac[5]; + return true; + } + if (len == 8) + { + std::memcpy(identity_.id, mac, 8); + return true; + } + return false; +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/platform/qnx/phc_adjuster.cpp b/score/TimeSlave/code/gptp/platform/qnx/phc_adjuster.cpp new file mode 100644 index 0000000..dfc8aab --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/qnx/phc_adjuster.cpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/phc/phc_adjuster.h" + +#include + +// Extern C functions from qnx_raw_shim.cpp +extern "C" int qnx_phc_open(const char* phc_dev); +extern "C" int qnx_phc_adjtime_step(int phc_fd, long long offset_ns); +extern "C" int qnx_phc_adjfreq_ppb(int phc_fd, long long freq_ppb); + +namespace score +{ +namespace ts +{ +namespace details +{ + +PhcAdjuster::PhcAdjuster(PhcConfig cfg) : cfg_{std::move(cfg)} +{ + if (cfg_.enabled && !cfg_.device.empty()) + { + phc_fd_ = qnx_phc_open(cfg_.device.c_str()); + } +} + +PhcAdjuster::~PhcAdjuster() +{ + phc_fd_ = -1; +} + +void PhcAdjuster::AdjustOffset(std::int64_t offset_ns) +{ + if (!cfg_.enabled) + return; + + // Only step-correct large offsets; small ones are handled by frequency slew + if (std::abs(offset_ns) < cfg_.step_threshold_ns) + return; + + (void)qnx_phc_adjtime_step(phc_fd_, static_cast(offset_ns)); +} + +void PhcAdjuster::AdjustFrequency(double rate_ratio) +{ + if (!cfg_.enabled) + return; + + // Convert rate_ratio to ppb offset from 1.0 + // rate_ratio = slave_interval / master_interval + // ppb = (rate_ratio - 1.0) * 1e9 + const auto ppb = static_cast((rate_ratio - 1.0) * 1e9); + + (void)qnx_phc_adjfreq_ppb(phc_fd_, ppb); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp new file mode 100644 index 0000000..bf8f107 --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp @@ -0,0 +1,561 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// QNX BPF-based raw socket shim for gPTP frame capture and transmission. +// Provides qnx_raw_open / qnx_raw_recv / qnx_raw_send / qnx_phc_* symbols +// declared in raw_socket.cpp (extern "C"). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// QNX SDP 8.0: PTP API constants (from io-sock/ptp.h, inlined to avoid +// struct PortIdentity redefinition conflict with details/ptp_types.h). +#define PTP_GET_TIME 0x102 +#define PTP_SET_TIME 0x103 +struct ptp_time +{ + int64_t sec; + int32_t nsec; +}; + +// Inlined ptp_tstmp (from io-sock/ptp.h) — avoids PortIdentity name collision. +// A TX loopback frame contains an Ethernet header followed by this struct. +struct PtpTstmp +{ + uint32_t uid; + ptp_time time; +}; + +// ── EtherType constants ─────────────────────────────────────────────────────── +#ifndef ETH_P_8021Q +#define ETH_P_8021Q 0x8100U +#endif +#ifndef ETH_P_1588 +#define ETH_P_1588 0x88F7U +#endif + +// ── Self-contained ethernet header layout ──────────────────────────────────── +struct GptpEthHdr +{ + unsigned char h_dest[6]; + unsigned char h_source[6]; + uint16_t h_proto; +}; + +static constexpr int64_t kNsPerSec = 1'000'000'000LL; +static constexpr std::size_t kMaxBpfBufSz = 65536U; +static constexpr int kMaxTxScanTries = 8; + +// Caplen of a BPF TX loopback frame injected by the PTP driver: +// Ethernet header (14 B) + ptp_tstmp payload (4 + 12 = 16 B) = 30 B +static constexpr int kTxLoopbackCaplen = + static_cast(sizeof(GptpEthHdr) + sizeof(PtpTstmp)); + +// ── BPF kernel filter: pass only IEEE 802.1AS (ETH_P_1588) frames ──────────── +// BPF_LD H ABS 12 — load EtherType (bytes 12-13) +// BPF_JEQ ETH_P_1588 — jump if match +// BPF_RET (u_int)-1 — keep entire packet +// BPF_RET 0 — drop +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static struct bpf_insn kPtp1588FilterInsns[] = { + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETH_P_1588, 0, 1), + BPF_STMT(BPF_RET + BPF_K, static_cast(-1)), + BPF_STMT(BPF_RET + BPF_K, 0), +}; +static const u_int kPtp1588FilterLen = + static_cast(sizeof(kPtp1588FilterInsns) / sizeof(kPtp1588FilterInsns[0])); + +// ── Per-thread BPF context ─────────────────────────────────────────────────── +struct QnxRawContext +{ + int bpf_fd = -1; + u_int bpf_buflen = 0; + char iface_name[IFNAMSIZ]{}; + unsigned char bpf_buf[kMaxBpfBufSz]{}; + ssize_t bpf_n = 0; + ssize_t bpf_off = 0; + bool initialized = false; + unsigned char tx_frame[ETHER_HDR_LEN + 1500]{}; + + // Secondary BPF fd with BIOCSSEESENT=1 for reading TX loopback timestamps. + // Lazily opened on first qnx_raw_send() call. + int tx_loopback_fd = -1; + u_int tx_loopback_buflen = 0; + unsigned char tx_loopback_buf[kMaxBpfBufSz]{}; + + ~QnxRawContext() + { + if (bpf_fd >= 0) + { + ::close(bpf_fd); + bpf_fd = -1; + } + if (tx_loopback_fd >= 0) + { + ::close(tx_loopback_fd); + tx_loopback_fd = -1; + } + } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +thread_local QnxRawContext g_qnx_ctx; + +// ── Internal helpers ────────────────────────────────────────────────────────── + +// Convert a bpf_xhdr hardware timestamp to timespec. +// bpf_ts::bt_sec — seconds (int64_t) +// bpf_ts::bt_frac — binary fraction of a second (uint64_t, unit = 2^-64 s) +// This is equivalent to bintime2timespec() from . +static void bpf_ts_to_timespec(const bpf_xhdr* bh, struct timespec* ts) noexcept +{ + ts->tv_sec = static_cast(bh->bh_tstamp.bt_sec); + const uint64_t top32 = bh->bh_tstamp.bt_frac >> 32U; + ts->tv_nsec = static_cast((top32 * 1'000'000'000ULL) >> 32U); +} + +// Parse an Ethernet/VLAN frame; return byte offset of PTP payload or -1. +static int ptp_payload_offset(const unsigned char* frame, int caplen) +{ + if (caplen < static_cast(sizeof(GptpEthHdr))) + return -1; + + GptpEthHdr eth{}; + std::memcpy(ð, frame, sizeof(GptpEthHdr)); + uint16_t etype = ntohs(eth.h_proto); + int offset = static_cast(sizeof(GptpEthHdr)); + + if (etype == ETH_P_8021Q) + { + if (caplen < offset + 4) + return -1; + uint16_t inner{}; + std::memcpy(&inner, frame + offset + 2, sizeof(uint16_t)); + etype = ntohs(inner); + offset += 4; + } + + return (etype == ETH_P_1588) ? offset : -1; +} + +// Open a secondary BPF fd on the same interface as main_fd, with +// BIOCSSEESENT=1 so our own TX frames appear as loopback records. +// Stores the resulting buffer length in g_qnx_ctx.tx_loopback_buflen. +// Returns the new fd or -1. +static int open_tx_loopback_fd(int main_fd) noexcept +{ + // Retrieve interface name from the already-bound main fd. + ::ifreq ifr{}; + if (::ioctl(main_fd, BIOCGETIF, &ifr) < 0) + return -1; + + char devpath[256]{}; + const char* sock_env = std::getenv("SOCK"); + if (sock_env != nullptr && sock_env[0] != '\0') + std::snprintf(devpath, sizeof(devpath), "%s/dev/bpf0", sock_env); + else + std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); + + int lfd = ::open(devpath, O_RDWR); + if (lfd < 0) + return -1; + + if (::ioctl(lfd, BIOCSETIF, &ifr) < 0) + { + ::close(lfd); + return -1; + } + + // Enable loopback so our sent frames are visible on this fd. + u_int one = 1U; + (void)::ioctl(lfd, BIOCSSEESENT, &one); + (void)::ioctl(lfd, BIOCIMMEDIATE, &one); + + // Request PTP hardware timestamps in bpf_xhdr format. + u_int bpf_ts = BPF_T_PTP | BPF_T_BINTIME; + (void)::ioctl(lfd, BIOCSTSTAMP, &bpf_ts); + + // Apply the same ETH_P_1588 kernel filter. + struct bpf_program prog{kPtp1588FilterLen, kPtp1588FilterInsns}; + (void)::ioctl(lfd, BIOCSETF, &prog); + + u_int buflen = 0U; + if (::ioctl(lfd, BIOCGBLEN, &buflen) < 0 || buflen == 0U || buflen > kMaxBpfBufSz) + { + ::close(lfd); + return -1; + } + g_qnx_ctx.tx_loopback_buflen = buflen; + return lfd; +} + +// ── Public C interface ──────────────────────────────────────────────────────── + +extern "C" int qnx_raw_open(const char* ifname) +{ + if (ifname == nullptr) + { + errno = EINVAL; + return -1; + } + + std::strlcpy(g_qnx_ctx.iface_name, ifname, sizeof(g_qnx_ctx.iface_name)); + + char devpath[256]{}; + const char* sock_env = std::getenv("SOCK"); + if (sock_env != nullptr && sock_env[0] != '\0') + std::snprintf(devpath, sizeof(devpath), "%s/dev/bpf0", sock_env); + else + std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); + + int fd = ::open(devpath, O_RDWR); + if (fd < 0) + return -1; + + ::ifreq ifr{}; + std::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + if (::ioctl(fd, BIOCSETIF, &ifr) < 0) + { + ::close(fd); + return -1; + } + + // Do NOT see our own TX frames on the main fd; use tx_loopback_fd instead. + int zero = 0; + (void)::ioctl(fd, BIOCSSEESENT, &zero); + + u_int yes = 1U; + (void)::ioctl(fd, BIOCIMMEDIATE, &yes); + (void)::ioctl(fd, BIOCPROMISC, &yes); + + // Request PTP hardware timestamps in bpf_xhdr format (IEEE 1588 clock). + // Falls back gracefully: if unsupported, timestamps will be zero and + // qnx_raw_recv() will fall back to CLOCK_REALTIME. + u_int bpf_ts = BPF_T_PTP | BPF_T_BINTIME; + (void)::ioctl(fd, BIOCSTSTAMP, &bpf_ts); + + // Install kernel BPF filter: discard all non-ETH_P_1588 frames early. + struct bpf_program prog{kPtp1588FilterLen, kPtp1588FilterInsns}; + (void)::ioctl(fd, BIOCSETF, &prog); // best-effort; userspace filter still runs + + if (::ioctl(fd, BIOCGBLEN, &g_qnx_ctx.bpf_buflen) < 0) + { + ::close(fd); + return -1; + } + if (g_qnx_ctx.bpf_buflen > kMaxBpfBufSz) + { + ::close(fd); + errno = ENOMEM; + return -1; + } + + g_qnx_ctx.bpf_fd = fd; + g_qnx_ctx.initialized = true; + return fd; +} + +extern "C" int qnx_raw_recv(int fd, void* buf, int buf_len, timespec* hwts, int nonblock) +{ + if (fd < 0 || buf == nullptr || buf_len <= 0 || hwts == nullptr) + { + errno = EINVAL; + return -1; + } + if (!g_qnx_ctx.initialized || g_qnx_ctx.bpf_buflen == 0) + { + errno = EINVAL; + return -1; + } + + if (nonblock != 0) + { + int flags = ::fcntl(fd, F_GETFL, 0); + if (flags >= 0) + (void)::fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } + + for (;;) + { + // Refill BPF read buffer when exhausted. + if (g_qnx_ctx.bpf_off >= g_qnx_ctx.bpf_n) + { + ssize_t n = ::read(fd, g_qnx_ctx.bpf_buf, g_qnx_ctx.bpf_buflen); + if (n < 0) + return -1; + if (n == 0) + { + if (nonblock != 0) + { + errno = EAGAIN; + return -1; + } + continue; + } + g_qnx_ctx.bpf_n = n; + g_qnx_ctx.bpf_off = 0; + } + + // Need at least sizeof(bpf_xhdr) bytes for the header. + if (g_qnx_ctx.bpf_off + static_cast(sizeof(bpf_xhdr)) > g_qnx_ctx.bpf_n) + { + g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; + continue; + } + + // Verify 8-byte alignment required by bpf_xhdr. + const auto ptr_val = + reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); + if (ptr_val % alignof(bpf_xhdr) != 0U) + { + g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; + continue; + } + + const auto* bh = + reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); + + // Bounds checks. + if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || + bh->bh_caplen > static_cast(g_qnx_ctx.bpf_n) || + g_qnx_ctx.bpf_off + static_cast(bh->bh_hdrlen) + + static_cast(bh->bh_caplen) > + g_qnx_ctx.bpf_n) + { + g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; + continue; + } + + const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; + const int caplen = static_cast(bh->bh_caplen); + const ssize_t next_off = + g_qnx_ctx.bpf_off + + static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); + + // Skip TX loopback frames (BIOCSSEESENT=0 should prevent them on the + // main fd, but guard defensively: a loopback frame has a fixed small + // caplen equal to ETH header + ptp_tstmp, not a valid PTP message). + if (caplen == kTxLoopbackCaplen) + { + g_qnx_ctx.bpf_off = next_off; + continue; + } + + const int ptp_off = ptp_payload_offset(pkt, caplen); + if (ptp_off >= 0) + { + // Use PTP hardware RX timestamp from bpf_xhdr. + // bt_sec==0 && bt_frac==0 means the driver did not provide a HW + // timestamp; fall back to CLOCK_REALTIME in that case. + if (bh->bh_tstamp.bt_sec != 0 || bh->bh_tstamp.bt_frac != 0) + { + bpf_ts_to_timespec(bh, hwts); + } + else + { + (void)::clock_gettime(CLOCK_REALTIME, hwts); + } + + const int frame_len = std::min(caplen, buf_len); + std::memcpy(buf, pkt, static_cast(frame_len)); + g_qnx_ctx.bpf_off = next_off; + return frame_len; + } + + g_qnx_ctx.bpf_off = next_off; + } +} + +extern "C" int qnx_raw_send(int fd, const void* buf, int len, timespec* hwts) +{ + if (fd < 0 || buf == nullptr || len <= 0 || hwts == nullptr) + { + errno = EINVAL; + return -1; + } + if (static_cast(len) > 1500U) + { + errno = EMSGSIZE; + return -1; + } + + std::memcpy(g_qnx_ctx.tx_frame, buf, static_cast(len)); + ssize_t n = ::write(fd, g_qnx_ctx.tx_frame, static_cast(len)); + if (n < 0) + return -1; + + // Attempt to obtain a hardware TX timestamp via the BPF loopback mechanism: + // 1. BIOCGTSTAMPID returns the UID assigned to the just-sent frame. + // 2. The driver inserts a loopback record on fds with BIOCSSEESENT=1; + // its payload is a ptp_tstmp struct carrying the actual HW timestamp. + // 3. We scan the secondary loopback fd for a record whose uid matches. + // If any step fails, we fall back to a CLOCK_REALTIME software timestamp. + uint32_t tx_uid = 0U; + if (::ioctl(fd, BIOCGTSTAMPID, &tx_uid) == 0) + { + // Lazy-open the secondary fd (needs BIOCGETIF to recover iface name). + if (g_qnx_ctx.tx_loopback_fd < 0) + g_qnx_ctx.tx_loopback_fd = open_tx_loopback_fd(fd); + + if (g_qnx_ctx.tx_loopback_fd >= 0 && g_qnx_ctx.tx_loopback_buflen > 0) + { + const int lfd = g_qnx_ctx.tx_loopback_fd; + + // Non-blocking scan: the loopback frame typically arrives within + // a few microseconds; we try kMaxTxScanTries reads. + int flags = ::fcntl(lfd, F_GETFL, 0); + (void)::fcntl(lfd, F_SETFL, (flags >= 0 ? flags : 0) | O_NONBLOCK); + + for (int tries = 0; tries < kMaxTxScanTries; ++tries) + { + ssize_t nr = ::read(lfd, g_qnx_ctx.tx_loopback_buf, + g_qnx_ctx.tx_loopback_buflen); + if (nr <= 0) + break; + + ssize_t off = 0; + while (off + static_cast(sizeof(bpf_xhdr)) <= nr) + { + const auto pv = reinterpret_cast( + g_qnx_ctx.tx_loopback_buf + off); + if (pv % alignof(bpf_xhdr) != 0U) + break; + + const auto* bh = reinterpret_cast( + g_qnx_ctx.tx_loopback_buf + off); + + if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || + off + static_cast(bh->bh_hdrlen) + + static_cast(bh->bh_caplen) > + nr) + break; + + const unsigned char* pkt = + reinterpret_cast(bh) + bh->bh_hdrlen; + const int caplen = static_cast(bh->bh_caplen); + const ssize_t next = off + static_cast( + BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); + + // A TX loopback record has a fixed caplen and contains a + // ptp_tstmp payload right after the Ethernet header. + if (caplen == kTxLoopbackCaplen) + { + const auto* tstmp = reinterpret_cast( + pkt + sizeof(GptpEthHdr)); + if (tstmp->uid == tx_uid) + { + hwts->tv_sec = static_cast(tstmp->time.sec); + hwts->tv_nsec = static_cast(tstmp->time.nsec); + return static_cast(len); + } + } + off = next; + } + } + } + } + + // Fallback: software TX timestamp. + (void)::clock_gettime(CLOCK_REALTIME, hwts); + return static_cast(len); +} + +// ── PHC clock adjustment (QNX SDP 8.0 io-sock/ptp.h ioctl path) ────────────── + +extern "C" int qnx_phc_open(const char* phc_dev) +{ + if (phc_dev != nullptr && phc_dev[0] != '\0' && phc_dev[0] != '/') + std::strlcpy(g_qnx_ctx.iface_name, phc_dev, sizeof(g_qnx_ctx.iface_name)); + return 0; +} + +extern "C" int qnx_phc_adjtime_step(int /*phc_fd*/, long long offset_ns) +{ + if (offset_ns == 0) + return 0; + + const int s = ::socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) + return -1; + + struct + { + struct ifdrv ifd; + struct ptp_time tm; + } cmd{}; + + std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); + cmd.ifd.ifd_len = sizeof(cmd.tm); + cmd.ifd.ifd_data = &cmd.tm; + cmd.ifd.ifd_cmd = PTP_GET_TIME; + + if (::ioctl(s, SIOCGDRVSPEC, &cmd) == -1) + { + ::close(s); + return -1; + } + + const int64_t cur_ns = cmd.tm.sec * kNsPerSec + static_cast(cmd.tm.nsec); + const int64_t new_ns = cur_ns + static_cast(offset_ns); + + cmd.tm.sec = new_ns / kNsPerSec; + cmd.tm.nsec = static_cast(new_ns % kNsPerSec); + if (cmd.tm.nsec < 0) + { + cmd.tm.nsec += static_cast(kNsPerSec); + cmd.tm.sec -= 1; + } + + cmd.ifd.ifd_cmd = PTP_SET_TIME; + const int r = ::ioctl(s, SIOCSDRVSPEC, &cmd); + ::close(s); + return r; +} + +extern "C" int qnx_phc_adjfreq_ppb(int /*phc_fd*/, long long freq_ppb) +{ + if (freq_ppb == 0) + return 0; + + const int s = ::socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) + return -1; + + // Convert ppb to ppm (EMAC_PTP_ADJ_FREQ_PPM expects ppm) + int ppm = static_cast(freq_ppb / 1000LL); + + struct + { + struct ifdrv ifd; + int adj_ppm; + } cmd{}; + + std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); + cmd.ifd.ifd_len = sizeof(cmd.adj_ppm); + cmd.ifd.ifd_data = &cmd.adj_ppm; + cmd.ifd.ifd_cmd = 0x200; // EMAC_PTP_ADJ_FREQ_PPM + cmd.adj_ppm = ppm; + + const int r = ::ioctl(s, SIOCGDRVSPEC, &cmd); + ::close(s); + return r; +} diff --git a/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp new file mode 100644 index 0000000..237457b --- /dev/null +++ b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/raw_socket.h" + +#include +#include +#include +#include +#include +#include +#include + +// QNX raw shim C linkage (provided by existing qnx_raw_shim target) +extern "C" { +int qnx_raw_open(const char* ifname); +int qnx_raw_recv(int fd, void* buf, int len, ::timespec* hwts, int nonblock); +int qnx_raw_send(int fd, void* buf, int len, ::timespec* hwts); +} // extern "C" + +namespace score +{ +namespace ts +{ +namespace details +{ + +RawSocket::~RawSocket() +{ + Close(); +} + +bool RawSocket::Open(const std::string& iface) +{ + Close(); + fd_ = qnx_raw_open(iface.c_str()); + if (fd_ < 0) + return false; + iface_ = iface; + return true; +} + +bool RawSocket::EnableHwTimestamping() +{ + // HW timestamping configured inside qnx_raw_open; nothing more needed. + return true; +} + +void RawSocket::Close() +{ + if (fd_ >= 0) + { + ::close(fd_); + fd_ = -1; + } + iface_.clear(); +} + +int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, + ::timespec& hwts, int timeout_ms) +{ + if (fd_ < 0 || buf == nullptr || buf_len == 0) + return -1; + + const int nonblock = (timeout_ms == 0) ? 1 : 0; + // QNX shim: nonblock==0 means blocking; only full non-blocking is supported. + // For timeout > 0 we fall back to a blocking call (best effort). + (void)timeout_ms; + return qnx_raw_recv(fd_, buf, static_cast(buf_len), &hwts, nonblock); +} + +int RawSocket::Send(const void* buf, int len, ::timespec& hwts) +{ + if (fd_ < 0 || buf == nullptr || len <= 0) + return -1; + return qnx_raw_send(fd_, const_cast(buf), len, &hwts); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/record/BUILD b/score/TimeSlave/code/gptp/record/BUILD new file mode 100644 index 0000000..3dd006a --- /dev/null +++ b/score/TimeSlave/code/gptp/record/BUILD @@ -0,0 +1,42 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "recorder", + srcs = ["recorder.cpp"], + hdrs = ["recorder.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], +) + +cc_test( + name = "recorder_test", + srcs = ["recorder_test.cpp"], + tags = ["unit"], + deps = [ + ":recorder", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":recorder_test"], + test_suites_from_sub_packages = [], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/gptp/record/recorder.cpp b/score/TimeSlave/code/gptp/record/recorder.cpp new file mode 100644 index 0000000..b189875 --- /dev/null +++ b/score/TimeSlave/code/gptp/record/recorder.cpp @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/record/recorder.h" + +namespace score +{ +namespace ts +{ +namespace details +{ + +Recorder::Recorder(Config cfg) : cfg_{std::move(cfg)} +{ + if (cfg_.enabled) + { + file_.open(cfg_.file_path, std::ios::out | std::ios::app); + if (file_.is_open()) + { + // Write CSV header if the file is empty + file_.seekp(0, std::ios::end); + if (file_.tellp() == 0) + { + file_ << "mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags\n"; + } + } + } +} + +void Recorder::Record(const RecordEntry& entry) +{ + if (!cfg_.enabled || !file_.is_open()) + return; + + std::lock_guard lk(mutex_); + file_ << entry.mono_ns << ',' + << static_cast(entry.event) << ',' + << entry.offset_ns << ',' + << entry.pdelay_ns << ',' + << entry.seq_id << ',' + << static_cast(entry.status_flags) << '\n'; + file_.flush(); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/record/recorder.h b/score/TimeSlave/code/gptp/record/recorder.h new file mode 100644 index 0000000..d775d82 --- /dev/null +++ b/score/TimeSlave/code/gptp/record/recorder.h @@ -0,0 +1,86 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_RECORD_RECORDER_H +#define SCORE_TIMESLAVE_CODE_GPTP_RECORD_RECORDER_H + +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Event types that can be recorded. +enum class RecordEvent : std::uint8_t +{ + kSyncReceived = 0, + kPdelayCompleted = 1, + kClockJump = 2, + kOffsetThreshold = 3, + kProbe = 4, +}; + +/// A single record entry written to the log file. +struct RecordEntry +{ + std::int64_t mono_ns{0}; + RecordEvent event{RecordEvent::kSyncReceived}; + std::int64_t offset_ns{0}; + std::int64_t pdelay_ns{0}; + std::uint16_t seq_id{0}; + std::uint8_t status_flags{0}; +}; + +/** + * @brief Thread-safe CSV file recorder for gPTP events. + * + * When enabled, appends CSV lines to the configured file path. + * Format: mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags + */ +class Recorder final +{ + public: + struct Config + { + bool enabled = false; + std::string file_path = "/var/log/gptp_record.csv"; + std::int64_t offset_threshold_ns = 1'000'000LL; ///< 1 ms + }; + + explicit Recorder(Config cfg); + ~Recorder() = default; + + Recorder(const Recorder&) = delete; + Recorder& operator=(const Recorder&) = delete; + + bool IsEnabled() const { return cfg_.enabled && file_.is_open(); } + + /// Record an entry. Thread-safe. + void Record(const RecordEntry& entry); + + private: + Config cfg_; + std::mutex mutex_; + std::ofstream file_; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_RECORD_RECORDER_H diff --git a/score/TimeSlave/code/gptp/record/recorder_test.cpp b/score/TimeSlave/code/gptp/record/recorder_test.cpp new file mode 100644 index 0000000..7115a95 --- /dev/null +++ b/score/TimeSlave/code/gptp/record/recorder_test.cpp @@ -0,0 +1,176 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/record/recorder.h" + +#include + +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +std::string TempPath() +{ + return "/tmp/recorder_test_" + std::to_string(::getpid()) + ".csv"; +} + +} // namespace + +// ── Disabled recorder ──────────────────────────────────────────────────────── + +TEST(RecorderTest, Disabled_IsEnabledReturnsFalse) +{ + Recorder::Config cfg; + cfg.enabled = false; + Recorder r{cfg}; + EXPECT_FALSE(r.IsEnabled()); +} + +TEST(RecorderTest, Disabled_RecordDoesNotCrash) +{ + Recorder::Config cfg; + cfg.enabled = false; + Recorder r{cfg}; + EXPECT_NO_THROW(r.Record(RecordEntry{})); +} + +// ── Enabled with bad path ───────────────────────────────────────────────────── + +TEST(RecorderTest, Enabled_BadPath_IsEnabledReturnsFalse) +{ + Recorder::Config cfg; + cfg.enabled = true; + cfg.file_path = "/no/such/dir/recorder_test.csv"; + Recorder r{cfg}; + EXPECT_FALSE(r.IsEnabled()); +} + +TEST(RecorderTest, Enabled_BadPath_RecordDoesNotCrash) +{ + Recorder::Config cfg; + cfg.enabled = true; + cfg.file_path = "/no/such/dir/recorder_test.csv"; + Recorder r{cfg}; + EXPECT_NO_THROW(r.Record(RecordEntry{})); +} + +// ── Enabled with valid path ─────────────────────────────────────────────────── + +class RecorderFileTest : public ::testing::Test +{ + protected: + void SetUp() override { path_ = TempPath(); } + void TearDown() override { std::remove(path_.c_str()); } + + Recorder MakeRecorder() + { + Recorder::Config cfg; + cfg.enabled = true; + cfg.file_path = path_; + return Recorder{cfg}; + } + + std::string path_; +}; + +TEST_F(RecorderFileTest, IsEnabled_ReturnsTrue) +{ + auto r = MakeRecorder(); + EXPECT_TRUE(r.IsEnabled()); +} + +TEST_F(RecorderFileTest, NewFile_ContainsCsvHeader) +{ + { auto r = MakeRecorder(); } // destructor closes file + + std::ifstream f(path_); + std::string line; + ASSERT_TRUE(std::getline(f, line)); + EXPECT_EQ(line, "mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags"); +} + +TEST_F(RecorderFileTest, Record_WritesOneDataLine) +{ + auto r = MakeRecorder(); + + RecordEntry e{}; + e.mono_ns = 123456789LL; + e.event = RecordEvent::kSyncReceived; + e.offset_ns = -500LL; + e.pdelay_ns = 1000LL; + e.seq_id = 42U; + e.status_flags = 0x03U; + r.Record(e); + + // Flush by destroying the recorder before reading back + r.Record(RecordEntry{}); // second line +} + +TEST_F(RecorderFileTest, Record_MultipleEntries_AllFlushedToFile) +{ + { + auto r = MakeRecorder(); + for (int i = 0; i < 5; ++i) + { + RecordEntry e{}; + e.mono_ns = static_cast(i) * 1'000'000LL; + e.event = RecordEvent::kPdelayCompleted; + e.seq_id = static_cast(i); + r.Record(e); + } + } + + // Count lines: header + 5 data lines = 6 + std::ifstream f(path_); + int lines = 0; + std::string line; + while (std::getline(f, line)) + ++lines; + EXPECT_EQ(lines, 6); +} + +TEST_F(RecorderFileTest, Record_FieldsWrittenCorrectly) +{ + { + auto r = MakeRecorder(); + RecordEntry e{}; + e.mono_ns = 9'000'000'000LL; + e.event = RecordEvent::kClockJump; + e.offset_ns = 12345LL; + e.pdelay_ns = 999LL; + e.seq_id = 7U; + e.status_flags = 0x01U; + r.Record(e); + } + + std::ifstream f(path_); + std::string header, data; + ASSERT_TRUE(std::getline(f, header)); + ASSERT_TRUE(std::getline(f, data)); + + // Format: mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags + EXPECT_EQ(data, "9000000000,2,12345,999,7,1"); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/BUILD b/score/libTSClient/BUILD new file mode 100644 index 0000000..1807bcc --- /dev/null +++ b/score/libTSClient/BUILD @@ -0,0 +1,54 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "gptp_ipc", + srcs = [ + "gptp_ipc_publisher.cpp", + "gptp_ipc_receiver.cpp", + ], + hdrs = [ + "gptp_ipc.h", + "gptp_ipc_channel.h", + "gptp_ipc_publisher.h", + "gptp_ipc_receiver.h", + ], + features = COMPILER_WARNING_FEATURES, + linkopts = ["-lrt"], + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [ + "//score/TimeDaemon/code/common/data_types:ptp_time_info", + ], +) + +cc_test( + name = "gptp_ipc_test", + srcs = ["gptp_ipc_test.cpp"], + tags = ["unit"], + deps = [ + ":gptp_ipc", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [":gptp_ipc_test"], + test_suites_from_sub_packages = [], + visibility = ["//score:__subpackages__"], +) diff --git a/score/libTSClient/gptp_ipc.h b/score/libTSClient/gptp_ipc.h new file mode 100644 index 0000000..73ebf44 --- /dev/null +++ b/score/libTSClient/gptp_ipc.h @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_H + +#include "score/libTSClient/gptp_ipc_channel.h" +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_receiver.h" + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_H diff --git a/score/libTSClient/gptp_ipc_channel.h b/score/libTSClient/gptp_ipc_channel.h new file mode 100644 index 0000000..651c12b --- /dev/null +++ b/score/libTSClient/gptp_ipc_channel.h @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_CHANNEL_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_CHANNEL_H + +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Default POSIX shared memory name for the gPTP IPC channel. +static constexpr char kGptpIpcName[] = "/gptp_ptp_info"; + +/// Magic number to validate the shared memory region ('GPTP'). +static constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; + +/** + * @brief Shared memory layout for gPTP IPC (seqlock protocol). + * + * Single-writer (TimeSlave), multi-reader (TimeDaemon via RealPTPEngine). + * Aligned to 64 bytes (cache line) to avoid false sharing. + * + * Seqlock protocol: + * - Writer: seq++ (odd = writing), write data, seq_confirm = seq (even = readable) + * - Reader: read seq, read data, read seq_confirm; retry if seq != seq_confirm or odd + */ +struct alignas(64) GptpIpcRegion +{ + std::uint32_t magic{kGptpIpcMagic}; + std::atomic seq{0}; + score::td::PtpTimeInfo data{}; + std::atomic seq_confirm{0}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_CHANNEL_H diff --git a/score/libTSClient/gptp_ipc_publisher.cpp b/score/libTSClient/gptp_ipc_publisher.cpp new file mode 100644 index 0000000..6a31a17 --- /dev/null +++ b/score/libTSClient/gptp_ipc_publisher.cpp @@ -0,0 +1,97 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_publisher.h" + +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +GptpIpcPublisher::~GptpIpcPublisher() +{ + Destroy(); +} + +bool GptpIpcPublisher::Init(const std::string& ipc_name) +{ + ipc_name_ = ipc_name; + + shm_fd_ = ::shm_open(ipc_name_.c_str(), O_CREAT | O_RDWR, 0666); + if (shm_fd_ < 0) + return false; + + if (::ftruncate(shm_fd_, static_cast(sizeof(GptpIpcRegion))) != 0) + { + ::close(shm_fd_); // LCOV_EXCL_LINE + shm_fd_ = -1; // LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE + } + + void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), + PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd_, 0); + if (ptr == MAP_FAILED) + { + ::close(shm_fd_); // LCOV_EXCL_LINE + shm_fd_ = -1; // LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE + } + + region_ = new (ptr) GptpIpcRegion{}; + return true; +} + +void GptpIpcPublisher::Publish(const score::td::PtpTimeInfo& info) +{ + if (region_ == nullptr) + return; + + const std::uint32_t next = region_->seq.load(std::memory_order_relaxed) + 1U; + region_->seq.store(next, std::memory_order_release); + + std::atomic_thread_fence(std::memory_order_release); + std::memcpy(®ion_->data, &info, sizeof(score::td::PtpTimeInfo)); + std::atomic_thread_fence(std::memory_order_release); + + region_->seq_confirm.store(next + 1U, std::memory_order_release); + region_->seq.store(next + 1U, std::memory_order_release); +} + +void GptpIpcPublisher::Destroy() +{ + if (region_ != nullptr) + { + ::munmap(region_, sizeof(GptpIpcRegion)); + region_ = nullptr; + } + if (shm_fd_ >= 0) + { + ::close(shm_fd_); + shm_fd_ = -1; + } + if (!ipc_name_.empty()) + { + ::shm_unlink(ipc_name_.c_str()); + ipc_name_.clear(); + } +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/gptp_ipc_publisher.h b/score/libTSClient/gptp_ipc_publisher.h new file mode 100644 index 0000000..50a857e --- /dev/null +++ b/score/libTSClient/gptp_ipc_publisher.h @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_PUBLISHER_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_PUBLISHER_H + +#include "score/libTSClient/gptp_ipc_channel.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief Single-writer publisher for the gPTP IPC channel. + * + * Creates the POSIX shared memory segment and writes PtpTimeInfo using + * the seqlock protocol. Used by TimeSlave. + */ +class GptpIpcPublisher final +{ + public: + GptpIpcPublisher() = default; + ~GptpIpcPublisher(); + + GptpIpcPublisher(const GptpIpcPublisher&) = delete; + GptpIpcPublisher& operator=(const GptpIpcPublisher&) = delete; + + /// Create and map the shared memory segment. + /// @return true on success. + bool Init(const std::string& ipc_name = kGptpIpcName); + + /// Publish a PtpTimeInfo snapshot using seqlock. + void Publish(const score::td::PtpTimeInfo& info); + + /// Unmap and unlink the shared memory segment. + void Destroy(); + + private: + GptpIpcRegion* region_{nullptr}; + int shm_fd_{-1}; + std::string ipc_name_; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_PUBLISHER_H diff --git a/score/libTSClient/gptp_ipc_receiver.cpp b/score/libTSClient/gptp_ipc_receiver.cpp new file mode 100644 index 0000000..8cfd5bf --- /dev/null +++ b/score/libTSClient/gptp_ipc_receiver.cpp @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_receiver.h" + +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +static constexpr int kMaxRetries = 20; + +GptpIpcReceiver::~GptpIpcReceiver() +{ + Close(); +} + +bool GptpIpcReceiver::Init(const std::string& ipc_name) +{ + shm_fd_ = ::shm_open(ipc_name.c_str(), O_RDONLY, 0); + if (shm_fd_ < 0) + return false; + + void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), + PROT_READ, MAP_SHARED, shm_fd_, 0); + if (ptr == MAP_FAILED) + { + ::close(shm_fd_); + shm_fd_ = -1; + return false; + } + + region_ = static_cast(ptr); + + if (region_->magic != kGptpIpcMagic) + { + Close(); + return false; + } + + return true; +} + +std::optional GptpIpcReceiver::Receive() +{ + if (region_ == nullptr) + return std::nullopt; + + for (int attempt = 0; attempt < kMaxRetries; ++attempt) + { + const std::uint32_t seq1 = region_->seq.load(std::memory_order_acquire); + + if ((seq1 & 1U) != 0U) + continue; + + std::atomic_thread_fence(std::memory_order_acquire); + score::td::PtpTimeInfo data{}; + std::memcpy(&data, ®ion_->data, sizeof(score::td::PtpTimeInfo)); + std::atomic_thread_fence(std::memory_order_acquire); + + const std::uint32_t seq2 = region_->seq_confirm.load(std::memory_order_acquire); + + if (seq1 == seq2) + return data; + } + + return std::nullopt; +} + +void GptpIpcReceiver::Close() +{ + if (region_ != nullptr) + { + ::munmap(const_cast(region_), sizeof(GptpIpcRegion)); + region_ = nullptr; + } + if (shm_fd_ >= 0) + { + ::close(shm_fd_); + shm_fd_ = -1; + } +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/gptp_ipc_receiver.h b/score/libTSClient/gptp_ipc_receiver.h new file mode 100644 index 0000000..3d0bc3a --- /dev/null +++ b/score/libTSClient/gptp_ipc_receiver.h @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_RECEIVER_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_RECEIVER_H + +#include "score/libTSClient/gptp_ipc_channel.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/** + * @brief Multi-reader receiver for the gPTP IPC channel. + * + * Opens an existing POSIX shared memory segment (read-only) and reads + * PtpTimeInfo using the seqlock protocol. Used by RealPTPEngine. + */ +class GptpIpcReceiver final +{ + public: + GptpIpcReceiver() = default; + ~GptpIpcReceiver(); + + GptpIpcReceiver(const GptpIpcReceiver&) = delete; + GptpIpcReceiver& operator=(const GptpIpcReceiver&) = delete; + + /// Open and map the shared memory segment (read-only). + /// @return true on success. + bool Init(const std::string& ipc_name = kGptpIpcName); + + /// Read a PtpTimeInfo snapshot using seqlock (up to 20 retries). + /// @return The data if consistent, or std::nullopt on contention failure. + std::optional Receive(); + + /// Unmap the shared memory segment. + void Close(); + + private: + const GptpIpcRegion* region_{nullptr}; + int shm_fd_{-1}; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_RECEIVER_H diff --git a/score/libTSClient/gptp_ipc_test.cpp b/score/libTSClient/gptp_ipc_test.cpp new file mode 100644 index 0000000..387f0a9 --- /dev/null +++ b/score/libTSClient/gptp_ipc_test.cpp @@ -0,0 +1,339 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_channel.h" +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_receiver.h" + +#include + +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +namespace +{ + +// Generate a unique POSIX shm name per invocation (avoids cross-test pollution). +std::string UniqueShmName() +{ + static std::atomic counter{0}; + return "/gptp_ipc_ut_" + std::to_string(::getpid()) + "_" + + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +// RAII helper: creates shm manually (without GptpIpcPublisher) for edge-case +// testing; cleans up in destructor. +struct ManualShm +{ + std::string name; + void* ptr = MAP_FAILED; + std::size_t size = sizeof(GptpIpcRegion); + + explicit ManualShm(const std::string& n) : name{n} + { + const int fd = ::shm_open(name.c_str(), O_CREAT | O_RDWR, 0666); + if (fd < 0) + return; + if (::ftruncate(fd, static_cast(size)) != 0) + { + ::close(fd); + return; + } + ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ::close(fd); + } + + ~ManualShm() + { + if (ptr != MAP_FAILED) + ::munmap(ptr, size); + ::shm_unlink(name.c_str()); + } + + bool Valid() const { return ptr != MAP_FAILED; } + GptpIpcRegion* Region() { return static_cast(ptr); } +}; + +} // namespace + +// ── GptpIpcPublisher ────────────────────────────────────────────────────────── + +class GptpIpcPublisherTest : public ::testing::Test +{ + protected: + void TearDown() override { pub_.Destroy(); } + + GptpIpcPublisher pub_; +}; + +TEST_F(GptpIpcPublisherTest, Init_ValidName_ReturnsTrue) +{ + EXPECT_TRUE(pub_.Init(UniqueShmName())); +} + + +TEST_F(GptpIpcPublisherTest, Publish_WithoutInit_DoesNotCrash) +{ + // region_ is nullptr; Publish() must return silently. + score::td::PtpTimeInfo info{}; + EXPECT_NO_THROW(pub_.Publish(info)); +} + +TEST_F(GptpIpcPublisherTest, Destroy_CalledTwice_DoesNotCrash) +{ + ASSERT_TRUE(pub_.Init(UniqueShmName())); + pub_.Destroy(); + EXPECT_NO_THROW(pub_.Destroy()); +} + +TEST_F(GptpIpcPublisherTest, Destroy_WithoutInit_DoesNotCrash) +{ + EXPECT_NO_THROW(pub_.Destroy()); +} + +// ── GptpIpcReceiver ─────────────────────────────────────────────────────────── + +class GptpIpcReceiverTest : public ::testing::Test +{ + protected: + void TearDown() override { rx_.Close(); } + + GptpIpcReceiver rx_; +}; + +TEST_F(GptpIpcReceiverTest, Init_ShmNotExist_ReturnsFalse) +{ + EXPECT_FALSE(rx_.Init("/gptp_nonexistent_" + std::to_string(::getpid()))); +} + +TEST_F(GptpIpcReceiverTest, Close_WithoutInit_DoesNotCrash) +{ + EXPECT_NO_THROW(rx_.Close()); +} + +TEST_F(GptpIpcReceiverTest, Close_CalledTwice_DoesNotCrash) +{ + EXPECT_NO_THROW(rx_.Close()); + EXPECT_NO_THROW(rx_.Close()); +} + +TEST_F(GptpIpcReceiverTest, Receive_WithoutInit_ReturnsNullopt) +{ + EXPECT_FALSE(rx_.Receive().has_value()); +} + +// ── Publisher + Receiver roundtrip ──────────────────────────────────────────── + +class GptpIpcRoundtripTest : public ::testing::Test +{ + protected: + void SetUp() override { name_ = UniqueShmName(); } + void TearDown() override + { + rx_.Close(); + pub_.Destroy(); + } + + std::string name_; + GptpIpcPublisher pub_; + GptpIpcReceiver rx_; +}; + +TEST_F(GptpIpcRoundtripTest, ReceiverInit_AfterPublisherInit_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + EXPECT_TRUE(rx_.Init(name_)); +} + +TEST_F(GptpIpcRoundtripTest, ReceiverReceive_BeforeAnyPublish_ReturnsDefaultData) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + // seq == seq_confirm == 0: both even and equal → seqlock considers readable. + auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->ptp_assumed_time, std::chrono::nanoseconds{0}); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_BasicFields_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::td::PtpTimeInfo info{}; + info.ptp_assumed_time = std::chrono::nanoseconds{1'234'567'890LL}; + info.rate_deviation = 0.75; + info.status.is_synchronized = true; + info.status.is_correct = true; + + pub_.Publish(info); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->ptp_assumed_time, info.ptp_assumed_time); + EXPECT_DOUBLE_EQ(result->rate_deviation, info.rate_deviation); + EXPECT_TRUE(result->status.is_synchronized); + EXPECT_TRUE(result->status.is_correct); + EXPECT_FALSE(result->status.is_timeout); + EXPECT_FALSE(result->status.is_time_jump_future); + EXPECT_FALSE(result->status.is_time_jump_past); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_StatusFlags_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::td::PtpTimeInfo info{}; + info.status.is_timeout = true; + info.status.is_time_jump_future = true; + info.status.is_time_jump_past = false; + info.status.is_synchronized = false; + info.status.is_correct = false; + + pub_.Publish(info); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result->status.is_timeout); + EXPECT_TRUE(result->status.is_time_jump_future); + EXPECT_FALSE(result->status.is_time_jump_past); + EXPECT_FALSE(result->status.is_synchronized); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_SyncFupData_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::td::PtpTimeInfo info{}; + info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + info.sync_fup_data.reference_global_timestamp = 100'000'001'000ULL; + info.sync_fup_data.reference_local_timestamp = 100'000'001'500ULL; + info.sync_fup_data.sync_ingress_timestamp = 100'000'001'500ULL; + info.sync_fup_data.correction_field = 42U; + info.sync_fup_data.sequence_id = 77; + info.sync_fup_data.pdelay = 3'000U; + info.sync_fup_data.port_number = 1; + info.sync_fup_data.clock_identity = 0xAABBCCDDEEFF0011ULL; + + pub_.Publish(info); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->sync_fup_data.precise_origin_timestamp, 100'000'000'000ULL); + EXPECT_EQ(result->sync_fup_data.reference_global_timestamp, 100'000'001'000ULL); + EXPECT_EQ(result->sync_fup_data.sequence_id, 77); + EXPECT_EQ(result->sync_fup_data.pdelay, 3'000U); + EXPECT_EQ(result->sync_fup_data.clock_identity, 0xAABBCCDDEEFF0011ULL); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_PDelayData_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::td::PtpTimeInfo info{}; + info.pdelay_data.request_origin_timestamp = 1'000'000'000ULL; + info.pdelay_data.request_receipt_timestamp = 1'000'001'000ULL; + info.pdelay_data.response_origin_timestamp = 1'000'001'000ULL; + info.pdelay_data.response_receipt_timestamp = 1'000'002'000ULL; + info.pdelay_data.pdelay = 1'000U; + info.pdelay_data.req_port_number = 1; + info.pdelay_data.resp_port_number = 2; + info.pdelay_data.req_clock_identity = 0x1122334455667788ULL; + + pub_.Publish(info); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->pdelay_data.request_origin_timestamp, 1'000'000'000ULL); + EXPECT_EQ(result->pdelay_data.pdelay, 1'000U); + EXPECT_EQ(result->pdelay_data.req_port_number, 1); + EXPECT_EQ(result->pdelay_data.resp_port_number, 2); + EXPECT_EQ(result->pdelay_data.req_clock_identity, 0x1122334455667788ULL); +} + +TEST_F(GptpIpcRoundtripTest, MultiplePublish_LastValueIsVisible) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + for (int i = 1; i <= 5; ++i) + { + score::td::PtpTimeInfo info{}; + info.ptp_assumed_time = + std::chrono::nanoseconds{static_cast(i) * 1'000'000'000LL}; + pub_.Publish(info); + } + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->ptp_assumed_time, std::chrono::nanoseconds{5'000'000'000LL}); +} + +// ── Edge cases via ManualShm ────────────────────────────────────────────────── + +TEST_F(GptpIpcRoundtripTest, ReceiverInit_WrongMagic_ReturnsFalse) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + // Placement-new initializes magic = kGptpIpcMagic; overwrite with bad value. + new (shm.Region()) GptpIpcRegion{}; + const std::uint32_t bad = 0xDEADBEEFU; + std::memcpy(shm.ptr, &bad, sizeof(bad)); + + EXPECT_FALSE(rx_.Init(name_)); +} + +TEST_F(GptpIpcRoundtripTest, Receive_PersistentOddSeq_ExhaustsRetriesAndReturnsNullopt) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + // seq=1 (odd = writer active), seq_confirm=0; seqlock never resolves. + auto* region = new (shm.Region()) GptpIpcRegion{}; + region->seq.store(1U, std::memory_order_relaxed); + region->seq_confirm.store(0U, std::memory_order_relaxed); + + ASSERT_TRUE(rx_.Init(name_)); + EXPECT_FALSE(rx_.Receive().has_value()); +} + +TEST_F(GptpIpcRoundtripTest, Receive_SeqConfirmMismatch_ExhaustsRetriesAndReturnsNullopt) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + // seq=4 (even, not writing) but seq_confirm=2 → mismatch: write still pending. + auto* region = new (shm.Region()) GptpIpcRegion{}; + region->seq.store(4U, std::memory_order_relaxed); + region->seq_confirm.store(2U, std::memory_order_relaxed); + + ASSERT_TRUE(rx_.Init(name_)); + EXPECT_FALSE(rx_.Receive().has_value()); +} + +} // namespace details +} // namespace ts +} // namespace score From 8729c5925ae72ebe9ffea305410a4bb0fb258d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 27 Mar 2026 13:56:41 +0800 Subject: [PATCH 02/12] Update the doc folder, solve the check problem --- docs/TimeSlave/_assets/timeslave_class.puml | 115 +++++++++ .../_assets/timeslave_data_flow.puml | 61 +++++ .../_assets/timeslave_deployment.puml | 59 +++++ .../_assets/gptp_engine_class.puml | 95 ++++++++ .../gptp_engine/_assets/gptp_threading.puml | 53 ++++ docs/TimeSlave/gptp_engine/index.rst | 227 ++++++++++++++++++ docs/TimeSlave/index.rst | 153 ++++++++++++ .../libTSClient/_assets/ipc_channel.puml | 46 ++++ .../libTSClient/_assets/ipc_sequence.puml | 46 ++++ docs/TimeSlave/libTSClient/index.rst | 175 ++++++++++++++ .../real/details/real_ptp_engine.cpp | 21 +- .../real/details/real_ptp_engine.h | 12 +- .../real/details/real_ptp_engine_test.cpp | 60 +++-- .../code/ptp_machine/real/factory.cpp | 4 +- .../code/ptp_machine/real/factory.h | 5 +- .../real/gptp_real_machine_test.cpp | 24 +- .../TimeSlave/code/application/time_slave.cpp | 17 +- score/TimeSlave/code/application/time_slave.h | 6 +- .../code/gptp/details/frame_codec.cpp | 16 +- .../TimeSlave/code/gptp/details/frame_codec.h | 4 +- .../code/gptp/details/i_raw_socket.h | 5 +- .../code/gptp/details/message_parser.cpp | 23 +- .../code/gptp/details/message_parser.h | 4 +- .../code/gptp/details/message_parser_test.cpp | 24 +- .../code/gptp/details/network_identity.h | 5 +- .../code/gptp/details/pdelay_measurer.cpp | 71 +++--- .../code/gptp/details/pdelay_measurer.h | 16 +- .../gptp/details/pdelay_measurer_test.cpp | 20 +- score/TimeSlave/code/gptp/details/ptp_types.h | 87 ++++--- .../TimeSlave/code/gptp/details/raw_socket.h | 18 +- .../code/gptp/details/sync_state_machine.cpp | 60 ++--- .../code/gptp/details/sync_state_machine.h | 34 +-- .../gptp/details/sync_state_machine_test.cpp | 34 +-- score/TimeSlave/code/gptp/gptp_engine.cpp | 104 ++++---- score/TimeSlave/code/gptp/gptp_engine.h | 60 +++-- .../TimeSlave/code/gptp/gptp_engine_test.cpp | 131 +++++----- .../TimeSlave/code/gptp/instrument/probe.cpp | 5 +- score/TimeSlave/code/gptp/instrument/probe.h | 48 ++-- .../code/gptp/instrument/probe_test.cpp | 24 +- score/TimeSlave/code/gptp/phc/phc_adjuster.h | 13 +- .../gptp/platform/linux/network_identity.cpp | 2 +- .../code/gptp/platform/linux/phc_adjuster.cpp | 16 +- .../code/gptp/platform/linux/raw_socket.cpp | 64 +++-- .../gptp/platform/qnx/network_identity.cpp | 4 +- .../code/gptp/platform/qnx/qnx_raw_shim.cpp | 121 +++++----- .../code/gptp/platform/qnx/raw_socket.cpp | 9 +- score/TimeSlave/code/gptp/record/recorder.cpp | 8 +- score/TimeSlave/code/gptp/record/recorder.h | 25 +- .../code/gptp/record/recorder_test.cpp | 48 ++-- score/libTSClient/gptp_ipc_channel.h | 4 +- score/libTSClient/gptp_ipc_publisher.cpp | 17 +- score/libTSClient/gptp_ipc_publisher.h | 4 +- score/libTSClient/gptp_ipc_receiver.cpp | 5 +- score/libTSClient/gptp_ipc_receiver.h | 2 +- score/libTSClient/gptp_ipc_test.cpp | 87 ++++--- 55 files changed, 1686 insertions(+), 715 deletions(-) create mode 100644 docs/TimeSlave/_assets/timeslave_class.puml create mode 100644 docs/TimeSlave/_assets/timeslave_data_flow.puml create mode 100644 docs/TimeSlave/_assets/timeslave_deployment.puml create mode 100644 docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml create mode 100644 docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml create mode 100644 docs/TimeSlave/gptp_engine/index.rst create mode 100644 docs/TimeSlave/index.rst create mode 100644 docs/TimeSlave/libTSClient/_assets/ipc_channel.puml create mode 100644 docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml create mode 100644 docs/TimeSlave/libTSClient/index.rst diff --git a/docs/TimeSlave/_assets/timeslave_class.puml b/docs/TimeSlave/_assets/timeslave_class.puml new file mode 100644 index 0000000..68b3738 --- /dev/null +++ b/docs/TimeSlave/_assets/timeslave_class.puml @@ -0,0 +1,115 @@ +@startuml +!theme plain + +title TimeSlave Class Diagram + +package "score::ts" { + + class TimeSlave { + - engine_ : GptpEngine + - publisher_ : GptpIpcPublisher + - clock_ : HighPrecisionLocalSteadyClock + + Initialize() : score::cpp::expected + + Run(stop_token) : score::cpp::expected + + Deinitialize() : score::cpp::expected + } + + class GptpEngine { + - options_ : GptpEngineOptions + - rx_thread_ : std::thread + - pdelay_thread_ : std::thread + - sync_sm_ : SyncStateMachine + - pdelay_ : PeerDelayMeasurer + - socket_ : IRawSocket + - codec_ : FrameCodec + - parser_ : MessageParser + - phc_ : PhcAdjuster + - snapshot_mutex_ : std::mutex + - latest_snapshot_ : PtpTimeInfo + + Initialize() : bool + + Deinitialize() : void + + ReadPTPSnapshot() : PtpTimeInfo + - RxThreadFunc(stop_token) : void + - PdelayThreadFunc(stop_token) : void + } + + struct GptpEngineOptions { + + interface_name : std::string + + pdelay_interval_ms : uint32_t + + sync_timeout_ms : uint32_t + + time_jump_forward_ns : int64_t + + time_jump_backward_ns : int64_t + + phc_config : PhcConfig + } + + TimeSlave *-- GptpEngine + TimeSlave *-- "1" GptpIpcPublisher +} + +package "score::ts::gptp::details" { + class FrameCodec { + + ParseEthernetHeader(buf) : EthernetHeader + + AddEthernetHeader(buf, dst_mac, src_mac) : void + } + + class MessageParser { + + Parse(payload, hw_ts) : std::optional + } + + class SyncStateMachine { + - last_sync_ : PTPMessage + - last_offset_ns_ : int64_t + - neighbor_rate_ratio_ : double + - timeout_ : std::atomic + + OnSync(msg) : void + + OnFollowUp(msg) : std::optional + + IsTimeout() : bool + + GetNeighborRateRatio() : double + } + + class PeerDelayMeasurer { + - mutex_ : std::mutex + - result_ : PDelayResult + + SendRequest(socket) : void + + OnResponse(msg) : void + + OnResponseFollowUp(msg) : void + + GetResult() : PDelayResult + } + + struct SyncResult { + + master_ns : int64_t + + offset_ns : int64_t + + sync_fup_data : SyncFupData + + time_jump_forward : bool + + time_jump_backward : bool + } + + struct PDelayResult { + + path_delay_ns : int64_t + + valid : bool + } +} + +package "score::ts::gptp::phc" { + class PhcAdjuster { + - config_ : PhcConfig + - fd_ : int + + IsEnabled() : bool + + AdjustOffset(offset_ns) : void + + AdjustFrequency(ppb) : void + } + + struct PhcConfig { + + enabled : bool + + device_path : std::string + + step_threshold_ns : int64_t + } +} + +GptpEngine *-- FrameCodec +GptpEngine *-- MessageParser +GptpEngine *-- SyncStateMachine +GptpEngine *-- PeerDelayMeasurer +GptpEngine *-- PhcAdjuster + +@enduml diff --git a/docs/TimeSlave/_assets/timeslave_data_flow.puml b/docs/TimeSlave/_assets/timeslave_data_flow.puml new file mode 100644 index 0000000..235c3a7 --- /dev/null +++ b/docs/TimeSlave/_assets/timeslave_data_flow.puml @@ -0,0 +1,61 @@ +@startuml +!theme plain + +title TimeSlave Data Flow + +participant "Network\n(gPTP Master)" as NET +participant "RawSocket" as SOCK +participant "FrameCodec" as FC +participant "MessageParser" as MP +participant "SyncStateMachine" as SSM +participant "PeerDelayMeasurer" as PDM +participant "PhcAdjuster" as PHC +participant "GptpEngine" as GE +participant "GptpIpcPublisher" as PUB +participant "SharedMemory" as SHM + +== RxThread — Sync/FollowUp Processing == + +NET -> SOCK : gPTP Sync frame\n(EtherType 0x88F7) +SOCK -> FC : raw buffer + HW timestamp +FC -> MP : Ethernet payload +MP -> SSM : OnSync(PTPMessage) +SSM -> SSM : store Sync timestamp + +NET -> SOCK : gPTP FollowUp frame +SOCK -> FC : raw buffer + HW timestamp +FC -> MP : Ethernet payload +MP -> SSM : OnFollowUp(PTPMessage) +SSM -> SSM : compute offset & neighborRateRatio +SSM --> MP : SyncResult{master_ns, offset_ns,\ntime_jump_flags} + +MP --> GE : update latest_snapshot_\n(mutex protected) + +== PdelayThread — Peer Delay Measurement == + +GE -> PDM : SendRequest() +PDM -> SOCK : PDelayReq frame +NET --> SOCK : PDelayResp frame +SOCK -> FC : raw buffer + HW timestamp +FC -> MP : Ethernet payload +MP -> PDM : OnResponse(msg) +NET --> SOCK : PDelayRespFollowUp frame +SOCK -> FC : raw buffer + HW timestamp +FC -> MP : Ethernet payload +MP -> PDM : OnResponseFollowUp(msg) +PDM -> PDM : path_delay = ((t2-t1)+(t4-t3c))/2 + +PDM --> GE : update PDelayResult + +== PHC Adjustment == + +GE -> PHC : AdjustOffset(offset_ns) +PHC -> PHC : step or frequency slew + +== Periodic Publish to Shared Memory == + +GE -> GE : ReadPTPSnapshot() +GE -> PUB : Publish(PtpTimeInfo) +PUB -> SHM : seqlock write\n(atomic seq++, memcpy, seq++) + +@enduml diff --git a/docs/TimeSlave/_assets/timeslave_deployment.puml b/docs/TimeSlave/_assets/timeslave_deployment.puml new file mode 100644 index 0000000..b168817 --- /dev/null +++ b/docs/TimeSlave/_assets/timeslave_deployment.puml @@ -0,0 +1,59 @@ +@startuml +!theme plain + +title TimeSlave Deployment View + +node "ECU" { + package "TimeSlave Process" as TSP { + component [GptpEngine] as GE + component [GptpIpcPublisher] as PUB + + package "RxThread" as RXT { + component [FrameCodec] as FC + component [MessageParser] as MP + component [SyncStateMachine] as SSM + } + + package "PdelayThread" as PDT { + component [PeerDelayMeasurer] as PDM + } + + component [PhcAdjuster] as PHC + component [ProbeManager] as PM + component [Recorder] as REC + } + + package "TimeDaemon Process" as TDP { + component [GptpIpcReceiver] as RCV + } + + database "Shared Memory\n/gptp_ptp_info" as SHM + + interface "Raw Socket\n(AF_PACKET)" as SOCK + interface "PHC Device\n(/dev/ptpN)" as PHCDEV +} + +cloud "Network" as NET + +GE --> RXT +GE --> PDT +GE --> PHC +GE --> PUB + +FC --> MP +MP --> SSM +MP --> PDM + +PUB --> SHM : seqlock write +RCV --> SHM : seqlock read + +RXT --> SOCK : recv +PDT --> SOCK : send/recv + +PHC --> PHCDEV : clock_adjtime + +SOCK --> NET : gPTP frames\nEtherType 0x88F7 + +PM --> REC : probe events + +@enduml diff --git a/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml b/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml new file mode 100644 index 0000000..f4550ad --- /dev/null +++ b/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml @@ -0,0 +1,95 @@ +@startuml +!theme plain + +title gPTP Engine Internal Class Diagram + +package "score::ts::gptp" { + + class GptpEngine { + - options_ : GptpEngineOptions + - rx_thread_ : std::thread + - pdelay_thread_ : std::thread + - socket_ : std::unique_ptr + - codec_ : FrameCodec + - parser_ : MessageParser + - sync_sm_ : SyncStateMachine + - pdelay_ : PeerDelayMeasurer + - phc_ : PhcAdjuster + - probe_mgr_ : ProbeManager + - recorder_ : Recorder + - snapshot_mutex_ : std::mutex + - latest_snapshot_ : PtpTimeInfo + + Initialize() : bool + + Deinitialize() : void + + ReadPTPSnapshot() : PtpTimeInfo + } + + interface IRawSocket { + + Open(iface) : bool + + EnableHwTimestamping() : bool + + Recv(buf, timeout_ms) : RecvResult + + Send(buf, hw_ts) : bool + + GetFd() : int + + Close() : void + } + + class "RawSocket\n<>" as LinuxSocket { + AF_PACKET + SO_TIMESTAMPING + } + + class "RawSocket\n<>" as QnxSocket { + QNX raw-socket shim + } + + interface INetworkIdentity { + + Resolve(iface) : bool + + GetClockIdentity() : ClockIdentity + } + + class NetworkIdentity { + Derives EUI-64 from MAC\n(inserts 0xFF 0xFE) + } + + IRawSocket <|.. LinuxSocket + IRawSocket <|.. QnxSocket + INetworkIdentity <|.. NetworkIdentity + + GptpEngine *-- IRawSocket + GptpEngine *-- INetworkIdentity +} + +package "score::ts::gptp::details" { + class FrameCodec + class MessageParser + class SyncStateMachine + class PeerDelayMeasurer +} + +package "score::ts::gptp::phc" { + class PhcAdjuster +} + +package "score::ts::gptp::instrument" { + class ProbeManager { + + {static} Instance() : ProbeManager& + + Record(point, data) : void + + SetRecorder(recorder) : void + } + + class Recorder { + - file_ : std::ofstream + - mutex_ : std::mutex + + Record(entry) : void + } + + ProbeManager --> Recorder +} + +GptpEngine *-- FrameCodec +GptpEngine *-- MessageParser +GptpEngine *-- SyncStateMachine +GptpEngine *-- PeerDelayMeasurer +GptpEngine *-- PhcAdjuster +GptpEngine *-- ProbeManager + +@enduml diff --git a/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml b/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml new file mode 100644 index 0000000..93921e2 --- /dev/null +++ b/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml @@ -0,0 +1,53 @@ +@startuml +!theme plain + +title gPTP Engine Threading Model + +concise "RxThread" as RX +concise "PdelayThread" as PD +concise "Main Thread\n(TimeSlave)" as MAIN + +@0 +MAIN is "Initialize" + +@50 +RX is "Waiting" +PD is "Waiting" +MAIN is "Running" + +@100 +RX is "Recv Sync" +PD is "Sleep\n(interval)" + +@150 +RX is "Parse + SSM" + +@200 +RX is "Recv FollowUp" + +@250 +RX is "Parse + SSM\ncompute offset" + +@300 +RX is "Update snapshot" +PD is "SendRequest" + +@350 +RX is "Waiting" +PD is "Recv Resp" + +@400 +PD is "Recv RespFUp\ncompute delay" + +@450 +PD is "Update result" + +@500 +MAIN is "ReadPTPSnapshot\n→ Publish IPC" +RX is "Waiting" +PD is "Sleep\n(interval)" + +@550 +MAIN is "Running" + +@enduml diff --git a/docs/TimeSlave/gptp_engine/index.rst b/docs/TimeSlave/gptp_engine/index.rst new file mode 100644 index 0000000..e734c5e --- /dev/null +++ b/docs/TimeSlave/gptp_engine/index.rst @@ -0,0 +1,227 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _gptp_engine_design: + +############################ +gPTP Engine Design +############################ + +.. contents:: Table of Contents + :depth: 3 + :local: + +Overview +======== + +The ``GptpEngine`` is the core protocol engine of TimeSlave. It implements the IEEE 802.1AS +gPTP slave clock functionality by managing two dedicated threads for network I/O and peer +delay measurement. + +Class view +========== + +.. raw:: html + +
+ +.. uml:: _assets/gptp_engine_class.puml + :alt: gPTP Engine Class Diagram + +.. raw:: html + +
+ +Threading model +=============== + +The GptpEngine operates with two background threads: + +.. raw:: html + +
+ +.. uml:: _assets/gptp_threading.puml + :alt: gPTP Engine Threading Model + +.. raw:: html + +
+ +RxThread +-------- + +The RxThread is the primary receive path. It runs a continuous loop: + +1. **Recv** — Blocks on ``IRawSocket::Recv()`` with a configurable timeout, receiving raw + Ethernet frames with hardware timestamps from the NIC. + +2. **Decode** — ``FrameCodec::ParseEthernetHeader()`` strips the Ethernet header (with VLAN + support) and validates the EtherType (``0x88F7``). + +3. **Parse** — ``MessageParser::Parse()`` decodes the PTP payload into a ``PTPMessage`` + structure, identifying the message type (Sync, FollowUp, PdelayResp, PdelayRespFollowUp). + +4. **Dispatch** — Based on message type: + + - **Sync** → ``SyncStateMachine::OnSync()`` stores the Sync timestamp + - **FollowUp** → ``SyncStateMachine::OnFollowUp()`` correlates with the preceding Sync, + computes ``offset_ns`` and ``neighborRateRatio``, and detects time jumps + - **PdelayResp** → ``PeerDelayMeasurer::OnResponse()`` + - **PdelayRespFollowUp** → ``PeerDelayMeasurer::OnResponseFollowUp()`` + +5. **Snapshot** — After processing, the latest ``PtpTimeInfo`` snapshot is updated under + mutex protection. + +PdelayThread +------------ + +The PdelayThread performs IEEE 802.1AS peer delay measurement on a periodic interval +(configurable via ``GptpEngineOptions::pdelay_interval_ms``): + +1. **Send** — ``PeerDelayMeasurer::SendRequest()`` transmits a ``PDelayReq`` frame via the + raw socket, capturing the hardware transmit timestamp (``t1``). + +2. **Receive** — The RxThread dispatches incoming ``PDelayResp`` (providing ``t2``) and + ``PDelayRespFollowUp`` (providing ``t3c``) to the ``PeerDelayMeasurer``. + +3. **Compute** — The peer delay is computed using the IEEE 802.1AS formula: + + .. code-block:: text + + path_delay = ((t2 - t1) + (t4 - t3c)) / 2 + + where ``t4`` is the local hardware receive timestamp of the PDelayResp frame. + +Thread safety is ensured via a mutex in ``PeerDelayMeasurer``, as ``SendRequest()`` runs on +the PdelayThread while ``OnResponse()``/``OnResponseFollowUp()`` are called from the +RxThread. + +Core components +=============== + +FrameCodec +---------- + +Handles raw Ethernet frame encoding and decoding: + +- ``ParseEthernetHeader()`` — Parses source/destination MAC, handles 802.1Q VLAN tags, + extracts EtherType and payload offset. +- ``AddEthernetHeader()`` — Constructs Ethernet headers for outgoing PDelayReq frames using + the standard PTP multicast destination MAC (``01:80:C2:00:00:0E``). + +MessageParser +------------- + +Parses the PTP wire format (IEEE 1588-v2) from raw payload bytes: + +- Validates the PTP header (version, domain, message length). +- Decodes message-type-specific bodies: ``SyncBody``, ``FollowUpBody``, ``PdelayReqBody``, + ``PdelayRespBody``, ``PdelayRespFollowUpBody``. +- All wire structures are packed (``__attribute__((packed))``) for direct memory mapping. + +SyncStateMachine +---------------- + +Implements the two-step Sync/FollowUp correlation logic: + +- **OnSync(msg)** — Stores the Sync message and its hardware receive timestamp. +- **OnFollowUp(msg)** — Matches with the preceding Sync by sequence ID, then computes: + + - ``offset_ns`` = master_time - slave_receive_time - path_delay + - ``neighborRateRatio`` from successive Sync intervals (master vs. slave clock progression) + - Time jump detection (forward/backward) against configurable thresholds + +- **Timeout detection** — Uses ``std::atomic`` for thread-safe timeout status, + set when no Sync is received within ``sync_timeout_ms``. + +PeerDelayMeasurer +----------------- + +Implements the IEEE 802.1AS two-step peer delay measurement protocol: + +- Manages the four timestamps (``t1``, ``t2``, ``t3c``, ``t4``) across two threads. +- ``SendRequest()`` — Builds and sends a PDelayReq frame, records ``t1`` from the + hardware transmit timestamp. +- ``OnResponse()`` / ``OnResponseFollowUp()`` — Records ``t2``, ``t3c``, ``t4`` and + computes the path delay when all timestamps are available. +- Returns ``PDelayResult`` with ``path_delay_ns`` and a ``valid`` flag. + +PhcAdjuster +----------- + +Synchronizes the PTP Hardware Clock (PHC) on the NIC: + +- **Step correction** — For large offsets exceeding ``step_threshold_ns``, applies an + immediate time step to the PHC. +- **Frequency slew** — For smaller offsets, adjusts the clock frequency (in ppb) for + smooth convergence. +- Platform-specific: Linux uses ``clock_adjtime()``, QNX uses EMAC PTP ioctls. +- Configured via ``PhcConfig`` (device path, step threshold, enable/disable flag). + +Instrumentation +=============== + +ProbeManager +------------ + +A singleton that records probe events at key processing points. Probe points include: + +- ``RxPacketReceived`` — Raw frame received from socket +- ``SyncFrameParsed`` — Sync message successfully parsed +- ``FollowUpProcessed`` — Offset computed from Sync/FollowUp pair +- ``OffsetComputed`` — Final offset value available +- ``PdelayReqSent`` — PDelayReq frame transmitted +- ``PdelayCompleted`` — Peer delay measurement completed +- ``PhcAdjusted`` — PHC adjustment applied + +The ``GPTP_PROBE()`` macro provides zero-overhead when probing is disabled. + +Recorder +-------- + +Thread-safe CSV file writer that persists probe events and other diagnostic data. Each +``RecordEntry`` contains a timestamp, event type, offset, peer delay, sequence ID, and +status flags. + +Configuration +============= + +The ``GptpEngineOptions`` struct provides all configurable parameters: + +.. list-table:: + :header-rows: 1 + :widths: 30 15 55 + + * - Parameter + - Type + - Description + * - ``interface_name`` + - string + - Network interface for gPTP frames (e.g., ``eth0``) + * - ``pdelay_interval_ms`` + - uint32_t + - Interval between PDelayReq transmissions + * - ``sync_timeout_ms`` + - uint32_t + - Timeout for Sync message reception before declaring timeout state + * - ``time_jump_forward_ns`` + - int64_t + - Threshold for forward time jump detection + * - ``time_jump_backward_ns`` + - int64_t + - Threshold for backward time jump detection + * - ``phc_config`` + - PhcConfig + - PHC device path, step threshold, and enable flag diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst new file mode 100644 index 0000000..2db74f3 --- /dev/null +++ b/docs/TimeSlave/index.rst @@ -0,0 +1,153 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _timeslave_design: + +############################ +TimeSlave Design +############################ + +.. contents:: Table of Contents + :depth: 3 + :local: + +Overview +======== + +**TimeSlave** is a standalone process that implements the gPTP (IEEE 802.1AS) slave endpoint +for the Eclipse SCORE time synchronization system. It receives gPTP Sync/FollowUp messages +from a Time Master on the Ethernet network, measures peer delay, optionally adjusts the PTP +Hardware Clock (PHC), and publishes the resulting ``PtpTimeInfo`` to shared memory for +consumption by the **TimeDaemon**. + +TimeSlave is deployed as a separate process from TimeDaemon to isolate the real-time +network I/O (raw socket operations, hardware timestamping) from the higher-level time +validation and distribution logic. + +Architecture +============ + +The TimeSlave process is composed of the following major components: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Component + - Responsibility + * - **TimeSlave Application** + - Lifecycle management (Initialize, Run, Deinitialize). Integrates with ``score::mw::lifecycle``. + * - **GptpEngine** + - Core gPTP protocol engine. Manages RxThread and PdelayThread. + * - **libTSClient (GptpIpcPublisher)** + - Publishes ``PtpTimeInfo`` to POSIX shared memory using a seqlock protocol. + * - **PhcAdjuster** + - Adjusts the PTP Hardware Clock via step or frequency corrections. + * - **ProbeManager / Recorder** + - Runtime instrumentation and CSV-based event recording. + +Deployment view +--------------- + +.. raw:: html + +
+ +.. uml:: _assets/timeslave_deployment.puml + :alt: TimeSlave Deployment View + +.. raw:: html + +
+ +Class view +---------- + +.. raw:: html + +
+ +.. uml:: _assets/timeslave_class.puml + :alt: TimeSlave Class Diagram + +.. raw:: html + +
+ +Data flow +--------- + +The end-to-end data flow from network frame reception to shared memory publication: + +.. raw:: html + +
+ +.. uml:: _assets/timeslave_data_flow.puml + :alt: TimeSlave Data Flow + +.. raw:: html + +
+ +Application lifecycle +===================== + +The ``TimeSlave`` class extends ``score::mw::lifecycle::Application`` and follows the +standard SCORE lifecycle pattern: + +1. **Initialize** — Creates the ``GptpEngine`` with configured options, initializes + the ``GptpIpcPublisher`` (creates shared memory segment), and prepares the + ``HighPrecisionLocalSteadyClock`` for local time reference. + +2. **Run** — Starts the GptpEngine (which spawns RxThread and PdelayThread internally). + Enters a periodic loop that: + + - Calls ``GptpEngine::ReadPTPSnapshot()`` to get the latest time measurement + - Enriches the snapshot with the local clock timestamp + - Publishes to shared memory via ``GptpIpcPublisher::Publish()`` + - Monitors the ``stop_token`` for graceful shutdown + +3. **Deinitialize** — Stops the GptpEngine threads, destroys the shared memory segment. + +Platform support +================ + +TimeSlave supports two target platforms with platform-specific implementations: + +.. list-table:: + :header-rows: 1 + :widths: 20 40 40 + + * - Component + - Linux + - QNX + * - Raw Socket + - ``AF_PACKET`` with ``SO_TIMESTAMPING`` + - QNX raw-socket shim + * - Network Identity + - ``ioctl(SIOCGIFHWADDR)`` + - QNX-specific MAC resolution + * - PHC Adjuster + - ``clock_adjtime()`` + - EMAC PTP ioctls + +Platform selection is handled at compile time via Bazel ``select()`` in the BUILD files. + +.. toctree:: + :maxdepth: 2 + :caption: Detailed Design + + gptp_engine/index + libTSClient/index diff --git a/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml b/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml new file mode 100644 index 0000000..69e04b4 --- /dev/null +++ b/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml @@ -0,0 +1,46 @@ +@startuml +!theme plain + +title libTSClient Shared Memory IPC + +package "TimeSlave Process" { + class GptpIpcPublisher { + - region_ : GptpIpcRegion* + - fd_ : int + + Init(name) : bool + + Publish(info) : void + + Destroy() : void + } +} + +package "Shared Memory" { + class GptpIpcRegion <> { + + magic : uint32_t = 0x47505440 + + seq : std::atomic + + data : PtpTimeInfo + -- + 64-byte aligned for\ncache line efficiency + } +} + +package "TimeDaemon Process" { + class GptpIpcReceiver { + - region_ : const GptpIpcRegion* + - fd_ : int + + Init(name) : bool + + Receive() : std::optional + + Close() : void + } +} + +GptpIpcPublisher --> GptpIpcRegion : "shm_open(O_CREAT)\nmmap(PROT_WRITE)" +GptpIpcReceiver --> GptpIpcRegion : "shm_open(O_RDONLY)\nmmap(PROT_READ)" + +note right of GptpIpcRegion + **Seqlock Protocol:** + Writer: seq++ → memcpy → seq++ + Reader: read seq (even) → memcpy → check seq + Retry up to 20 times on torn read +end note + +@enduml diff --git a/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml b/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml new file mode 100644 index 0000000..46fa582 --- /dev/null +++ b/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml @@ -0,0 +1,46 @@ +@startuml +!theme plain + +title libTSClient Seqlock IPC Protocol + +participant "TimeSlave\n(GptpIpcPublisher)" as PUB +participant "SharedMemory\n(GptpIpcRegion)" as SHM +participant "TimeDaemon\n(GptpIpcReceiver)" as RCV + +== Initialization == + +PUB -> SHM : shm_open("/gptp_ptp_info", O_CREAT | O_RDWR) +PUB -> SHM : ftruncate(sizeof(GptpIpcRegion)) +PUB -> SHM : mmap(PROT_READ | PROT_WRITE) +PUB -> SHM : write magic = 0x47505440 + +... + +RCV -> SHM : shm_open("/gptp_ptp_info", O_RDONLY) +RCV -> SHM : mmap(PROT_READ) +RCV -> SHM : verify magic == 0x47505440 + +== Publish (Writer Side) == + +PUB -> SHM : seq.fetch_add(1, release) // seq becomes odd +PUB -> SHM : memcpy(data, &info, sizeof) +PUB -> SHM : seq.fetch_add(1, release) // seq becomes even + +== Receive (Reader Side) == + +loop up to 20 retries + RCV -> SHM : s1 = seq.load(acquire) + alt s1 is odd (write in progress) + RCV -> RCV : retry + else s1 is even + RCV -> SHM : memcpy(&local, data, sizeof) + RCV -> SHM : s2 = seq.load(acquire) + alt s1 == s2 + RCV --> RCV : return PtpTimeInfo + else s1 != s2 (torn read) + RCV -> RCV : retry + end + end +end + +@enduml diff --git a/docs/TimeSlave/libTSClient/index.rst b/docs/TimeSlave/libTSClient/index.rst new file mode 100644 index 0000000..b843b5c --- /dev/null +++ b/docs/TimeSlave/libTSClient/index.rst @@ -0,0 +1,175 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _libtsclient_design: + +############################ +libTSClient Design +############################ + +.. contents:: Table of Contents + :depth: 3 + :local: + +Overview +======== + +**libTSClient** is the shared memory IPC library that connects the TimeSlave process to the +TimeDaemon process. It provides a lock-free, single-writer/multi-reader communication +channel using a **seqlock protocol** over POSIX shared memory. + +The library is intentionally minimal — it consists of three headers and two source files — +to keep the IPC boundary simple, auditable, and suitable for safety-critical deployments. + +Architecture +============ + +.. raw:: html + +
+ +.. uml:: _assets/ipc_channel.puml + :alt: libTSClient IPC Architecture + +.. raw:: html + +
+ +Components +========== + +GptpIpcChannel +-------------- + +Defines the shared memory layout as the ``GptpIpcRegion`` structure: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Field + - Type + - Purpose + * - ``magic`` + - ``uint32_t`` + - Validation constant (``0x47505440``). Used by the receiver to confirm the shared + memory segment is valid and initialized. + * - ``seq`` + - ``std::atomic`` + - Seqlock counter. Odd values indicate a write in progress; even values indicate + a consistent state. + * - ``data`` + - ``PtpTimeInfo`` + - The actual time synchronization payload (PTP status, Sync/FollowUp data, + peer delay data, local clock reference). + +The structure is aligned to 64 bytes (cache line size) to prevent false sharing between +the writer and reader processes. + +The default POSIX shared memory name is ``/gptp_ptp_info`` (defined as ``kGptpIpcName``). + +GptpIpcPublisher +---------------- + +The **single-writer** component, used by TimeSlave: + +- ``Init(name)`` — Creates the POSIX shared memory segment via ``shm_open(O_CREAT | O_RDWR)``, + sizes it with ``ftruncate()``, maps it with ``mmap(PROT_READ | PROT_WRITE)``, and writes + the magic number. + +- ``Publish(info)`` — Writes a ``PtpTimeInfo`` using the seqlock protocol: + + 1. Increment ``seq`` (becomes odd — signals write in progress) + 2. ``memcpy`` the data + 3. Increment ``seq`` (becomes even — signals write complete) + +- ``Destroy()`` — Unmaps and unlinks the shared memory segment. + +GptpIpcReceiver +--------------- + +The **multi-reader** component, used by the TimeDaemon (via ``RealPTPEngine``): + +- ``Init(name)`` — Opens the existing shared memory segment via ``shm_open(O_RDONLY)`` and + maps it with ``mmap(PROT_READ)``. Validates the magic number. + +- ``Receive()`` — Reads ``PtpTimeInfo`` using the seqlock protocol with up to 20 retries: + + 1. Read ``seq`` (must be even, otherwise retry) + 2. ``memcpy`` the data + 3. Read ``seq`` again (must match step 1, otherwise retry — torn read detected) + 4. Return ``std::optional`` (empty if all retries exhausted) + +- ``Close()`` — Unmaps the shared memory. + +Seqlock protocol +================ + +.. raw:: html + +
+ +.. uml:: _assets/ipc_sequence.puml + :alt: Seqlock IPC Protocol Sequence + +.. raw:: html + +
+ +The seqlock provides the following properties: + +- **Lock-free for readers** — Readers never block the writer. A torn read is detected and + retried transparently. +- **Single writer** — Only one process (TimeSlave) writes to the shared memory. No + writer-writer contention. +- **Bounded retry** — The receiver retries up to 20 times. Under normal operation, + retries are rare because the write is a single ``memcpy`` of a small struct. +- **Cache-line alignment** — The 64-byte alignment of ``GptpIpcRegion`` prevents false + sharing, which is critical for cross-process shared memory performance. + +Data type +========= + +The ``PtpTimeInfo`` structure (defined in ``score/TimeDaemon/code/common/data_types/ptp_time_info.h``) +is the payload transferred through the IPC channel. It contains: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Field + - Content + * - ``PtpStatus`` + - Synchronization flags (synchronized, timeout, time leap indicators) + * - ``SyncFupData`` + - Sync and FollowUp message timestamps and correction fields + * - ``PDelayData`` + - Peer delay measurement results + * - Local clock value + - Reference timestamp from ``HighPrecisionLocalSteadyClock`` + +Build integration +================= + +The library is built as a Bazel ``cc_library`` target: + +.. code-block:: text + + //score/libTSClient:gptp_ipc + +It links against ``-lrt`` for POSIX shared memory (``shm_open``, ``shm_unlink``) and +depends on the ``PtpTimeInfo`` data type from the TimeDaemon common module. + +Both TimeSlave and TimeDaemon link against ``libTSClient`` — the publisher side in +TimeSlave, the receiver side in TimeDaemon. diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp index 6e46287..8258250 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp @@ -22,10 +22,7 @@ namespace td namespace details { -RealPTPEngine::RealPTPEngine(std::string ipc_name) noexcept - : ipc_name_{std::move(ipc_name)} -{ -} +RealPTPEngine::RealPTPEngine(std::string ipc_name) noexcept : ipc_name_{std::move(ipc_name)} {} bool RealPTPEngine::Initialize() { @@ -35,13 +32,11 @@ bool RealPTPEngine::Initialize() initialized_ = receiver_.Init(ipc_name_); if (initialized_) { - score::mw::log::LogInfo(kGPtpMachineContext) - << "RealPTPEngine: connected to IPC channel " << ipc_name_; + score::mw::log::LogInfo(kGPtpMachineContext) << "RealPTPEngine: connected to IPC channel " << ipc_name_; } else { - score::mw::log::LogError(kGPtpMachineContext) - << "RealPTPEngine: failed to open IPC channel " << ipc_name_; + score::mw::log::LogError(kGPtpMachineContext) << "RealPTPEngine: failed to open IPC channel " << ipc_name_; } return initialized_; } @@ -67,18 +62,18 @@ bool RealPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) cached_ = result.value(); - const bool time_ok = ReadTimeValueAndStatus(info); + const bool time_ok = ReadTimeValueAndStatus(info); const bool pdelay_ok = ReadPDelayMeasurementData(info); - const bool sync_ok = ReadSyncMeasurementData(info); + const bool sync_ok = ReadSyncMeasurementData(info); return time_ok && pdelay_ok && sync_ok; } bool RealPTPEngine::ReadTimeValueAndStatus(PtpTimeInfo& info) noexcept { - info.local_time = cached_.local_time; + info.local_time = cached_.local_time; info.ptp_assumed_time = cached_.ptp_assumed_time; - info.rate_deviation = cached_.rate_deviation; - info.status = cached_.status; + info.rate_deviation = cached_.rate_deviation; + info.status = cached_.status; return true; } diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h index a7215d7..992637c 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h @@ -34,10 +34,10 @@ class RealPTPEngine final explicit RealPTPEngine(std::string ipc_name = score::ts::details::kGptpIpcName) noexcept; ~RealPTPEngine() noexcept = default; - RealPTPEngine(const RealPTPEngine&) = delete; + RealPTPEngine(const RealPTPEngine&) = delete; RealPTPEngine& operator=(const RealPTPEngine&) = delete; - RealPTPEngine(RealPTPEngine&&) = delete; - RealPTPEngine& operator=(RealPTPEngine&&) = delete; + RealPTPEngine(RealPTPEngine&&) = delete; + RealPTPEngine& operator=(RealPTPEngine&&) = delete; /// Open and map the IPC channel. /// @return true on success. @@ -57,10 +57,10 @@ class RealPTPEngine final bool ReadSyncMeasurementData(PtpTimeInfo& info) const noexcept; private: - std::string ipc_name_; + std::string ipc_name_; score::ts::details::GptpIpcReceiver receiver_; - bool initialized_{false}; - PtpTimeInfo cached_{}; + bool initialized_{false}; + PtpTimeInfo cached_{}; }; } // namespace details diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp index 0677b91..94b6254 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp @@ -41,33 +41,33 @@ PtpTimeInfo MakeTestInfo() { PtpTimeInfo info{}; info.ptp_assumed_time = std::chrono::nanoseconds{9'876'543'210LL}; - info.rate_deviation = -0.25; + info.rate_deviation = -0.25; - info.status.is_synchronized = true; - info.status.is_correct = true; - info.status.is_timeout = false; + info.status.is_synchronized = true; + info.status.is_correct = true; + info.status.is_timeout = false; info.status.is_time_jump_future = false; - info.status.is_time_jump_past = false; + info.status.is_time_jump_past = false; - info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; info.sync_fup_data.reference_global_timestamp = 100'000'000'500ULL; - info.sync_fup_data.reference_local_timestamp = 100'000'001'000ULL; - info.sync_fup_data.sync_ingress_timestamp = 100'000'001'000ULL; - info.sync_fup_data.correction_field = 8U; - info.sync_fup_data.sequence_id = 55; - info.sync_fup_data.pdelay = 4'000U; - info.sync_fup_data.port_number = 1; - info.sync_fup_data.clock_identity = 0xCAFEBABEDEAD0001ULL; - - info.pdelay_data.request_origin_timestamp = 200'000'000'000ULL; - info.pdelay_data.request_receipt_timestamp = 200'000'001'000ULL; - info.pdelay_data.response_origin_timestamp = 200'000'001'000ULL; + info.sync_fup_data.reference_local_timestamp = 100'000'001'000ULL; + info.sync_fup_data.sync_ingress_timestamp = 100'000'001'000ULL; + info.sync_fup_data.correction_field = 8U; + info.sync_fup_data.sequence_id = 55; + info.sync_fup_data.pdelay = 4'000U; + info.sync_fup_data.port_number = 1; + info.sync_fup_data.clock_identity = 0xCAFEBABEDEAD0001ULL; + + info.pdelay_data.request_origin_timestamp = 200'000'000'000ULL; + info.pdelay_data.request_receipt_timestamp = 200'000'001'000ULL; + info.pdelay_data.response_origin_timestamp = 200'000'001'000ULL; info.pdelay_data.response_receipt_timestamp = 200'000'002'000ULL; - info.pdelay_data.pdelay = 1'000U; - info.pdelay_data.req_port_number = 2; - info.pdelay_data.resp_port_number = 3; - info.pdelay_data.req_clock_identity = 0x0102030405060708ULL; - info.pdelay_data.resp_clock_identity = 0x0807060504030201ULL; + info.pdelay_data.pdelay = 1'000U; + info.pdelay_data.req_port_number = 2; + info.pdelay_data.resp_port_number = 3; + info.pdelay_data.req_clock_identity = 0x0102030405060708ULL; + info.pdelay_data.resp_clock_identity = 0x0807060504030201ULL; return info; } @@ -78,7 +78,7 @@ class RealPTPEngineTest : public ::testing::Test protected: void SetUp() override { - name_ = UniqueShmName(); + name_ = UniqueShmName(); engine_ = std::make_unique(name_); } @@ -88,7 +88,7 @@ class RealPTPEngineTest : public ::testing::Test pub_.Destroy(); } - std::string name_; + std::string name_; score::ts::details::GptpIpcPublisher pub_; std::unique_ptr engine_; }; @@ -187,10 +187,8 @@ TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesSyncFupDataCorrectly) PtpTimeInfo result{}; ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); - EXPECT_EQ(result.sync_fup_data.precise_origin_timestamp, - expected.sync_fup_data.precise_origin_timestamp); - EXPECT_EQ(result.sync_fup_data.reference_global_timestamp, - expected.sync_fup_data.reference_global_timestamp); + EXPECT_EQ(result.sync_fup_data.precise_origin_timestamp, expected.sync_fup_data.precise_origin_timestamp); + EXPECT_EQ(result.sync_fup_data.reference_global_timestamp, expected.sync_fup_data.reference_global_timestamp); EXPECT_EQ(result.sync_fup_data.sequence_id, expected.sync_fup_data.sequence_id); EXPECT_EQ(result.sync_fup_data.pdelay, expected.sync_fup_data.pdelay); EXPECT_EQ(result.sync_fup_data.clock_identity, expected.sync_fup_data.clock_identity); @@ -209,10 +207,8 @@ TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesPDelayDataCorrectly) EXPECT_EQ(result.pdelay_data.pdelay, expected.pdelay_data.pdelay); EXPECT_EQ(result.pdelay_data.req_port_number, expected.pdelay_data.req_port_number); EXPECT_EQ(result.pdelay_data.resp_port_number, expected.pdelay_data.resp_port_number); - EXPECT_EQ(result.pdelay_data.req_clock_identity, - expected.pdelay_data.req_clock_identity); - EXPECT_EQ(result.pdelay_data.resp_clock_identity, - expected.pdelay_data.resp_clock_identity); + EXPECT_EQ(result.pdelay_data.req_clock_identity, expected.pdelay_data.req_clock_identity); + EXPECT_EQ(result.pdelay_data.resp_clock_identity, expected.pdelay_data.resp_clock_identity); } // ── Individual sub-methods (called after ReadPTPSnapshot populates cache) ───── diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.cpp b/score/TimeDaemon/code/ptp_machine/real/factory.cpp index 5d53a87..a9e9027 100644 --- a/score/TimeDaemon/code/ptp_machine/real/factory.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/factory.cpp @@ -17,9 +17,7 @@ namespace score namespace td { -std::shared_ptr CreateGPTPRealMachine( - const std::string& name, - const std::string& ipc_name) +std::shared_ptr CreateGPTPRealMachine(const std::string& name, const std::string& ipc_name) { constexpr std::chrono::milliseconds updateInterval(50); return std::make_shared(name, updateInterval, ipc_name); diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.h b/score/TimeDaemon/code/ptp_machine/real/factory.h index de324c9..d32bbaf 100644 --- a/score/TimeDaemon/code/ptp_machine/real/factory.h +++ b/score/TimeDaemon/code/ptp_machine/real/factory.h @@ -35,9 +35,8 @@ namespace td * @param ipc_name IPC channel name (default: kGptpIpcName). * @return A fully configured GPTPRealMachine instance. */ -std::shared_ptr CreateGPTPRealMachine( - const std::string& name, - const std::string& ipc_name = score::ts::details::kGptpIpcName); +std::shared_ptr CreateGPTPRealMachine(const std::string& name, + const std::string& ipc_name = score::ts::details::kGptpIpcName); } // namespace td } // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp index 71291d8..705c40c 100644 --- a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp @@ -10,8 +10,8 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/factory.h" #include "score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h" +#include "score/TimeDaemon/code/ptp_machine/real/factory.h" #include "score/libTSClient/gptp_ipc_publisher.h" #include @@ -39,12 +39,12 @@ std::string UniqueShmName() score::td::PtpTimeInfo MakePublishedInfo() { score::td::PtpTimeInfo info{}; - info.ptp_assumed_time = std::chrono::nanoseconds{5'000'000'000LL}; - info.rate_deviation = 0.5; - info.status.is_synchronized = true; - info.status.is_correct = true; + info.ptp_assumed_time = std::chrono::nanoseconds{5'000'000'000LL}; + info.rate_deviation = 0.5; + info.status.is_synchronized = true; + info.status.is_correct = true; info.sync_fup_data.sequence_id = 7U; - info.sync_fup_data.pdelay = 1'000U; + info.sync_fup_data.pdelay = 1'000U; return info; } @@ -76,12 +76,12 @@ class GPTPRealMachineIntegrationTest : public ::testing::Test pub_.Destroy(); } - std::string name_; - score::ts::details::GptpIpcPublisher pub_; - std::shared_ptr machine_; - std::promise promise_; - PtpTimeInfo published_{}; - std::mutex mu_; + std::string name_; + score::ts::details::GptpIpcPublisher pub_; + std::shared_ptr machine_; + std::promise promise_; + PtpTimeInfo published_{}; + std::mutex mu_; }; TEST_F(GPTPRealMachineIntegrationTest, GetName_ReturnsConstructionName) diff --git a/score/TimeSlave/code/application/time_slave.cpp b/score/TimeSlave/code/application/time_slave.cpp index 23d97e6..8ab865f 100644 --- a/score/TimeSlave/code/application/time_slave.cpp +++ b/score/TimeSlave/code/application/time_slave.cpp @@ -1,11 +1,3 @@ -/* - * @Author: chenhao.gao chenhao.gao@ecarxgroup.com - * @Date: 2026-03-25 10:20:36 - * @LastEditors: chenhao.gao chenhao.gao@ecarxgroup.com - * @LastEditTime: 2026-03-25 16:03:13 - * @FilePath: /score_inc_time/score/TimeSlave/code/application/time_slave.cpp - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ /******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * @@ -33,8 +25,7 @@ namespace ts TimeSlave::TimeSlave() = default; -std::int32_t TimeSlave::Initialize( - const score::mw::lifecycle::ApplicationContext& /*context*/) +std::int32_t TimeSlave::Initialize(const score::mw::lifecycle::ApplicationContext& /*context*/) { // Create the high-precision local clock for the gPTP engine score::time::HighPrecisionLocalSteadyClock::FactoryImpl clock_factory{}; @@ -44,15 +35,13 @@ std::int32_t TimeSlave::Initialize( if (!engine_->Initialize()) { - score::mw::log::LogError(kGPtpMachineContext) - << "TimeSlave: GptpEngine initialization failed"; + score::mw::log::LogError(kGPtpMachineContext) << "TimeSlave: GptpEngine initialization failed"; return -1; } if (!publisher_.Init()) { - score::mw::log::LogError(kGPtpMachineContext) - << "TimeSlave: shared memory publisher initialization failed"; + score::mw::log::LogError(kGPtpMachineContext) << "TimeSlave: shared memory publisher initialization failed"; return -1; } diff --git a/score/TimeSlave/code/application/time_slave.h b/score/TimeSlave/code/application/time_slave.h index 9f3795c..5ae32a9 100644 --- a/score/TimeSlave/code/application/time_slave.h +++ b/score/TimeSlave/code/application/time_slave.h @@ -48,9 +48,9 @@ class TimeSlave final : public score::mw::lifecycle::Application std::int32_t Run(const score::cpp::stop_token& token) override; private: - details::GptpEngineOptions opts_; - std::unique_ptr engine_; - details::GptpIpcPublisher publisher_; + details::GptpEngineOptions opts_; + std::unique_ptr engine_; + details::GptpIpcPublisher publisher_; }; } // namespace ts diff --git a/score/TimeSlave/code/gptp/details/frame_codec.cpp b/score/TimeSlave/code/gptp/details/frame_codec.cpp index d12dd41..11491c7 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec.cpp +++ b/score/TimeSlave/code/gptp/details/frame_codec.cpp @@ -29,8 +29,7 @@ namespace int Str2Mac(const char* s, unsigned char mac[kMacAddrLen]) noexcept { unsigned int b[kMacAddrLen]{}; - if (std::sscanf(s, "%x:%x:%x:%x:%x:%x", - &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != kMacAddrLen) + if (std::sscanf(s, "%x:%x:%x:%x:%x:%x", &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != kMacAddrLen) { return -1; } @@ -41,9 +40,7 @@ int Str2Mac(const char* s, unsigned char mac[kMacAddrLen]) noexcept } // namespace -bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, - int frame_len, - int& ptp_offset) const +bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, int frame_len, int& ptp_offset) const { const int kEthHdrLen = static_cast(sizeof(ethhdr)); if (frame_len < kEthHdrLen) @@ -59,8 +56,7 @@ bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, // Skip 4-byte VLAN tag; re-read EtherType if (frame_len < kEthHdrLen + kVlanTagLen + 2) return false; - const uint16_t inner_etype_be = - *reinterpret_cast(frame + kEthHdrLen + kVlanTagLen); + const uint16_t inner_etype_be = *reinterpret_cast(frame + kEthHdrLen + kVlanTagLen); if (ntohs(inner_etype_be) != static_cast(kEthP1588)) return false; ptp_offset = kEthHdrLen + kVlanTagLen; @@ -74,8 +70,7 @@ bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, return true; } -bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, - unsigned int& buf_len) const +bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, unsigned int& buf_len) const { constexpr unsigned int kMaxFrameSize = 2048U; const unsigned int kHdrLen = static_cast(sizeof(ethhdr)); @@ -86,8 +81,7 @@ bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, std::memmove(buf + kHdrLen, buf, buf_len); auto* hdr = reinterpret_cast(buf); - if (Str2Mac(kPtpSrcMac, hdr->h_source) != 0 || - Str2Mac(kPtpDstMac, hdr->h_dest) != 0) + if (Str2Mac(kPtpSrcMac, hdr->h_source) != 0 || Str2Mac(kPtpDstMac, hdr->h_dest) != 0) { return false; } diff --git a/score/TimeSlave/code/gptp/details/frame_codec.h b/score/TimeSlave/code/gptp/details/frame_codec.h index 6a49687..425105c 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec.h +++ b/score/TimeSlave/code/gptp/details/frame_codec.h @@ -44,9 +44,7 @@ class FrameCodec final * @param ptp_offset Output: byte offset where the PTP message starts. * @return true if @p frame contains a PTP/1588 Ethertype, false otherwise. */ - bool ParseEthernetHeader(const std::uint8_t* frame, - int frame_len, - int& ptp_offset) const; + bool ParseEthernetHeader(const std::uint8_t* frame, int frame_len, int& ptp_offset) const; /** * @brief Prepend an Ethernet header for PTP multicast transmission. diff --git a/score/TimeSlave/code/gptp/details/i_raw_socket.h b/score/TimeSlave/code/gptp/details/i_raw_socket.h index 4ac3fd9..9858693 100644 --- a/score/TimeSlave/code/gptp/details/i_raw_socket.h +++ b/score/TimeSlave/code/gptp/details/i_raw_socket.h @@ -13,10 +13,10 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_RAW_SOCKET_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_RAW_SOCKET_H +#include #include #include #include -#include namespace score { @@ -42,8 +42,7 @@ class IRawSocket /// Receive one frame. /// @return Number of bytes received, 0 on timeout, -1 on error. - virtual int Recv(std::uint8_t* buf, std::size_t buf_len, - ::timespec& hwts, int timeout_ms) = 0; + virtual int Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) = 0; /// Send one frame. /// @return Number of bytes sent, or -1 on error. diff --git a/score/TimeSlave/code/gptp/details/message_parser.cpp b/score/TimeSlave/code/gptp/details/message_parser.cpp index b3ac7e0..fadc468 100644 --- a/score/TimeSlave/code/gptp/details/message_parser.cpp +++ b/score/TimeSlave/code/gptp/details/message_parser.cpp @@ -63,25 +63,23 @@ Timestamp LoadTimestamp(const std::uint8_t* p) noexcept } // namespace -bool GptpMessageParser::Parse(const std::uint8_t* payload, - std::size_t payload_len, - PTPMessage& msg) const +bool GptpMessageParser::Parse(const std::uint8_t* payload, std::size_t payload_len, PTPMessage& msg) const { if (payload == nullptr || payload_len < sizeof(PTPHeader)) return false; - msg.ptpHdr.tsmt = payload[0]; - msg.ptpHdr.version = payload[1]; - msg.ptpHdr.messageLength = LoadU16(payload + 2); - msg.ptpHdr.domainNumber = payload[4]; - msg.ptpHdr.reserved1 = payload[5]; + msg.ptpHdr.tsmt = payload[0]; + msg.ptpHdr.version = payload[1]; + msg.ptpHdr.messageLength = LoadU16(payload + 2); + msg.ptpHdr.domainNumber = payload[4]; + msg.ptpHdr.reserved1 = payload[5]; std::memcpy(msg.ptpHdr.flagField, payload + 6, 2); msg.ptpHdr.correctionField = static_cast(LoadBe64(payload + 8)); - msg.ptpHdr.reserved2 = LoadU32(payload + 16); + msg.ptpHdr.reserved2 = LoadU32(payload + 16); std::memcpy(msg.ptpHdr.sourcePortIdentity.clockIdentity.id, payload + 20, 8); msg.ptpHdr.sourcePortIdentity.portNumber = LoadU16(payload + 28); - msg.ptpHdr.sequenceId = LoadU16(payload + 30); - msg.ptpHdr.controlField = payload[32]; + msg.ptpHdr.sequenceId = LoadU16(payload + 30); + msg.ptpHdr.controlField = payload[32]; msg.ptpHdr.logMessageInterval = static_cast(payload[33]); msg.msgtype = msg.ptpHdr.tsmt & 0x0FU; @@ -102,8 +100,7 @@ bool GptpMessageParser::Parse(const std::uint8_t* payload, case kPtpMsgtypePdelayRespFollowUp: if (payload_len >= kBodyOffset + sizeof(Timestamp)) - msg.pdelay_resp_fup.responseOriginReceiptTimestamp = - LoadTimestamp(payload + kBodyOffset); + msg.pdelay_resp_fup.responseOriginReceiptTimestamp = LoadTimestamp(payload + kBodyOffset); break; default: diff --git a/score/TimeSlave/code/gptp/details/message_parser.h b/score/TimeSlave/code/gptp/details/message_parser.h index a1bd4c3..904b5c7 100644 --- a/score/TimeSlave/code/gptp/details/message_parser.h +++ b/score/TimeSlave/code/gptp/details/message_parser.h @@ -45,9 +45,7 @@ class GptpMessageParser final * * @return true if the payload contains a valid IEEE 1588 / 802.1AS header. */ - bool Parse(const std::uint8_t* payload, - std::size_t payload_len, - PTPMessage& msg) const; + bool Parse(const std::uint8_t* payload, std::size_t payload_len, PTPMessage& msg) const; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/details/message_parser_test.cpp b/score/TimeSlave/code/gptp/details/message_parser_test.cpp index d5ed88a..f42afdc 100644 --- a/score/TimeSlave/code/gptp/details/message_parser_test.cpp +++ b/score/TimeSlave/code/gptp/details/message_parser_test.cpp @@ -29,9 +29,9 @@ namespace { // PTP header occupies exactly 34 bytes on the wire. -constexpr std::size_t kHdrSize = 34U; +constexpr std::size_t kHdrSize = 34U; // Timestamp body = 10 bytes (u16 + u32 + u32). -constexpr std::size_t kTsSize = 10U; +constexpr std::size_t kTsSize = 10U; // Store a 16-bit big-endian value at buf[off]. void PutU16Be(std::uint8_t* buf, std::size_t off, std::uint16_t val) @@ -58,13 +58,13 @@ void PutU64Be(std::uint8_t* buf, std::size_t off, std::uint64_t val) // Build a minimal PTP payload of type `msgtype` with the given header fields. // Optionally appends a 10-byte Timestamp body (seconds_lsb + nanoseconds). -std::vector BuildPayload(std::uint8_t msgtype, +std::vector BuildPayload(std::uint8_t msgtype, std::uint16_t seqId, - std::int64_t correction = 0, + std::int64_t correction = 0, std::uint16_t port_number = 0, - std::uint64_t clock_id = 0, - std::uint32_t ts_sec_lsb = 0, - std::uint32_t ts_ns = 0) + std::uint64_t clock_id = 0, + std::uint32_t ts_sec_lsb = 0, + std::uint32_t ts_ns = 0) { const std::size_t total = kHdrSize + kTsSize; std::vector buf(total, 0); @@ -81,7 +81,7 @@ std::vector BuildPayload(std::uint8_t msgtype, buf[32] = kCtlFollowUp; // Timestamp body at offset 34: seconds_msb(u16) + seconds_lsb(u32) + nanoseconds(u32) - PutU16Be(buf.data(), kHdrSize, 0U); // seconds_msb = 0 + PutU16Be(buf.data(), kHdrSize, 0U); // seconds_msb = 0 PutU32Be(buf.data(), kHdrSize + 2, ts_sec_lsb); PutU32Be(buf.data(), kHdrSize + 6, ts_ns); @@ -143,7 +143,7 @@ TEST_F(MessageParserTest, Header_CorrectionField_DecodedCorrectly) TEST_F(MessageParserTest, Header_SourcePortIdentity_DecodedCorrectly) { const std::uint64_t kClockId = 0xCAFEBABEDEAD0001ULL; - const std::uint16_t kPort = 3U; + const std::uint16_t kPort = 3U; auto buf = BuildPayload(kPtpMsgtypeSync, 1U, 0, kPort, kClockId); PTPMessage msg{}; ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); @@ -157,7 +157,7 @@ TEST_F(MessageParserTest, FollowUp_Body_TimestampDecodedCorrectly) { // precise_origin = 2 seconds + 500_000_000 ns const std::uint32_t kSecLsb = 2U; - const std::uint32_t kNs = 500'000'000U; + const std::uint32_t kNs = 500'000'000U; auto buf = BuildPayload(kPtpMsgtypeFollowUp, 99U, 0, 0, 0, kSecLsb, kNs); PTPMessage msg{}; ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); @@ -171,7 +171,7 @@ TEST_F(MessageParserTest, FollowUp_Body_TimestampDecodedCorrectly) TEST_F(MessageParserTest, PdelayResp_Body_TimestampDecodedCorrectly) { const std::uint32_t kSecLsb = 3U; - const std::uint32_t kNs = 123'456'789U; + const std::uint32_t kNs = 123'456'789U; auto buf = BuildPayload(kPtpMsgtypePdelayResp, 5U, 0, 0, 0, kSecLsb, kNs); PTPMessage msg{}; ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); @@ -185,7 +185,7 @@ TEST_F(MessageParserTest, PdelayResp_Body_TimestampDecodedCorrectly) TEST_F(MessageParserTest, PdelayRespFollowUp_Body_TimestampDecodedCorrectly) { const std::uint32_t kSecLsb = 7U; - const std::uint32_t kNs = 999'000'000U; + const std::uint32_t kNs = 999'000'000U; auto buf = BuildPayload(kPtpMsgtypePdelayRespFollowUp, 11U, 0, 0, 0, kSecLsb, kNs); PTPMessage msg{}; ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); diff --git a/score/TimeSlave/code/gptp/details/network_identity.h b/score/TimeSlave/code/gptp/details/network_identity.h index b334488..3150bc0 100644 --- a/score/TimeSlave/code/gptp/details/network_identity.h +++ b/score/TimeSlave/code/gptp/details/network_identity.h @@ -40,7 +40,10 @@ class NetworkIdentity : public INetworkIdentity bool Resolve(const std::string& iface_name) override; /// Return the resolved identity. Valid only after a successful Resolve(). - ClockIdentity GetClockIdentity() const override { return identity_; } + ClockIdentity GetClockIdentity() const override + { + return identity_; + } private: ClockIdentity identity_{}; diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp index 9f5f596..c13eae6 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -23,34 +23,30 @@ namespace ts namespace details { -PeerDelayMeasurer::PeerDelayMeasurer( - const ClockIdentity& local_identity) noexcept - : local_identity_{local_identity} -{ -} +PeerDelayMeasurer::PeerDelayMeasurer(const ClockIdentity& local_identity) noexcept : local_identity_{local_identity} {} int PeerDelayMeasurer::SendRequest(IRawSocket& socket) { PTPMessage req{}; - req.ptpHdr.tsmt = kPtpMsgtypePdelayReq | kPtpTransportSpecific; - req.ptpHdr.version = kPtpVersion; - req.ptpHdr.domainNumber = 0; + req.ptpHdr.tsmt = kPtpMsgtypePdelayReq | kPtpTransportSpecific; + req.ptpHdr.version = kPtpVersion; + req.ptpHdr.domainNumber = 0; req.ptpHdr.messageLength = htons(sizeof(PdelayReqBody)); - req.ptpHdr.flagField[0] = 0; - req.ptpHdr.flagField[1] = 0; + req.ptpHdr.flagField[0] = 0; + req.ptpHdr.flagField[1] = 0; req.ptpHdr.correctionField = 0; - req.ptpHdr.reserved2 = 0; + req.ptpHdr.reserved2 = 0; req.ptpHdr.sourcePortIdentity.clockIdentity = local_identity_; - req.ptpHdr.sourcePortIdentity.portNumber = htons(0x0001U); - req.ptpHdr.sequenceId = htons(static_cast(seqnum_)); - req.ptpHdr.controlField = kCtlOther; + req.ptpHdr.sourcePortIdentity.portNumber = htons(0x0001U); + req.ptpHdr.sequenceId = htons(static_cast(seqnum_)); + req.ptpHdr.controlField = kCtlOther; req.ptpHdr.logMessageInterval = 0x7F; // Save a copy with host-byte-order sequence ID for later matching { std::lock_guard lk(mutex_); - req_ = req; - req_.ptpHdr.sequenceId = static_cast(seqnum_); + req_ = req; + req_.ptpHdr.sequenceId = static_cast(seqnum_); } ++seqnum_; @@ -65,8 +61,7 @@ int PeerDelayMeasurer::SendRequest(IRawSocket& socket) if (r > 0) { std::lock_guard lk(mutex_); - req_.sendHardwareTS = TmvT{ - static_cast(hwts.tv_sec) * kNsPerSec + hwts.tv_nsec}; + req_.sendHardwareTS = TmvT{static_cast(hwts.tv_sec) * kNsPerSec + hwts.tv_nsec}; } return r; } @@ -101,9 +96,9 @@ void PeerDelayMeasurer::ComputeAndStore() noexcept // t2 = remote receipt time (from Pdelay_Resp body: requestReceiptTimestamp) const TmvT t2 = resp_.parseMessageTs; // t3 = remote send time (from Pdelay_Resp_FUP body) + corrections - const TmvT t3 = resp_fup_.parseMessageTs; - const TmvT c1 = CorrectionToTmv(resp_.ptpHdr.correctionField); - const TmvT c2 = CorrectionToTmv(resp_fup_.ptpHdr.correctionField); + const TmvT t3 = resp_fup_.parseMessageTs; + const TmvT c1 = CorrectionToTmv(resp_.ptpHdr.correctionField); + const TmvT c2 = CorrectionToTmv(resp_fup_.ptpHdr.correctionField); const TmvT t3c = TmvT{t3.ns + c1.ns + c2.ns}; // t4 = local HW receive timestamp of Pdelay_Resp const TmvT t4 = resp_.recvHardwareTS; @@ -112,25 +107,21 @@ void PeerDelayMeasurer::ComputeAndStore() noexcept PDelayResult r{}; r.path_delay_ns = delay; - r.valid = true; - - score::td::PDelayData& d = r.pdelay_data; - d.request_origin_timestamp = static_cast(t1.ns); - d.request_receipt_timestamp = static_cast(t2.ns); - d.response_origin_timestamp = static_cast(t3.ns); - d.response_receipt_timestamp = static_cast(t4.ns); - d.reference_global_timestamp = static_cast(t3c.ns); - d.reference_local_timestamp = static_cast(t4.ns); - d.sequence_id = resp_.ptpHdr.sequenceId; - d.pdelay = static_cast(delay); - d.req_port_number = - req_.ptpHdr.sourcePortIdentity.portNumber; - d.req_clock_identity = - ClockIdentityToU64(req_.ptpHdr.sourcePortIdentity.clockIdentity); - d.resp_port_number = - resp_.ptpHdr.sourcePortIdentity.portNumber; - d.resp_clock_identity = - ClockIdentityToU64(resp_.ptpHdr.sourcePortIdentity.clockIdentity); + r.valid = true; + + score::td::PDelayData& d = r.pdelay_data; + d.request_origin_timestamp = static_cast(t1.ns); + d.request_receipt_timestamp = static_cast(t2.ns); + d.response_origin_timestamp = static_cast(t3.ns); + d.response_receipt_timestamp = static_cast(t4.ns); + d.reference_global_timestamp = static_cast(t3c.ns); + d.reference_local_timestamp = static_cast(t4.ns); + d.sequence_id = resp_.ptpHdr.sequenceId; + d.pdelay = static_cast(delay); + d.req_port_number = req_.ptpHdr.sourcePortIdentity.portNumber; + d.req_clock_identity = ClockIdentityToU64(req_.ptpHdr.sourcePortIdentity.clockIdentity); + d.resp_port_number = resp_.ptpHdr.sourcePortIdentity.portNumber; + d.resp_clock_identity = ClockIdentityToU64(resp_.ptpHdr.sourcePortIdentity.clockIdentity); result_ = r; } diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.h b/score/TimeSlave/code/gptp/details/pdelay_measurer.h index f8ce97b..981f3bb 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.h +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.h @@ -13,9 +13,9 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" #include "score/TimeSlave/code/gptp/details/ptp_types.h" -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" #include #include @@ -30,9 +30,9 @@ namespace details /// Result produced by a completed Pdelay measurement cycle. struct PDelayResult { - std::int64_t path_delay_ns{0}; + std::int64_t path_delay_ns{0}; score::td::PDelayData pdelay_data{}; - bool valid{false}; + bool valid{false}; }; /** @@ -71,11 +71,11 @@ class PeerDelayMeasurer final mutable std::mutex mutex_; - int seqnum_{0}; - PTPMessage req_{}; - PTPMessage resp_{}; - PTPMessage resp_fup_{}; - PDelayResult result_{}; + int seqnum_{0}; + PTPMessage req_{}; + PTPMessage resp_{}; + PTPMessage resp_fup_{}; + PDelayResult result_{}; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp index 21d37a3..f0362f3 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp @@ -28,15 +28,15 @@ namespace // seqId must be 0 to match the default-constructed req_ inside PeerDelayMeasurer // (req_.ptpHdr.sequenceId == 0 before SendRequest is ever called). PTPMessage MakeResp(std::uint16_t seqId, - std::int64_t parse_ts_ns, // t2 or t3 - std::int64_t recv_hw_ns = 0, // t4 (only used in Resp, not FUP) - std::int64_t corr_ns = 0) noexcept + std::int64_t parse_ts_ns, // t2 or t3 + std::int64_t recv_hw_ns = 0, // t4 (only used in Resp, not FUP) + std::int64_t corr_ns = 0) noexcept { PTPMessage msg{}; - msg.ptpHdr.sequenceId = seqId; + msg.ptpHdr.sequenceId = seqId; msg.ptpHdr.correctionField = corr_ns << 16; // CorrectionToTmv does >> 16 - msg.parseMessageTs.ns = parse_ts_ns; - msg.recvHardwareTS.ns = recv_hw_ns; + msg.parseMessageTs.ns = parse_ts_ns; + msg.recvHardwareTS.ns = recv_hw_ns; return msg; } @@ -135,11 +135,11 @@ TEST_F(PeerDelayMeasurerTest, PDelayData_TimestampFields_PopulatedCorrectly) measurer_.OnResponseFollowUp(MakeResp(0U, /*t3=*/80LL)); const score::td::PDelayData& d = measurer_.GetResult().pdelay_data; - EXPECT_EQ(d.request_origin_timestamp, 0ULL); // t1 - EXPECT_EQ(d.request_receipt_timestamp, 100ULL); // t2 - EXPECT_EQ(d.response_origin_timestamp, 80ULL); // t3 + EXPECT_EQ(d.request_origin_timestamp, 0ULL); // t1 + EXPECT_EQ(d.request_receipt_timestamp, 100ULL); // t2 + EXPECT_EQ(d.response_origin_timestamp, 80ULL); // t3 EXPECT_EQ(d.response_receipt_timestamp, 180ULL); // t4 - EXPECT_EQ(d.pdelay, 100ULL); // computed delay + EXPECT_EQ(d.pdelay, 100ULL); // computed delay } // ── Multiple cycles: result updated on each valid completion ────────────────── diff --git a/score/TimeSlave/code/gptp/details/ptp_types.h b/score/TimeSlave/code/gptp/details/ptp_types.h index 2828c16..187bbec 100644 --- a/score/TimeSlave/code/gptp/details/ptp_types.h +++ b/score/TimeSlave/code/gptp/details/ptp_types.h @@ -13,9 +13,9 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PTP_TYPES_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PTP_TYPES_H +#include #include #include -#include #ifndef _QNX_PLAT #include @@ -25,7 +25,7 @@ struct ethhdr { unsigned char h_dest[6]; unsigned char h_source[6]; - uint16_t h_proto; + uint16_t h_proto; }; #endif @@ -41,23 +41,23 @@ namespace details { // ─── EtherType constants ──────────────────────────────────────────────────── -constexpr int kEthP1588 = 0x88F7; +constexpr int kEthP1588 = 0x88F7; constexpr int kEthP8021Q = 0x8100; // ─── MAC / buffer sizes ───────────────────────────────────────────────────── -constexpr int kMacAddrLen = 6; -constexpr int kVlanTagLen = 4; +constexpr int kMacAddrLen = 6; +constexpr int kVlanTagLen = 4; // ─── PTP message-type codes ───────────────────────────────────────────────── -constexpr std::uint8_t kPtpMsgtypeSync = 0x0; -constexpr std::uint8_t kPtpMsgtypePdelayReq = 0x2; -constexpr std::uint8_t kPtpMsgtypePdelayResp = 0x3; -constexpr std::uint8_t kPtpMsgtypeFollowUp = 0x8; +constexpr std::uint8_t kPtpMsgtypeSync = 0x0; +constexpr std::uint8_t kPtpMsgtypePdelayReq = 0x2; +constexpr std::uint8_t kPtpMsgtypePdelayResp = 0x3; +constexpr std::uint8_t kPtpMsgtypeFollowUp = 0x8; constexpr std::uint8_t kPtpMsgtypePdelayRespFollowUp = 0xA; // ─── PTP header constants ──────────────────────────────────────────────────── constexpr std::uint8_t kPtpTransportSpecific = (1U << 4U); -constexpr std::uint8_t kPtpVersion = 2U; +constexpr std::uint8_t kPtpVersion = 2U; constexpr std::int64_t kNsPerSec = 1'000'000'000LL; @@ -68,12 +68,12 @@ constexpr const char* kPtpDstMac = "01:80:C2:00:00:0E"; // ─── Control field ─────────────────────────────────────────────────────────── enum ControlField : std::uint8_t { - kCtlSync = 0, + kCtlSync = 0, kCtlDelayReq = 1, kCtlFollowUp = 2, kCtlDelayResp = 3, kCtlManagement = 4, - kCtlOther = 5 + kCtlOther = 5 }; // ─── State machine states ──────────────────────────────────────────────────── @@ -111,18 +111,18 @@ struct PACKED Timestamp struct PACKED PTPHeader { - std::uint8_t tsmt{0}; - std::uint8_t version{0}; - std::uint16_t messageLength{0}; - std::uint8_t domainNumber{0}; - std::uint8_t reserved1{0}; - std::uint8_t flagField[2]{}; - std::int64_t correctionField{0}; - std::uint32_t reserved2{0}; - PortIdentity sourcePortIdentity{}; - std::uint16_t sequenceId{0}; - std::uint8_t controlField{0}; - std::int8_t logMessageInterval{0}; + std::uint8_t tsmt{0}; + std::uint8_t version{0}; + std::uint16_t messageLength{0}; + std::uint8_t domainNumber{0}; + std::uint8_t reserved1{0}; + std::uint8_t flagField[2]{}; + std::int64_t correctionField{0}; + std::uint32_t reserved2{0}; + PortIdentity sourcePortIdentity{}; + std::uint16_t sequenceId{0}; + std::uint8_t controlField{0}; + std::int8_t logMessageInterval{0}; }; struct PACKED SyncBody @@ -139,22 +139,22 @@ struct PACKED FollowUpBody struct PACKED PdelayReqBody { - PTPHeader ptpHdr{}; - Timestamp requestReceiptTimestamp{}; + PTPHeader ptpHdr{}; + Timestamp requestReceiptTimestamp{}; PortIdentity reserved{}; }; struct PACKED PdelayRespBody { - PTPHeader ptpHdr{}; - Timestamp responseOriginTimestamp{}; + PTPHeader ptpHdr{}; + Timestamp responseOriginTimestamp{}; PortIdentity requestingPortIdentity{}; }; struct PACKED PdelayRespFollowUpBody { - PTPHeader ptpHdr{}; - Timestamp responseOriginReceiptTimestamp{}; + PTPHeader ptpHdr{}; + Timestamp responseOriginReceiptTimestamp{}; PortIdentity requestingPortIdentity{}; }; @@ -167,19 +167,19 @@ struct PTPMessage { union PACKED { - PTPHeader ptpHdr; - SyncBody sync; - FollowUpBody follow_up; - PdelayReqBody pdelay_req; - PdelayRespBody pdelay_resp; + PTPHeader ptpHdr; + SyncBody sync; + FollowUpBody follow_up; + PdelayReqBody pdelay_req; + PdelayRespBody pdelay_resp; PdelayRespFollowUpBody pdelay_resp_fup; - RawMessageData data; + RawMessageData data; }; std::uint8_t msgtype{0}; - TmvT sendHardwareTS{}; - TmvT parseMessageTs{}; - TmvT recvHardwareTS{}; + TmvT sendHardwareTS{}; + TmvT parseMessageTs{}; + TmvT recvHardwareTS{}; }; static_assert(sizeof(PTPMessage) <= 1600, "PTPMessage too large"); @@ -187,16 +187,15 @@ static_assert(sizeof(PTPMessage) <= 1600, "PTPMessage too large"); // ─── Timestamp conversion helpers ──────────────────────────────────────────── inline TmvT TimestampToTmv(const Timestamp& ts) noexcept { - const std::uint64_t sec = (static_cast(ts.seconds_msb) << 32U) | - static_cast(ts.seconds_lsb); - return TmvT{static_cast(sec * static_cast(kNsPerSec) + - ts.nanoseconds)}; + const std::uint64_t sec = + (static_cast(ts.seconds_msb) << 32U) | static_cast(ts.seconds_lsb); + return TmvT{static_cast(sec * static_cast(kNsPerSec) + ts.nanoseconds)}; } inline Timestamp TmvToTimestamp(const TmvT& x) noexcept { Timestamp t{}; - const std::uint64_t sec = static_cast(x.ns) / 1'000'000'000ULL; + const std::uint64_t sec = static_cast(x.ns) / 1'000'000'000ULL; const std::uint64_t nsec = static_cast(x.ns) % 1'000'000'000ULL; t.seconds_lsb = static_cast(sec & 0xFFFFFFFFULL); t.seconds_msb = static_cast((sec >> 32U) & 0xFFFFULL); diff --git a/score/TimeSlave/code/gptp/details/raw_socket.h b/score/TimeSlave/code/gptp/details/raw_socket.h index 36ea437..b0be138 100644 --- a/score/TimeSlave/code/gptp/details/raw_socket.h +++ b/score/TimeSlave/code/gptp/details/raw_socket.h @@ -15,11 +15,11 @@ #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" +#include #include #include #include #include -#include namespace score { @@ -40,10 +40,10 @@ class RawSocket : public IRawSocket RawSocket() noexcept = default; ~RawSocket() override; - RawSocket(const RawSocket&) = delete; + RawSocket(const RawSocket&) = delete; RawSocket& operator=(const RawSocket&) = delete; - RawSocket(RawSocket&&) = delete; - RawSocket& operator=(RawSocket&&) = delete; + RawSocket(RawSocket&&) = delete; + RawSocket& operator=(RawSocket&&) = delete; /// Open the socket bound to @p iface. Returns false on failure. bool Open(const std::string& iface) override; @@ -62,8 +62,7 @@ class RawSocket : public IRawSocket /// @param hwts Output: hardware receive timestamp (zeroed if unavailable). /// @param timeout_ms <0 block indefinitely, 0 non-blocking, >0 timeout in ms. /// @return Number of bytes received, 0 on timeout, -1 on error. - int Recv(std::uint8_t* buf, std::size_t buf_len, - ::timespec& hwts, int timeout_ms) override; + int Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) override; /// Send one frame. /// @@ -74,10 +73,13 @@ class RawSocket : public IRawSocket int Send(const void* buf, int len, ::timespec& hwts) override; /// Return the underlying file descriptor (for advanced use / polling). - int GetFd() const override { return fd_; } + int GetFd() const override + { + return fd_; + } private: - int fd_{-1}; + int fd_{-1}; std::string iface_{}; }; diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp index d97afc8..8c89af2 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp @@ -33,8 +33,7 @@ std::int64_t MonoNs() noexcept } // namespace -SyncStateMachine::SyncStateMachine( - std::int64_t jump_future_threshold_ns) noexcept +SyncStateMachine::SyncStateMachine(std::int64_t jump_future_threshold_ns) noexcept : jump_future_threshold_ns_{jump_future_threshold_ns} { } @@ -45,7 +44,7 @@ void SyncStateMachine::OnSync(const PTPMessage& msg) { case SyncState::kEmpty: last_sync_ = msg; - state_ = SyncState::kHaveSync; + state_ = SyncState::kHaveSync; break; case SyncState::kHaveSync: @@ -56,20 +55,19 @@ void SyncStateMachine::OnSync(const PTPMessage& msg) case SyncState::kHaveFup: // Buffered FUP is now stale; start fresh with the new Sync last_sync_ = msg; - state_ = SyncState::kHaveSync; + state_ = SyncState::kHaveSync; break; } } -std::optional SyncStateMachine::OnFollowUp( - const PTPMessage& msg) +std::optional SyncStateMachine::OnFollowUp(const PTPMessage& msg) { switch (state_) { case SyncState::kEmpty: // FUP arrived before its Sync — buffer it and wait last_fup_ = msg; - state_ = SyncState::kHaveFup; + state_ = SyncState::kHaveFup; return std::nullopt; case SyncState::kHaveFup: @@ -82,7 +80,7 @@ std::optional SyncStateMachine::OnFollowUp( { // Sequence-ID mismatch: buffer the FUP and wait for matching Sync last_fup_ = msg; - state_ = SyncState::kHaveFup; + state_ = SyncState::kHaveFup; return std::nullopt; } @@ -96,24 +94,21 @@ std::optional SyncStateMachine::OnFollowUp( return std::nullopt; } -bool SyncStateMachine::IsTimeout(std::int64_t mono_now_ns, - std::int64_t timeout_ns) const +bool SyncStateMachine::IsTimeout(std::int64_t mono_now_ns, std::int64_t timeout_ns) const { if (timeout_ns <= 0) return false; const std::int64_t last = last_sync_mono_ns_.load(std::memory_order_acquire); if (last == 0) - return false; // never synchronized yet — not a "timeout" + return false; // never synchronized yet — not a "timeout" return (mono_now_ns - last) > timeout_ns; } -SyncResult SyncStateMachine::BuildResult( - const PTPMessage& sync, - const PTPMessage& fup) noexcept +SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessage& fup) noexcept { const TmvT sync_corr = CorrectionToTmv(sync.ptpHdr.correctionField); - const TmvT fup_corr = CorrectionToTmv(fup.ptpHdr.correctionField); - const TmvT fup_ts = TimestampToTmv(fup.follow_up.preciseOriginTimestamp); + const TmvT fup_corr = CorrectionToTmv(fup.ptpHdr.correctionField); + const TmvT fup_ts = TimestampToTmv(fup.follow_up.preciseOriginTimestamp); const std::int64_t master_ns = fup_ts.ns + sync_corr.ns + fup_corr.ns; const std::int64_t offset_ns = sync.recvHardwareTS.ns - master_ns; @@ -131,35 +126,28 @@ SyncResult SyncStateMachine::BuildResult( r.is_time_jump_future = true; } - score::td::SyncFupData& d = r.sync_fup_data; - d.precise_origin_timestamp = - static_cast(fup_ts.ns); - d.reference_global_timestamp = - static_cast(master_ns); - d.reference_local_timestamp = - static_cast(sync.recvHardwareTS.ns); - d.sync_ingress_timestamp = - static_cast(sync.recvHardwareTS.ns); - d.correction_field = - static_cast(sync.ptpHdr.correctionField); - d.sequence_id = fup.ptpHdr.sequenceId; - d.pdelay = 0U; // filled by GptpEngine from IPeerDelayMeasurer - d.port_number = sync.ptpHdr.sourcePortIdentity.portNumber; - d.clock_identity = - ClockIdentityToU64(sync.ptpHdr.sourcePortIdentity.clockIdentity); + score::td::SyncFupData& d = r.sync_fup_data; + d.precise_origin_timestamp = static_cast(fup_ts.ns); + d.reference_global_timestamp = static_cast(master_ns); + d.reference_local_timestamp = static_cast(sync.recvHardwareTS.ns); + d.sync_ingress_timestamp = static_cast(sync.recvHardwareTS.ns); + d.correction_field = static_cast(sync.ptpHdr.correctionField); + d.sequence_id = fup.ptpHdr.sequenceId; + d.pdelay = 0U; // filled by GptpEngine from IPeerDelayMeasurer + d.port_number = sync.ptpHdr.sourcePortIdentity.portNumber; + d.clock_identity = ClockIdentityToU64(sync.ptpHdr.sourcePortIdentity.clockIdentity); // IEEE 802.1AS Clause 11.4.1 if (prev_slave_rx_ns_ != 0 && prev_master_origin_ns_ != 0) { - const std::int64_t slave_interval = sync.recvHardwareTS.ns - prev_slave_rx_ns_; + const std::int64_t slave_interval = sync.recvHardwareTS.ns - prev_slave_rx_ns_; const std::int64_t master_interval = master_ns - prev_master_origin_ns_; if (master_interval > 0) { - neighbor_rate_ratio_ = - static_cast(slave_interval) / static_cast(master_interval); + neighbor_rate_ratio_ = static_cast(slave_interval) / static_cast(master_interval); } } - prev_slave_rx_ns_ = sync.recvHardwareTS.ns; + prev_slave_rx_ns_ = sync.recvHardwareTS.ns; prev_master_origin_ns_ = master_ns; last_master_ns_ = master_ns; diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.h b/score/TimeSlave/code/gptp/details/sync_state_machine.h index 73340b1..abc657f 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.h +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.h @@ -13,8 +13,8 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H -#include "score/TimeSlave/code/gptp/details/ptp_types.h" #include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/TimeSlave/code/gptp/details/ptp_types.h" #include #include @@ -30,11 +30,11 @@ namespace details /// Output produced by a successful Sync+FollowUp pairing. struct SyncResult { - std::int64_t master_ns{0}; ///< Grandmaster time (ns since epoch) - std::int64_t offset_ns{0}; ///< local hw_ts − master_ns - score::td::SyncFupData sync_fup_data{}; ///< Ready to copy into PtpTimeInfo (pdelay field filled by engine) - bool is_time_jump_future{false}; - bool is_time_jump_past{false}; + std::int64_t master_ns{0}; ///< Grandmaster time (ns since epoch) + std::int64_t offset_ns{0}; ///< local hw_ts − master_ns + score::td::SyncFupData sync_fup_data{}; ///< Ready to copy into PtpTimeInfo (pdelay field filled by engine) + bool is_time_jump_future{false}; + bool is_time_jump_past{false}; }; /** @@ -54,8 +54,7 @@ class SyncStateMachine final public: /// @param jump_future_threshold_ns Offset delta above which the state is /// flagged as a future time jump. Set to 0 to disable detection. - explicit SyncStateMachine( - std::int64_t jump_future_threshold_ns = 500'000'000LL) noexcept; + explicit SyncStateMachine(std::int64_t jump_future_threshold_ns = 500'000'000LL) noexcept; /// Called when a Sync message is received (with its HW receive timestamp /// already stored in @p msg.recvHardwareTS). @@ -67,26 +66,27 @@ class SyncStateMachine final /// @return true if no valid Sync+FUP has been received for longer than /// @p timeout_ns nanoseconds (monotonic). - bool IsTimeout(std::int64_t mono_now_ns, - std::int64_t timeout_ns) const; + bool IsTimeout(std::int64_t mono_now_ns, std::int64_t timeout_ns) const; /// @return The latest computed neighborRateRatio (1.0 until first pair). - double GetNeighborRateRatio() const { return neighbor_rate_ratio_; } + double GetNeighborRateRatio() const + { + return neighbor_rate_ratio_; + } private: - SyncResult BuildResult(const PTPMessage& sync, - const PTPMessage& fup) noexcept; + SyncResult BuildResult(const PTPMessage& sync, const PTPMessage& fup) noexcept; - SyncState state_{SyncState::kEmpty}; - PTPMessage last_sync_{}; - PTPMessage last_fup_{}; + SyncState state_{SyncState::kEmpty}; + PTPMessage last_sync_{}; + PTPMessage last_fup_{}; std::int64_t last_master_ns_{0}; std::int64_t jump_future_threshold_ns_; // neighborRateRatio computation (IEEE 802.1AS Clause 11.4.1) std::int64_t prev_slave_rx_ns_{0}; std::int64_t prev_master_origin_ns_{0}; - double neighbor_rate_ratio_{1.0}; + double neighbor_rate_ratio_{1.0}; /// Monotonic timestamp of the last successful Sync+FUP pair (ns). /// Atomic so that IsTimeout() can be called from a different thread. diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp index 1b78754..8b3b7bd 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp @@ -29,27 +29,23 @@ namespace // Build a Sync PTPMessage with the given sequence ID and hardware RX timestamp. // The correctionField encodes correction in sub-ns units (<<16 so >>16 == 0). -PTPMessage MakeSync(std::uint16_t seqId, - std::int64_t recv_hw_ns, - std::int64_t corr_ns = 0LL) noexcept +PTPMessage MakeSync(std::uint16_t seqId, std::int64_t recv_hw_ns, std::int64_t corr_ns = 0LL) noexcept { PTPMessage msg{}; - msg.msgtype = kPtpMsgtypeSync; - msg.ptpHdr.sequenceId = seqId; - msg.ptpHdr.correctionField = corr_ns << 16; // CorrectionToTmv does >> 16 - msg.recvHardwareTS.ns = recv_hw_ns; + msg.msgtype = kPtpMsgtypeSync; + msg.ptpHdr.sequenceId = seqId; + msg.ptpHdr.correctionField = corr_ns << 16; // CorrectionToTmv does >> 16 + msg.recvHardwareTS.ns = recv_hw_ns; return msg; } // Build a FollowUp PTPMessage with the given sequence ID and precise origin // timestamp (in nanoseconds since epoch). -PTPMessage MakeFollowUp(std::uint16_t seqId, - std::int64_t origin_ns, - std::int64_t corr_ns = 0LL) noexcept +PTPMessage MakeFollowUp(std::uint16_t seqId, std::int64_t origin_ns, std::int64_t corr_ns = 0LL) noexcept { PTPMessage msg{}; - msg.msgtype = kPtpMsgtypeFollowUp; - msg.ptpHdr.sequenceId = seqId; + msg.msgtype = kPtpMsgtypeFollowUp; + msg.ptpHdr.sequenceId = seqId; msg.ptpHdr.correctionField = corr_ns << 16; // Encode origin_ns into the preciseOriginTimestamp wire field. msg.follow_up.preciseOriginTimestamp = TmvToTimestamp(TmvT{origin_ns}); @@ -58,10 +54,7 @@ PTPMessage MakeFollowUp(std::uint16_t seqId, // Helper: deliver a matching Sync+FollowUp pair and return the SyncResult. // Aborts the test if the pair does not produce a result. -SyncResult DeliverPair(SyncStateMachine& ssm, - std::uint16_t seqId, - std::int64_t recv_hw_ns, - std::int64_t origin_ns) +SyncResult DeliverPair(SyncStateMachine& ssm, std::uint16_t seqId, std::int64_t recv_hw_ns, std::int64_t origin_ns) { ssm.OnSync(MakeSync(seqId, recv_hw_ns)); auto result = ssm.OnFollowUp(MakeFollowUp(seqId, origin_ns)); @@ -141,8 +134,7 @@ TEST_F(SyncStateMachineTest, SyncFupData_PreciseOriginTimestamp_MatchesInput) ssm_.OnSync(MakeSync(1U, 6'000'000'000LL)); auto result = ssm_.OnFollowUp(MakeFollowUp(1U, kOrigin)); ASSERT_TRUE(result.has_value()); - EXPECT_EQ(static_cast(result->sync_fup_data.precise_origin_timestamp), - kOrigin); + EXPECT_EQ(static_cast(result->sync_fup_data.precise_origin_timestamp), kOrigin); } // ── Jump detection ──────────────────────────────────────────────────────────── @@ -153,8 +145,7 @@ TEST_F(SyncStateMachineTest, JumpPast_Detected_OnSecondPair) DeliverPair(ssm_, 1U, 2'100'000'000LL, 2'000'000'000LL); // Second pair: master_ns goes backward → is_time_jump_past - auto result = ssm_.OnFollowUp( - MakeFollowUp(2U, 1'000'000'000LL)); // no Sync preceding this on new seqId + auto result = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); // no Sync preceding this on new seqId ssm_.OnSync(MakeSync(2U, 3'000'000'000LL)); auto r2 = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); @@ -218,8 +209,7 @@ TEST_F(SyncStateMachineTest, IsTimeout_AfterSuccessfulPair_WithLargeNow_ReturnsT { DeliverPair(ssm_, 1U, 1'000'000'000LL, 900'000'000LL); // Provide a mono_now far in the future; timeout = 1 s - EXPECT_TRUE(ssm_.IsTimeout(std::numeric_limits::max(), - 1'000'000'000LL)); + EXPECT_TRUE(ssm_.IsTimeout(std::numeric_limits::max(), 1'000'000'000LL)); } TEST_F(SyncStateMachineTest, IsTimeout_AfterSuccessfulPair_WithSmallDelta_ReturnsFalse) diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp index c827962..18fd0e7 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -14,11 +14,11 @@ #include "score/TimeSlave/code/gptp/details/network_identity.h" #include "score/TimeSlave/code/gptp/details/raw_socket.h" -#include "score/mw/log/logging.h" #include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/mw/log/logging.h" -#include #include +#include namespace score { @@ -30,8 +30,8 @@ namespace details namespace { -constexpr int kRxTimeoutMs = 100; // poll timeout; keeps RxLoop responsive to shutdown -constexpr int kRxBufferSize = 2048; +constexpr int kRxTimeoutMs = 100; // poll timeout; keeps RxLoop responsive to shutdown +constexpr int kRxBufferSize = 2048; std::int64_t MonoNs() noexcept { @@ -42,9 +42,8 @@ std::int64_t MonoNs() noexcept } // namespace -GptpEngine::GptpEngine( - GptpEngineOptions opts, - std::unique_ptr local_clock) noexcept +GptpEngine::GptpEngine(GptpEngineOptions opts, + std::unique_ptr local_clock) noexcept : opts_{std::move(opts)}, local_clock_{std::move(local_clock)}, socket_{std::make_unique()}, @@ -56,11 +55,10 @@ GptpEngine::GptpEngine( { } -GptpEngine::GptpEngine( - GptpEngineOptions opts, - std::unique_ptr local_clock, - std::unique_ptr socket, - std::unique_ptr identity) noexcept +GptpEngine::GptpEngine(GptpEngineOptions opts, + std::unique_ptr local_clock, + std::unique_ptr socket, + std::unique_ptr identity) noexcept : opts_{std::move(opts)}, local_clock_{std::move(local_clock)}, socket_{std::move(socket)}, @@ -85,13 +83,11 @@ bool GptpEngine::Initialize() if (!identity_->Resolve(opts_.iface_name)) { score::mw::log::LogError(score::td::kGPtpMachineContext) - << "GptpEngine: failed to resolve ClockIdentity for " - << opts_.iface_name; + << "GptpEngine: failed to resolve ClockIdentity for " << opts_.iface_name; return false; } - pdelay_ = std::make_unique( - identity_->GetClockIdentity()); + pdelay_ = std::make_unique(identity_->GetClockIdentity()); if (!socket_->Open(opts_.iface_name)) { @@ -103,16 +99,14 @@ bool GptpEngine::Initialize() if (!socket_->EnableHwTimestamping()) { score::mw::log::LogWarn(score::td::kGPtpMachineContext) - << "GptpEngine: HW timestamping not available on " - << opts_.iface_name << ", falling back to SW timestamps"; + << "GptpEngine: HW timestamping not available on " << opts_.iface_name << ", falling back to SW timestamps"; } running_.store(true, std::memory_order_release); if (::pthread_create(&rx_thread_, nullptr, &RxThreadEntry, this) != 0) { - score::mw::log::LogError(score::td::kGPtpMachineContext) - << "GptpEngine: failed to create RxThread"; + score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create RxThread"; running_.store(false, std::memory_order_release); socket_->Close(); return false; @@ -121,15 +115,13 @@ bool GptpEngine::Initialize() if (::pthread_create(&pdelay_thread_, nullptr, &PdelayThreadEntry, this) != 0) { - score::mw::log::LogError(score::td::kGPtpMachineContext) - << "GptpEngine: failed to create PdelayThread"; + score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread"; (void)Deinitialize(); return false; } pdelay_started_ = true; - score::mw::log::LogInfo(score::td::kGPtpMachineContext) - << "GptpEngine initialized on " << opts_.iface_name; + score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine initialized on " << opts_.iface_name; return true; } @@ -161,8 +153,7 @@ bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) return false; const std::int64_t mono_now = MonoNs(); - const std::int64_t timeout_ns = - static_cast(opts_.sync_timeout_ms) * 1'000'000LL; + const std::int64_t timeout_ns = static_cast(opts_.sync_timeout_ms) * 1'000'000LL; const bool timed_out = sync_sm_.IsTimeout(mono_now, timeout_ns); @@ -171,8 +162,8 @@ bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) if (timed_out) { snapshot_.status.is_synchronized = false; - snapshot_.status.is_timeout = true; - snapshot_.status.is_correct = false; + snapshot_.status.is_timeout = true; + snapshot_.status.is_correct = false; } info = snapshot_; return true; @@ -195,7 +186,7 @@ void* GptpEngine::PdelayThreadEntry(void* arg) noexcept void GptpEngine::RxLoop() noexcept { std::uint8_t buf[kRxBufferSize]; - ::timespec hwts{}; + ::timespec hwts{}; while (running_.load(std::memory_order_acquire)) { @@ -212,18 +203,14 @@ void GptpEngine::PdelayLoop() noexcept ::timespec next{}; ::clock_gettime(CLOCK_MONOTONIC, &next); // Configurable warm-up before first Pdelay_Req (default 2 s) - const std::int64_t warmup_ns = - static_cast(opts_.pdelay_warmup_ms) * 1'000'000LL; + const std::int64_t warmup_ns = static_cast(opts_.pdelay_warmup_ms) * 1'000'000LL; const std::int64_t next_warmup_ns = - static_cast(next.tv_sec) * 1'000'000'000LL + - next.tv_nsec + warmup_ns; - next.tv_sec = static_cast(next_warmup_ns / 1'000'000'000LL); + static_cast(next.tv_sec) * 1'000'000'000LL + next.tv_nsec + warmup_ns; + next.tv_sec = static_cast(next_warmup_ns / 1'000'000'000LL); next.tv_nsec = static_cast(next_warmup_ns % 1'000'000'000LL); const std::int64_t interval_ns = - static_cast( - opts_.pdelay_interval_ms > 0 ? opts_.pdelay_interval_ms : 1000) - * 1'000'000LL; + static_cast(opts_.pdelay_interval_ms > 0 ? opts_.pdelay_interval_ms : 1000) * 1'000'000LL; while (running_.load(std::memory_order_acquire)) { @@ -237,29 +224,26 @@ void GptpEngine::PdelayLoop() noexcept } const std::int64_t next_ns = - static_cast(next.tv_sec) * 1'000'000'000LL + - next.tv_nsec + interval_ns; - next.tv_sec = static_cast(next_ns / 1'000'000'000LL); + static_cast(next.tv_sec) * 1'000'000'000LL + next.tv_nsec + interval_ns; + next.tv_sec = static_cast(next_ns / 1'000'000'000LL); next.tv_nsec = static_cast(next_ns % 1'000'000'000LL); } } -void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, - const ::timespec& hwts) noexcept +void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timespec& hwts) noexcept { int ptp_offset = 0; if (!codec_.ParseEthernetHeader(frame, len, ptp_offset)) return; - const auto* payload = frame + ptp_offset; + const auto* payload = frame + ptp_offset; const std::size_t payload_len = static_cast(len - ptp_offset); PTPMessage msg{}; if (!parser_.Parse(payload, payload_len, msg)) return; - const TmvT hw_ts{ - static_cast(hwts.tv_sec) * 1'000'000'000LL + hwts.tv_nsec}; + const TmvT hw_ts{static_cast(hwts.tv_sec) * 1'000'000'000LL + hwts.tv_nsec}; switch (msg.msgtype) { @@ -279,8 +263,7 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, if (pdr.valid) { result->offset_ns -= pdr.path_delay_ns; - result->sync_fup_data.pdelay = - static_cast(pdr.path_delay_ns); + result->sync_fup_data.pdelay = static_cast(pdr.path_delay_ns); } else { @@ -293,15 +276,13 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, case kPtpMsgtypePdelayResp: msg.recvHardwareTS = hw_ts; - msg.parseMessageTs = - TimestampToTmv(msg.pdelay_resp.responseOriginTimestamp); + msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp.responseOriginTimestamp); if (pdelay_) pdelay_->OnResponse(msg); break; case kPtpMsgtypePdelayRespFollowUp: - msg.parseMessageTs = - TimestampToTmv(msg.pdelay_resp_fup.responseOriginReceiptTimestamp); + msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp_fup.responseOriginReceiptTimestamp); if (pdelay_) pdelay_->OnResponseFollowUp(msg); break; @@ -311,26 +292,23 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, } } -void GptpEngine::UpdateSnapshot(const SyncResult& sync, - const PDelayResult& pdelay) noexcept +void GptpEngine::UpdateSnapshot(const SyncResult& sync, const PDelayResult& pdelay) noexcept { std::lock_guard lk(snapshot_mutex_); - const std::int64_t local_rx_ns = - static_cast(sync.sync_fup_data.reference_local_timestamp); + const std::int64_t local_rx_ns = static_cast(sync.sync_fup_data.reference_local_timestamp); snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; - snapshot_.local_time = local_clock_->Now(); - snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); + snapshot_.local_time = local_clock_->Now(); + snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); - snapshot_.status.is_synchronized = true; - snapshot_.status.is_timeout = false; + snapshot_.status.is_synchronized = true; + snapshot_.status.is_timeout = false; snapshot_.status.is_time_jump_future = sync.is_time_jump_future; - snapshot_.status.is_time_jump_past = sync.is_time_jump_past; - snapshot_.status.is_correct = - !sync.is_time_jump_future && !sync.is_time_jump_past; + snapshot_.status.is_time_jump_past = sync.is_time_jump_past; + snapshot_.status.is_correct = !sync.is_time_jump_future && !sync.is_time_jump_past; snapshot_.sync_fup_data = sync.sync_fup_data; - snapshot_.pdelay_data = pdelay.pdelay_data; + snapshot_.pdelay_data = pdelay.pdelay_data; } } // namespace details diff --git a/score/TimeSlave/code/gptp/gptp_engine.h b/score/TimeSlave/code/gptp/gptp_engine.h index 1c09e9c..9170477 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.h +++ b/score/TimeSlave/code/gptp/gptp_engine.h @@ -22,12 +22,12 @@ #include "score/TimeSlave/code/gptp/details/ptp_types.h" #include "score/TimeSlave/code/gptp/details/sync_state_machine.h" +#include #include #include #include #include #include -#include #include namespace score @@ -40,11 +40,11 @@ namespace details /// Configuration for GptpEngine. struct GptpEngineOptions { - std::string iface_name = "eth0"; ///< Network interface for gPTP - int pdelay_interval_ms = 1000; ///< Period between Pdelay_Req transmissions (ms) - int pdelay_warmup_ms = 2000; ///< Delay before first Pdelay_Req (ms) - int sync_timeout_ms = 3300; ///< Declare timeout after this many ms without Sync - std::int64_t jump_future_threshold_ns = 500'000'000LL; ///< 500 ms + std::string iface_name = "eth0"; ///< Network interface for gPTP + int pdelay_interval_ms = 1000; ///< Period between Pdelay_Req transmissions (ms) + int pdelay_warmup_ms = 2000; ///< Delay before first Pdelay_Req (ms) + int sync_timeout_ms = 3300; ///< Declare timeout after this many ms without Sync + std::int64_t jump_future_threshold_ns = 500'000'000LL; ///< 500 ms }; /** @@ -58,23 +58,21 @@ struct GptpEngineOptions class GptpEngine final { public: - explicit GptpEngine( - GptpEngineOptions opts, - std::unique_ptr local_clock) noexcept; + explicit GptpEngine(GptpEngineOptions opts, + std::unique_ptr local_clock) noexcept; /// Constructor for testing: inject fake socket and identity. - GptpEngine( - GptpEngineOptions opts, - std::unique_ptr local_clock, - std::unique_ptr socket, - std::unique_ptr identity) noexcept; + GptpEngine(GptpEngineOptions opts, + std::unique_ptr local_clock, + std::unique_ptr socket, + std::unique_ptr identity) noexcept; ~GptpEngine() noexcept; - GptpEngine(const GptpEngine&) = delete; + GptpEngine(const GptpEngine&) = delete; GptpEngine& operator=(const GptpEngine&) = delete; - GptpEngine(GptpEngine&&) = delete; - GptpEngine& operator=(GptpEngine&&) = delete; + GptpEngine(GptpEngine&&) = delete; + GptpEngine& operator=(GptpEngine&&) = delete; /// Open the raw socket, enable HW timestamping, resolve the ClockIdentity, /// and start the Rx and Pdelay background threads. @@ -95,29 +93,27 @@ class GptpEngine final void RxLoop() noexcept; void PdelayLoop() noexcept; - void HandlePacket(const std::uint8_t* frame, int len, - const ::timespec& hwts) noexcept; - void UpdateSnapshot(const SyncResult& sync, - const PDelayResult& pdelay) noexcept; + void HandlePacket(const std::uint8_t* frame, int len, const ::timespec& hwts) noexcept; + void UpdateSnapshot(const SyncResult& sync, const PDelayResult& pdelay) noexcept; GptpEngineOptions opts_; std::unique_ptr local_clock_; - std::unique_ptr socket_; - std::unique_ptr identity_; - FrameCodec codec_; - GptpMessageParser parser_; - SyncStateMachine sync_sm_; - std::unique_ptr pdelay_; + std::unique_ptr socket_; + std::unique_ptr identity_; + FrameCodec codec_; + GptpMessageParser parser_; + SyncStateMachine sync_sm_; + std::unique_ptr pdelay_; mutable std::mutex snapshot_mutex_; - score::td::PtpTimeInfo snapshot_{}; + score::td::PtpTimeInfo snapshot_{}; std::atomic running_{false}; - pthread_t rx_thread_{}; - pthread_t pdelay_thread_{}; - bool rx_started_{false}; - bool pdelay_started_{false}; + pthread_t rx_thread_{}; + pthread_t pdelay_thread_{}; + bool rx_started_{false}; + bool pdelay_started_{false}; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/gptp_engine_test.cpp b/score/TimeSlave/code/gptp/gptp_engine_test.cpp index 90d7b30..76d6918 100644 --- a/score/TimeSlave/code/gptp/gptp_engine_test.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine_test.cpp @@ -44,8 +44,7 @@ class FakeClock final : public score::time::HighPrecisionLocalSteadyClock public: score::time::HighPrecisionLocalSteadyClock::time_point Now() noexcept override { - return score::time::HighPrecisionLocalSteadyClock::time_point{ - std::chrono::nanoseconds{42'000'000'000LL}}; + return score::time::HighPrecisionLocalSteadyClock::time_point{std::chrono::nanoseconds{42'000'000'000LL}}; } }; @@ -63,8 +62,14 @@ class FakeSocket final : public IRawSocket cv_.notify_one(); } - bool Open(const std::string&) override { return true; } - bool EnableHwTimestamping() override { return hw_ts_ok_; } + bool Open(const std::string&) override + { + return true; + } + bool EnableHwTimestamping() override + { + return hw_ts_ok_; + } void Close() override { @@ -75,13 +80,13 @@ class FakeSocket final : public IRawSocket cv_.notify_all(); } - int Recv(std::uint8_t* buf, std::size_t buf_len, - ::timespec& hwts, int timeout_ms) override + int Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) override { std::unique_lock lk(mtx_); const auto timeout = std::chrono::milliseconds(timeout_ms > 0 ? timeout_ms : 100); - cv_.wait_for(lk, timeout, - [this] { return closed_ || !frames_.empty(); }); + cv_.wait_for(lk, timeout, [this] { + return closed_ || !frames_.empty(); + }); if (closed_) return -1; if (frames_.empty()) @@ -94,17 +99,26 @@ class FakeSocket final : public IRawSocket return static_cast(n); } - int Send(const void*, int len, ::timespec&) override { return len; } - int GetFd() const override { return -1; } + int Send(const void*, int len, ::timespec&) override + { + return len; + } + int GetFd() const override + { + return -1; + } - void SetHwTsOk(bool v) { hw_ts_ok_ = v; } + void SetHwTsOk(bool v) + { + hw_ts_ok_ = v; + } private: std::deque, ::timespec>> frames_; - std::mutex mtx_; + std::mutex mtx_; std::condition_variable cv_; - bool closed_{false}; - bool hw_ts_ok_{true}; + bool closed_{false}; + bool hw_ts_ok_{true}; }; // ── FakeIdentity ────────────────────────────────────────────────────────────── @@ -114,7 +128,10 @@ class FakeIdentity final : public INetworkIdentity public: explicit FakeIdentity(bool resolve_ok = true) : resolve_ok_{resolve_ok} {} - bool Resolve(const std::string&) override { return resolve_ok_; } + bool Resolve(const std::string&) override + { + return resolve_ok_; + } ClockIdentity GetClockIdentity() const override { @@ -145,14 +162,15 @@ void AppendEthHeader(std::vector& buf) // Build a 34-byte PTP header at the back of buf. void AppendPtpHeader(std::vector& buf, - std::uint8_t msgtype, std::uint16_t seqId, + std::uint8_t msgtype, + std::uint16_t seqId, std::uint8_t ctlField = 0) { const std::size_t start = buf.size(); buf.resize(start + 34, 0); std::uint8_t* p = buf.data() + start; - p[0] = static_cast(0x10U | (msgtype & 0x0FU)); // tsmt - p[1] = 0x02; // version + p[0] = static_cast(0x10U | (msgtype & 0x0FU)); // tsmt + p[1] = 0x02; // version const std::uint16_t len = htons(static_cast(buf.size() - 14)); std::memcpy(p + 2, &len, 2); const std::uint16_t seq = htons(seqId); @@ -161,12 +179,11 @@ void AppendPtpHeader(std::vector& buf, } // Append a 10-byte Timestamp body (sec_msb=0, sec_lsb, ns). -void AppendTimestamp(std::vector& buf, - std::uint32_t sec_lsb, std::uint32_t ns) +void AppendTimestamp(std::vector& buf, std::uint32_t sec_lsb, std::uint32_t ns) { const std::uint16_t msb = htons(0U); - const std::uint32_t sl = htonl(sec_lsb); - const std::uint32_t n = htonl(ns); + const std::uint32_t sl = htonl(sec_lsb); + const std::uint32_t n = htonl(ns); const std::uint8_t* p; p = reinterpret_cast(&msb); buf.insert(buf.end(), p, p + 2); @@ -181,13 +198,11 @@ std::vector MakeSyncFrame(std::uint16_t seqId) std::vector f; AppendEthHeader(f); AppendPtpHeader(f, kPtpMsgtypeSync, seqId, /*ctl=*/0); - AppendTimestamp(f, 0, 0); // Sync body (origin timestamp, unused) + AppendTimestamp(f, 0, 0); // Sync body (origin timestamp, unused) return f; } -std::vector MakeFollowUpFrame(std::uint16_t seqId, - std::uint32_t sec_lsb, - std::uint32_t ns) +std::vector MakeFollowUpFrame(std::uint16_t seqId, std::uint32_t sec_lsb, std::uint32_t ns) { std::vector f; AppendEthHeader(f); @@ -201,7 +216,7 @@ std::vector MakePdelayRespFrame(std::uint16_t seqId) std::vector f; AppendEthHeader(f); AppendPtpHeader(f, kPtpMsgtypePdelayResp, seqId, /*ctl=*/5); - AppendTimestamp(f, 1, 0); // responseOriginTimestamp + AppendTimestamp(f, 1, 0); // responseOriginTimestamp // requesting port identity (10 bytes) f.resize(f.size() + 10, 0); return f; @@ -212,8 +227,8 @@ std::vector MakePdelayRespFupFrame(std::uint16_t seqId) std::vector f; AppendEthHeader(f); AppendPtpHeader(f, kPtpMsgtypePdelayRespFollowUp, seqId, /*ctl=*/5); - AppendTimestamp(f, 2, 0); // responseOriginReceiptTimestamp - f.resize(f.size() + 10, 0); // requesting port identity + AppendTimestamp(f, 2, 0); // responseOriginReceiptTimestamp + f.resize(f.size() + 10, 0); // requesting port identity return f; } @@ -230,10 +245,10 @@ std::vector MakeUnknownFrame() GptpEngineOptions FastOptions() { GptpEngineOptions o; - o.iface_name = "lo"; - o.pdelay_warmup_ms = 0; // no warmup — first Pdelay_Req fires immediately - o.pdelay_interval_ms = 10; // 10 ms cycle - o.sync_timeout_ms = 3300; + o.iface_name = "lo"; + o.pdelay_warmup_ms = 0; // no warmup — first Pdelay_Req fires immediately + o.pdelay_interval_ms = 10; // 10 ms cycle + o.sync_timeout_ms = 3300; o.jump_future_threshold_ns = 500'000'000LL; return o; } @@ -260,11 +275,13 @@ class GptpEngineTest : public ::testing::Test protected: void SetUp() override { - engine_ = std::make_unique( - FastOptions(), std::make_unique()); + engine_ = std::make_unique(FastOptions(), std::make_unique()); } - void TearDown() override { engine_->Deinitialize(); } + void TearDown() override + { + engine_->Deinitialize(); + } std::unique_ptr engine_; }; @@ -275,19 +292,19 @@ class GptpEngineFakeTest : public ::testing::Test protected: void SetUp() override { - auto sock = std::make_unique(); + auto sock = std::make_unique(); auto identity = std::make_unique(); - socket_raw_ = sock.get(); + socket_raw_ = sock.get(); engine_ = std::make_unique( - FastOptions(), - std::make_unique(), - std::move(sock), - std::move(identity)); + FastOptions(), std::make_unique(), std::move(sock), std::move(identity)); } - void TearDown() override { engine_->Deinitialize(); } + void TearDown() override + { + engine_->Deinitialize(); + } - FakeSocket* socket_raw_{nullptr}; + FakeSocket* socket_raw_{nullptr}; std::unique_ptr engine_; }; @@ -330,7 +347,7 @@ TEST_F(GptpEngineFakeTest, Initialize_WithFakeSocket_ReturnsTrue) TEST_F(GptpEngineFakeTest, Initialize_CalledTwice_ReturnsTrueOnSecondCall) { EXPECT_TRUE(engine_->Initialize()); - EXPECT_TRUE(engine_->Initialize()); // already running → returns true + EXPECT_TRUE(engine_->Initialize()); // already running → returns true } TEST_F(GptpEngineFakeTest, Deinitialize_AfterInitialize_ReturnsTrue) @@ -358,10 +375,9 @@ TEST_F(GptpEngineFakeTest, ReadPTPSnapshot_NotSynchronized_BeforeAnySync) TEST(GptpEngineIdentityFailTest, Initialize_IdentityResolveFails_ReturnsFalse) { - auto sock = std::make_unique(); + auto sock = std::make_unique(); auto identity = std::make_unique(/*resolve_ok=*/false); - GptpEngine eng{FastOptions(), std::make_unique(), - std::move(sock), std::move(identity)}; + GptpEngine eng{FastOptions(), std::make_unique(), std::move(sock), std::move(identity)}; EXPECT_FALSE(eng.Initialize()); EXPECT_TRUE(eng.Deinitialize()); } @@ -382,7 +398,7 @@ TEST_F(GptpEngineFakeTest, HandlePacket_SyncFollowUp_SnapshotBecomesSync) // Send Sync then FollowUp with the same seqId. ::timespec hwts{}; - hwts.tv_sec = 1; + hwts.tv_sec = 1; hwts.tv_nsec = 500'000'000L; socket_raw_->Push(MakeSyncFrame(1U), hwts); socket_raw_->Push(MakeFollowUpFrame(1U, /*sec=*/2, /*ns=*/0)); @@ -434,7 +450,7 @@ TEST_F(GptpEngineFakeTest, HandlePacket_UnknownMsgtype_DefaultBranchNocrash) TEST_F(GptpEngineFakeTest, HandlePacket_TooShortFrame_EarlyReturn) { ASSERT_TRUE(engine_->Initialize()); - socket_raw_->Push({0x01, 0x02, 0x03}); // < 14 bytes, ParseEthernetHeader returns false + socket_raw_->Push({0x01, 0x02, 0x03}); // < 14 bytes, ParseEthernetHeader returns false std::this_thread::sleep_for(std::chrono::milliseconds(30)); } @@ -444,14 +460,13 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) { // Use a very short timeout (50 ms) so we can trigger it quickly. GptpEngineOptions opts = FastOptions(); - opts.sync_timeout_ms = 50; + opts.sync_timeout_ms = 50; - auto sock = std::make_unique(); + auto sock = std::make_unique(); auto identity = std::make_unique(); FakeSocket* raw_sock = sock.get(); - GptpEngine eng{opts, std::make_unique(), - std::move(sock), std::move(identity)}; + GptpEngine eng{opts, std::make_unique(), std::move(sock), std::move(identity)}; ASSERT_TRUE(eng.Initialize()); // First receive a Sync+FUP so the state machine records a timestamp. @@ -466,7 +481,11 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) { score::td::PtpTimeInfo tmp{}; eng.ReadPTPSnapshot(tmp); - if (tmp.status.is_synchronized) { got_sync = true; break; } + if (tmp.status.is_synchronized) + { + got_sync = true; + break; + } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } ASSERT_TRUE(got_sync) << "engine never became synchronized"; @@ -486,7 +505,7 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) TEST(GptpEngineRealSocketTest, Initialize_NonExistentInterface_ReturnsFalse) { GptpEngineOptions opts; - opts.iface_name = "nonexistent_iface_xyz"; + opts.iface_name = "nonexistent_iface_xyz"; opts.pdelay_warmup_ms = 0; GptpEngine eng{opts, std::make_unique()}; EXPECT_FALSE(eng.Initialize()); diff --git a/score/TimeSlave/code/gptp/instrument/probe.cpp b/score/TimeSlave/code/gptp/instrument/probe.cpp index c9b9087..1312455 100644 --- a/score/TimeSlave/code/gptp/instrument/probe.cpp +++ b/score/TimeSlave/code/gptp/instrument/probe.cpp @@ -24,7 +24,6 @@ namespace ts namespace details { - ProbeManager& ProbeManager::Instance() { static ProbeManager instance; @@ -34,9 +33,7 @@ ProbeManager& ProbeManager::Instance() void ProbeManager::Trace(ProbePoint point, const ProbeData& data) { score::mw::log::LogDebug(score::td::kGPtpMachineContext) - << "PROBE point=" << static_cast(point) - << " ts=" << data.ts_mono_ns - << " val=" << data.value_ns + << "PROBE point=" << static_cast(point) << " ts=" << data.ts_mono_ns << " val=" << data.value_ns << " seq=" << data.seq_id; if (recorder_ != nullptr && recorder_->IsEnabled()) diff --git a/score/TimeSlave/code/gptp/instrument/probe.h b/score/TimeSlave/code/gptp/instrument/probe.h index d740d6d..6b33bd2 100644 --- a/score/TimeSlave/code/gptp/instrument/probe.h +++ b/score/TimeSlave/code/gptp/instrument/probe.h @@ -28,20 +28,20 @@ namespace details /// Measurement probe points within the gPTP pipeline. enum class ProbePoint : std::uint8_t { - kRxPacketReceived = 0, - kSyncFrameParsed = 1, + kRxPacketReceived = 0, + kSyncFrameParsed = 1, kFollowUpProcessed = 2, - kOffsetComputed = 3, - kPdelayReqSent = 4, - kPdelayCompleted = 5, - kPhcAdjusted = 6, + kOffsetComputed = 3, + kPdelayReqSent = 4, + kPdelayCompleted = 5, + kPhcAdjusted = 6, }; /// Data payload for a single probe event. struct ProbeData { - std::int64_t ts_mono_ns{0}; - std::int64_t value_ns{0}; + std::int64_t ts_mono_ns{0}; + std::int64_t value_ns{0}; std::uint32_t seq_id{0}; }; @@ -56,11 +56,20 @@ class ProbeManager final public: static ProbeManager& Instance(); - void SetEnabled(bool enabled) { enabled_.store(enabled, std::memory_order_release); } - bool IsEnabled() const { return enabled_.load(std::memory_order_acquire); } + void SetEnabled(bool enabled) + { + enabled_.store(enabled, std::memory_order_release); + } + bool IsEnabled() const + { + return enabled_.load(std::memory_order_acquire); + } /// Optional: link to a Recorder for persistent probe output. - void SetRecorder(Recorder* recorder) { recorder_ = recorder; } + void SetRecorder(Recorder* recorder) + { + recorder_ = recorder; + } /// Record a probe event. Thread-safe. void Trace(ProbePoint point, const ProbeData& data); @@ -68,7 +77,7 @@ class ProbeManager final private: ProbeManager() = default; std::atomic enabled_{false}; - Recorder* recorder_{nullptr}; + Recorder* recorder_{nullptr}; }; /// Returns the current monotonic timestamp in nanoseconds. @@ -80,14 +89,13 @@ std::int64_t ProbeMonoNs() noexcept; // Convenience macro: zero overhead when probing is disabled. // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define GPTP_PROBE(point, ...) \ - do \ - { \ - if (::score::ts::details::ProbeManager::Instance().IsEnabled()) \ - { \ - ::score::ts::details::ProbeManager::Instance().Trace( \ - point, {__VA_ARGS__}); \ - } \ +#define GPTP_PROBE(point, ...) \ + do \ + { \ + if (::score::ts::details::ProbeManager::Instance().IsEnabled()) \ + { \ + ::score::ts::details::ProbeManager::Instance().Trace(point, {__VA_ARGS__}); \ + } \ } while (0) #endif // SCORE_TIMESLAVE_CODE_GPTP_INSTRUMENT_PROBE_H diff --git a/score/TimeSlave/code/gptp/instrument/probe_test.cpp b/score/TimeSlave/code/gptp/instrument/probe_test.cpp index cf0854c..e8f0bea 100644 --- a/score/TimeSlave/code/gptp/instrument/probe_test.cpp +++ b/score/TimeSlave/code/gptp/instrument/probe_test.cpp @@ -66,10 +66,9 @@ TEST_F(ProbeManagerTest, Trace_WhenDisabled_DoesNotCrash) { ProbeData d{}; d.ts_mono_ns = 1'000'000LL; - d.value_ns = 500LL; - d.seq_id = 1U; - EXPECT_NO_THROW( - ProbeManager::Instance().Trace(ProbePoint::kSyncFrameParsed, d)); + d.value_ns = 500LL; + d.seq_id = 1U; + EXPECT_NO_THROW(ProbeManager::Instance().Trace(ProbePoint::kSyncFrameParsed, d)); } // ── Trace when enabled without recorder ─────────────────────────────────────── @@ -79,10 +78,9 @@ TEST_F(ProbeManagerTest, Trace_WhenEnabled_NoRecorder_DoesNotCrash) ProbeManager::Instance().SetEnabled(true); ProbeData d{}; d.ts_mono_ns = 2'000'000LL; - d.value_ns = -100LL; - d.seq_id = 2U; - EXPECT_NO_THROW( - ProbeManager::Instance().Trace(ProbePoint::kFollowUpProcessed, d)); + d.value_ns = -100LL; + d.seq_id = 2U; + EXPECT_NO_THROW(ProbeManager::Instance().Trace(ProbePoint::kFollowUpProcessed, d)); } // ── Trace with recorder attached ───────────────────────────────────────────── @@ -94,9 +92,9 @@ class ProbeManagerWithRecorderTest : public ::testing::Test { path_ = "/tmp/probe_test_" + std::to_string(::getpid()) + ".csv"; Recorder::Config cfg; - cfg.enabled = true; + cfg.enabled = true; cfg.file_path = path_; - recorder_ = std::make_unique(cfg); + recorder_ = std::make_unique(cfg); ProbeManager::Instance().SetEnabled(true); ProbeManager::Instance().SetRecorder(recorder_.get()); @@ -109,7 +107,7 @@ class ProbeManagerWithRecorderTest : public ::testing::Test std::remove(path_.c_str()); } - std::string path_; + std::string path_; std::unique_ptr recorder_; }; @@ -117,8 +115,8 @@ TEST_F(ProbeManagerWithRecorderTest, Trace_WritesToRecorder) { ProbeData d{}; d.ts_mono_ns = 3'000'000LL; - d.value_ns = 42LL; - d.seq_id = 3U; + d.value_ns = 42LL; + d.seq_id = 3U; ProbeManager::Instance().Trace(ProbePoint::kPdelayCompleted, d); // Flush by replacing recorder (which closes file in destructor) diff --git a/score/TimeSlave/code/gptp/phc/phc_adjuster.h b/score/TimeSlave/code/gptp/phc/phc_adjuster.h index eaf544b..a75fd25 100644 --- a/score/TimeSlave/code/gptp/phc/phc_adjuster.h +++ b/score/TimeSlave/code/gptp/phc/phc_adjuster.h @@ -26,9 +26,9 @@ namespace details /// Configuration for PHC hardware clock synchronization. struct PhcConfig { - bool enabled = false; - std::string device = ""; ///< QNX: "emac0", Linux: "/dev/ptp0" - std::int64_t step_threshold_ns = 100'000'000LL; ///< >100ms = step, else slew + bool enabled = false; + std::string device = ""; ///< QNX: "emac0", Linux: "/dev/ptp0" + std::int64_t step_threshold_ns = 100'000'000LL; ///< >100ms = step, else slew }; /** @@ -49,7 +49,10 @@ class PhcAdjuster final PhcAdjuster& operator=(const PhcAdjuster&) = delete; /// @return true if hardware clock adjustment is enabled. - bool IsEnabled() const { return cfg_.enabled; } + bool IsEnabled() const + { + return cfg_.enabled; + } /// Apply a time step or slew based on offset magnitude. /// If |offset_ns| > step_threshold_ns, a step correction is applied; @@ -62,7 +65,7 @@ class PhcAdjuster final private: PhcConfig cfg_; - int phc_fd_{-1}; + int phc_fd_{-1}; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp b/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp index a3860bb..228ae50 100644 --- a/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/network_identity.cpp @@ -13,12 +13,12 @@ #include "score/TimeSlave/code/gptp/details/network_identity.h" #include -#include #include #include #include #include #include +#include namespace score { diff --git a/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp index 3cad558..2f4d782 100644 --- a/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp @@ -12,12 +12,12 @@ ********************************************************************************/ #include "score/TimeSlave/code/gptp/phc/phc_adjuster.h" -#include #include #include #include #include #include +#include namespace score { @@ -72,15 +72,17 @@ void PhcAdjuster::AdjustOffset(std::int64_t offset_ns) if (std::abs(offset_ns) < cfg_.step_threshold_ns) return; - struct timex tx{}; + struct timex tx + { + }; tx.modes = ADJ_SETOFFSET | ADJ_NANO; - tx.time.tv_sec = static_cast(offset_ns / 1'000'000'000LL); + tx.time.tv_sec = static_cast(offset_ns / 1'000'000'000LL); tx.time.tv_usec = static_cast(offset_ns % 1'000'000'000LL); // Handle negative sub-second values if (tx.time.tv_usec < 0) { - tx.time.tv_sec -= 1; + tx.time.tv_sec -= 1; tx.time.tv_usec += 1'000'000'000L; } @@ -99,9 +101,11 @@ void PhcAdjuster::AdjustFrequency(double rate_ratio) const double ppb = (rate_ratio - 1.0) * 1e9; const long scaled_ppm = static_cast(ppb / 1000.0 * 65536.0); - struct timex tx{}; + struct timex tx + { + }; tx.modes = ADJ_FREQUENCY; - tx.freq = scaled_ppm; + tx.freq = scaled_ppm; (void)phc_clock_adjtime(phc_fd_to_clockid(phc_fd_), &tx); } diff --git a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp index 587d2db..90e03fc 100644 --- a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp @@ -13,17 +13,17 @@ #include "score/TimeSlave/code/gptp/details/raw_socket.h" #include -#include -#include +#include +#include +#include #include #include #include #include #include #include -#include -#include -#include +#include +#include namespace score { @@ -37,13 +37,13 @@ namespace void DrainErrQueue(int fd) noexcept { - char buf[2048]; - ::iovec iov{buf, sizeof(buf)}; - char ctrl[2048]; - ::msghdr msg{}; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = ctrl; + char buf[2048]; + ::iovec iov{buf, sizeof(buf)}; + char ctrl[2048]; + ::msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); while (::recvmsg(fd, &msg, MSG_ERRQUEUE) > 0) @@ -75,9 +75,9 @@ bool RawSocket::Open(const std::string& iface) } ::sockaddr_ll sa{}; - sa.sll_family = AF_PACKET; + sa.sll_family = AF_PACKET; sa.sll_protocol = htons(ETH_P_1588); - sa.sll_ifindex = ifr.ifr_ifindex; + sa.sll_ifindex = ifr.ifr_ifindex; if (::bind(fd, reinterpret_cast<::sockaddr*>(&sa), sizeof(sa)) < 0) { ::close(fd); @@ -85,10 +85,9 @@ bool RawSocket::Open(const std::string& iface) } // SO_BINDTODEVICE: best-effort, don't fail if it doesn't work - (void)::setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, - iface.c_str(), static_cast(iface.size())); + (void)::setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), static_cast(iface.size())); - fd_ = fd; + fd_ = fd; iface_ = iface; return true; } @@ -98,12 +97,12 @@ bool RawSocket::EnableHwTimestamping() if (fd_ < 0) return false; - ::ifreq ifr{}; + ::ifreq ifr{}; ::hwtstamp_config cfg{}; std::strncpy(ifr.ifr_name, iface_.c_str(), IFNAMSIZ - 1); ifr.ifr_data = reinterpret_cast(&cfg); - cfg.tx_type = HWTSTAMP_TX_ON; + cfg.tx_type = HWTSTAMP_TX_ON; cfg.rx_filter = HWTSTAMP_FILTER_ALL; if (::ioctl(fd_, SIOCSHWTSTAMP, &ifr) < 0) @@ -113,11 +112,8 @@ bool RawSocket::EnableHwTimestamping() (void)::ioctl(fd_, SIOCSHWTSTAMP, &ifr); } - const int ts_opts = SOF_TIMESTAMPING_TX_HARDWARE | - SOF_TIMESTAMPING_RX_HARDWARE | - SOF_TIMESTAMPING_RAW_HARDWARE; - if (::setsockopt(fd_, SOL_SOCKET, SO_TIMESTAMPING, - &ts_opts, sizeof(ts_opts)) < 0) + const int ts_opts = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; + if (::setsockopt(fd_, SOL_SOCKET, SO_TIMESTAMPING, &ts_opts, sizeof(ts_opts)) < 0) { return false; } @@ -134,8 +130,7 @@ void RawSocket::Close() iface_.clear(); } -int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, - ::timespec& hwts, int timeout_ms) +int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) { if (fd_ < 0 || buf == nullptr || buf_len == 0) return -1; @@ -144,16 +139,16 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::pollfd pfd{fd_, POLLIN, 0}; const int pr = ::poll(&pfd, 1, timeout_ms); if (pr == 0) - return 0; // timeout + return 0; // timeout if (pr < 0) return -1; - char ctrl[1024]; - ::iovec iov{buf, buf_len}; + char ctrl[1024]; + ::iovec iov{buf, buf_len}; ::msghdr msg{}; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = ctrl; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); const int len = static_cast(::recvmsg(fd_, &msg, 0)); @@ -161,8 +156,7 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, return -1; std::memset(&hwts, 0, sizeof(hwts)); - for (::cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; - cm = CMSG_NXTHDR(&msg, cm)) + for (::cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; cm = CMSG_NXTHDR(&msg, cm)) { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMPING) { @@ -190,7 +184,7 @@ int RawSocket::Send(const void* buf, int len, ::timespec& hwts) if (::poll(&pfd, 1, -1) > 0 && (pfd.revents & POLLERR) != 0) { std::uint8_t tmp[2048]; - ::timespec tx_hwts{}; + ::timespec tx_hwts{}; (void)Recv(tmp, sizeof(tmp), tx_hwts, 0); hwts = tx_hwts; } diff --git a/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp b/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp index 1140167..7172bec 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/network_identity.cpp @@ -13,11 +13,11 @@ #include "score/TimeSlave/code/gptp/details/network_identity.h" #include -#include #include #include #include #include +#include namespace score { @@ -52,7 +52,7 @@ int ReadMac(const char* iface_name, unsigned char out_mac[8]) noexcept const auto* sdl = reinterpret_cast(ifa->ifa_addr); const auto* mac = reinterpret_cast(LLADDR(sdl)); - const int len = static_cast(sdl->sdl_alen); + const int len = static_cast(sdl->sdl_alen); if (len == 6 || len == 8) { std::memcpy(out_mac, mac, static_cast(len)); diff --git a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp index bf8f107..44436fd 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp @@ -16,15 +16,15 @@ // declared in raw_socket.cpp (extern "C"). #include -#include -#include -#include #include #include #include #include #include #include +#include +#include +#include // QNX SDP 8.0: PTP API constants (from io-sock/ptp.h, inlined to avoid // struct PortIdentity redefinition conflict with details/ptp_types.h). @@ -57,17 +57,16 @@ struct GptpEthHdr { unsigned char h_dest[6]; unsigned char h_source[6]; - uint16_t h_proto; + uint16_t h_proto; }; -static constexpr int64_t kNsPerSec = 1'000'000'000LL; -static constexpr std::size_t kMaxBpfBufSz = 65536U; -static constexpr int kMaxTxScanTries = 8; +static constexpr int64_t kNsPerSec = 1'000'000'000LL; +static constexpr std::size_t kMaxBpfBufSz = 65536U; +static constexpr int kMaxTxScanTries = 8; // Caplen of a BPF TX loopback frame injected by the PTP driver: // Ethernet header (14 B) + ptp_tstmp payload (4 + 12 = 16 B) = 30 B -static constexpr int kTxLoopbackCaplen = - static_cast(sizeof(GptpEthHdr) + sizeof(PtpTstmp)); +static constexpr int kTxLoopbackCaplen = static_cast(sizeof(GptpEthHdr) + sizeof(PtpTstmp)); // ── BPF kernel filter: pass only IEEE 802.1AS (ETH_P_1588) frames ──────────── // BPF_LD H ABS 12 — load EtherType (bytes 12-13) @@ -81,25 +80,24 @@ static struct bpf_insn kPtp1588FilterInsns[] = { BPF_STMT(BPF_RET + BPF_K, static_cast(-1)), BPF_STMT(BPF_RET + BPF_K, 0), }; -static const u_int kPtp1588FilterLen = - static_cast(sizeof(kPtp1588FilterInsns) / sizeof(kPtp1588FilterInsns[0])); +static const u_int kPtp1588FilterLen = static_cast(sizeof(kPtp1588FilterInsns) / sizeof(kPtp1588FilterInsns[0])); // ── Per-thread BPF context ─────────────────────────────────────────────────── struct QnxRawContext { - int bpf_fd = -1; - u_int bpf_buflen = 0; - char iface_name[IFNAMSIZ]{}; + int bpf_fd = -1; + u_int bpf_buflen = 0; + char iface_name[IFNAMSIZ]{}; unsigned char bpf_buf[kMaxBpfBufSz]{}; - ssize_t bpf_n = 0; - ssize_t bpf_off = 0; - bool initialized = false; + ssize_t bpf_n = 0; + ssize_t bpf_off = 0; + bool initialized = false; unsigned char tx_frame[ETHER_HDR_LEN + 1500]{}; // Secondary BPF fd with BIOCSSEESENT=1 for reading TX loopback timestamps. // Lazily opened on first qnx_raw_send() call. - int tx_loopback_fd = -1; - u_int tx_loopback_buflen = 0; + int tx_loopback_fd = -1; + u_int tx_loopback_buflen = 0; unsigned char tx_loopback_buf[kMaxBpfBufSz]{}; ~QnxRawContext() @@ -128,9 +126,9 @@ thread_local QnxRawContext g_qnx_ctx; // This is equivalent to bintime2timespec() from . static void bpf_ts_to_timespec(const bpf_xhdr* bh, struct timespec* ts) noexcept { - ts->tv_sec = static_cast(bh->bh_tstamp.bt_sec); + ts->tv_sec = static_cast(bh->bh_tstamp.bt_sec); const uint64_t top32 = bh->bh_tstamp.bt_frac >> 32U; - ts->tv_nsec = static_cast((top32 * 1'000'000'000ULL) >> 32U); + ts->tv_nsec = static_cast((top32 * 1'000'000'000ULL) >> 32U); } // Parse an Ethernet/VLAN frame; return byte offset of PTP payload or -1. @@ -142,7 +140,7 @@ static int ptp_payload_offset(const unsigned char* frame, int caplen) GptpEthHdr eth{}; std::memcpy(ð, frame, sizeof(GptpEthHdr)); uint16_t etype = ntohs(eth.h_proto); - int offset = static_cast(sizeof(GptpEthHdr)); + int offset = static_cast(sizeof(GptpEthHdr)); if (etype == ETH_P_8021Q) { @@ -150,7 +148,7 @@ static int ptp_payload_offset(const unsigned char* frame, int caplen) return -1; uint16_t inner{}; std::memcpy(&inner, frame + offset + 2, sizeof(uint16_t)); - etype = ntohs(inner); + etype = ntohs(inner); offset += 4; } @@ -195,7 +193,10 @@ static int open_tx_loopback_fd(int main_fd) noexcept (void)::ioctl(lfd, BIOCSTSTAMP, &bpf_ts); // Apply the same ETH_P_1588 kernel filter. - struct bpf_program prog{kPtp1588FilterLen, kPtp1588FilterInsns}; + struct bpf_program prog + { + kPtp1588FilterLen, kPtp1588FilterInsns + }; (void)::ioctl(lfd, BIOCSETF, &prog); u_int buflen = 0U; @@ -254,7 +255,10 @@ extern "C" int qnx_raw_open(const char* ifname) (void)::ioctl(fd, BIOCSTSTAMP, &bpf_ts); // Install kernel BPF filter: discard all non-ETH_P_1588 frames early. - struct bpf_program prog{kPtp1588FilterLen, kPtp1588FilterInsns}; + struct bpf_program prog + { + kPtp1588FilterLen, kPtp1588FilterInsns + }; (void)::ioctl(fd, BIOCSETF, &prog); // best-effort; userspace filter still runs if (::ioctl(fd, BIOCGBLEN, &g_qnx_ctx.bpf_buflen) < 0) @@ -269,7 +273,7 @@ extern "C" int qnx_raw_open(const char* ifname) return -1; } - g_qnx_ctx.bpf_fd = fd; + g_qnx_ctx.bpf_fd = fd; g_qnx_ctx.initialized = true; return fd; } @@ -311,7 +315,7 @@ extern "C" int qnx_raw_recv(int fd, void* buf, int buf_len, timespec* hwts, int } continue; } - g_qnx_ctx.bpf_n = n; + g_qnx_ctx.bpf_n = n; g_qnx_ctx.bpf_off = 0; } @@ -323,33 +327,28 @@ extern "C" int qnx_raw_recv(int fd, void* buf, int buf_len, timespec* hwts, int } // Verify 8-byte alignment required by bpf_xhdr. - const auto ptr_val = - reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); + const auto ptr_val = reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); if (ptr_val % alignof(bpf_xhdr) != 0U) { g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; continue; } - const auto* bh = - reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); + const auto* bh = reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); // Bounds checks. if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || bh->bh_caplen > static_cast(g_qnx_ctx.bpf_n) || - g_qnx_ctx.bpf_off + static_cast(bh->bh_hdrlen) + - static_cast(bh->bh_caplen) > + g_qnx_ctx.bpf_off + static_cast(bh->bh_hdrlen) + static_cast(bh->bh_caplen) > g_qnx_ctx.bpf_n) { g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; continue; } - const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; - const int caplen = static_cast(bh->bh_caplen); - const ssize_t next_off = - g_qnx_ctx.bpf_off + - static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); + const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; + const int caplen = static_cast(bh->bh_caplen); + const ssize_t next_off = g_qnx_ctx.bpf_off + static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); // Skip TX loopback frames (BIOCSSEESENT=0 should prevent them on the // main fd, but guard defensively: a loopback frame has a fixed small @@ -427,43 +426,35 @@ extern "C" int qnx_raw_send(int fd, const void* buf, int len, timespec* hwts) for (int tries = 0; tries < kMaxTxScanTries; ++tries) { - ssize_t nr = ::read(lfd, g_qnx_ctx.tx_loopback_buf, - g_qnx_ctx.tx_loopback_buflen); + ssize_t nr = ::read(lfd, g_qnx_ctx.tx_loopback_buf, g_qnx_ctx.tx_loopback_buflen); if (nr <= 0) break; ssize_t off = 0; while (off + static_cast(sizeof(bpf_xhdr)) <= nr) { - const auto pv = reinterpret_cast( - g_qnx_ctx.tx_loopback_buf + off); + const auto pv = reinterpret_cast(g_qnx_ctx.tx_loopback_buf + off); if (pv % alignof(bpf_xhdr) != 0U) break; - const auto* bh = reinterpret_cast( - g_qnx_ctx.tx_loopback_buf + off); + const auto* bh = reinterpret_cast(g_qnx_ctx.tx_loopback_buf + off); if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || - off + static_cast(bh->bh_hdrlen) + - static_cast(bh->bh_caplen) > - nr) + off + static_cast(bh->bh_hdrlen) + static_cast(bh->bh_caplen) > nr) break; - const unsigned char* pkt = - reinterpret_cast(bh) + bh->bh_hdrlen; - const int caplen = static_cast(bh->bh_caplen); - const ssize_t next = off + static_cast( - BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); + const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; + const int caplen = static_cast(bh->bh_caplen); + const ssize_t next = off + static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); // A TX loopback record has a fixed caplen and contains a // ptp_tstmp payload right after the Ethernet header. if (caplen == kTxLoopbackCaplen) { - const auto* tstmp = reinterpret_cast( - pkt + sizeof(GptpEthHdr)); + const auto* tstmp = reinterpret_cast(pkt + sizeof(GptpEthHdr)); if (tstmp->uid == tx_uid) { - hwts->tv_sec = static_cast(tstmp->time.sec); + hwts->tv_sec = static_cast(tstmp->time.sec); hwts->tv_nsec = static_cast(tstmp->time.nsec); return static_cast(len); } @@ -499,14 +490,14 @@ extern "C" int qnx_phc_adjtime_step(int /*phc_fd*/, long long offset_ns) struct { - struct ifdrv ifd; + struct ifdrv ifd; struct ptp_time tm; } cmd{}; std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); - cmd.ifd.ifd_len = sizeof(cmd.tm); + cmd.ifd.ifd_len = sizeof(cmd.tm); cmd.ifd.ifd_data = &cmd.tm; - cmd.ifd.ifd_cmd = PTP_GET_TIME; + cmd.ifd.ifd_cmd = PTP_GET_TIME; if (::ioctl(s, SIOCGDRVSPEC, &cmd) == -1) { @@ -517,16 +508,16 @@ extern "C" int qnx_phc_adjtime_step(int /*phc_fd*/, long long offset_ns) const int64_t cur_ns = cmd.tm.sec * kNsPerSec + static_cast(cmd.tm.nsec); const int64_t new_ns = cur_ns + static_cast(offset_ns); - cmd.tm.sec = new_ns / kNsPerSec; + cmd.tm.sec = new_ns / kNsPerSec; cmd.tm.nsec = static_cast(new_ns % kNsPerSec); if (cmd.tm.nsec < 0) { cmd.tm.nsec += static_cast(kNsPerSec); - cmd.tm.sec -= 1; + cmd.tm.sec -= 1; } cmd.ifd.ifd_cmd = PTP_SET_TIME; - const int r = ::ioctl(s, SIOCSDRVSPEC, &cmd); + const int r = ::ioctl(s, SIOCSDRVSPEC, &cmd); ::close(s); return r; } @@ -546,14 +537,14 @@ extern "C" int qnx_phc_adjfreq_ppb(int /*phc_fd*/, long long freq_ppb) struct { struct ifdrv ifd; - int adj_ppm; + int adj_ppm; } cmd{}; std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); - cmd.ifd.ifd_len = sizeof(cmd.adj_ppm); + cmd.ifd.ifd_len = sizeof(cmd.adj_ppm); cmd.ifd.ifd_data = &cmd.adj_ppm; - cmd.ifd.ifd_cmd = 0x200; // EMAC_PTP_ADJ_FREQ_PPM - cmd.adj_ppm = ppm; + cmd.ifd.ifd_cmd = 0x200; // EMAC_PTP_ADJ_FREQ_PPM + cmd.adj_ppm = ppm; const int r = ::ioctl(s, SIOCGDRVSPEC, &cmd); ::close(s); diff --git a/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp index 237457b..a970708 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp @@ -12,13 +12,13 @@ ********************************************************************************/ #include "score/TimeSlave/code/gptp/details/raw_socket.h" -#include -#include +#include #include #include -#include #include #include +#include +#include // QNX raw shim C linkage (provided by existing qnx_raw_shim target) extern "C" { @@ -65,8 +65,7 @@ void RawSocket::Close() iface_.clear(); } -int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, - ::timespec& hwts, int timeout_ms) +int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) { if (fd_ < 0 || buf == nullptr || buf_len == 0) return -1; diff --git a/score/TimeSlave/code/gptp/record/recorder.cpp b/score/TimeSlave/code/gptp/record/recorder.cpp index b189875..f56ee55 100644 --- a/score/TimeSlave/code/gptp/record/recorder.cpp +++ b/score/TimeSlave/code/gptp/record/recorder.cpp @@ -42,12 +42,8 @@ void Recorder::Record(const RecordEntry& entry) return; std::lock_guard lk(mutex_); - file_ << entry.mono_ns << ',' - << static_cast(entry.event) << ',' - << entry.offset_ns << ',' - << entry.pdelay_ns << ',' - << entry.seq_id << ',' - << static_cast(entry.status_flags) << '\n'; + file_ << entry.mono_ns << ',' << static_cast(entry.event) << ',' << entry.offset_ns << ',' << entry.pdelay_ns + << ',' << entry.seq_id << ',' << static_cast(entry.status_flags) << '\n'; file_.flush(); } diff --git a/score/TimeSlave/code/gptp/record/recorder.h b/score/TimeSlave/code/gptp/record/recorder.h index d775d82..839bf16 100644 --- a/score/TimeSlave/code/gptp/record/recorder.h +++ b/score/TimeSlave/code/gptp/record/recorder.h @@ -28,22 +28,22 @@ namespace details /// Event types that can be recorded. enum class RecordEvent : std::uint8_t { - kSyncReceived = 0, + kSyncReceived = 0, kPdelayCompleted = 1, - kClockJump = 2, + kClockJump = 2, kOffsetThreshold = 3, - kProbe = 4, + kProbe = 4, }; /// A single record entry written to the log file. struct RecordEntry { std::int64_t mono_ns{0}; - RecordEvent event{RecordEvent::kSyncReceived}; + RecordEvent event{RecordEvent::kSyncReceived}; std::int64_t offset_ns{0}; std::int64_t pdelay_ns{0}; std::uint16_t seq_id{0}; - std::uint8_t status_flags{0}; + std::uint8_t status_flags{0}; }; /** @@ -57,8 +57,8 @@ class Recorder final public: struct Config { - bool enabled = false; - std::string file_path = "/var/log/gptp_record.csv"; + bool enabled = false; + std::string file_path = "/var/log/gptp_record.csv"; std::int64_t offset_threshold_ns = 1'000'000LL; ///< 1 ms }; @@ -68,15 +68,18 @@ class Recorder final Recorder(const Recorder&) = delete; Recorder& operator=(const Recorder&) = delete; - bool IsEnabled() const { return cfg_.enabled && file_.is_open(); } + bool IsEnabled() const + { + return cfg_.enabled && file_.is_open(); + } /// Record an entry. Thread-safe. void Record(const RecordEntry& entry); private: - Config cfg_; - std::mutex mutex_; - std::ofstream file_; + Config cfg_; + std::mutex mutex_; + std::ofstream file_; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/record/recorder_test.cpp b/score/TimeSlave/code/gptp/record/recorder_test.cpp index 7115a95..35736dd 100644 --- a/score/TimeSlave/code/gptp/record/recorder_test.cpp +++ b/score/TimeSlave/code/gptp/record/recorder_test.cpp @@ -58,7 +58,7 @@ TEST(RecorderTest, Disabled_RecordDoesNotCrash) TEST(RecorderTest, Enabled_BadPath_IsEnabledReturnsFalse) { Recorder::Config cfg; - cfg.enabled = true; + cfg.enabled = true; cfg.file_path = "/no/such/dir/recorder_test.csv"; Recorder r{cfg}; EXPECT_FALSE(r.IsEnabled()); @@ -67,7 +67,7 @@ TEST(RecorderTest, Enabled_BadPath_IsEnabledReturnsFalse) TEST(RecorderTest, Enabled_BadPath_RecordDoesNotCrash) { Recorder::Config cfg; - cfg.enabled = true; + cfg.enabled = true; cfg.file_path = "/no/such/dir/recorder_test.csv"; Recorder r{cfg}; EXPECT_NO_THROW(r.Record(RecordEntry{})); @@ -78,13 +78,19 @@ TEST(RecorderTest, Enabled_BadPath_RecordDoesNotCrash) class RecorderFileTest : public ::testing::Test { protected: - void SetUp() override { path_ = TempPath(); } - void TearDown() override { std::remove(path_.c_str()); } + void SetUp() override + { + path_ = TempPath(); + } + void TearDown() override + { + std::remove(path_.c_str()); + } Recorder MakeRecorder() { Recorder::Config cfg; - cfg.enabled = true; + cfg.enabled = true; cfg.file_path = path_; return Recorder{cfg}; } @@ -100,10 +106,12 @@ TEST_F(RecorderFileTest, IsEnabled_ReturnsTrue) TEST_F(RecorderFileTest, NewFile_ContainsCsvHeader) { - { auto r = MakeRecorder(); } // destructor closes file + { + auto r = MakeRecorder(); + } // destructor closes file std::ifstream f(path_); - std::string line; + std::string line; ASSERT_TRUE(std::getline(f, line)); EXPECT_EQ(line, "mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags"); } @@ -113,11 +121,11 @@ TEST_F(RecorderFileTest, Record_WritesOneDataLine) auto r = MakeRecorder(); RecordEntry e{}; - e.mono_ns = 123456789LL; - e.event = RecordEvent::kSyncReceived; - e.offset_ns = -500LL; - e.pdelay_ns = 1000LL; - e.seq_id = 42U; + e.mono_ns = 123456789LL; + e.event = RecordEvent::kSyncReceived; + e.offset_ns = -500LL; + e.pdelay_ns = 1000LL; + e.seq_id = 42U; e.status_flags = 0x03U; r.Record(e); @@ -132,9 +140,9 @@ TEST_F(RecorderFileTest, Record_MultipleEntries_AllFlushedToFile) for (int i = 0; i < 5; ++i) { RecordEntry e{}; - e.mono_ns = static_cast(i) * 1'000'000LL; - e.event = RecordEvent::kPdelayCompleted; - e.seq_id = static_cast(i); + e.mono_ns = static_cast(i) * 1'000'000LL; + e.event = RecordEvent::kPdelayCompleted; + e.seq_id = static_cast(i); r.Record(e); } } @@ -153,11 +161,11 @@ TEST_F(RecorderFileTest, Record_FieldsWrittenCorrectly) { auto r = MakeRecorder(); RecordEntry e{}; - e.mono_ns = 9'000'000'000LL; - e.event = RecordEvent::kClockJump; - e.offset_ns = 12345LL; - e.pdelay_ns = 999LL; - e.seq_id = 7U; + e.mono_ns = 9'000'000'000LL; + e.event = RecordEvent::kClockJump; + e.offset_ns = 12345LL; + e.pdelay_ns = 999LL; + e.seq_id = 7U; e.status_flags = 0x01U; r.Record(e); } diff --git a/score/libTSClient/gptp_ipc_channel.h b/score/libTSClient/gptp_ipc_channel.h index 651c12b..f7cc936 100644 --- a/score/libTSClient/gptp_ipc_channel.h +++ b/score/libTSClient/gptp_ipc_channel.h @@ -43,9 +43,9 @@ static constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; */ struct alignas(64) GptpIpcRegion { - std::uint32_t magic{kGptpIpcMagic}; + std::uint32_t magic{kGptpIpcMagic}; std::atomic seq{0}; - score::td::PtpTimeInfo data{}; + score::td::PtpTimeInfo data{}; std::atomic seq_confirm{0}; }; diff --git a/score/libTSClient/gptp_ipc_publisher.cpp b/score/libTSClient/gptp_ipc_publisher.cpp index 6a31a17..2c4cbc2 100644 --- a/score/libTSClient/gptp_ipc_publisher.cpp +++ b/score/libTSClient/gptp_ipc_publisher.cpp @@ -12,10 +12,10 @@ ********************************************************************************/ #include "score/libTSClient/gptp_ipc_publisher.h" -#include #include #include #include +#include namespace score { @@ -39,18 +39,17 @@ bool GptpIpcPublisher::Init(const std::string& ipc_name) if (::ftruncate(shm_fd_, static_cast(sizeof(GptpIpcRegion))) != 0) { - ::close(shm_fd_); // LCOV_EXCL_LINE - shm_fd_ = -1; // LCOV_EXCL_LINE - return false; // LCOV_EXCL_LINE + ::close(shm_fd_); // LCOV_EXCL_LINE + shm_fd_ = -1; // LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE } - void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), - PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd_, 0); + void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd_, 0); if (ptr == MAP_FAILED) { - ::close(shm_fd_); // LCOV_EXCL_LINE - shm_fd_ = -1; // LCOV_EXCL_LINE - return false; // LCOV_EXCL_LINE + ::close(shm_fd_); // LCOV_EXCL_LINE + shm_fd_ = -1; // LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE } region_ = new (ptr) GptpIpcRegion{}; diff --git a/score/libTSClient/gptp_ipc_publisher.h b/score/libTSClient/gptp_ipc_publisher.h index 50a857e..b0f4509 100644 --- a/score/libTSClient/gptp_ipc_publisher.h +++ b/score/libTSClient/gptp_ipc_publisher.h @@ -51,8 +51,8 @@ class GptpIpcPublisher final private: GptpIpcRegion* region_{nullptr}; - int shm_fd_{-1}; - std::string ipc_name_; + int shm_fd_{-1}; + std::string ipc_name_; }; } // namespace details diff --git a/score/libTSClient/gptp_ipc_receiver.cpp b/score/libTSClient/gptp_ipc_receiver.cpp index 8cfd5bf..fbc6422 100644 --- a/score/libTSClient/gptp_ipc_receiver.cpp +++ b/score/libTSClient/gptp_ipc_receiver.cpp @@ -12,10 +12,10 @@ ********************************************************************************/ #include "score/libTSClient/gptp_ipc_receiver.h" -#include #include #include #include +#include namespace score { @@ -37,8 +37,7 @@ bool GptpIpcReceiver::Init(const std::string& ipc_name) if (shm_fd_ < 0) return false; - void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), - PROT_READ, MAP_SHARED, shm_fd_, 0); + void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), PROT_READ, MAP_SHARED, shm_fd_, 0); if (ptr == MAP_FAILED) { ::close(shm_fd_); diff --git a/score/libTSClient/gptp_ipc_receiver.h b/score/libTSClient/gptp_ipc_receiver.h index 3d0bc3a..4d4dc49 100644 --- a/score/libTSClient/gptp_ipc_receiver.h +++ b/score/libTSClient/gptp_ipc_receiver.h @@ -53,7 +53,7 @@ class GptpIpcReceiver final private: const GptpIpcRegion* region_{nullptr}; - int shm_fd_{-1}; + int shm_fd_{-1}; }; } // namespace details diff --git a/score/libTSClient/gptp_ipc_test.cpp b/score/libTSClient/gptp_ipc_test.cpp index 387f0a9..fbaa0f4 100644 --- a/score/libTSClient/gptp_ipc_test.cpp +++ b/score/libTSClient/gptp_ipc_test.cpp @@ -16,11 +16,11 @@ #include -#include -#include #include #include #include +#include +#include namespace score { @@ -44,9 +44,9 @@ std::string UniqueShmName() // testing; cleans up in destructor. struct ManualShm { - std::string name; - void* ptr = MAP_FAILED; - std::size_t size = sizeof(GptpIpcRegion); + std::string name; + void* ptr = MAP_FAILED; + std::size_t size = sizeof(GptpIpcRegion); explicit ManualShm(const std::string& n) : name{n} { @@ -69,8 +69,14 @@ struct ManualShm ::shm_unlink(name.c_str()); } - bool Valid() const { return ptr != MAP_FAILED; } - GptpIpcRegion* Region() { return static_cast(ptr); } + bool Valid() const + { + return ptr != MAP_FAILED; + } + GptpIpcRegion* Region() + { + return static_cast(ptr); + } }; } // namespace @@ -80,7 +86,10 @@ struct ManualShm class GptpIpcPublisherTest : public ::testing::Test { protected: - void TearDown() override { pub_.Destroy(); } + void TearDown() override + { + pub_.Destroy(); + } GptpIpcPublisher pub_; }; @@ -90,7 +99,6 @@ TEST_F(GptpIpcPublisherTest, Init_ValidName_ReturnsTrue) EXPECT_TRUE(pub_.Init(UniqueShmName())); } - TEST_F(GptpIpcPublisherTest, Publish_WithoutInit_DoesNotCrash) { // region_ is nullptr; Publish() must return silently. @@ -115,7 +123,10 @@ TEST_F(GptpIpcPublisherTest, Destroy_WithoutInit_DoesNotCrash) class GptpIpcReceiverTest : public ::testing::Test { protected: - void TearDown() override { rx_.Close(); } + void TearDown() override + { + rx_.Close(); + } GptpIpcReceiver rx_; }; @@ -146,16 +157,19 @@ TEST_F(GptpIpcReceiverTest, Receive_WithoutInit_ReturnsNullopt) class GptpIpcRoundtripTest : public ::testing::Test { protected: - void SetUp() override { name_ = UniqueShmName(); } + void SetUp() override + { + name_ = UniqueShmName(); + } void TearDown() override { rx_.Close(); pub_.Destroy(); } - std::string name_; + std::string name_; GptpIpcPublisher pub_; - GptpIpcReceiver rx_; + GptpIpcReceiver rx_; }; TEST_F(GptpIpcRoundtripTest, ReceiverInit_AfterPublisherInit_ReturnsTrue) @@ -180,10 +194,10 @@ TEST_F(GptpIpcRoundtripTest, PublishReceive_BasicFields_RoundtripCorrectly) ASSERT_TRUE(rx_.Init(name_)); score::td::PtpTimeInfo info{}; - info.ptp_assumed_time = std::chrono::nanoseconds{1'234'567'890LL}; - info.rate_deviation = 0.75; + info.ptp_assumed_time = std::chrono::nanoseconds{1'234'567'890LL}; + info.rate_deviation = 0.75; info.status.is_synchronized = true; - info.status.is_correct = true; + info.status.is_correct = true; pub_.Publish(info); @@ -204,11 +218,11 @@ TEST_F(GptpIpcRoundtripTest, PublishReceive_StatusFlags_RoundtripCorrectly) ASSERT_TRUE(rx_.Init(name_)); score::td::PtpTimeInfo info{}; - info.status.is_timeout = true; + info.status.is_timeout = true; info.status.is_time_jump_future = true; - info.status.is_time_jump_past = false; - info.status.is_synchronized = false; - info.status.is_correct = false; + info.status.is_time_jump_past = false; + info.status.is_synchronized = false; + info.status.is_correct = false; pub_.Publish(info); @@ -226,15 +240,15 @@ TEST_F(GptpIpcRoundtripTest, PublishReceive_SyncFupData_RoundtripCorrectly) ASSERT_TRUE(rx_.Init(name_)); score::td::PtpTimeInfo info{}; - info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; info.sync_fup_data.reference_global_timestamp = 100'000'001'000ULL; - info.sync_fup_data.reference_local_timestamp = 100'000'001'500ULL; - info.sync_fup_data.sync_ingress_timestamp = 100'000'001'500ULL; - info.sync_fup_data.correction_field = 42U; - info.sync_fup_data.sequence_id = 77; - info.sync_fup_data.pdelay = 3'000U; - info.sync_fup_data.port_number = 1; - info.sync_fup_data.clock_identity = 0xAABBCCDDEEFF0011ULL; + info.sync_fup_data.reference_local_timestamp = 100'000'001'500ULL; + info.sync_fup_data.sync_ingress_timestamp = 100'000'001'500ULL; + info.sync_fup_data.correction_field = 42U; + info.sync_fup_data.sequence_id = 77; + info.sync_fup_data.pdelay = 3'000U; + info.sync_fup_data.port_number = 1; + info.sync_fup_data.clock_identity = 0xAABBCCDDEEFF0011ULL; pub_.Publish(info); @@ -253,14 +267,14 @@ TEST_F(GptpIpcRoundtripTest, PublishReceive_PDelayData_RoundtripCorrectly) ASSERT_TRUE(rx_.Init(name_)); score::td::PtpTimeInfo info{}; - info.pdelay_data.request_origin_timestamp = 1'000'000'000ULL; - info.pdelay_data.request_receipt_timestamp = 1'000'001'000ULL; - info.pdelay_data.response_origin_timestamp = 1'000'001'000ULL; + info.pdelay_data.request_origin_timestamp = 1'000'000'000ULL; + info.pdelay_data.request_receipt_timestamp = 1'000'001'000ULL; + info.pdelay_data.response_origin_timestamp = 1'000'001'000ULL; info.pdelay_data.response_receipt_timestamp = 1'000'002'000ULL; - info.pdelay_data.pdelay = 1'000U; - info.pdelay_data.req_port_number = 1; - info.pdelay_data.resp_port_number = 2; - info.pdelay_data.req_clock_identity = 0x1122334455667788ULL; + info.pdelay_data.pdelay = 1'000U; + info.pdelay_data.req_port_number = 1; + info.pdelay_data.resp_port_number = 2; + info.pdelay_data.req_clock_identity = 0x1122334455667788ULL; pub_.Publish(info); @@ -281,8 +295,7 @@ TEST_F(GptpIpcRoundtripTest, MultiplePublish_LastValueIsVisible) for (int i = 1; i <= 5; ++i) { score::td::PtpTimeInfo info{}; - info.ptp_assumed_time = - std::chrono::nanoseconds{static_cast(i) * 1'000'000'000LL}; + info.ptp_assumed_time = std::chrono::nanoseconds{static_cast(i) * 1'000'000'000LL}; pub_.Publish(info); } From 85ea119ffdbd0e2457f4df8e7ab29a47cf9e08c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 27 Mar 2026 14:28:26 +0800 Subject: [PATCH 03/12] fix bazel build failed --- score/TimeSlave/code/application/BUILD | 2 +- .../TimeSlave/code/application/time_slave.cpp | 4 +-- score/TimeSlave/code/common/BUILD | 18 ++++++++++ .../TimeSlave/code/common/logging_contexts.h | 34 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 score/TimeSlave/code/common/BUILD create mode 100644 score/TimeSlave/code/common/logging_contexts.h diff --git a/score/TimeSlave/code/application/BUILD b/score/TimeSlave/code/application/BUILD index d83c578..4f7eab4 100644 --- a/score/TimeSlave/code/application/BUILD +++ b/score/TimeSlave/code/application/BUILD @@ -23,7 +23,7 @@ cc_binary( features = COMPILER_WARNING_FEATURES, tags = ["QM"], deps = [ - "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeSlave/code/common:logging_contexts", "//score/TimeSlave/code/gptp:gptp_engine", "//score/libTSClient:gptp_ipc", "//score/time/HighPrecisionLocalSteadyClock", diff --git a/score/TimeSlave/code/application/time_slave.cpp b/score/TimeSlave/code/application/time_slave.cpp index 8ab865f..c4e8d3e 100644 --- a/score/TimeSlave/code/application/time_slave.cpp +++ b/score/TimeSlave/code/application/time_slave.cpp @@ -12,7 +12,7 @@ ********************************************************************************/ #include "score/TimeSlave/code/application/time_slave.h" -#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/TimeSlave/code/common/logging_contexts.h" #include "score/mw/log/logging.h" #include "score/time/HighPrecisionLocalSteadyClock/details/factory_impl.h" @@ -57,7 +57,7 @@ std::int32_t TimeSlave::Run(const score::cpp::stop_token& token) while (!token.stop_requested()) { - PtpTimeInfo info{}; + score::td::PtpTimeInfo info{}; if (engine_->ReadPTPSnapshot(info)) { publisher_.Publish(info); diff --git a/score/TimeSlave/code/common/BUILD b/score/TimeSlave/code/common/BUILD new file mode 100644 index 0000000..45f383d --- /dev/null +++ b/score/TimeSlave/code/common/BUILD @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +cc_library( + name = "logging_contexts", + hdrs = ["logging_contexts.h"], + visibility = ["//score/TimeSlave:__subpackages__"], +) diff --git a/score/TimeSlave/code/common/logging_contexts.h b/score/TimeSlave/code/common/logging_contexts.h new file mode 100644 index 0000000..dca150e --- /dev/null +++ b/score/TimeSlave/code/common/logging_contexts.h @@ -0,0 +1,34 @@ +/* + * @Author: chenhao.gao chenhao.gao@ecarxgroup.com + * @Date: 2026-03-27 14:02:10 + * @LastEditors: chenhao.gao chenhao.gao@ecarxgroup.com + * @LastEditTime: 2026-03-27 14:03:37 + * @FilePath: /score_inc_time/score/TimeSlave/code/common/logging_contexts.h + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_COMMON_LOGGING_CONTEXTS_H +#define SCORE_TIMESLAVE_CODE_COMMON_LOGGING_CONTEXTS_H + +namespace score +{ +namespace ts +{ + +constexpr auto kGPtpMachineContext = "GPTP_SLAVE"; + +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_COMMON_LOGGING_CONTEXTS_H From 56648fbbeb3555603e8f468d51c358a4ca17213e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 27 Mar 2026 14:49:52 +0800 Subject: [PATCH 04/12] Add docs content --- docs/TimeSlave/_assets/gptp_engine_class.puml | 104 ++++ docs/TimeSlave/_assets/gptp_threading.puml | 51 ++ docs/TimeSlave/_assets/ipc_channel.puml | 52 ++ docs/TimeSlave/_assets/ipc_sequence.puml | 46 ++ docs/TimeSlave/_assets/timeslave_class.puml | 32 +- .../_assets/timeslave_data_flow.puml | 20 +- .../_assets/timeslave_deployment.puml | 32 +- docs/TimeSlave/index.rst | 474 +++++++++++++++--- .../TimeSlave/code/common/logging_contexts.h | 12 + score/TimeSlave/code/gptp/BUILD | 5 +- 10 files changed, 708 insertions(+), 120 deletions(-) create mode 100644 docs/TimeSlave/_assets/gptp_engine_class.puml create mode 100644 docs/TimeSlave/_assets/gptp_threading.puml create mode 100644 docs/TimeSlave/_assets/ipc_channel.puml create mode 100644 docs/TimeSlave/_assets/ipc_sequence.puml diff --git a/docs/TimeSlave/_assets/gptp_engine_class.puml b/docs/TimeSlave/_assets/gptp_engine_class.puml new file mode 100644 index 0000000..c29842d --- /dev/null +++ b/docs/TimeSlave/_assets/gptp_engine_class.puml @@ -0,0 +1,104 @@ +@startuml +!theme plain + +title gPTP Engine Internal Class Diagram + +legend top left + |= Color |= Description | + | <#LightSalmon> | gPTP Engine core | + | <#Wheat> | Protocol processing | + | <#Lavender> | PHC adjustment | + | <#LightSkyBlue> | Platform abstraction | + | <#Beige> | Instrumentation | +endlegend + +package "score::ts::gptp" { + + class GptpEngine #LightSalmon { + - options_ : GptpEngineOptions + - rx_thread_ : std::thread + - pdelay_thread_ : std::thread + - socket_ : std::unique_ptr + - codec_ : FrameCodec + - parser_ : MessageParser + - sync_sm_ : SyncStateMachine + - pdelay_ : PeerDelayMeasurer + - phc_ : PhcAdjuster + - probe_mgr_ : ProbeManager + - recorder_ : Recorder + - snapshot_mutex_ : std::mutex + - latest_snapshot_ : PtpTimeInfo + + Initialize() : bool + + Deinitialize() : void + + ReadPTPSnapshot() : PtpTimeInfo + } + + interface IRawSocket #LightSkyBlue { + + Open(iface) : bool + + EnableHwTimestamping() : bool + + Recv(buf, timeout_ms) : RecvResult + + Send(buf, hw_ts) : bool + + GetFd() : int + + Close() : void + } + + class "RawSocket\n<>" as LinuxSocket #LightSkyBlue { + AF_PACKET + SO_TIMESTAMPING + } + + class "RawSocket\n<>" as QnxSocket #LightSkyBlue { + QNX raw-socket shim + } + + interface INetworkIdentity #LightSkyBlue { + + Resolve(iface) : bool + + GetClockIdentity() : ClockIdentity + } + + class NetworkIdentity #LightSkyBlue { + Derives EUI-64 from MAC\n(inserts 0xFF 0xFE) + } + + IRawSocket <|.. LinuxSocket + IRawSocket <|.. QnxSocket + INetworkIdentity <|.. NetworkIdentity + + GptpEngine *-- IRawSocket + GptpEngine *-- INetworkIdentity +} + +package "score::ts::gptp::details" { + class FrameCodec #Wheat + class MessageParser #Wheat + class SyncStateMachine #Wheat + class PeerDelayMeasurer #Wheat +} + +package "score::ts::gptp::phc" { + class PhcAdjuster #Lavender +} + +package "score::ts::gptp::instrument" { + class ProbeManager #Beige { + + {static} Instance() : ProbeManager& + + Record(point, data) : void + + SetRecorder(recorder) : void + } + + class Recorder #Beige { + - file_ : std::ofstream + - mutex_ : std::mutex + + Record(entry) : void + } + + ProbeManager --> Recorder +} + +GptpEngine *-- FrameCodec +GptpEngine *-- MessageParser +GptpEngine *-- SyncStateMachine +GptpEngine *-- PeerDelayMeasurer +GptpEngine *-- PhcAdjuster +GptpEngine *-- ProbeManager + +@enduml diff --git a/docs/TimeSlave/_assets/gptp_threading.puml b/docs/TimeSlave/_assets/gptp_threading.puml new file mode 100644 index 0000000..79ee3b2 --- /dev/null +++ b/docs/TimeSlave/_assets/gptp_threading.puml @@ -0,0 +1,51 @@ +@startuml gptp_threading_model + +title gPTP Engine Threading Model + +legend top left + |= Color |= Description | + | <#LightSalmon> | RxThread | + | <#LightSkyBlue> | PdelayThread | + | <#LightCyan> | Main Thread (TimeSlave) | +endlegend + +|#LightCyan| Main Thread +start +:Initialize GptpEngine; +:Start RxThread; +:Start PdelayThread; + +fork + |#LightSalmon| RxThread + repeat + :Wait for gPTP frame; + :Recv Sync frame; + :Parse + SyncStateMachine\nstore Sync timestamp; + :Recv FollowUp frame; + :Parse + SyncStateMachine\ncompute offset & rate ratio; + :Update latest_snapshot_\n(mutex protected); + repeat while (stop_token?) + stop + +fork again + |#LightSkyBlue| PdelayThread + repeat + :Sleep(pdelay_interval_ms); + :Send PDelayReq; + :Recv PDelayResp; + :Recv PDelayRespFollowUp\ncompute path delay; + :Update PDelayResult; + repeat while (stop_token?) + stop + +fork again + |#LightCyan| Main Thread + repeat + :ReadPTPSnapshot(); + :Publish PtpTimeInfo\nvia GptpIpcPublisher; + repeat while (stop_token?) + stop + +end fork + +@enduml diff --git a/docs/TimeSlave/_assets/ipc_channel.puml b/docs/TimeSlave/_assets/ipc_channel.puml new file mode 100644 index 0000000..bc7d9b4 --- /dev/null +++ b/docs/TimeSlave/_assets/ipc_channel.puml @@ -0,0 +1,52 @@ +@startuml +!theme plain + +title libTSClient Shared Memory IPC + +legend top left + |= Color |= Description | + | <#LightPink> | IPC components | + | <#LightCyan> | Shared memory region | +endlegend + +package "TimeSlave Process" { + class GptpIpcPublisher #LightPink { + - region_ : GptpIpcRegion* + - fd_ : int + + Init(name) : bool + + Publish(info) : void + + Destroy() : void + } +} + +package "Shared Memory" { + class GptpIpcRegion <> #LightCyan { + + magic : uint32_t = 0x47505440 + + seq : std::atomic + + data : PtpTimeInfo + -- + 64-byte aligned for\ncache line efficiency + } +} + +package "TimeDaemon Process" { + class GptpIpcReceiver #LightPink { + - region_ : const GptpIpcRegion* + - fd_ : int + + Init(name) : bool + + Receive() : std::optional + + Close() : void + } +} + +GptpIpcPublisher --> GptpIpcRegion : "shm_open(O_CREAT)\nmmap(PROT_WRITE)" +GptpIpcReceiver --> GptpIpcRegion : "shm_open(O_RDONLY)\nmmap(PROT_READ)" + +note right of GptpIpcRegion + **Seqlock Protocol:** + Writer: seq++ → memcpy → seq++ + Reader: read seq (even) → memcpy → check seq + Retry up to 20 times on torn read +end note + +@enduml diff --git a/docs/TimeSlave/_assets/ipc_sequence.puml b/docs/TimeSlave/_assets/ipc_sequence.puml new file mode 100644 index 0000000..7e7bda3 --- /dev/null +++ b/docs/TimeSlave/_assets/ipc_sequence.puml @@ -0,0 +1,46 @@ +@startuml +!theme plain + +title libTSClient Seqlock IPC Protocol + +participant "TimeSlave\n(GptpIpcPublisher)" as PUB #LightPink +participant "SharedMemory\n(GptpIpcRegion)" as SHM #LightCyan +participant "TimeDaemon\n(GptpIpcReceiver)" as RCV #LightPink + +== Initialization == + +PUB -> SHM : shm_open("/gptp_ptp_info", O_CREAT | O_RDWR) +PUB -> SHM : ftruncate(sizeof(GptpIpcRegion)) +PUB -> SHM : mmap(PROT_READ | PROT_WRITE) +PUB -> SHM : write magic = 0x47505440 + +... + +RCV -> SHM : shm_open("/gptp_ptp_info", O_RDONLY) +RCV -> SHM : mmap(PROT_READ) +RCV -> SHM : verify magic == 0x47505440 + +== Publish (Writer Side) == + +PUB -> SHM : seq.fetch_add(1, release) // seq becomes odd +PUB -> SHM : memcpy(data, &info, sizeof) +PUB -> SHM : seq.fetch_add(1, release) // seq becomes even + +== Receive (Reader Side) == + +loop up to 20 retries + RCV -> SHM : s1 = seq.load(acquire) + alt s1 is odd (write in progress) + RCV -> RCV : retry + else s1 is even + RCV -> SHM : memcpy(&local, data, sizeof) + RCV -> SHM : s2 = seq.load(acquire) + alt s1 == s2 + RCV --> RCV : return PtpTimeInfo + else s1 != s2 (torn read) + RCV -> RCV : retry + end + end +end + +@enduml diff --git a/docs/TimeSlave/_assets/timeslave_class.puml b/docs/TimeSlave/_assets/timeslave_class.puml index 68b3738..e612d6f 100644 --- a/docs/TimeSlave/_assets/timeslave_class.puml +++ b/docs/TimeSlave/_assets/timeslave_class.puml @@ -3,9 +3,19 @@ title TimeSlave Class Diagram +legend top left + |= Color |= Description | + | <#LightCyan> | TimeSlave application | + | <#LightSalmon> | gPTP Engine core | + | <#Wheat> | Protocol processing | + | <#Lavender> | PHC adjustment | + | <#LightPink> | IPC components | + | <#Beige> | Data structures | +endlegend + package "score::ts" { - class TimeSlave { + class TimeSlave #LightCyan { - engine_ : GptpEngine - publisher_ : GptpIpcPublisher - clock_ : HighPrecisionLocalSteadyClock @@ -14,7 +24,7 @@ package "score::ts" { + Deinitialize() : score::cpp::expected } - class GptpEngine { + class GptpEngine #LightSalmon { - options_ : GptpEngineOptions - rx_thread_ : std::thread - pdelay_thread_ : std::thread @@ -33,7 +43,7 @@ package "score::ts" { - PdelayThreadFunc(stop_token) : void } - struct GptpEngineOptions { + struct GptpEngineOptions #Beige { + interface_name : std::string + pdelay_interval_ms : uint32_t + sync_timeout_ms : uint32_t @@ -47,16 +57,16 @@ package "score::ts" { } package "score::ts::gptp::details" { - class FrameCodec { + class FrameCodec #Wheat { + ParseEthernetHeader(buf) : EthernetHeader + AddEthernetHeader(buf, dst_mac, src_mac) : void } - class MessageParser { + class MessageParser #Wheat { + Parse(payload, hw_ts) : std::optional } - class SyncStateMachine { + class SyncStateMachine #Wheat { - last_sync_ : PTPMessage - last_offset_ns_ : int64_t - neighbor_rate_ratio_ : double @@ -67,7 +77,7 @@ package "score::ts::gptp::details" { + GetNeighborRateRatio() : double } - class PeerDelayMeasurer { + class PeerDelayMeasurer #Wheat { - mutex_ : std::mutex - result_ : PDelayResult + SendRequest(socket) : void @@ -76,7 +86,7 @@ package "score::ts::gptp::details" { + GetResult() : PDelayResult } - struct SyncResult { + struct SyncResult #Beige { + master_ns : int64_t + offset_ns : int64_t + sync_fup_data : SyncFupData @@ -84,14 +94,14 @@ package "score::ts::gptp::details" { + time_jump_backward : bool } - struct PDelayResult { + struct PDelayResult #Beige { + path_delay_ns : int64_t + valid : bool } } package "score::ts::gptp::phc" { - class PhcAdjuster { + class PhcAdjuster #Lavender { - config_ : PhcConfig - fd_ : int + IsEnabled() : bool @@ -99,7 +109,7 @@ package "score::ts::gptp::phc" { + AdjustFrequency(ppb) : void } - struct PhcConfig { + struct PhcConfig #Beige { + enabled : bool + device_path : std::string + step_threshold_ns : int64_t diff --git a/docs/TimeSlave/_assets/timeslave_data_flow.puml b/docs/TimeSlave/_assets/timeslave_data_flow.puml index 235c3a7..d7c4b15 100644 --- a/docs/TimeSlave/_assets/timeslave_data_flow.puml +++ b/docs/TimeSlave/_assets/timeslave_data_flow.puml @@ -3,16 +3,16 @@ title TimeSlave Data Flow -participant "Network\n(gPTP Master)" as NET -participant "RawSocket" as SOCK -participant "FrameCodec" as FC -participant "MessageParser" as MP -participant "SyncStateMachine" as SSM -participant "PeerDelayMeasurer" as PDM -participant "PhcAdjuster" as PHC -participant "GptpEngine" as GE -participant "GptpIpcPublisher" as PUB -participant "SharedMemory" as SHM +participant "Network\n(gPTP Master)" as NET #Beige +participant "RawSocket" as SOCK #LightSkyBlue +participant "FrameCodec" as FC #Wheat +participant "MessageParser" as MP #Wheat +participant "SyncStateMachine" as SSM #Wheat +participant "PeerDelayMeasurer" as PDM #Wheat +participant "PhcAdjuster" as PHC #Lavender +participant "GptpEngine" as GE #LightSalmon +participant "GptpIpcPublisher" as PUB #LightPink +participant "SharedMemory" as SHM #LightPink == RxThread — Sync/FollowUp Processing == diff --git a/docs/TimeSlave/_assets/timeslave_deployment.puml b/docs/TimeSlave/_assets/timeslave_deployment.puml index b168817..e70de49 100644 --- a/docs/TimeSlave/_assets/timeslave_deployment.puml +++ b/docs/TimeSlave/_assets/timeslave_deployment.puml @@ -5,26 +5,26 @@ title TimeSlave Deployment View node "ECU" { package "TimeSlave Process" as TSP { - component [GptpEngine] as GE - component [GptpIpcPublisher] as PUB + component [GptpEngine] as GE #LightSalmon + component [GptpIpcPublisher] as PUB #LightPink package "RxThread" as RXT { - component [FrameCodec] as FC - component [MessageParser] as MP - component [SyncStateMachine] as SSM + component [FrameCodec] as FC #Wheat + component [MessageParser] as MP #Wheat + component [SyncStateMachine] as SSM #Wheat } package "PdelayThread" as PDT { - component [PeerDelayMeasurer] as PDM + component [PeerDelayMeasurer] as PDM #Wheat } - component [PhcAdjuster] as PHC - component [ProbeManager] as PM - component [Recorder] as REC + component [PhcAdjuster] as PHC #Lavender + component [ProbeManager] as PM #Beige + component [Recorder] as REC #Beige } package "TimeDaemon Process" as TDP { - component [GptpIpcReceiver] as RCV + component [GptpIpcReceiver] as RCV #LightPink } database "Shared Memory\n/gptp_ptp_info" as SHM @@ -44,15 +44,15 @@ FC --> MP MP --> SSM MP --> PDM -PUB --> SHM : seqlock write -RCV --> SHM : seqlock read +PUB -[#green]-> SHM : seqlock write +RCV -[#green]-> SHM : seqlock read -RXT --> SOCK : recv -PDT --> SOCK : send/recv +RXT -[#blue]-> SOCK : recv +PDT -[#blue]-> SOCK : send/recv -PHC --> PHCDEV : clock_adjtime +PHC -[#orange]-> PHCDEV : clock_adjtime -SOCK --> NET : gPTP frames\nEtherType 0x88F7 +SOCK -[#blue]-> NET : gPTP frames\nEtherType 0x88F7 PM --> REC : probe events diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 2db74f3..84d3843 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -1,134 +1,372 @@ -.. - # ******************************************************************************* - # Copyright (c) 2026 Contributors to the Eclipse Foundation - # - # See the NOTICE file(s) distributed with this work for additional - # information regarding copyright ownership. - # - # This program and the accompanying materials are made available under the - # terms of the Apache License Version 2.0 which is available at - # https://www.apache.org/licenses/LICENSE-2.0 - # - # SPDX-License-Identifier: Apache-2.0 - # ******************************************************************************* - -.. _timeslave_design: - -############################ -TimeSlave Design -############################ +Concept for TimeSlave +====================== .. contents:: Table of Contents :depth: 3 :local: -Overview -======== +TimeSlave concept +------------------ -**TimeSlave** is a standalone process that implements the gPTP (IEEE 802.1AS) slave endpoint -for the Eclipse SCORE time synchronization system. It receives gPTP Sync/FollowUp messages -from a Time Master on the Ethernet network, measures peer delay, optionally adjusts the PTP -Hardware Clock (PHC), and publishes the resulting ``PtpTimeInfo`` to shared memory for -consumption by the **TimeDaemon**. +Use Cases +~~~~~~~~~ -TimeSlave is deployed as a separate process from TimeDaemon to isolate the real-time -network I/O (raw socket operations, hardware timestamping) from the higher-level time -validation and distribution logic. +TimeSlave is a standalone gPTP (IEEE 802.1AS) slave endpoint process that implements the low-level time synchronization protocol for the Eclipse SCORE time system. It is deployed as a separate process from the TimeDaemon to isolate real-time network I/O from the higher-level time validation and distribution logic. -Architecture -============ +More precisely we can specify the following use cases for the TimeSlave: -The TimeSlave process is composed of the following major components: +1. Receiving gPTP Sync/FollowUp messages from a Time Master on the Ethernet network +2. Measuring peer delay via the IEEE 802.1AS PDelayReq/PDelayResp exchange +3. Optionally adjusting the PTP Hardware Clock (PHC) on the NIC +4. Publishing the resulting ``PtpTimeInfo`` to shared memory for consumption by the TimeDaemon -.. list-table:: - :header-rows: 1 - :widths: 25 75 - - * - Component - - Responsibility - * - **TimeSlave Application** - - Lifecycle management (Initialize, Run, Deinitialize). Integrates with ``score::mw::lifecycle``. - * - **GptpEngine** - - Core gPTP protocol engine. Manages RxThread and PdelayThread. - * - **libTSClient (GptpIpcPublisher)** - - Publishes ``PtpTimeInfo`` to POSIX shared memory using a seqlock protocol. - * - **PhcAdjuster** - - Adjusts the PTP Hardware Clock via step or frequency corrections. - * - **ProbeManager / Recorder** - - Runtime instrumentation and CSV-based event recording. - -Deployment view ---------------- +The raw architectural diagram is represented below. .. raw:: html
.. uml:: _assets/timeslave_deployment.puml - :alt: TimeSlave Deployment View + :alt: Raw architectural diagram .. raw:: html
+Components decomposition +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The design consists of several sw components: + +1. `TimeSlave Application <#timeslave-application-sw-component>`_ +2. `GptpEngine <#gptpengine-sw-component>`_ +3. `FrameCodec <#framecodec-sw-component>`_ +4. `MessageParser <#messageparser-sw-component>`_ +5. `SyncStateMachine <#syncstatemachine-sw-component>`_ +6. `PeerDelayMeasurer <#peerdelaymeasurer-sw-component>`_ +7. `PhcAdjuster <#phcadjuster-sw-component>`_ +8. `libTSClient <#libtsclient-sw-component>`_ + Class view ----------- +~~~~~~~~~~ + +Main classes and components are presented on this diagram: .. raw:: html
.. uml:: _assets/timeslave_class.puml - :alt: TimeSlave Class Diagram + :alt: Class View + :width: 100% + :align: center .. raw:: html
-Data flow ---------- +Data and control flow +~~~~~~~~~~~~~~~~~~~~~ -The end-to-end data flow from network frame reception to shared memory publication: +The Data and Control flow are presented in the following diagram: .. raw:: html
.. uml:: _assets/timeslave_data_flow.puml - :alt: TimeSlave Data Flow + :alt: Data and Control flow View + +.. raw:: html + +
+ +On this view you could see several "workers" scopes: + +1. RxThread scope +2. PdelayThread scope +3. Main thread (periodic publish) scope + +Each control flow is implemented with the dedicated thread and is independent from another ones. + +Control flows +^^^^^^^^^^^^^ + +RxThread scope +'''''''''''''' + +This control flow is responsible for the: + +1. receive raw gPTP Ethernet frames with hardware timestamps from the NIC via raw sockets +2. decode and parse the PTP messages (Sync, FollowUp, PdelayResp, PdelayRespFollowUp) +3. correlate Sync/FollowUp pairs and compute clock offset and neighborRateRatio +4. update the shared ``PtpTimeInfo`` snapshot under mutex protection + +PdelayThread scope +''''''''''''''''''' + +This control flow is responsible for the: + +1. periodically transmit PDelayReq frames and capture hardware transmit timestamps +2. coordinate with the RxThread to receive PDelayResp and PDelayRespFollowUp messages +3. compute the peer delay using the IEEE 802.1AS formula: ``path_delay = ((t2 - t1) + (t4 - t3c)) / 2`` + +Main thread (periodic publish) scope +'''''''''''''''''''''''''''''''''''''' + +This control flow is responsible for the: + +1. periodically call ``GptpEngine::ReadPTPSnapshot()`` to get the latest time measurement +2. enrich the snapshot with the local clock timestamp from ``HighPrecisionLocalSteadyClock`` +3. publish to shared memory via ``GptpIpcPublisher::Publish()`` + +Data types or events +^^^^^^^^^^^^^^^^^^^^ + +There are several data types, which components are communicating to each other: + +PTPMessage +'''''''''' + +``PTPMessage`` is a union-based container for decoded gPTP messages including the hardware receive timestamp. It is produced by ``MessageParser`` and consumed by ``SyncStateMachine`` and ``PeerDelayMeasurer``. + +SyncResult +'''''''''' + +``SyncResult`` is produced by ``SyncStateMachine::OnFollowUp()`` and contains the computed master timestamp, clock offset, Sync/FollowUp data, and time jump flags (forward/backward). + +PDelayResult +'''''''''''' + +``PDelayResult`` is produced by ``PeerDelayMeasurer`` and contains the computed path delay in nanoseconds and a validity flag. + +PtpTimeInfo +'''''''''''' + +``PtpTimeInfo`` is the aggregated snapshot that combines PTP status flags, Sync/FollowUp data, peer delay data, and a local clock reference. This is the data published to shared memory for the TimeDaemon. + +SW Components decomposition +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TimeSlave Application SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``TimeSlave Application`` component is the main entry point for the TimeSlave process. It extends ``score::mw::lifecycle::Application`` and is responsible for orchestrating the overall lifecycle of the GptpEngine and the IPC publisher. + +Component requirements +'''''''''''''''''''''' + +The ``TimeSlave Application`` has the following requirements: + +- The ``TimeSlave Application`` shall implement the ``Initialize()`` method to create the ``GptpEngine`` with configured options, initialize the ``GptpIpcPublisher`` (creates the shared memory segment), and prepare the ``HighPrecisionLocalSteadyClock`` for local time reference +- The ``TimeSlave Application`` shall implement the ``Run()`` method to start the GptpEngine, enter a periodic publish loop, and monitor the ``stop_token`` for graceful shutdown +- The ``TimeSlave Application`` shall implement the ``Deinitialize()`` method to stop the GptpEngine threads and destroy the shared memory segment +- The ``TimeSlave Application`` shall periodically read the latest ``PtpTimeInfo`` snapshot, enrich it with the local clock timestamp, and publish it via ``GptpIpcPublisher`` + +GptpEngine SW component +^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``GptpEngine`` component is the core gPTP protocol engine. It manages two background threads (RxThread and PdelayThread) for network I/O and peer delay measurement, and exposes a thread-safe ``ReadPTPSnapshot()`` method for the main thread to read the latest time measurement. + +Component requirements +'''''''''''''''''''''' + +The ``GptpEngine`` has the following requirements: + +- The ``GptpEngine`` shall manage an RxThread for receiving and parsing gPTP frames from raw Ethernet sockets +- The ``GptpEngine`` shall manage a PdelayThread for periodic peer delay measurement +- The ``GptpEngine`` shall provide a thread-safe ``ReadPTPSnapshot()`` method that returns the latest ``PtpTimeInfo`` +- The ``GptpEngine`` shall support configurable parameters via ``GptpEngineOptions`` (interface name, PDelay interval, sync timeout, time jump thresholds, PHC configuration) +- The ``GptpEngine`` shall support exchangeability of the raw socket implementation for different platforms (Linux, QNX) + +Class view +'''''''''' + +The Class Diagram is presented below: + +.. raw:: html + +
+ +.. uml:: _assets/gptp_engine_class.puml + :alt: Class Diagram + +.. raw:: html + +
+ +Threading model +''''''''''''''' + +The GptpEngine operates with two background threads. The threading model is represented below: + +.. raw:: html + +
+ +.. uml:: _assets/gptp_threading.puml + :alt: Threading Model .. raw:: html
-Application lifecycle -===================== +Concurrency aspects +''''''''''''''''''' + +The ``GptpEngine`` uses the following synchronization mechanisms: + +- A ``std::mutex`` protects the ``latest_snapshot_`` field, shared between the RxThread (writer) and the main thread (reader via ``ReadPTPSnapshot()``) +- The ``PeerDelayMeasurer`` uses its own ``std::mutex`` to synchronize between the PdelayThread (``SendRequest()``) and the RxThread (``OnResponse()``, ``OnResponseFollowUp()``) +- The ``SyncStateMachine`` uses ``std::atomic`` for the timeout flag, which is read from the main thread and written from the RxThread -The ``TimeSlave`` class extends ``score::mw::lifecycle::Application`` and follows the -standard SCORE lifecycle pattern: +FrameCodec SW component +^^^^^^^^^^^^^^^^^^^^^^^^^ -1. **Initialize** — Creates the ``GptpEngine`` with configured options, initializes - the ``GptpIpcPublisher`` (creates shared memory segment), and prepares the - ``HighPrecisionLocalSteadyClock`` for local time reference. +The ``FrameCodec`` component handles raw Ethernet frame encoding and decoding for gPTP communication. -2. **Run** — Starts the GptpEngine (which spawns RxThread and PdelayThread internally). - Enters a periodic loop that: +Component requirements +'''''''''''''''''''''' - - Calls ``GptpEngine::ReadPTPSnapshot()`` to get the latest time measurement - - Enriches the snapshot with the local clock timestamp - - Publishes to shared memory via ``GptpIpcPublisher::Publish()`` - - Monitors the ``stop_token`` for graceful shutdown +The ``FrameCodec`` has the following requirements: -3. **Deinitialize** — Stops the GptpEngine threads, destroys the shared memory segment. +- The ``FrameCodec`` shall parse incoming Ethernet frames, extracting source/destination MAC addresses, handling 802.1Q VLAN tags, and validating the EtherType (``0x88F7``) +- The ``FrameCodec`` shall construct outgoing Ethernet headers for PDelayReq frames using the standard PTP multicast destination MAC (``01:80:C2:00:00:0E``) + +MessageParser SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``MessageParser`` component parses the PTP wire format (IEEE 1588-v2) from raw payload bytes. + +Component requirements +'''''''''''''''''''''' + +The ``MessageParser`` has the following requirements: + +- The ``MessageParser`` shall validate the PTP header (version, domain, message length) +- The ``MessageParser`` shall decode all relevant message types: Sync, FollowUp, PdelayReq, PdelayResp, PdelayRespFollowUp +- The ``MessageParser`` shall use packed wire structures (``__attribute__((packed))``) for direct memory mapping of PTP messages + +SyncStateMachine SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``SyncStateMachine`` component implements the two-step Sync/FollowUp correlation logic. It correlates incoming Sync and FollowUp messages by sequence ID, computes the clock offset and neighbor rate ratio, and detects time jumps. + +Component requirements +'''''''''''''''''''''' + +The ``SyncStateMachine`` has the following requirements: + +- The ``SyncStateMachine`` shall store Sync messages and correlate them with subsequent FollowUp messages by sequence ID +- The ``SyncStateMachine`` shall compute the clock offset: ``offset_ns = master_time - slave_receive_time - path_delay`` +- The ``SyncStateMachine`` shall compute the ``neighborRateRatio`` from successive Sync intervals (master vs. slave clock progression) +- The ``SyncStateMachine`` shall detect forward and backward time jumps against configurable thresholds +- The ``SyncStateMachine`` shall provide thread-safe timeout detection via ``std::atomic``, set when no Sync is received within the configured timeout + +PeerDelayMeasurer SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``PeerDelayMeasurer`` component implements the IEEE 802.1AS two-step peer delay measurement protocol. It manages the four timestamps (``t1``, ``t2``, ``t3c``, ``t4``) across two threads. + +Component requirements +'''''''''''''''''''''' + +The ``PeerDelayMeasurer`` has the following requirements: + +- The ``PeerDelayMeasurer`` shall transmit PDelayReq frames and capture the hardware transmit timestamp (``t1``) +- The ``PeerDelayMeasurer`` shall receive PDelayResp (providing ``t2``, ``t4``) and PDelayRespFollowUp (providing ``t3c``) messages +- The ``PeerDelayMeasurer`` shall compute the peer delay using the IEEE 802.1AS formula: ``path_delay = ((t2 - t1) + (t4 - t3c)) / 2`` +- The ``PeerDelayMeasurer`` shall provide thread-safe access to the ``PDelayResult`` via a mutex, as ``SendRequest()`` runs on the PdelayThread while response handlers are called from the RxThread + +PhcAdjuster SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``PhcAdjuster`` component synchronizes the PTP Hardware Clock (PHC) on the NIC. It applies step corrections for large offsets and frequency slew for smooth convergence of small offsets. + +Component requirements +'''''''''''''''''''''' + +The ``PhcAdjuster`` has the following requirements: + +- The ``PhcAdjuster`` shall apply an immediate time step correction for offsets exceeding ``step_threshold_ns`` +- The ``PhcAdjuster`` shall apply frequency slew (in ppb) for offsets below the step threshold +- The ``PhcAdjuster`` shall support platform-specific implementations: ``clock_adjtime()`` on Linux, EMAC PTP ioctls on QNX +- The ``PhcAdjuster`` shall be configurable via ``PhcConfig`` (device path, step threshold, enable/disable flag) + +libTSClient SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``libTSClient`` component is the shared memory IPC library that connects the TimeSlave process to the TimeDaemon process. It provides a lock-free, single-writer/multi-reader communication channel using a seqlock protocol over POSIX shared memory. + +The component provides two sub components: publisher and receiver to be deployed on the TimeSlave and TimeDaemon sides accordingly. + +Component requirements +'''''''''''''''''''''' + +The ``libTSClient`` has the following requirements: + +- The ``libTSClient`` shall define a shared memory layout (``GptpIpcRegion``) with a magic number for validation, an atomic seqlock counter, and a ``PtpTimeInfo`` data payload +- The ``libTSClient`` shall align the shared memory region to 64 bytes (cache line size) to prevent false sharing +- The ``libTSClient`` shall provide a ``GptpIpcPublisher`` component that creates and manages the POSIX shared memory segment and writes ``PtpTimeInfo`` using the seqlock protocol +- The ``libTSClient`` shall provide a ``GptpIpcReceiver`` component that opens the shared memory segment read-only and reads ``PtpTimeInfo`` with up to 20 seqlock retries +- The ``libTSClient`` shall use the POSIX shared memory name ``/gptp_ptp_info`` by default + +Class view +'''''''''' + +The Class Diagram is presented below: + +.. raw:: html + +
+ +.. uml:: _assets/ipc_channel.puml + :alt: Class Diagram + +.. raw:: html + +
+ +Publish new data +'''''''''''''''' + +When ``TimeSlave Application`` has a new ``PtpTimeInfo`` snapshot, it publishes to the shared memory via the seqlock protocol: + +1. Increment ``seq`` (becomes odd — signals write in progress) +2. ``memcpy`` the data +3. Increment ``seq`` (becomes even — signals write complete) + +Receive data +'''''''''''' + +From TimeDaemon side, the receiver reads from the shared memory using the seqlock protocol with bounded retry: + +1. Read ``seq`` (must be even, otherwise retry) +2. ``memcpy`` the data +3. Read ``seq`` again (must match step 1, otherwise retry — torn read detected) +4. Return ``std::optional`` (empty if all 20 retries exhausted) + +The seqlock protocol workflow is presented in the following sequence diagram: + +.. raw:: html + +
+ +.. uml:: _assets/ipc_sequence.puml + :alt: Seqlock Protocol + +.. raw:: html + +
Platform support -================ +~~~~~~~~~~~~~~~~~ -TimeSlave supports two target platforms with platform-specific implementations: +TimeSlave supports two target platforms with platform-specific implementations selected at compile time via Bazel ``select()``: -.. list-table:: +.. list-table:: Platform Implementations :header-rows: 1 - :widths: 20 40 40 + :widths: 25 35 40 * - Component - Linux @@ -142,12 +380,84 @@ TimeSlave supports two target platforms with platform-specific implementations: * - PHC Adjuster - ``clock_adjtime()`` - EMAC PTP ioctls + * - HighPrecisionLocalSteadyClock + - ``std::chrono`` system clock + - QTIME clock API + +The ``IRawSocket`` and ``INetworkIdentity`` interfaces provide the abstraction boundary. Platform-specific source files are organized under ``score/TimeSlave/code/gptp/platform/linux/`` and ``score/TimeSlave/code/gptp/platform/qnx/``. + +Instrumentation +~~~~~~~~~~~~~~~~ + +ProbeManager +^^^^^^^^^^^^ -Platform selection is handled at compile time via Bazel ``select()`` in the BUILD files. +The ``ProbeManager`` is a singleton that records probe events at key processing points in the gPTP engine. Probe points include: -.. toctree:: - :maxdepth: 2 - :caption: Detailed Design +- ``RxPacketReceived`` — Raw frame received from socket +- ``SyncFrameParsed`` — Sync message successfully parsed +- ``FollowUpProcessed`` — Offset computed from Sync/FollowUp pair +- ``OffsetComputed`` — Final offset value available +- ``PdelayReqSent`` — PDelayReq frame transmitted +- ``PdelayCompleted`` — Peer delay measurement completed +- ``PhcAdjusted`` — PHC adjustment applied - gptp_engine/index - libTSClient/index +The ``GPTP_PROBE()`` macro provides zero-overhead when probing is disabled. + +Recorder +^^^^^^^^^ + +Thread-safe CSV file writer that persists probe events and other diagnostic data. Each ``RecordEntry`` contains a timestamp, event type, offset, peer delay, sequence ID, and status flags. + +Variability +~~~~~~~~~~~ + +Configuration +^^^^^^^^^^^^^ + +The ``GptpEngineOptions`` struct provides all configurable parameters for the gPTP engine: + +.. list-table:: GptpEngine Configuration + :header-rows: 1 + :widths: 30 15 55 + + * - Parameter + - Type + - Description + * - ``interface_name`` + - string + - Network interface for gPTP frames (e.g., ``eth0``) + * - ``pdelay_interval_ms`` + - uint32_t + - Interval between PDelayReq transmissions + * - ``sync_timeout_ms`` + - uint32_t + - Timeout for Sync message reception before declaring timeout state + * - ``time_jump_forward_ns`` + - int64_t + - Threshold for forward time jump detection + * - ``time_jump_backward_ns`` + - int64_t + - Threshold for backward time jump detection + * - ``phc_config`` + - PhcConfig + - PHC device path, step threshold, and enable flag + +The ``PhcConfig`` struct additionally contains: + +.. list-table:: PhcAdjuster Configuration + :header-rows: 1 + :widths: 30 15 55 + + * - Parameter + - Type + - Description + * - ``enabled`` + - bool + - Enable or disable PHC adjustment + * - ``device_path`` + - string + - Path to the PHC device (e.g., ``/dev/ptp0``) + * - ``step_threshold_ns`` + - int64_t + - Offset threshold above which a step correction is applied instead of frequency slew diff --git a/score/TimeSlave/code/common/logging_contexts.h b/score/TimeSlave/code/common/logging_contexts.h index dca150e..00ecc9e 100644 --- a/score/TimeSlave/code/common/logging_contexts.h +++ b/score/TimeSlave/code/common/logging_contexts.h @@ -1,3 +1,15 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ /* * @Author: chenhao.gao chenhao.gao@ecarxgroup.com * @Date: 2026-03-27 14:02:10 diff --git a/score/TimeSlave/code/gptp/BUILD b/score/TimeSlave/code/gptp/BUILD index 98dcbfa..ca025a0 100644 --- a/score/TimeSlave/code/gptp/BUILD +++ b/score/TimeSlave/code/gptp/BUILD @@ -20,7 +20,10 @@ cc_library( hdrs = ["gptp_engine.h"], features = COMPILER_WARNING_FEATURES, linkopts = select({ - "@platforms//os:qnx": ["-lsocket", "-lc"], + "@platforms//os:qnx": [ + "-lsocket", + "-lc", + ], "//conditions:default": ["-lpthread"], }), tags = ["QM"], From 81e30fe424ad8f4e57863e1d8a66aa7760c5089c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Wed, 1 Apr 2026 13:29:32 +0800 Subject: [PATCH 05/12] code review issue resolved --- .../real/details/real_ptp_engine.cpp | 27 +-- .../real/details/real_ptp_engine.h | 12 -- .../real/details/real_ptp_engine_test.cpp | 60 ------ score/TimeSlave/code/application/time_slave.h | 10 +- .../TimeSlave/code/common/logging_contexts.h | 20 -- score/TimeSlave/code/gptp/details/BUILD | 23 +++ .../TimeSlave/code/gptp/details/clock_util.h | 40 ++++ .../code/gptp/details/frame_codec.cpp | 63 ++++--- .../TimeSlave/code/gptp/details/frame_codec.h | 7 +- .../code/gptp/details/frame_codec_test.cpp | 20 +- .../code/gptp/details/message_parser.cpp | 48 ++++- .../code/gptp/details/message_parser_test.cpp | 39 +++- .../code/gptp/details/pdelay_measurer.cpp | 67 +++++-- .../code/gptp/details/pdelay_measurer.h | 4 +- score/TimeSlave/code/gptp/details/ptp_types.h | 73 ++++---- .../TimeSlave/code/gptp/details/raw_socket.h | 5 +- .../code/gptp/details/raw_socket_test.cpp | 174 ++++++++++++++++++ .../code/gptp/details/sync_state_machine.cpp | 44 ++--- .../code/gptp/details/sync_state_machine.h | 5 +- .../gptp/details/sync_state_machine_test.cpp | 5 +- score/TimeSlave/code/gptp/gptp_engine.cpp | 91 +++++---- score/TimeSlave/code/gptp/gptp_engine.h | 10 +- .../TimeSlave/code/gptp/gptp_engine_test.cpp | 88 ++++++++- .../TimeSlave/code/gptp/instrument/probe.cpp | 10 +- score/TimeSlave/code/gptp/instrument/probe.h | 4 +- score/TimeSlave/code/gptp/phc/phc_adjuster.h | 2 +- .../code/gptp/platform/linux/phc_adjuster.cpp | 19 +- .../code/gptp/platform/linux/raw_socket.cpp | 75 +++++--- score/TimeSlave/code/gptp/record/recorder.cpp | 19 +- score/TimeSlave/code/gptp/record/recorder.h | 6 +- .../code/gptp/record/recorder_test.cpp | 46 +++++ score/libTSClient/gptp_ipc_channel.h | 8 +- score/libTSClient/gptp_ipc_publisher.cpp | 20 +- score/libTSClient/gptp_ipc_receiver.cpp | 32 +++- score/libTSClient/gptp_ipc_test.cpp | 42 ++++- 35 files changed, 846 insertions(+), 372 deletions(-) create mode 100644 score/TimeSlave/code/gptp/details/clock_util.h create mode 100644 score/TimeSlave/code/gptp/details/raw_socket_test.cpp diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp index 8258250..6d94fd0 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp @@ -60,32 +60,7 @@ bool RealPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) if (!result.has_value()) return false; - cached_ = result.value(); - - const bool time_ok = ReadTimeValueAndStatus(info); - const bool pdelay_ok = ReadPDelayMeasurementData(info); - const bool sync_ok = ReadSyncMeasurementData(info); - return time_ok && pdelay_ok && sync_ok; -} - -bool RealPTPEngine::ReadTimeValueAndStatus(PtpTimeInfo& info) noexcept -{ - info.local_time = cached_.local_time; - info.ptp_assumed_time = cached_.ptp_assumed_time; - info.rate_deviation = cached_.rate_deviation; - info.status = cached_.status; - return true; -} - -bool RealPTPEngine::ReadPDelayMeasurementData(PtpTimeInfo& info) const noexcept -{ - info.pdelay_data = cached_.pdelay_data; - return true; -} - -bool RealPTPEngine::ReadSyncMeasurementData(PtpTimeInfo& info) const noexcept -{ - info.sync_fup_data = cached_.sync_fup_data; + info = result.value(); return true; } diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h index 992637c..fd47403 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h @@ -39,28 +39,16 @@ class RealPTPEngine final RealPTPEngine(RealPTPEngine&&) = delete; RealPTPEngine& operator=(RealPTPEngine&&) = delete; - /// Open and map the IPC channel. - /// @return true on success. bool Initialize(); - /// Unmap the IPC channel. - /// @return true (always succeeds). bool Deinitialize(); - /// Read a fresh snapshot from the IPC channel and populate @p info. - /// Delegates to ReadTimeValueAndStatus, ReadPDelayMeasurementData, - /// and ReadSyncMeasurementData. bool ReadPTPSnapshot(PtpTimeInfo& info); - bool ReadTimeValueAndStatus(PtpTimeInfo& info) noexcept; - bool ReadPDelayMeasurementData(PtpTimeInfo& info) const noexcept; - bool ReadSyncMeasurementData(PtpTimeInfo& info) const noexcept; - private: std::string ipc_name_; score::ts::details::GptpIpcReceiver receiver_; bool initialized_{false}; - PtpTimeInfo cached_{}; }; } // namespace details diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp index 94b6254..5ab42b4 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp @@ -211,66 +211,6 @@ TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesPDelayDataCorrectly) EXPECT_EQ(result.pdelay_data.resp_clock_identity, expected.pdelay_data.resp_clock_identity); } -// ── Individual sub-methods (called after ReadPTPSnapshot populates cache) ───── - -TEST_F(RealPTPEngineTest, ReadTimeValueAndStatus_FromCachedData_AlwaysReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - pub_.Publish(MakeTestInfo()); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo snap{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); - - // Call again on a fresh struct — should use the cached data. - PtpTimeInfo result{}; - EXPECT_TRUE(engine_->ReadTimeValueAndStatus(result)); - EXPECT_EQ(result.ptp_assumed_time, snap.ptp_assumed_time); - EXPECT_DOUBLE_EQ(result.rate_deviation, snap.rate_deviation); - EXPECT_EQ(result.status.is_synchronized, snap.status.is_synchronized); -} - -TEST_F(RealPTPEngineTest, ReadPDelayMeasurementData_FromCachedData_AlwaysReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - pub_.Publish(MakeTestInfo()); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo snap{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); - - PtpTimeInfo result{}; - EXPECT_TRUE(engine_->ReadPDelayMeasurementData(result)); - EXPECT_EQ(result.pdelay_data.pdelay, snap.pdelay_data.pdelay); -} - -TEST_F(RealPTPEngineTest, ReadSyncMeasurementData_FromCachedData_AlwaysReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - pub_.Publish(MakeTestInfo()); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo snap{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(snap)); - - PtpTimeInfo result{}; - EXPECT_TRUE(engine_->ReadSyncMeasurementData(result)); - EXPECT_EQ(result.sync_fup_data.sequence_id, snap.sync_fup_data.sequence_id); -} - -// Sub-methods on default-constructed cache (before any snapshot) return true -// with zeroed data. -TEST_F(RealPTPEngineTest, SubMethods_BeforeSnapshot_ReturnTrueWithZeroData) -{ - ASSERT_TRUE(pub_.Init(name_)); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo result{}; - EXPECT_TRUE(engine_->ReadTimeValueAndStatus(result)); - EXPECT_TRUE(engine_->ReadPDelayMeasurementData(result)); - EXPECT_TRUE(engine_->ReadSyncMeasurementData(result)); - EXPECT_EQ(result.ptp_assumed_time, std::chrono::nanoseconds{0}); -} } // namespace details } // namespace td diff --git a/score/TimeSlave/code/application/time_slave.h b/score/TimeSlave/code/application/time_slave.h index 5ae32a9..80c7182 100644 --- a/score/TimeSlave/code/application/time_slave.h +++ b/score/TimeSlave/code/application/time_slave.h @@ -36,13 +36,13 @@ namespace ts class TimeSlave final : public score::mw::lifecycle::Application { public: - explicit TimeSlave(); + TimeSlave(); ~TimeSlave() noexcept override = default; - TimeSlave(TimeSlave&&) noexcept = delete; - TimeSlave(const TimeSlave&) noexcept = delete; - TimeSlave& operator=(TimeSlave&&) & noexcept = delete; - TimeSlave& operator=(const TimeSlave&) & noexcept = delete; + TimeSlave(TimeSlave&&) = delete; + TimeSlave(const TimeSlave&) = delete; + TimeSlave& operator=(TimeSlave&&) & = delete; + TimeSlave& operator=(const TimeSlave&) & = delete; std::int32_t Initialize(const score::mw::lifecycle::ApplicationContext& context) override; std::int32_t Run(const score::cpp::stop_token& token) override; diff --git a/score/TimeSlave/code/common/logging_contexts.h b/score/TimeSlave/code/common/logging_contexts.h index 00ecc9e..7a12c23 100644 --- a/score/TimeSlave/code/common/logging_contexts.h +++ b/score/TimeSlave/code/common/logging_contexts.h @@ -1,23 +1,3 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ -/* - * @Author: chenhao.gao chenhao.gao@ecarxgroup.com - * @Date: 2026-03-27 14:02:10 - * @LastEditors: chenhao.gao chenhao.gao@ecarxgroup.com - * @LastEditTime: 2026-03-27 14:03:37 - * @FilePath: /score_inc_time/score/TimeSlave/code/common/logging_contexts.h - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ /******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * diff --git a/score/TimeSlave/code/gptp/details/BUILD b/score/TimeSlave/code/gptp/details/BUILD index e1f1c20..d727051 100644 --- a/score/TimeSlave/code/gptp/details/BUILD +++ b/score/TimeSlave/code/gptp/details/BUILD @@ -80,6 +80,15 @@ cc_library( deps = [":ptp_types"], ) +cc_library( + name = "clock_util", + hdrs = ["clock_util.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [":ptp_types"], +) + cc_library( name = "sync_state_machine", srcs = ["sync_state_machine.cpp"], @@ -88,6 +97,7 @@ cc_library( tags = ["QM"], visibility = ["//score:__subpackages__"], deps = [ + ":clock_util", ":ptp_types", "//score/TimeDaemon/code/common/data_types:ptp_time_info", ], @@ -142,6 +152,18 @@ cc_library( ], ) +cc_test( + name = "raw_socket_test", + srcs = ["raw_socket_test.cpp"], + tags = ["unit"], + deps = [ + ":network_identity", + ":raw_socket", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + cc_test( name = "pdelay_measurer_test", srcs = ["pdelay_measurer_test.cpp"], @@ -192,6 +214,7 @@ cc_unit_test_suites_for_host_and_qnx( ":frame_codec_test", ":message_parser_test", ":pdelay_measurer_test", + ":raw_socket_test", ":sync_state_machine_test", ], test_suites_from_sub_packages = [], diff --git a/score/TimeSlave/code/gptp/details/clock_util.h b/score/TimeSlave/code/gptp/details/clock_util.h new file mode 100644 index 0000000..bfa3dde --- /dev/null +++ b/score/TimeSlave/code/gptp/details/clock_util.h @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_CLOCK_UTIL_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_CLOCK_UTIL_H + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +inline std::int64_t MonoNs() noexcept +{ + ::timespec ts{}; + if (::clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0; + return static_cast(ts.tv_sec) * kNsPerSec + ts.tv_nsec; +} + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_CLOCK_UTIL_H diff --git a/score/TimeSlave/code/gptp/details/frame_codec.cpp b/score/TimeSlave/code/gptp/details/frame_codec.cpp index 11491c7..a195b0d 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec.cpp +++ b/score/TimeSlave/code/gptp/details/frame_codec.cpp @@ -13,7 +13,7 @@ #include "score/TimeSlave/code/gptp/details/frame_codec.h" #include -#include +#include #include namespace score @@ -26,65 +26,68 @@ namespace details namespace { -int Str2Mac(const char* s, unsigned char mac[kMacAddrLen]) noexcept -{ - unsigned int b[kMacAddrLen]{}; - if (std::sscanf(s, "%x:%x:%x:%x:%x:%x", &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != kMacAddrLen) - { - return -1; - } - for (int i = 0; i < kMacAddrLen; ++i) - mac[i] = static_cast(b[i]); - return 0; -} +constexpr std::array kPtpDstMacBytes = { + 0x01U, 0x80U, 0xC2U, 0x00U, 0x00U, 0x0EU}; + +constexpr std::size_t kVlanTciLen = 2U; } // namespace bool FrameCodec::ParseEthernetHeader(const std::uint8_t* frame, int frame_len, int& ptp_offset) const { - const int kEthHdrLen = static_cast(sizeof(ethhdr)); - if (frame_len < kEthHdrLen) + // Convert to size_t once (after the negative guard) to avoid signed/unsigned + // comparisons when mixing frame_len (int) with size constants (std::size_t). + if (frame_len <= 0) + return false; + const std::size_t len = static_cast(frame_len); + + constexpr std::size_t kEthHdrLen = sizeof(ethhdr); + if (len < kEthHdrLen) return false; ethhdr hdr{}; std::memcpy(&hdr, frame, sizeof(hdr)); - const auto etype = static_cast(ntohs(hdr.h_proto)); + const auto etype = static_cast(ntohs(hdr.h_proto)); - if (etype == static_cast(kEthP8021Q)) + if (etype == kEthP8021Q) { - // Skip 4-byte VLAN tag; re-read EtherType - if (frame_len < kEthHdrLen + kVlanTagLen + 2) + // After the 14-byte ethhdr, the 802.1Q VLAN overhead is: + // offset 14–15: TCI (2 bytes) + // offset 16–17: inner EtherType (2 bytes) ← read from here + // offset 18+ : PTP payload ← ptp_offset + if (len < kEthHdrLen + kVlanTagLen + 2U) return false; - const uint16_t inner_etype_be = *reinterpret_cast(frame + kEthHdrLen + kVlanTagLen); - if (ntohs(inner_etype_be) != static_cast(kEthP1588)) + std::uint16_t inner_etype_be{}; + std::memcpy(&inner_etype_be, frame + kEthHdrLen + kVlanTciLen, sizeof(inner_etype_be)); + if (static_cast(ntohs(inner_etype_be)) != kEthP1588) return false; - ptp_offset = kEthHdrLen + kVlanTagLen; + ptp_offset = static_cast(kEthHdrLen + kVlanTagLen); return true; } - if (etype != static_cast(kEthP1588)) + if (etype != kEthP1588) return false; - ptp_offset = kEthHdrLen; + ptp_offset = static_cast(kEthHdrLen); return true; } -bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, unsigned int& buf_len) const +bool FrameCodec::AddEthernetHeader(std::uint8_t* buf, + unsigned int& buf_len, + const std::array& src_mac, + std::size_t buf_capacity) const { - constexpr unsigned int kMaxFrameSize = 2048U; const unsigned int kHdrLen = static_cast(sizeof(ethhdr)); - if (buf_len + kHdrLen > kMaxFrameSize) + if (buf_capacity < kHdrLen || buf_len > static_cast(buf_capacity) - kHdrLen) return false; std::memmove(buf + kHdrLen, buf, buf_len); auto* hdr = reinterpret_cast(buf); - if (Str2Mac(kPtpSrcMac, hdr->h_source) != 0 || Str2Mac(kPtpDstMac, hdr->h_dest) != 0) - { - return false; - } + std::memcpy(hdr->h_dest, kPtpDstMacBytes.data(), kMacAddrLen); + std::memcpy(hdr->h_source, src_mac.data(), kMacAddrLen); hdr->h_proto = htons(static_cast(kEthP1588)); buf_len += kHdrLen; diff --git a/score/TimeSlave/code/gptp/details/frame_codec.h b/score/TimeSlave/code/gptp/details/frame_codec.h index 425105c..020146f 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec.h +++ b/score/TimeSlave/code/gptp/details/frame_codec.h @@ -15,6 +15,7 @@ #include "score/TimeSlave/code/gptp/details/ptp_types.h" +#include #include #include @@ -54,9 +55,13 @@ class FrameCodec final * * @param buf Buffer large enough to hold existing payload plus header. * @param buf_len In/out: payload length → frame length after prepend. + * @param src_mac Source MAC address (should be the port's own MAC). * @return true on success, false if the buffer would overflow. */ - bool AddEthernetHeader(std::uint8_t* buf, unsigned int& buf_len) const; + bool AddEthernetHeader(std::uint8_t* buf, + unsigned int& buf_len, + const std::array& src_mac, + std::size_t buf_capacity) const; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/details/frame_codec_test.cpp b/score/TimeSlave/code/gptp/details/frame_codec_test.cpp index ca5c486..1912db4 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec_test.cpp +++ b/score/TimeSlave/code/gptp/details/frame_codec_test.cpp @@ -15,6 +15,7 @@ #include #include +#include #include namespace score @@ -74,15 +75,16 @@ TEST_F(FrameCodecParseTest, Eth1588_Valid_ReturnsTrueAndOffset14) TEST_F(FrameCodecParseTest, Vlan8021Q_ValidPtpInner_ReturnsTrueAndOffset18) { - // VLAN-tagged: ethhdr(14) + VLAN tag(4) + inner EtherType(2) + payload - // Minimum valid length = 20; inner EtherType is at bytes [18..19] + // IEEE 802.1Q layout: ethhdr(14) | TCI(2) | inner EtherType(2) | payload + // offset 14-15: TCI + // offset 16-17: inner EtherType ← written here + // offset 18+ : PTP payload ← ptp_offset == 18 == 14 + kVlanTagLen auto buf = MakeEthFrame(static_cast(kEthP8021Q), 60); - // Inner EtherType = kEthP1588 at offset 14 + kVlanTagLen = 18 const std::uint16_t inner_be = htons(static_cast(kEthP1588)); - std::memcpy(&buf[14 + kVlanTagLen], &inner_be, 2); + std::memcpy(&buf[14 + 2], &inner_be, 2); // inner EtherType at offset 16 int offset = -1; ASSERT_TRUE(codec_.ParseEthernetHeader(buf.data(), 60, offset)); - EXPECT_EQ(offset, 14 + kVlanTagLen); + EXPECT_EQ(offset, 14 + kVlanTagLen); // PTP payload at offset 18 } TEST_F(FrameCodecParseTest, Vlan8021Q_TooShortForInnerType_ReturnsFalse) @@ -122,7 +124,8 @@ TEST_F(FrameCodecParseTest, AddEthernetHeader_NormalPayload_ReturnsTrueAndIncrem buf[1] = 0xAD; unsigned int len = kPayloadLen; - ASSERT_TRUE(codec_.AddEthernetHeader(buf, len)); + const std::array src_mac = {0x02U, 0x00U, 0x00U, 0xFFU, 0x00U, 0x11U}; + ASSERT_TRUE(codec_.AddEthernetHeader(buf, len, src_mac, sizeof(buf))); EXPECT_EQ(len, kPayloadLen + 14U); // Payload was shifted right by 14 bytes @@ -138,10 +141,11 @@ TEST_F(FrameCodecParseTest, AddEthernetHeader_NormalPayload_ReturnsTrueAndIncrem TEST_F(FrameCodecParseTest, AddEthernetHeader_PayloadTooLarge_ReturnsFalse) { - constexpr unsigned int kTooBig = 2048U; // buf_len + 14 > 2048 + constexpr unsigned int kTooBig = 2048U; // buf_len + 14 > capacity std::uint8_t buf[4096] = {}; unsigned int len = kTooBig; - EXPECT_FALSE(codec_.AddEthernetHeader(buf, len)); + const std::array src_mac = {0x02U, 0x00U, 0x00U, 0xFFU, 0x00U, 0x11U}; + EXPECT_FALSE(codec_.AddEthernetHeader(buf, len, src_mac, kTooBig)); } } // namespace details diff --git a/score/TimeSlave/code/gptp/details/message_parser.cpp b/score/TimeSlave/code/gptp/details/message_parser.cpp index fadc468..687db78 100644 --- a/score/TimeSlave/code/gptp/details/message_parser.cpp +++ b/score/TimeSlave/code/gptp/details/message_parser.cpp @@ -15,11 +15,19 @@ #include #include +namespace +{ + +inline std::uint64_t ByteSwap64(std::uint64_t v) noexcept +{ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -#define BSWAP64(x) __builtin_bswap64(x) + return __builtin_bswap64(v); #else -#define BSWAP64(x) (x) + return v; #endif +} + +} // namespace namespace score { @@ -49,7 +57,7 @@ std::uint64_t LoadBe64(const std::uint8_t* p) noexcept { std::uint64_t v{}; std::memcpy(&v, p, sizeof(v)); - return BSWAP64(v); + return ByteSwap64(v); } Timestamp LoadTimestamp(const std::uint8_t* p) noexcept @@ -68,6 +76,17 @@ bool GptpMessageParser::Parse(const std::uint8_t* payload, std::size_t payload_l if (payload == nullptr || payload_len < sizeof(PTPHeader)) return false; + const std::uint16_t declared_len = + static_cast((static_cast(payload[2]) << 8U) | payload[3]); + if (declared_len < sizeof(PTPHeader) || static_cast(declared_len) > payload_len) + return false; + + // Validate transportSpecific nibble (must be 0x1 for 802.1AS) and PTP version. + if ((payload[0] & 0xF0U) != kPtpTransportSpecific) + return false; + if ((payload[1] & 0x0FU) != kPtpVersion) + return false; + msg.ptpHdr.tsmt = payload[0]; msg.ptpHdr.version = payload[1]; msg.ptpHdr.messageLength = LoadU16(payload + 2); @@ -95,12 +114,33 @@ bool GptpMessageParser::Parse(const std::uint8_t* payload, std::size_t payload_l case kPtpMsgtypePdelayResp: if (payload_len >= kBodyOffset + sizeof(Timestamp)) - msg.pdelay_resp.responseOriginTimestamp = LoadTimestamp(payload + kBodyOffset); + { + msg.pdelay_resp.requestReceiptTimestamp = LoadTimestamp(payload + kBodyOffset); + // Also parse requestingPortIdentity (8-byte ClockIdentity + 2-byte portNumber) + // so that ComputeAndStoreUnlocked() can verify the response is addressed to us. + constexpr std::size_t kPidOffset = kBodyOffset + sizeof(Timestamp); // = 44 + if (payload_len >= kPidOffset + 10U) + { + std::memcpy(msg.pdelay_resp.requestingPortIdentity.clockIdentity.id, + payload + kPidOffset, 8); + msg.pdelay_resp.requestingPortIdentity.portNumber = LoadU16(payload + kPidOffset + 8U); + } + } break; case kPtpMsgtypePdelayRespFollowUp: if (payload_len >= kBodyOffset + sizeof(Timestamp)) + { msg.pdelay_resp_fup.responseOriginReceiptTimestamp = LoadTimestamp(payload + kBodyOffset); + constexpr std::size_t kPidOffset = kBodyOffset + sizeof(Timestamp); // = 44 + if (payload_len >= kPidOffset + 10U) + { + std::memcpy(msg.pdelay_resp_fup.requestingPortIdentity.clockIdentity.id, + payload + kPidOffset, 8); + msg.pdelay_resp_fup.requestingPortIdentity.portNumber = + LoadU16(payload + kPidOffset + 8U); + } + } break; default: diff --git a/score/TimeSlave/code/gptp/details/message_parser_test.cpp b/score/TimeSlave/code/gptp/details/message_parser_test.cpp index f42afdc..94980da 100644 --- a/score/TimeSlave/code/gptp/details/message_parser_test.cpp +++ b/score/TimeSlave/code/gptp/details/message_parser_test.cpp @@ -78,7 +78,7 @@ std::vector BuildPayload(std::uint8_t msgtype, std::memcpy(buf.data() + 20, &clock_id, 8); PutU16Be(buf.data(), 28, port_number); PutU16Be(buf.data(), 30, seqId); - buf[32] = kCtlFollowUp; + buf[32] = static_cast(ControlField::kFollowUp); // Timestamp body at offset 34: seconds_msb(u16) + seconds_lsb(u32) + nanoseconds(u32) PutU16Be(buf.data(), kHdrSize, 0U); // seconds_msb = 0 @@ -176,8 +176,8 @@ TEST_F(MessageParserTest, PdelayResp_Body_TimestampDecodedCorrectly) PTPMessage msg{}; ASSERT_TRUE(parser_.Parse(buf.data(), buf.size(), msg)); EXPECT_EQ(msg.msgtype, kPtpMsgtypePdelayResp); - EXPECT_EQ(msg.pdelay_resp.responseOriginTimestamp.seconds_lsb, kSecLsb); - EXPECT_EQ(msg.pdelay_resp.responseOriginTimestamp.nanoseconds, kNs); + EXPECT_EQ(msg.pdelay_resp.requestReceiptTimestamp.seconds_lsb, kSecLsb); + EXPECT_EQ(msg.pdelay_resp.requestReceiptTimestamp.nanoseconds, kNs); } // ── PdelayRespFollowUp body ─────────────────────────────────────────────────── @@ -205,6 +205,39 @@ TEST_F(MessageParserTest, UnknownMsgtype_ReturnsTrue_HeaderParsed) EXPECT_EQ(msg.msgtype, kPtpMsgtypePdelayReq); } +// ── TimestampToTmv / TmvToTimestamp overflow guards ─────────────────────────── + +TEST_F(MessageParserTest, TimestampToTmv_SecExceedsMax_ReturnsZero) +{ + // seconds_msb=3 → sec = 3 * 2^32 = 12,884,901,888 > kMaxSec (9,223,372,036) + Timestamp ts{}; + ts.seconds_msb = 3U; + ts.seconds_lsb = 0U; + ts.nanoseconds = 0U; + const TmvT result = TimestampToTmv(ts); + EXPECT_EQ(result.ns, 0LL); +} + +TEST_F(MessageParserTest, TimestampToTmv_TotalNsExceedsMax_ReturnsZero) +{ + // sec = kMaxSec = 9,223,372,036 (seconds_msb=2, seconds_lsb=633,437,444) + // total_ns = kMaxSec * 1e9 + 854,775,808 > INT64_MAX + Timestamp ts{}; + ts.seconds_msb = 2U; + ts.seconds_lsb = 633'437'444U; + ts.nanoseconds = 854'775'808U; + const TmvT result = TimestampToTmv(ts); + EXPECT_EQ(result.ns, 0LL); +} + +TEST_F(MessageParserTest, TmvToTimestamp_NegativeNs_ReturnsZeroTimestamp) +{ + const Timestamp ts = TmvToTimestamp(TmvT{-1LL}); + EXPECT_EQ(ts.seconds_msb, 0U); + EXPECT_EQ(ts.seconds_lsb, 0U); + EXPECT_EQ(ts.nanoseconds, 0U); +} + } // namespace details } // namespace ts } // namespace score diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp index c13eae6..4b2dbb1 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -14,6 +14,7 @@ #include "score/TimeSlave/code/gptp/details/frame_codec.h" #include +#include #include namespace score @@ -38,22 +39,41 @@ int PeerDelayMeasurer::SendRequest(IRawSocket& socket) req.ptpHdr.reserved2 = 0; req.ptpHdr.sourcePortIdentity.clockIdentity = local_identity_; req.ptpHdr.sourcePortIdentity.portNumber = htons(0x0001U); - req.ptpHdr.sequenceId = htons(static_cast(seqnum_)); - req.ptpHdr.controlField = kCtlOther; + req.ptpHdr.sequenceId = htons(seqnum_); + req.ptpHdr.controlField = static_cast(ControlField::kOther); req.ptpHdr.logMessageInterval = 0x7F; - // Save a copy with host-byte-order sequence ID for later matching + // Save a copy with host-byte-order fields for later matching. + // portNumber and sequenceId are stored in host byte order so that + // memcmp/equality checks in ComputeAndStoreUnlocked() agree with the + // host-order values produced by GgtpMessageParser (LoadU16/ntohs). { std::lock_guard lk(mutex_); req_ = req; - req_.ptpHdr.sequenceId = static_cast(seqnum_); + req_.ptpHdr.sequenceId = seqnum_; + req_.ptpHdr.sourcePortIdentity.portNumber = 0x0001U; // host byte order + req_.sendHardwareTS = TmvT{-1}; // sentinel: TX timestamp pending + ++seqnum_; // uint16_t: wraps naturally at 0xFFFF } - ++seqnum_; - auto buf = reinterpret_cast(&req); + // Derive the source MAC from the EUI-64 ClockIdentity (reverse EUI-48→EUI-64 + // expansion: OUI = id[0..2], vendor = id[5..7]). + const std::array src_mac = {local_identity_.id[0], + local_identity_.id[1], + local_identity_.id[2], + local_identity_.id[5], + local_identity_.id[6], + local_identity_.id[7]}; + + // Use a separate stack buffer — never alias the PTPMessage object itself as a + // raw frame buffer; AddEthernetHeader() shifts the payload in-place and would + // write beyond sizeof(PTPMessage). + std::uint8_t buf[2048]{}; unsigned int len = sizeof(PdelayReqBody); + std::memcpy(buf, &req, len); + FrameCodec codec; - if (!codec.AddEthernetHeader(buf, len)) + if (!codec.AddEthernetHeader(buf, len, src_mac, sizeof(buf))) return -1; ::timespec hwts{}; @@ -74,23 +94,35 @@ void PeerDelayMeasurer::OnResponse(const PTPMessage& msg) void PeerDelayMeasurer::OnResponseFollowUp(const PTPMessage& msg) { - { - std::lock_guard lk(mutex_); - resp_fup_ = msg; - } - ComputeAndStore(); -} -void PeerDelayMeasurer::ComputeAndStore() noexcept -{ std::lock_guard lk(mutex_); + resp_fup_ = msg; + ComputeAndStoreUnlocked(); +} - // All three messages must share the same sequence ID +void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept +{ if (req_.ptpHdr.sequenceId != resp_.ptpHdr.sequenceId) return; if (resp_.ptpHdr.sequenceId != resp_fup_.ptpHdr.sequenceId) return; + // Reject if t1 has not been recorded yet (TX timestamp still pending after Send()). + // Without this guard, a response arriving in the race window between the first + // lock release and the sendHardwareTS assignment would produce a garbage delay. + // Sentinel value -1 means "TX timestamp pending"; 0 is a valid timestamp (t=0). + if (req_.sendHardwareTS.ns < 0) + return; + + if (std::memcmp(&resp_.pdelay_resp.requestingPortIdentity, + &req_.ptpHdr.sourcePortIdentity, + sizeof(PortIdentity)) != 0) + return; + if (std::memcmp(&resp_fup_.pdelay_resp_fup.requestingPortIdentity, + &req_.ptpHdr.sourcePortIdentity, + sizeof(PortIdentity)) != 0) + return; + // t1 = HW send timestamp of our Pdelay_Req const TmvT t1 = req_.sendHardwareTS; // t2 = remote receipt time (from Pdelay_Resp body: requestReceiptTimestamp) @@ -105,6 +137,9 @@ void PeerDelayMeasurer::ComputeAndStore() noexcept const std::int64_t delay = ((t2.ns - t1.ns) + (t4.ns - t3c.ns)) / 2LL; + if (delay < 0) + return; + PDelayResult r{}; r.path_delay_ns = delay; r.valid = true; diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.h b/score/TimeSlave/code/gptp/details/pdelay_measurer.h index 981f3bb..1b8d882 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.h +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.h @@ -65,13 +65,13 @@ class PeerDelayMeasurer final PDelayResult GetResult() const; private: - void ComputeAndStore() noexcept; + void ComputeAndStoreUnlocked() noexcept; ClockIdentity local_identity_{}; mutable std::mutex mutex_; - int seqnum_{0}; + std::uint16_t seqnum_{0U}; PTPMessage req_{}; PTPMessage resp_{}; PTPMessage resp_fup_{}; diff --git a/score/TimeSlave/code/gptp/details/ptp_types.h b/score/TimeSlave/code/gptp/details/ptp_types.h index 187bbec..9d14d28 100644 --- a/score/TimeSlave/code/gptp/details/ptp_types.h +++ b/score/TimeSlave/code/gptp/details/ptp_types.h @@ -16,6 +16,7 @@ #include #include #include +#include #ifndef _QNX_PLAT #include @@ -29,9 +30,7 @@ struct ethhdr }; #endif -#ifndef PACKED -#define PACKED __attribute__((packed)) -#endif +#define SCORE_TS_PACKED __attribute__((packed)) namespace score { @@ -41,12 +40,12 @@ namespace details { // ─── EtherType constants ──────────────────────────────────────────────────── -constexpr int kEthP1588 = 0x88F7; -constexpr int kEthP8021Q = 0x8100; +constexpr std::uint16_t kEthP1588 = 0x88F7U; +constexpr std::uint16_t kEthP8021Q = 0x8100U; // ─── MAC / buffer sizes ───────────────────────────────────────────────────── -constexpr int kMacAddrLen = 6; -constexpr int kVlanTagLen = 4; +constexpr std::size_t kMacAddrLen = 6U; +constexpr std::size_t kVlanTagLen = 4U; // ─── PTP message-type codes ───────────────────────────────────────────────── constexpr std::uint8_t kPtpMsgtypeSync = 0x0; @@ -61,19 +60,15 @@ constexpr std::uint8_t kPtpVersion = 2U; constexpr std::int64_t kNsPerSec = 1'000'000'000LL; -// ─── MAC addresses ─────────────────────────────────────────────────────────── -constexpr const char* kPtpSrcMac = "02:00:00:FF:00:11"; -constexpr const char* kPtpDstMac = "01:80:C2:00:00:0E"; - // ─── Control field ─────────────────────────────────────────────────────────── -enum ControlField : std::uint8_t -{ - kCtlSync = 0, - kCtlDelayReq = 1, - kCtlFollowUp = 2, - kCtlDelayResp = 3, - kCtlManagement = 4, - kCtlOther = 5 +enum class ControlField : std::uint8_t +{ + kSync = 0, + kDelayReq = 1, + kFollowUp = 2, + kDelayResp = 3, + kManagement = 4, + kOther = 5 }; // ─── State machine states ──────────────────────────────────────────────────── @@ -90,26 +85,26 @@ struct TmvT std::int64_t ns{0}; }; -// ─── PTP wire structures (all PACKED) ──────────────────────────────────────── -struct PACKED ClockIdentity +// ─── PTP wire structures (all SCORE_TS_PACKED) ─────────────────────────────── +struct SCORE_TS_PACKED ClockIdentity { std::uint8_t id[8]{}; }; -struct PACKED PortIdentity +struct SCORE_TS_PACKED PortIdentity { ClockIdentity clockIdentity; std::uint16_t portNumber{0}; }; -struct PACKED Timestamp +struct SCORE_TS_PACKED Timestamp { std::uint16_t seconds_msb{0}; std::uint32_t seconds_lsb{0}; std::uint32_t nanoseconds{0}; }; -struct PACKED PTPHeader +struct SCORE_TS_PACKED PTPHeader { std::uint8_t tsmt{0}; std::uint8_t version{0}; @@ -125,47 +120,47 @@ struct PACKED PTPHeader std::int8_t logMessageInterval{0}; }; -struct PACKED SyncBody +struct SCORE_TS_PACKED SyncBody { PTPHeader ptpHdr{}; Timestamp originTimestamp{}; }; -struct PACKED FollowUpBody +struct SCORE_TS_PACKED FollowUpBody { PTPHeader ptpHdr{}; Timestamp preciseOriginTimestamp{}; }; -struct PACKED PdelayReqBody +struct SCORE_TS_PACKED PdelayReqBody { PTPHeader ptpHdr{}; Timestamp requestReceiptTimestamp{}; PortIdentity reserved{}; }; -struct PACKED PdelayRespBody +struct SCORE_TS_PACKED PdelayRespBody { PTPHeader ptpHdr{}; - Timestamp responseOriginTimestamp{}; + Timestamp requestReceiptTimestamp{}; ///< IEEE 802.1AS: t₂ — time the remote peer received our PdelayReq PortIdentity requestingPortIdentity{}; }; -struct PACKED PdelayRespFollowUpBody +struct SCORE_TS_PACKED PdelayRespFollowUpBody { PTPHeader ptpHdr{}; Timestamp responseOriginReceiptTimestamp{}; PortIdentity requestingPortIdentity{}; }; -struct PACKED RawMessageData +struct SCORE_TS_PACKED RawMessageData { std::uint8_t buffer[1500]{}; }; struct PTPMessage { - union PACKED + union SCORE_TS_PACKED { PTPHeader ptpHdr; SyncBody sync; @@ -189,11 +184,21 @@ inline TmvT TimestampToTmv(const Timestamp& ts) noexcept { const std::uint64_t sec = (static_cast(ts.seconds_msb) << 32U) | static_cast(ts.seconds_lsb); - return TmvT{static_cast(sec * static_cast(kNsPerSec) + ts.nanoseconds)}; + constexpr std::uint64_t kMaxNs = + static_cast(std::numeric_limits::max()); + constexpr std::uint64_t kMaxSec = kMaxNs / static_cast(kNsPerSec); + if (sec > kMaxSec) + return TmvT{}; + const std::uint64_t total_ns = sec * static_cast(kNsPerSec) + ts.nanoseconds; + if (total_ns > kMaxNs) + return TmvT{}; + return TmvT{static_cast(total_ns)}; } inline Timestamp TmvToTimestamp(const TmvT& x) noexcept { + if (x.ns < 0) + return Timestamp{}; // negative timestamps are invalid on the wire Timestamp t{}; const std::uint64_t sec = static_cast(x.ns) / 1'000'000'000ULL; const std::uint64_t nsec = static_cast(x.ns) % 1'000'000'000ULL; @@ -205,7 +210,7 @@ inline Timestamp TmvToTimestamp(const TmvT& x) noexcept inline TmvT CorrectionToTmv(std::int64_t corr) noexcept { - return TmvT{corr >> 16}; + return TmvT{corr / 65536LL}; } inline std::uint64_t ClockIdentityToU64(const ClockIdentity& ci) noexcept diff --git a/score/TimeSlave/code/gptp/details/raw_socket.h b/score/TimeSlave/code/gptp/details/raw_socket.h index b0be138..0f19657 100644 --- a/score/TimeSlave/code/gptp/details/raw_socket.h +++ b/score/TimeSlave/code/gptp/details/raw_socket.h @@ -16,6 +16,7 @@ #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" #include +#include #include #include #include @@ -75,11 +76,11 @@ class RawSocket : public IRawSocket /// Return the underlying file descriptor (for advanced use / polling). int GetFd() const override { - return fd_; + return fd_.load(std::memory_order_relaxed); } private: - int fd_{-1}; + std::atomic fd_{-1}; std::string iface_{}; }; diff --git a/score/TimeSlave/code/gptp/details/raw_socket_test.cpp b/score/TimeSlave/code/gptp/details/raw_socket_test.cpp new file mode 100644 index 0000000..ca7d566 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/raw_socket_test.cpp @@ -0,0 +1,174 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeSlave/code/gptp/details/network_identity.h" +#include "score/TimeSlave/code/gptp/details/raw_socket.h" + +#include + +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +// ── RawSocket — closed-state guard paths ───────────────────────────────────── + +TEST(RawSocketTest, DefaultConstruct_GetFd_ReturnsNegativeOne) +{ + RawSocket sock; + EXPECT_EQ(sock.GetFd(), -1); +} + +TEST(RawSocketTest, Close_WhenNotOpen_IsNoOp) +{ + RawSocket sock; + EXPECT_NO_THROW(sock.Close()); + EXPECT_EQ(sock.GetFd(), -1); +} + +TEST(RawSocketTest, EnableHwTimestamping_WhenNotOpen_ReturnsFalse) +{ + RawSocket sock; + EXPECT_FALSE(sock.EnableHwTimestamping()); +} + +TEST(RawSocketTest, Recv_WhenNotOpen_ReturnsNegativeOne) +{ + RawSocket sock; + std::uint8_t buf[64] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 0), -1); +} + +TEST(RawSocketTest, Recv_NullBuf_ReturnsNegativeOne) +{ + RawSocket sock; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(nullptr, 64U, hwts, 0), -1); +} + +TEST(RawSocketTest, Recv_ZeroBufLen_ReturnsNegativeOne) +{ + RawSocket sock; + std::uint8_t buf[1] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, 0U, hwts, 0), -1); +} + +TEST(RawSocketTest, Send_WhenNotOpen_ReturnsNegativeOne) +{ + RawSocket sock; + const std::uint8_t data[14] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 14, hwts), -1); +} + +TEST(RawSocketTest, Send_NullBuf_ReturnsNegativeOne) +{ + RawSocket sock; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(nullptr, 14, hwts), -1); +} + +TEST(RawSocketTest, Send_ZeroLen_ReturnsNegativeOne) +{ + RawSocket sock; + const std::uint8_t data[1] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 0, hwts), -1); +} + +TEST(RawSocketTest, Send_NegativeLen_ReturnsNegativeOne) +{ + RawSocket sock; + const std::uint8_t data[1] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, -1, hwts), -1); +} + +// ── RawSocket — invalid interface ──────────────────────────────────────────── + +TEST(RawSocketTest, Open_NonExistentInterface_ReturnsFalse) +{ + RawSocket sock; + EXPECT_FALSE(sock.Open("nonexistent_eth_zzz")); +} + +TEST(RawSocketTest, Open_NonExistentInterface_GetFdRemainsNegativeOne) +{ + RawSocket sock; + (void)sock.Open("nonexistent_eth_zzz"); + EXPECT_EQ(sock.GetFd(), -1); +} + +// ── NetworkIdentity ─────────────────────────────────────────────────────────── + +TEST(NetworkIdentityTest, GetClockIdentity_BeforeResolve_ReturnsZeroIdentity) +{ + NetworkIdentity ni; + const ClockIdentity id = ni.GetClockIdentity(); + for (const std::uint8_t b : id.id) + { + EXPECT_EQ(b, 0U); + } +} + +TEST(NetworkIdentityTest, Resolve_NonExistentInterface_ReturnsFalse) +{ + NetworkIdentity ni; + EXPECT_FALSE(ni.Resolve("nonexistent_eth_zzz")); +} + +TEST(NetworkIdentityTest, Resolve_NonExistentInterface_GetClockIdentityRemainsZero) +{ + NetworkIdentity ni; + (void)ni.Resolve("nonexistent_eth_zzz"); + const ClockIdentity id = ni.GetClockIdentity(); + for (const std::uint8_t b : id.id) + { + EXPECT_EQ(b, 0U); + } +} + +TEST(NetworkIdentityTest, Resolve_LoInterface_ReturnsTrue) +{ + // lo has MAC 00:00:00:00:00:00; the EUI-48→EUI-64 conversion inserts + // 0xFF 0xFE at positions 3–4 regardless of the MAC value. + NetworkIdentity ni; + EXPECT_TRUE(ni.Resolve("lo")); +} + +TEST(NetworkIdentityTest, GetClockIdentity_AfterResolveOnLo_HasFfFeBytes) +{ + NetworkIdentity ni; + ASSERT_TRUE(ni.Resolve("lo")); + const ClockIdentity id = ni.GetClockIdentity(); + // EUI-48 → EUI-64: bytes 3 and 4 must be 0xFF and 0xFE + EXPECT_EQ(id.id[3], 0xFFU); + EXPECT_EQ(id.id[4], 0xFEU); +} + +TEST(NetworkIdentityTest, Resolve_CalledTwice_SecondCallSucceeds) +{ + NetworkIdentity ni; + ASSERT_TRUE(ni.Resolve("lo")); + EXPECT_TRUE(ni.Resolve("lo")); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp index 8c89af2..92fd2cb 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp @@ -11,8 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ #include "score/TimeSlave/code/gptp/details/sync_state_machine.h" - -#include +#include "score/TimeSlave/code/gptp/details/clock_util.h" namespace score { @@ -21,20 +20,10 @@ namespace ts namespace details { -namespace -{ - -std::int64_t MonoNs() noexcept -{ - ::timespec ts{}; - ::clock_gettime(CLOCK_MONOTONIC, &ts); - return static_cast(ts.tv_sec) * kNsPerSec + ts.tv_nsec; -} - -} // namespace SyncStateMachine::SyncStateMachine(std::int64_t jump_future_threshold_ns) noexcept - : jump_future_threshold_ns_{jump_future_threshold_ns} + : jump_future_threshold_ns_{jump_future_threshold_ns}, + created_mono_ns_{MonoNs()} { } @@ -100,7 +89,10 @@ bool SyncStateMachine::IsTimeout(std::int64_t mono_now_ns, std::int64_t timeout_ return false; const std::int64_t last = last_sync_mono_ns_.load(std::memory_order_acquire); if (last == 0) - return false; // never synchronized yet — not a "timeout" + { + const std::int64_t start = created_mono_ns_.load(std::memory_order_relaxed); + return (mono_now_ns - start) > timeout_ns; + } return (mono_now_ns - last) > timeout_ns; } @@ -117,7 +109,7 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag r.master_ns = master_ns; r.offset_ns = offset_ns; - if (last_master_ns_ != 0) + if (has_previous_master_) { const std::int64_t delta = master_ns - last_master_ns_; if (delta < 0) @@ -126,12 +118,16 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag r.is_time_jump_future = true; } + const auto to_u64 = [](std::int64_t v) noexcept -> std::uint64_t { + return v >= 0 ? static_cast(v) : 0U; + }; + score::td::SyncFupData& d = r.sync_fup_data; - d.precise_origin_timestamp = static_cast(fup_ts.ns); - d.reference_global_timestamp = static_cast(master_ns); - d.reference_local_timestamp = static_cast(sync.recvHardwareTS.ns); - d.sync_ingress_timestamp = static_cast(sync.recvHardwareTS.ns); - d.correction_field = static_cast(sync.ptpHdr.correctionField); + d.precise_origin_timestamp = to_u64(fup_ts.ns); + d.reference_global_timestamp = to_u64(master_ns); + d.reference_local_timestamp = to_u64(sync.recvHardwareTS.ns); + d.sync_ingress_timestamp = to_u64(sync.recvHardwareTS.ns); + d.correction_field = to_u64(sync.ptpHdr.correctionField); d.sequence_id = fup.ptpHdr.sequenceId; d.pdelay = 0U; // filled by GptpEngine from IPeerDelayMeasurer d.port_number = sync.ptpHdr.sourcePortIdentity.portNumber; @@ -142,7 +138,10 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag { const std::int64_t slave_interval = sync.recvHardwareTS.ns - prev_slave_rx_ns_; const std::int64_t master_interval = master_ns - prev_master_origin_ns_; - if (master_interval > 0) + // Both intervals must be strictly positive: a non-positive slave_interval + // indicates a HW timestamp rollback or clock step, which would produce a + // nonsensical (negative or zero) rate ratio published to PtpTimeInfo. + if (master_interval > 0 && slave_interval > 0) { neighbor_rate_ratio_ = static_cast(slave_interval) / static_cast(master_interval); } @@ -151,6 +150,7 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag prev_master_origin_ns_ = master_ns; last_master_ns_ = master_ns; + has_previous_master_ = true; return r; } diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.h b/score/TimeSlave/code/gptp/details/sync_state_machine.h index abc657f..84072e3 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.h +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.h @@ -81,16 +81,15 @@ class SyncStateMachine final PTPMessage last_sync_{}; PTPMessage last_fup_{}; std::int64_t last_master_ns_{0}; + bool has_previous_master_{false}; std::int64_t jump_future_threshold_ns_; - // neighborRateRatio computation (IEEE 802.1AS Clause 11.4.1) std::int64_t prev_slave_rx_ns_{0}; std::int64_t prev_master_origin_ns_{0}; double neighbor_rate_ratio_{1.0}; - /// Monotonic timestamp of the last successful Sync+FUP pair (ns). - /// Atomic so that IsTimeout() can be called from a different thread. std::atomic last_sync_mono_ns_{0}; + std::atomic created_mono_ns_; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp index 8b3b7bd..ed7e2e8 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp @@ -201,8 +201,9 @@ TEST_F(SyncStateMachineTest, NeighborRateRatio_AfterTwoPairs_Computed) TEST_F(SyncStateMachineTest, IsTimeout_BeforeFirstSync_ReturnsFalse) { - // last_sync_mono_ns_ == 0; should never be considered a timeout - EXPECT_FALSE(ssm_.IsTimeout(std::numeric_limits::max(), 1LL)); + // Before first sync, IsTimeout uses the object creation time as baseline. + // Passing now=0 gives (0 - created_mono_ns_) which is negative → not > threshold. + EXPECT_FALSE(ssm_.IsTimeout(0LL, 1LL)); } TEST_F(SyncStateMachineTest, IsTimeout_AfterSuccessfulPair_WithLargeNow_ReturnsTrue) diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp index 18fd0e7..ea2cdd1 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -11,13 +11,13 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ #include "score/TimeSlave/code/gptp/gptp_engine.h" +#include "score/TimeSlave/code/gptp/details/clock_util.h" #include "score/TimeSlave/code/gptp/details/network_identity.h" #include "score/TimeSlave/code/gptp/details/raw_socket.h" #include "score/TimeDaemon/code/common/logging_contexts.h" #include "score/mw/log/logging.h" -#include #include namespace score @@ -33,13 +33,6 @@ namespace constexpr int kRxTimeoutMs = 100; // poll timeout; keeps RxLoop responsive to shutdown constexpr int kRxBufferSize = 2048; -std::int64_t MonoNs() noexcept -{ - ::timespec ts{}; - ::clock_gettime(CLOCK_MONOTONIC, &ts); - return static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; -} - } // namespace GptpEngine::GptpEngine(GptpEngineOptions opts, @@ -72,7 +65,7 @@ GptpEngine::GptpEngine(GptpEngineOptions opts, GptpEngine::~GptpEngine() noexcept { - (void)Deinitialize(); + Deinitialize(); } bool GptpEngine::Initialize() @@ -104,22 +97,28 @@ bool GptpEngine::Initialize() running_.store(true, std::memory_order_release); - if (::pthread_create(&rx_thread_, nullptr, &RxThreadEntry, this) != 0) + try { - score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create RxThread"; + rx_thread_ = std::thread([this]() noexcept { RxLoop(); }); + } + catch (const std::system_error& e) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create RxThread: " << e.what(); running_.store(false, std::memory_order_release); socket_->Close(); return false; } - rx_started_ = true; - if (::pthread_create(&pdelay_thread_, nullptr, &PdelayThreadEntry, this) != 0) + try + { + pdelay_thread_ = std::thread([this]() noexcept { PdelayLoop(); }); + } + catch (const std::system_error& e) { - score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread"; - (void)Deinitialize(); + score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread: " << e.what(); + Deinitialize(); return false; } - pdelay_started_ = true; score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine initialized on " << opts_.iface_name; return true; @@ -129,19 +128,13 @@ bool GptpEngine::Deinitialize() { running_.store(false, std::memory_order_release); - // Close the socket first so that the RxThread's poll() unblocks + // Close the socket first so that the RxThread's poll() unblocks. socket_->Close(); - if (rx_started_) - { - ::pthread_join(rx_thread_, nullptr); - rx_started_ = false; - } - if (pdelay_started_) - { - ::pthread_join(pdelay_thread_, nullptr); - pdelay_started_ = false; - } + if (rx_thread_.joinable()) + rx_thread_.join(); + if (pdelay_thread_.joinable()) + pdelay_thread_.join(); score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine deinitialized"; return true; @@ -155,9 +148,8 @@ bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) const std::int64_t mono_now = MonoNs(); const std::int64_t timeout_ns = static_cast(opts_.sync_timeout_ms) * 1'000'000LL; - const bool timed_out = sync_sm_.IsTimeout(mono_now, timeout_ns); - std::lock_guard lk(snapshot_mutex_); + const bool timed_out = sync_sm_.IsTimeout(mono_now, timeout_ns); snapshot_.local_time = local_clock_->Now(); if (timed_out) { @@ -169,20 +161,6 @@ bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) return true; } -void* GptpEngine::RxThreadEntry(void* arg) noexcept -{ - if (arg != nullptr) - static_cast(arg)->RxLoop(); - return nullptr; -} - -void* GptpEngine::PdelayThreadEntry(void* arg) noexcept -{ - if (arg != nullptr) - static_cast(arg)->PdelayLoop(); - return nullptr; -} - void GptpEngine::RxLoop() noexcept { std::uint8_t buf[kRxBufferSize]; @@ -201,7 +179,12 @@ void GptpEngine::RxLoop() noexcept void GptpEngine::PdelayLoop() noexcept { ::timespec next{}; - ::clock_gettime(CLOCK_MONOTONIC, &next); + if (::clock_gettime(CLOCK_MONOTONIC, &next) != 0) + { + score::mw::log::LogError(score::td::kGPtpMachineContext) + << "GptpEngine: clock_gettime failed in PdelayLoop, thread exiting"; + return; + } // Configurable warm-up before first Pdelay_Req (default 2 s) const std::int64_t warmup_ns = static_cast(opts_.pdelay_warmup_ms) * 1'000'000LL; const std::int64_t next_warmup_ns = @@ -214,7 +197,20 @@ void GptpEngine::PdelayLoop() noexcept while (running_.load(std::memory_order_acquire)) { - ::clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, nullptr); + const std::int64_t target_ns = + static_cast(next.tv_sec) * 1'000'000'000LL + next.tv_nsec; + + while (running_.load(std::memory_order_acquire)) + { + const std::int64_t remaining = target_ns - MonoNs(); + if (remaining <= 0) + break; + constexpr std::int64_t kSliceNs = 50'000'000LL; + const std::int64_t sleep_ns = remaining < kSliceNs ? remaining : kSliceNs; + const ::timespec slice{0, static_cast(sleep_ns)}; + ::clock_nanosleep(CLOCK_MONOTONIC, 0, &slice, nullptr); + } + if (!running_.load(std::memory_order_acquire)) break; @@ -223,8 +219,7 @@ void GptpEngine::PdelayLoop() noexcept (void)pdelay_->SendRequest(*socket_); } - const std::int64_t next_ns = - static_cast(next.tv_sec) * 1'000'000'000LL + next.tv_nsec + interval_ns; + const std::int64_t next_ns = target_ns + interval_ns; next.tv_sec = static_cast(next_ns / 1'000'000'000LL); next.tv_nsec = static_cast(next_ns % 1'000'000'000LL); } @@ -276,7 +271,7 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp case kPtpMsgtypePdelayResp: msg.recvHardwareTS = hw_ts; - msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp.responseOriginTimestamp); + msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp.requestReceiptTimestamp); if (pdelay_) pdelay_->OnResponse(msg); break; diff --git a/score/TimeSlave/code/gptp/gptp_engine.h b/score/TimeSlave/code/gptp/gptp_engine.h index 9170477..d63f4a9 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.h +++ b/score/TimeSlave/code/gptp/gptp_engine.h @@ -22,13 +22,13 @@ #include "score/TimeSlave/code/gptp/details/ptp_types.h" #include "score/TimeSlave/code/gptp/details/sync_state_machine.h" -#include #include #include #include #include #include #include +#include namespace score { @@ -88,8 +88,6 @@ class GptpEngine final bool ReadPTPSnapshot(score::td::PtpTimeInfo& info); private: - static void* RxThreadEntry(void* arg) noexcept; - static void* PdelayThreadEntry(void* arg) noexcept; void RxLoop() noexcept; void PdelayLoop() noexcept; @@ -110,10 +108,8 @@ class GptpEngine final score::td::PtpTimeInfo snapshot_{}; std::atomic running_{false}; - pthread_t rx_thread_{}; - pthread_t pdelay_thread_{}; - bool rx_started_{false}; - bool pdelay_started_{false}; + std::thread rx_thread_; + std::thread pdelay_thread_; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/gptp_engine_test.cpp b/score/TimeSlave/code/gptp/gptp_engine_test.cpp index 76d6918..0f07c07 100644 --- a/score/TimeSlave/code/gptp/gptp_engine_test.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine_test.cpp @@ -62,9 +62,14 @@ class FakeSocket final : public IRawSocket cv_.notify_one(); } + void SetOpenOk(bool v) + { + open_ok_ = v; + } + bool Open(const std::string&) override { - return true; + return open_ok_; } bool EnableHwTimestamping() override { @@ -119,6 +124,7 @@ class FakeSocket final : public IRawSocket std::condition_variable cv_; bool closed_{false}; bool hw_ts_ok_{true}; + bool open_ok_{true}; }; // ── FakeIdentity ────────────────────────────────────────────────────────────── @@ -512,6 +518,86 @@ TEST(GptpEngineRealSocketTest, Initialize_NonExistentInterface_ReturnsFalse) EXPECT_TRUE(eng.Deinitialize()); } +// ── Socket-open-fail path (lines 87–90 of gptp_engine.cpp) ─────────────────── + +TEST(GptpEngineSocketFailTest, Initialize_SocketOpenFails_ReturnsFalse) +{ + auto sock = std::make_unique(); + auto identity = std::make_unique(); + sock->SetOpenOk(false); + GptpEngine eng{FastOptions(), std::make_unique(), std::move(sock), std::move(identity)}; + EXPECT_FALSE(eng.Initialize()); + EXPECT_TRUE(eng.Deinitialize()); +} + +// ── Time-jump detection (UpdateSnapshot lines 301–303) ─────────────────────── + +namespace +{ + +bool WaitForFlag(GptpEngine& eng, bool (*pred)(const score::td::PtpTimeInfo&), int max_ms = 1000) +{ + for (int i = 0; i < max_ms / 10; ++i) + { + score::td::PtpTimeInfo info{}; + eng.ReadPTPSnapshot(info); + if (pred(info)) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return false; +} + +} // namespace + +TEST_F(GptpEngineFakeTest, HandlePacket_TwoSyncFup_TimeJumpFuture_Detected) +{ + ASSERT_TRUE(engine_->Initialize()); + + // Pair 1: master_ns ≈ 2 s + ::timespec hwts1{1, 0}; + socket_raw_->Push(MakeSyncFrame(1U), hwts1); + socket_raw_->Push(MakeFollowUpFrame(1U, /*sec=*/2U, /*ns=*/0U)); + + // Pair 2: master_ns ≈ 3 s (delta = 1 s > 500 ms threshold → is_time_jump_future) + ::timespec hwts2{2, 0}; + socket_raw_->Push(MakeSyncFrame(2U), hwts2); + socket_raw_->Push(MakeFollowUpFrame(2U, /*sec=*/3U, /*ns=*/0U)); + + const bool got = + WaitForFlag(*engine_, [](const score::td::PtpTimeInfo& i) { return i.status.is_time_jump_future; }); + EXPECT_TRUE(got); + + score::td::PtpTimeInfo info{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); + EXPECT_TRUE(info.status.is_time_jump_future); + EXPECT_FALSE(info.status.is_correct); +} + +TEST_F(GptpEngineFakeTest, HandlePacket_TwoSyncFup_TimeJumpPast_Detected) +{ + ASSERT_TRUE(engine_->Initialize()); + + // Pair 1: master_ns ≈ 3 s + ::timespec hwts1{1, 0}; + socket_raw_->Push(MakeSyncFrame(1U), hwts1); + socket_raw_->Push(MakeFollowUpFrame(1U, /*sec=*/3U, /*ns=*/0U)); + + // Pair 2: master_ns ≈ 2 s (delta < 0 → is_time_jump_past) + ::timespec hwts2{2, 0}; + socket_raw_->Push(MakeSyncFrame(2U), hwts2); + socket_raw_->Push(MakeFollowUpFrame(2U, /*sec=*/2U, /*ns=*/0U)); + + const bool got = + WaitForFlag(*engine_, [](const score::td::PtpTimeInfo& i) { return i.status.is_time_jump_past; }); + EXPECT_TRUE(got); + + score::td::PtpTimeInfo info{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); + EXPECT_TRUE(info.status.is_time_jump_past); + EXPECT_FALSE(info.status.is_correct); +} + } // namespace details } // namespace ts } // namespace score diff --git a/score/TimeSlave/code/gptp/instrument/probe.cpp b/score/TimeSlave/code/gptp/instrument/probe.cpp index 1312455..3bc0e6b 100644 --- a/score/TimeSlave/code/gptp/instrument/probe.cpp +++ b/score/TimeSlave/code/gptp/instrument/probe.cpp @@ -36,9 +36,10 @@ void ProbeManager::Trace(ProbePoint point, const ProbeData& data) << "PROBE point=" << static_cast(point) << " ts=" << data.ts_mono_ns << " val=" << data.value_ns << " seq=" << data.seq_id; - if (recorder_ != nullptr && recorder_->IsEnabled()) + Recorder* const rec = recorder_.load(std::memory_order_acquire); + if (rec != nullptr && rec->IsEnabled()) { - recorder_->Record(RecordEntry{ + rec->Record(RecordEntry{ data.ts_mono_ns, RecordEvent::kProbe, data.value_ns, @@ -52,8 +53,9 @@ void ProbeManager::Trace(ProbePoint point, const ProbeData& data) std::int64_t ProbeMonoNs() noexcept { ::timespec ts{}; - ::clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec * 1'000'000'000LL + ts.tv_nsec; + if (::clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0; + return static_cast(ts.tv_sec) * 1'000'000'000LL + static_cast(ts.tv_nsec); } } // namespace details diff --git a/score/TimeSlave/code/gptp/instrument/probe.h b/score/TimeSlave/code/gptp/instrument/probe.h index 6b33bd2..8e03863 100644 --- a/score/TimeSlave/code/gptp/instrument/probe.h +++ b/score/TimeSlave/code/gptp/instrument/probe.h @@ -68,7 +68,7 @@ class ProbeManager final /// Optional: link to a Recorder for persistent probe output. void SetRecorder(Recorder* recorder) { - recorder_ = recorder; + recorder_.store(recorder, std::memory_order_release); } /// Record a probe event. Thread-safe. @@ -77,7 +77,7 @@ class ProbeManager final private: ProbeManager() = default; std::atomic enabled_{false}; - Recorder* recorder_{nullptr}; + std::atomic recorder_{nullptr}; }; /// Returns the current monotonic timestamp in nanoseconds. diff --git a/score/TimeSlave/code/gptp/phc/phc_adjuster.h b/score/TimeSlave/code/gptp/phc/phc_adjuster.h index a75fd25..ca3a074 100644 --- a/score/TimeSlave/code/gptp/phc/phc_adjuster.h +++ b/score/TimeSlave/code/gptp/phc/phc_adjuster.h @@ -43,7 +43,7 @@ class PhcAdjuster final { public: explicit PhcAdjuster(PhcConfig cfg); - ~PhcAdjuster(); + ~PhcAdjuster() noexcept; PhcAdjuster(const PhcAdjuster&) = delete; PhcAdjuster& operator=(const PhcAdjuster&) = delete; diff --git a/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp index 2f4d782..733c6c7 100644 --- a/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/phc_adjuster.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace score { @@ -37,11 +38,9 @@ int phc_clock_adjtime(clockid_t clk_id, struct timex* tx) } // Construct a clockid from a PHC file descriptor (kernel convention). -// See linux/include/uapi/linux/time.h clockid_t phc_fd_to_clockid(int fd) { - // NOLINTNEXTLINE(hicpp-signed-bitwise) - return static_cast(~fd << 3 | 3); + return static_cast((~static_cast(fd) << 3U) | 3U); } } // namespace @@ -54,7 +53,7 @@ PhcAdjuster::PhcAdjuster(PhcConfig cfg) : cfg_{std::move(cfg)} } } -PhcAdjuster::~PhcAdjuster() +PhcAdjuster::~PhcAdjuster() noexcept { if (phc_fd_ >= 0) { @@ -94,12 +93,14 @@ void PhcAdjuster::AdjustFrequency(double rate_ratio) if (!cfg_.enabled || phc_fd_ < 0) return; - // Convert rate_ratio to ppb offset from 1.0, then to scaled ppm for kernel - // rate_ratio = slave_interval / master_interval - // ppb = (rate_ratio - 1.0) * 1e9 - // kernel expects freq in units of 2^-16 ppm = (ppb / 1000) * 65536 + if (!std::isfinite(rate_ratio) || rate_ratio < 0.5 || rate_ratio > 2.0) + return; + const double ppb = (rate_ratio - 1.0) * 1e9; - const long scaled_ppm = static_cast(ppb / 1000.0 * 65536.0); + const double raw_scaled = ppb / 1000.0 * 65536.0; + constexpr double kMaxScaled = 33'554'432.0; + const double clamped = raw_scaled < -kMaxScaled ? -kMaxScaled : (raw_scaled > kMaxScaled ? kMaxScaled : raw_scaled); + const long scaled_ppm = static_cast(clamped); struct timex tx { diff --git a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp index 90e03fc..cd0387e 100644 --- a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp @@ -68,6 +68,7 @@ bool RawSocket::Open(const std::string& iface) ::ifreq ifr{}; std::strncpy(ifr.ifr_name, iface.c_str(), IFNAMSIZ - 1); + ifr.ifr_name[IFNAMSIZ - 1] = '\0'; if (::ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { ::close(fd); @@ -87,33 +88,35 @@ bool RawSocket::Open(const std::string& iface) // SO_BINDTODEVICE: best-effort, don't fail if it doesn't work (void)::setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), static_cast(iface.size())); - fd_ = fd; + fd_.store(fd, std::memory_order_release); iface_ = iface; return true; } bool RawSocket::EnableHwTimestamping() { - if (fd_ < 0) + const int fd = fd_.load(std::memory_order_relaxed); + if (fd < 0) return false; ::ifreq ifr{}; ::hwtstamp_config cfg{}; std::strncpy(ifr.ifr_name, iface_.c_str(), IFNAMSIZ - 1); + ifr.ifr_name[IFNAMSIZ - 1] = '\0'; ifr.ifr_data = reinterpret_cast(&cfg); cfg.tx_type = HWTSTAMP_TX_ON; cfg.rx_filter = HWTSTAMP_FILTER_ALL; - if (::ioctl(fd_, SIOCSHWTSTAMP, &ifr) < 0) + if (::ioctl(fd, SIOCSHWTSTAMP, &ifr) < 0) { // Fall back to PTP-only filter cfg.rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT; - (void)::ioctl(fd_, SIOCSHWTSTAMP, &ifr); + (void)::ioctl(fd, SIOCSHWTSTAMP, &ifr); } const int ts_opts = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; - if (::setsockopt(fd_, SOL_SOCKET, SO_TIMESTAMPING, &ts_opts, sizeof(ts_opts)) < 0) + if (::setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &ts_opts, sizeof(ts_opts)) < 0) { return false; } @@ -122,21 +125,20 @@ bool RawSocket::EnableHwTimestamping() void RawSocket::Close() { - if (fd_ >= 0) - { - ::close(fd_); - fd_ = -1; - } + const int fd = fd_.exchange(-1, std::memory_order_acq_rel); + if (fd >= 0) + ::close(fd); iface_.clear(); } int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, int timeout_ms) { - if (fd_ < 0 || buf == nullptr || buf_len == 0) + const int fd = fd_.load(std::memory_order_acquire); + if (fd < 0 || buf == nullptr || buf_len == 0) return -1; // Poll with caller-specified timeout - ::pollfd pfd{fd_, POLLIN, 0}; + ::pollfd pfd{fd, POLLIN, 0}; const int pr = ::poll(&pfd, 1, timeout_ms); if (pr == 0) return 0; // timeout @@ -151,7 +153,7 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, in msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); - const int len = static_cast(::recvmsg(fd_, &msg, 0)); + const int len = static_cast(::recvmsg(fd, &msg, 0)); if (len < 0) return -1; @@ -160,6 +162,8 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, in { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMPING) { + if (cm->cmsg_len < CMSG_LEN(3 * sizeof(::timespec))) + continue; const auto* ts = reinterpret_cast(CMSG_DATA(cm)); if (ts[2].tv_sec != 0 || ts[2].tv_nsec != 0) hwts = ts[2]; @@ -170,27 +174,46 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, in int RawSocket::Send(const void* buf, int len, ::timespec& hwts) { - if (fd_ < 0 || buf == nullptr || len <= 0) + const int fd = fd_.load(std::memory_order_acquire); + if (fd < 0 || buf == nullptr || len <= 0) return -1; - DrainErrQueue(fd_); + DrainErrQueue(fd); - const int sent = static_cast(::send(fd_, buf, static_cast(len), 0)); + const int sent = static_cast(::send(fd, buf, static_cast(len), 0)); if (sent < 0) return -1; - // Retrieve TX hardware timestamp from error queue - ::pollfd pfd{fd_, POLLERR, 0}; - if (::poll(&pfd, 1, -1) > 0 && (pfd.revents & POLLERR) != 0) + constexpr int kTxTsTimeoutMs = 50; + ::pollfd pfd{fd, POLLERR, 0}; + std::memset(&hwts, 0, sizeof(hwts)); + if (::poll(&pfd, 1, kTxTsTimeoutMs) > 0 && (pfd.revents & POLLERR) != 0) { std::uint8_t tmp[2048]; - ::timespec tx_hwts{}; - (void)Recv(tmp, sizeof(tmp), tx_hwts, 0); - hwts = tx_hwts; - } - else - { - std::memset(&hwts, 0, sizeof(hwts)); + ::iovec iov{tmp, sizeof(tmp)}; + char ctrl[512]; + ::msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + + if (::recvmsg(fd, &msg, MSG_ERRQUEUE) >= 0) + { + for (::cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; cm = CMSG_NXTHDR(&msg, cm)) + { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMPING) + { + // SO_TIMESTAMPING delivers three timespec values: [0]=SW, [1]=HW-transformed, + // [2]=HW-raw. Verify the cmsg payload is large enough before indexing ts[2]. + if (cm->cmsg_len < CMSG_LEN(3 * sizeof(::timespec))) + continue; + const auto* ts = reinterpret_cast(CMSG_DATA(cm)); + if (ts[2].tv_sec != 0 || ts[2].tv_nsec != 0) + hwts = ts[2]; + } + } + } } return sent; } diff --git a/score/TimeSlave/code/gptp/record/recorder.cpp b/score/TimeSlave/code/gptp/record/recorder.cpp index f56ee55..006385f 100644 --- a/score/TimeSlave/code/gptp/record/recorder.cpp +++ b/score/TimeSlave/code/gptp/record/recorder.cpp @@ -19,16 +19,15 @@ namespace ts namespace details { -Recorder::Recorder(Config cfg) : cfg_{std::move(cfg)} +Recorder::Recorder(Config cfg) : cfg_{std::move(cfg)}, enabled_{cfg_.enabled} { if (cfg_.enabled) { file_.open(cfg_.file_path, std::ios::out | std::ios::app); if (file_.is_open()) { - // Write CSV header if the file is empty file_.seekp(0, std::ios::end); - if (file_.tellp() == 0) + if (file_.good() && file_.tellp() == std::streampos{0}) { file_ << "mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags\n"; } @@ -38,13 +37,23 @@ Recorder::Recorder(Config cfg) : cfg_{std::move(cfg)} void Recorder::Record(const RecordEntry& entry) { - if (!cfg_.enabled || !file_.is_open()) + if (!enabled_.load(std::memory_order_relaxed) || !file_.is_open()) return; std::lock_guard lk(mutex_); file_ << entry.mono_ns << ',' << static_cast(entry.event) << ',' << entry.offset_ns << ',' << entry.pdelay_ns << ',' << entry.seq_id << ',' << static_cast(entry.status_flags) << '\n'; - file_.flush(); + + ++flush_counter_; + if (flush_counter_ >= cfg_.flush_interval) + { + file_.flush(); + flush_counter_ = 0U; + if (!file_.good()) + { + enabled_.store(false, std::memory_order_relaxed); + } + } } } // namespace details diff --git a/score/TimeSlave/code/gptp/record/recorder.h b/score/TimeSlave/code/gptp/record/recorder.h index 839bf16..2fc4d95 100644 --- a/score/TimeSlave/code/gptp/record/recorder.h +++ b/score/TimeSlave/code/gptp/record/recorder.h @@ -13,6 +13,7 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_RECORD_RECORDER_H #define SCORE_TIMESLAVE_CODE_GPTP_RECORD_RECORDER_H +#include #include #include #include @@ -60,6 +61,7 @@ class Recorder final bool enabled = false; std::string file_path = "/var/log/gptp_record.csv"; std::int64_t offset_threshold_ns = 1'000'000LL; ///< 1 ms + std::uint32_t flush_interval = 8U; }; explicit Recorder(Config cfg); @@ -70,7 +72,7 @@ class Recorder final bool IsEnabled() const { - return cfg_.enabled && file_.is_open(); + return enabled_.load(std::memory_order_relaxed) && file_.is_open(); } /// Record an entry. Thread-safe. @@ -78,8 +80,10 @@ class Recorder final private: Config cfg_; + std::atomic enabled_{false}; std::mutex mutex_; std::ofstream file_; + std::uint32_t flush_counter_{0U}; }; } // namespace details diff --git a/score/TimeSlave/code/gptp/record/recorder_test.cpp b/score/TimeSlave/code/gptp/record/recorder_test.cpp index 35736dd..21ecce2 100644 --- a/score/TimeSlave/code/gptp/record/recorder_test.cpp +++ b/score/TimeSlave/code/gptp/record/recorder_test.cpp @@ -179,6 +179,52 @@ TEST_F(RecorderFileTest, Record_FieldsWrittenCorrectly) EXPECT_EQ(data, "9000000000,2,12345,999,7,1"); } +TEST_F(RecorderFileTest, ExistingFile_HeaderNotWrittenAgain) +{ + // Pre-populate the file so tellp() != 0 when the Recorder opens it in + // append mode — the header-write branch must be skipped. + { + std::ofstream f(path_); + f << "existing_line\n"; + } + + { + auto r = MakeRecorder(); + } + + std::ifstream f(path_); + std::string line1, line2; + ASSERT_TRUE(std::getline(f, line1)); + EXPECT_EQ(line1, "existing_line"); + if (std::getline(f, line2)) + { + EXPECT_NE(line2, "mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags"); + } +} + +TEST_F(RecorderFileTest, Record_FlushIntervalOne_TriggersFlushAfterEachRecord) +{ + // flush_interval=1: after the first Record() flush_counter_ reaches the + // threshold, exercising the flush branch and the counter reset path. + Recorder::Config cfg; + cfg.enabled = true; + cfg.file_path = path_; + cfg.flush_interval = 1U; + Recorder r{cfg}; + + RecordEntry e{}; + e.mono_ns = 42LL; + e.event = RecordEvent::kSyncReceived; + r.Record(e); + + std::ifstream f(path_); + int lines = 0; + std::string line; + while (std::getline(f, line)) + ++lines; + EXPECT_GE(lines, 2); // header + at least 1 data line +} + } // namespace details } // namespace ts } // namespace score diff --git a/score/libTSClient/gptp_ipc_channel.h b/score/libTSClient/gptp_ipc_channel.h index f7cc936..999d2a1 100644 --- a/score/libTSClient/gptp_ipc_channel.h +++ b/score/libTSClient/gptp_ipc_channel.h @@ -26,10 +26,10 @@ namespace details { /// Default POSIX shared memory name for the gPTP IPC channel. -static constexpr char kGptpIpcName[] = "/gptp_ptp_info"; +constexpr char kGptpIpcName[] = "/gptp_ptp_info"; /// Magic number to validate the shared memory region ('GPTP'). -static constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; +inline constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; /** * @brief Shared memory layout for gPTP IPC (seqlock protocol). @@ -43,10 +43,10 @@ static constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; */ struct alignas(64) GptpIpcRegion { - std::uint32_t magic{kGptpIpcMagic}; + std::atomic magic{kGptpIpcMagic}; std::atomic seq{0}; score::td::PtpTimeInfo data{}; - std::atomic seq_confirm{0}; + std::atomic seq_confirm{1}; }; } // namespace details diff --git a/score/libTSClient/gptp_ipc_publisher.cpp b/score/libTSClient/gptp_ipc_publisher.cpp index 2c4cbc2..7b84f9c 100644 --- a/score/libTSClient/gptp_ipc_publisher.cpp +++ b/score/libTSClient/gptp_ipc_publisher.cpp @@ -16,6 +16,10 @@ #include #include #include +#include + +static_assert(std::is_trivially_copyable::value, + "PtpTimeInfo must be trivially copyable for seqlock memcpy to be valid"); namespace score { @@ -31,9 +35,14 @@ GptpIpcPublisher::~GptpIpcPublisher() bool GptpIpcPublisher::Init(const std::string& ipc_name) { + if (region_ != nullptr) + return true; + ipc_name_ = ipc_name; - shm_fd_ = ::shm_open(ipc_name_.c_str(), O_CREAT | O_RDWR, 0666); + (void)::shm_unlink(ipc_name_.c_str()); + + shm_fd_ = ::shm_open(ipc_name_.c_str(), O_CREAT | O_RDWR, 0600); if (shm_fd_ < 0) return false; @@ -62,11 +71,13 @@ void GptpIpcPublisher::Publish(const score::td::PtpTimeInfo& info) return; const std::uint32_t next = region_->seq.load(std::memory_order_relaxed) + 1U; - region_->seq.store(next, std::memory_order_release); - + region_->seq.store(next, std::memory_order_relaxed); + // Release fence: prevents the data writes below from being reordered before + // the seq=odd store above on weakly-ordered CPUs (ARM64/QNX). The acquire + // half of acq_rel is unnecessary for a seqlock writer; release suffices here. std::atomic_thread_fence(std::memory_order_release); + std::memcpy(®ion_->data, &info, sizeof(score::td::PtpTimeInfo)); - std::atomic_thread_fence(std::memory_order_release); region_->seq_confirm.store(next + 1U, std::memory_order_release); region_->seq.store(next + 1U, std::memory_order_release); @@ -76,6 +87,7 @@ void GptpIpcPublisher::Destroy() { if (region_ != nullptr) { + region_->~GptpIpcRegion(); ::munmap(region_, sizeof(GptpIpcRegion)); region_ = nullptr; } diff --git a/score/libTSClient/gptp_ipc_receiver.cpp b/score/libTSClient/gptp_ipc_receiver.cpp index fbc6422..8d5e1c1 100644 --- a/score/libTSClient/gptp_ipc_receiver.cpp +++ b/score/libTSClient/gptp_ipc_receiver.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -33,10 +34,23 @@ GptpIpcReceiver::~GptpIpcReceiver() bool GptpIpcReceiver::Init(const std::string& ipc_name) { + if (region_ != nullptr) + return true; + shm_fd_ = ::shm_open(ipc_name.c_str(), O_RDONLY, 0); if (shm_fd_ < 0) return false; + { + struct ::stat st{}; + if (::fstat(shm_fd_, &st) != 0 || static_cast(st.st_size) < sizeof(GptpIpcRegion)) + { + ::close(shm_fd_); + shm_fd_ = -1; + return false; + } + } + void* ptr = ::mmap(nullptr, sizeof(GptpIpcRegion), PROT_READ, MAP_SHARED, shm_fd_, 0); if (ptr == MAP_FAILED) { @@ -47,7 +61,7 @@ bool GptpIpcReceiver::Init(const std::string& ipc_name) region_ = static_cast(ptr); - if (region_->magic != kGptpIpcMagic) + if (region_->magic.load(std::memory_order_acquire) != kGptpIpcMagic) { Close(); return false; @@ -66,16 +80,24 @@ std::optional GptpIpcReceiver::Receive() const std::uint32_t seq1 = region_->seq.load(std::memory_order_acquire); if ((seq1 & 1U) != 0U) - continue; + continue; // write in progress, retry - std::atomic_thread_fence(std::memory_order_acquire); score::td::PtpTimeInfo data{}; std::memcpy(&data, ®ion_->data, sizeof(score::td::PtpTimeInfo)); - std::atomic_thread_fence(std::memory_order_acquire); + + // acq_rel fence: prevents data reads from floating past the consistency checks below + // (release half prevents memcpy reordering after the fence on ARM64), and prevents + // the seq/seq_confirm loads below from floating before the data reads (acquire half). + std::atomic_thread_fence(std::memory_order_acq_rel); const std::uint32_t seq2 = region_->seq_confirm.load(std::memory_order_acquire); + // Re-read seq to detect a write that started AFTER our initial seq1 snapshot. + // Without this, a writer that sets seq=odd after seq1 was loaded would go undetected: + // seq_confirm would still hold the old even value, causing the reader to return + // partially-written data (seqlock race on all multi-core platforms). + const std::uint32_t seq3 = region_->seq.load(std::memory_order_acquire); - if (seq1 == seq2) + if (seq1 == seq2 && seq1 == seq3) return data; } diff --git a/score/libTSClient/gptp_ipc_test.cpp b/score/libTSClient/gptp_ipc_test.cpp index fbaa0f4..a73094c 100644 --- a/score/libTSClient/gptp_ipc_test.cpp +++ b/score/libTSClient/gptp_ipc_test.cpp @@ -118,6 +118,13 @@ TEST_F(GptpIpcPublisherTest, Destroy_WithoutInit_DoesNotCrash) EXPECT_NO_THROW(pub_.Destroy()); } +TEST_F(GptpIpcPublisherTest, Init_CalledTwice_ReturnsTrueOnSecondCall) +{ + // region_ != nullptr after first Init → second call returns true immediately. + ASSERT_TRUE(pub_.Init(UniqueShmName())); + EXPECT_TRUE(pub_.Init(UniqueShmName())); +} + // ── GptpIpcReceiver ─────────────────────────────────────────────────────────── class GptpIpcReceiverTest : public ::testing::Test @@ -152,6 +159,31 @@ TEST_F(GptpIpcReceiverTest, Receive_WithoutInit_ReturnsNullopt) EXPECT_FALSE(rx_.Receive().has_value()); } +TEST_F(GptpIpcReceiverTest, Init_CalledTwice_ReturnsTrueOnSecondCall) +{ + // region_ != nullptr after first Init → second call returns true immediately. + GptpIpcPublisher pub; + const std::string name = UniqueShmName(); + ASSERT_TRUE(pub.Init(name)); + ASSERT_TRUE(rx_.Init(name)); + EXPECT_TRUE(rx_.Init(name)); + pub.Destroy(); +} + +TEST_F(GptpIpcReceiverTest, Init_TooSmallShm_ReturnsFalse) +{ + // Create a shm segment smaller than GptpIpcRegion so the fstat size check fails. + const std::string name = UniqueShmName(); + const int fd = ::shm_open(name.c_str(), O_CREAT | O_RDWR, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(::ftruncate(fd, 1), 0); + ::close(fd); + + EXPECT_FALSE(rx_.Init(name)); + + ::shm_unlink(name.c_str()); +} + // ── Publisher + Receiver roundtrip ──────────────────────────────────────────── class GptpIpcRoundtripTest : public ::testing::Test @@ -178,14 +210,14 @@ TEST_F(GptpIpcRoundtripTest, ReceiverInit_AfterPublisherInit_ReturnsTrue) EXPECT_TRUE(rx_.Init(name_)); } -TEST_F(GptpIpcRoundtripTest, ReceiverReceive_BeforeAnyPublish_ReturnsDefaultData) +TEST_F(GptpIpcRoundtripTest, ReceiverReceive_BeforeAnyPublish_ReturnsNullopt) { ASSERT_TRUE(pub_.Init(name_)); ASSERT_TRUE(rx_.Init(name_)); - // seq == seq_confirm == 0: both even and equal → seqlock considers readable. - auto result = rx_.Receive(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->ptp_assumed_time, std::chrono::nanoseconds{0}); + // seq_confirm is initialised to 1 (≠ seq=0) by GptpIpcRegion's constructor, + // so the seqlock always mismatches before the first Publish() call. + // Receive() must exhaust its retries and return std::nullopt. + EXPECT_FALSE(rx_.Receive().has_value()); } TEST_F(GptpIpcRoundtripTest, PublishReceive_BasicFields_RoundtripCorrectly) From c1de3cd99461225bc321e12f5cc1628f027d039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Wed, 1 Apr 2026 14:41:41 +0800 Subject: [PATCH 06/12] [ECARX][TimeSlave]Add document content --- docs/TimeSlave/index.rst | 7 + docs/index.rst | 1 + score/TimeSlave/code/gptp/details/BUILD | 10 + .../code/gptp/details/i_os_syscalls.h | 102 ++++ .../TimeSlave/code/gptp/details/raw_socket.h | 5 +- .../code/gptp/details/raw_socket_test.cpp | 464 +++++++++++++++++- .../code/gptp/platform/linux/raw_socket.cpp | 45 +- .../code/gptp/platform/qnx/raw_socket.cpp | 2 + 8 files changed, 604 insertions(+), 32 deletions(-) create mode 100644 score/TimeSlave/code/gptp/details/i_os_syscalls.h diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 84d3843..923f349 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -461,3 +461,10 @@ The ``PhcConfig`` struct additionally contains: * - ``step_threshold_ns`` - int64_t - Offset threshold above which a step correction is applied instead of frequency slew + +.. toctree:: + :maxdepth: 2 + :hidden: + + gptp_engine/index + libTSClient/index diff --git a/docs/index.rst b/docs/index.rst index 14bf088..03e6a29 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ For a detailed concept and architectural design, please refer to the :doc:`TimeD :caption: Contents: time/index + TimeSlave/index Project Layout -------------- diff --git a/score/TimeSlave/code/gptp/details/BUILD b/score/TimeSlave/code/gptp/details/BUILD index d727051..634117e 100644 --- a/score/TimeSlave/code/gptp/details/BUILD +++ b/score/TimeSlave/code/gptp/details/BUILD @@ -31,6 +31,15 @@ cc_library( deps = [], ) +cc_library( + name = "i_os_syscalls", + hdrs = ["i_os_syscalls.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["QM"], + visibility = ["//score:__subpackages__"], + deps = [], +) + cc_library( name = "i_network_identity", hdrs = ["i_network_identity.h"], @@ -55,6 +64,7 @@ cc_library( tags = ["QM"], visibility = ["//score:__subpackages__"], deps = [ + ":i_os_syscalls", ":i_raw_socket", ":ptp_types", ], diff --git a/score/TimeSlave/code/gptp/details/i_os_syscalls.h b/score/TimeSlave/code/gptp/details/i_os_syscalls.h new file mode 100644 index 0000000..fd4e8f9 --- /dev/null +++ b/score/TimeSlave/code/gptp/details/i_os_syscalls.h @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_OS_SYSCALLS_H +#define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_OS_SYSCALLS_H + +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Thin abstraction over the POSIX system calls used by RawSocket. +/// The default production path calls the real syscalls; a fake can be injected +/// in unit tests to exercise every branch without requiring CAP_NET_RAW. +class IOsSyscalls +{ + public: + virtual ~IOsSyscalls() = default; + + virtual int socket_call(int domain, int type, int protocol) noexcept = 0; + virtual int ioctl_call(int fd, unsigned long req, void* arg) noexcept = 0; + virtual int bind_call(int fd, const ::sockaddr* addr, ::socklen_t addrlen) noexcept = 0; + virtual int setsockopt_call(int fd, + int level, + int optname, + const void* optval, + ::socklen_t optlen) noexcept = 0; + virtual int close_call(int fd) noexcept = 0; + virtual int poll_call(::pollfd* fds, ::nfds_t nfds, int timeout) noexcept = 0; + virtual ::ssize_t recvmsg_call(int fd, ::msghdr* msg, int flags) noexcept = 0; + virtual ::ssize_t send_call(int fd, const void* buf, ::size_t len, int flags) noexcept = 0; +}; + +/// Real production implementation — delegates directly to the OS. +class RealOsSyscalls final : public IOsSyscalls +{ + public: + static RealOsSyscalls& Instance() noexcept + { + static RealOsSyscalls s; + return s; + } + + int socket_call(int d, int t, int p) noexcept override + { + return ::socket(d, t, p); + } + int ioctl_call(int fd, unsigned long req, void* arg) noexcept override + { + return ::ioctl(fd, req, arg); + } + int bind_call(int fd, const ::sockaddr* a, ::socklen_t l) noexcept override + { + return ::bind(fd, a, l); + } + int setsockopt_call(int fd, int lv, int opt, const void* v, ::socklen_t l) noexcept override + { + return ::setsockopt(fd, lv, opt, v, l); + } + int close_call(int fd) noexcept override + { + return ::close(fd); + } + int poll_call(::pollfd* fds, ::nfds_t n, int t) noexcept override + { + return ::poll(fds, n, t); + } + ::ssize_t recvmsg_call(int fd, ::msghdr* m, int f) noexcept override + { + return ::recvmsg(fd, m, f); + } + ::ssize_t send_call(int fd, const void* b, ::size_t l, int f) noexcept override + { + return ::send(fd, b, l, f); + } + + private: + RealOsSyscalls() = default; +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_TIMESLAVE_CODE_GPTP_DETAILS_I_OS_SYSCALLS_H diff --git a/score/TimeSlave/code/gptp/details/raw_socket.h b/score/TimeSlave/code/gptp/details/raw_socket.h index 0f19657..9f148cf 100644 --- a/score/TimeSlave/code/gptp/details/raw_socket.h +++ b/score/TimeSlave/code/gptp/details/raw_socket.h @@ -13,6 +13,7 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_RAW_SOCKET_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_RAW_SOCKET_H +#include "score/TimeSlave/code/gptp/details/i_os_syscalls.h" #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" #include @@ -38,7 +39,8 @@ namespace details class RawSocket : public IRawSocket { public: - RawSocket() noexcept = default; + /// @param sys Optional syscall shim for unit testing. nullptr → real OS calls. + explicit RawSocket(IOsSyscalls* sys = nullptr) noexcept; ~RawSocket() override; RawSocket(const RawSocket&) = delete; @@ -80,6 +82,7 @@ class RawSocket : public IRawSocket } private: + IOsSyscalls* sys_{nullptr}; std::atomic fd_{-1}; std::string iface_{}; }; diff --git a/score/TimeSlave/code/gptp/details/raw_socket_test.cpp b/score/TimeSlave/code/gptp/details/raw_socket_test.cpp index ca7d566..4e9f786 100644 --- a/score/TimeSlave/code/gptp/details/raw_socket_test.cpp +++ b/score/TimeSlave/code/gptp/details/raw_socket_test.cpp @@ -15,7 +15,12 @@ #include +#include +#include +#include +#include #include +#include #include namespace score @@ -25,30 +30,222 @@ namespace ts namespace details { -// ── RawSocket — closed-state guard paths ───────────────────────────────────── +namespace +{ + +// ── FakeOsSyscalls ───────────────────────────────────────────────────────────── +// +// A controllable IOsSyscalls implementation that never touches the real OS. +// Each method has configurable return values; side-effects (like filling cmsg +// data) are controlled by boolean flags. + +class FakeOsSyscalls final : public IOsSyscalls +{ + public: + // ── socket ──────────────────────────────────────────────────────────────── + int socket_fd{42}; // fd returned on success + bool socket_fail{false}; // true → return -1 + + int socket_call(int /*domain*/, int /*type*/, int /*protocol*/) noexcept override + { + if (socket_fail) + { + errno = EPERM; + return -1; + } + return socket_fd; + } + + // ── ioctl ───────────────────────────────────────────────────────────────── + bool ioctl_siocgifindex_fail{false}; // SIOCGIFINDEX failure + bool ioctl_siocshwtstamp_fail{false}; // first SIOCSHWTSTAMP failure (fallback exercised) + + int ioctl_call(int /*fd*/, unsigned long req, void* /*arg*/) noexcept override + { + if (req == SIOCGIFINDEX) + return ioctl_siocgifindex_fail ? -1 : 0; + if (req == SIOCSHWTSTAMP) + { + if (ioctl_siocshwtstamp_fail) + { + ioctl_siocshwtstamp_fail = false; // first call fails; second succeeds + return -1; + } + return 0; + } + return 0; + } + + // ── bind ───────────────────────────────────────────────────────────────── + bool bind_fail{false}; + + int bind_call(int /*fd*/, const ::sockaddr* /*addr*/, ::socklen_t /*addrlen*/) noexcept override + { + return bind_fail ? -1 : 0; + } + + // ── setsockopt ──────────────────────────────────────────────────────────── + bool setsockopt_fail{false}; + + int setsockopt_call(int /*fd*/, + int /*level*/, + int /*optname*/, + const void* /*optval*/, + ::socklen_t /*optlen*/) noexcept override + { + return setsockopt_fail ? -1 : 0; + } + + // ── close ──────────────────────────────────────────────────────────────── + int close_count{0}; + int last_closed_fd{-1}; + + int close_call(int fd) noexcept override + { + ++close_count; + last_closed_fd = fd; + return 0; + } + + // ── poll ───────────────────────────────────────────────────────────────── + int poll_result{0}; // 0=timeout, -1=error, >0=ready + int poll_revents{POLLIN}; + + int poll_call(::pollfd* fds, ::nfds_t /*nfds*/, int /*timeout*/) noexcept override + { + if (fds != nullptr) + fds[0].revents = (poll_result > 0) ? static_cast(poll_revents) : 0; + return poll_result; + } + + // ── recvmsg ─────────────────────────────────────────────────────────────── + // Regular (non-errqueue) recvmsg — used by Recv(). + ::ssize_t recvmsg_result{-1}; // bytes returned; -1 = error + bool recvmsg_fill_hwts{false}; // fill SO_TIMESTAMPING cmsg with ts[2]={1,500000000} + + // MSG_ERRQUEUE recvmsg — used by DrainErrQueue (before send) and TX timestamp (after send). + // Sequence per Send() call: + // calls 0 .. errqueue_drain_count-1 → return 1 (drain loop body runs) + // call errqueue_drain_count → return -1 (terminates DrainErrQueue loop) + // call errqueue_drain_count+1 → TX timestamp: fill hwts if tx_fill_hwts, return tx_result + int errqueue_drain_count{0}; // how many drain entries to simulate + bool tx_fill_hwts{false}; // fill SO_TIMESTAMPING cmsg in TX-timestamp recvmsg + ::ssize_t tx_result{14}; // TX-timestamp recvmsg return value + + int recvmsg_call_count{0}; + + ::ssize_t recvmsg_call(int /*fd*/, ::msghdr* msg, int flags) noexcept override + { + ++recvmsg_call_count; + + if ((flags & MSG_ERRQUEUE) != 0) + { + const int idx = errqueue_call_count_++; + if (idx < errqueue_drain_count) + return 1; // drain entry present — loop body exercises + if (idx == errqueue_drain_count) + return -1; // end of drain queue → loop exits + // idx > errqueue_drain_count: this is the TX-timestamp recvmsg + if (tx_fill_hwts && msg != nullptr && msg->msg_control != nullptr && + msg->msg_controllen >= CMSG_SPACE(3 * sizeof(::timespec))) + { + auto* cm = reinterpret_cast<::cmsghdr*>(msg->msg_control); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SO_TIMESTAMPING; + cm->cmsg_len = CMSG_LEN(3 * sizeof(::timespec)); + auto* ts = reinterpret_cast<::timespec*>(CMSG_DATA(cm)); + ts[0] = {0, 0}; + ts[1] = {0, 0}; + ts[2] = {1, 500'000'000L}; + msg->msg_controllen = CMSG_SPACE(3 * sizeof(::timespec)); + } + return tx_result; + } + + // Regular recvmsg (from Recv()). + if (recvmsg_result < 0) + return -1; + + // Optionally inject SO_TIMESTAMPING cmsg for Recv() hwts extraction. + if (recvmsg_fill_hwts && msg != nullptr && msg->msg_control != nullptr && + msg->msg_controllen >= CMSG_SPACE(3 * sizeof(::timespec))) + { + auto* cm = reinterpret_cast<::cmsghdr*>(msg->msg_control); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SO_TIMESTAMPING; + cm->cmsg_len = CMSG_LEN(3 * sizeof(::timespec)); + auto* ts = reinterpret_cast<::timespec*>(CMSG_DATA(cm)); + ts[0] = {0, 0}; + ts[1] = {0, 0}; + ts[2] = {1, 500'000'000L}; + msg->msg_controllen = CMSG_SPACE(3 * sizeof(::timespec)); + } + + if (msg != nullptr && msg->msg_iov != nullptr && recvmsg_result > 0) + { + const std::size_t n = + std::min(static_cast(recvmsg_result), msg->msg_iov[0].iov_len); + std::memset(msg->msg_iov[0].iov_base, 0, n); + } + return recvmsg_result; + } + + private: + int errqueue_call_count_{0}; // counts MSG_ERRQUEUE recvmsg calls (resets per test instance) + + public: + // ── send ────────────────────────────────────────────────────────────────── + ::ssize_t send_result{14}; // bytes "sent"; -1 = error + + ::ssize_t send_call(int /*fd*/, + const void* /*buf*/, + ::size_t len, + int /*flags*/) noexcept override + { + if (send_result < 0) + return -1; + return static_cast<::ssize_t>(len); + } +}; + +// Helper: open the socket successfully using the given fake syscalls. +void OpenSocket(RawSocket& sock, FakeOsSyscalls& /*fake*/) +{ + (void)sock.Open("eth0"); +} + +} // namespace + +// ── RawSocket — closed-state guard paths ────────────────────────────────────── TEST(RawSocketTest, DefaultConstruct_GetFd_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + fake.socket_fail = true; // never actually open + RawSocket sock{&fake}; EXPECT_EQ(sock.GetFd(), -1); } TEST(RawSocketTest, Close_WhenNotOpen_IsNoOp) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; EXPECT_NO_THROW(sock.Close()); EXPECT_EQ(sock.GetFd(), -1); + EXPECT_EQ(fake.close_count, 0); // no real fd was open } TEST(RawSocketTest, EnableHwTimestamping_WhenNotOpen_ReturnsFalse) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; EXPECT_FALSE(sock.EnableHwTimestamping()); } TEST(RawSocketTest, Recv_WhenNotOpen_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; std::uint8_t buf[64] = {}; ::timespec hwts{}; EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 0), -1); @@ -56,14 +253,18 @@ TEST(RawSocketTest, Recv_WhenNotOpen_ReturnsNegativeOne) TEST(RawSocketTest, Recv_NullBuf_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); ::timespec hwts{}; EXPECT_EQ(sock.Recv(nullptr, 64U, hwts, 0), -1); } TEST(RawSocketTest, Recv_ZeroBufLen_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); std::uint8_t buf[1] = {}; ::timespec hwts{}; EXPECT_EQ(sock.Recv(buf, 0U, hwts, 0), -1); @@ -71,7 +272,8 @@ TEST(RawSocketTest, Recv_ZeroBufLen_ReturnsNegativeOne) TEST(RawSocketTest, Send_WhenNotOpen_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; const std::uint8_t data[14] = {}; ::timespec hwts{}; EXPECT_EQ(sock.Send(data, 14, hwts), -1); @@ -79,14 +281,18 @@ TEST(RawSocketTest, Send_WhenNotOpen_ReturnsNegativeOne) TEST(RawSocketTest, Send_NullBuf_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); ::timespec hwts{}; EXPECT_EQ(sock.Send(nullptr, 14, hwts), -1); } TEST(RawSocketTest, Send_ZeroLen_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); const std::uint8_t data[1] = {}; ::timespec hwts{}; EXPECT_EQ(sock.Send(data, 0, hwts), -1); @@ -94,16 +300,58 @@ TEST(RawSocketTest, Send_ZeroLen_ReturnsNegativeOne) TEST(RawSocketTest, Send_NegativeLen_ReturnsNegativeOne) { - RawSocket sock; + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); const std::uint8_t data[1] = {}; ::timespec hwts{}; EXPECT_EQ(sock.Send(data, -1, hwts), -1); } -// ── RawSocket — invalid interface ──────────────────────────────────────────── +// ── RawSocket — Open() failure paths ───────────────────────────────────────── + +TEST(RawSocketTest, Open_SocketCallFails_ReturnsFalse) +{ + FakeOsSyscalls fake; + fake.socket_fail = true; + RawSocket sock{&fake}; + EXPECT_FALSE(sock.Open("eth0")); + EXPECT_EQ(sock.GetFd(), -1); +} + +TEST(RawSocketTest, Open_IoctlSiocgifindexFails_ReturnsFalse) +{ + FakeOsSyscalls fake; + fake.ioctl_siocgifindex_fail = true; + RawSocket sock{&fake}; + EXPECT_FALSE(sock.Open("eth0")); + EXPECT_EQ(sock.GetFd(), -1); + // The fake fd must have been closed on failure + EXPECT_EQ(fake.close_count, 1); + EXPECT_EQ(fake.last_closed_fd, fake.socket_fd); +} + +TEST(RawSocketTest, Open_BindFails_ReturnsFalse) +{ + FakeOsSyscalls fake; + fake.bind_fail = true; + RawSocket sock{&fake}; + EXPECT_FALSE(sock.Open("eth0")); + EXPECT_EQ(sock.GetFd(), -1); + EXPECT_EQ(fake.close_count, 1); +} + +TEST(RawSocketTest, Open_Success_ReturnsTrueAndStoresFd) +{ + FakeOsSyscalls fake; + RawSocket sock{&fake}; + EXPECT_TRUE(sock.Open("eth0")); + EXPECT_EQ(sock.GetFd(), fake.socket_fd); +} TEST(RawSocketTest, Open_NonExistentInterface_ReturnsFalse) { + // Uses RealOsSyscalls; ioctl(SIOCGIFINDEX) will fail for unknown iface. RawSocket sock; EXPECT_FALSE(sock.Open("nonexistent_eth_zzz")); } @@ -115,6 +363,198 @@ TEST(RawSocketTest, Open_NonExistentInterface_GetFdRemainsNegativeOne) EXPECT_EQ(sock.GetFd(), -1); } +// ── RawSocket — Close() ─────────────────────────────────────────────────────── + +TEST(RawSocketTest, Close_AfterOpen_CallsCloseOnFd) +{ + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + EXPECT_EQ(sock.GetFd(), fake.socket_fd); + sock.Close(); + EXPECT_EQ(sock.GetFd(), -1); + EXPECT_EQ(fake.close_count, 1); + EXPECT_EQ(fake.last_closed_fd, fake.socket_fd); +} + +TEST(RawSocketTest, Close_CalledTwiceAfterOpen_IsIdempotent) +{ + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + sock.Close(); + EXPECT_NO_THROW(sock.Close()); + EXPECT_EQ(sock.GetFd(), -1); + EXPECT_EQ(fake.close_count, 1); // second Close() is a no-op +} + +TEST(RawSocketTest, Destructor_AfterOpen_ClosesSocket) +{ + FakeOsSyscalls fake; + { + RawSocket sock{&fake}; + OpenSocket(sock, fake); + EXPECT_EQ(sock.GetFd(), fake.socket_fd); + } // destructor calls Close() + EXPECT_EQ(fake.close_count, 1); +} + +// ── RawSocket — EnableHwTimestamping() ─────────────────────────────────────── + +TEST(RawSocketTest, EnableHwTimestamping_Success_ReturnsTrue) +{ + FakeOsSyscalls fake; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + EXPECT_TRUE(sock.EnableHwTimestamping()); +} + +TEST(RawSocketTest, EnableHwTimestamping_SiocshwtstampFallback_StillReturnsTrue) +{ + // First SIOCSHWTSTAMP ioctl fails → fallback (second call) is attempted. + FakeOsSyscalls fake; + fake.ioctl_siocshwtstamp_fail = true; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + EXPECT_TRUE(sock.EnableHwTimestamping()); +} + +TEST(RawSocketTest, EnableHwTimestamping_SetsockoptFails_ReturnsFalse) +{ + FakeOsSyscalls fake; + fake.setsockopt_fail = true; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + EXPECT_FALSE(sock.EnableHwTimestamping()); +} + +// ── RawSocket — Recv() ──────────────────────────────────────────────────────── + +TEST(RawSocketTest, Recv_PollTimeout_ReturnsZero) +{ + FakeOsSyscalls fake; + fake.poll_result = 0; // timeout + RawSocket sock{&fake}; + OpenSocket(sock, fake); + std::uint8_t buf[64] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 10), 0); +} + +TEST(RawSocketTest, Recv_PollError_ReturnsNegativeOne) +{ + FakeOsSyscalls fake; + fake.poll_result = -1; // poll error + RawSocket sock{&fake}; + OpenSocket(sock, fake); + std::uint8_t buf[64] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 10), -1); +} + +TEST(RawSocketTest, Recv_RecvmsgFails_ReturnsNegativeOne) +{ + FakeOsSyscalls fake; + fake.poll_result = 1; + fake.recvmsg_result = -1; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + std::uint8_t buf[64] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 10), -1); +} + +TEST(RawSocketTest, Recv_Success_NoTimestamp_ReturnsLen) +{ + FakeOsSyscalls fake; + fake.poll_result = 1; + fake.recvmsg_result = 14; // 14 bytes received + RawSocket sock{&fake}; + OpenSocket(sock, fake); + std::uint8_t buf[256] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 10), 14); + EXPECT_EQ(hwts.tv_sec, 0); + EXPECT_EQ(hwts.tv_nsec, 0); +} + +TEST(RawSocketTest, Recv_WithSoTimestampingCmsg_ExtractsHwts) +{ + FakeOsSyscalls fake; + fake.poll_result = 1; + fake.recvmsg_result = 14; + fake.recvmsg_fill_hwts = true; // inject ts[2]={1, 500_000_000} + RawSocket sock{&fake}; + OpenSocket(sock, fake); + std::uint8_t buf[256] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Recv(buf, sizeof(buf), hwts, 10), 14); + EXPECT_EQ(hwts.tv_sec, 1); + EXPECT_EQ(hwts.tv_nsec, 500'000'000L); +} + +// ── RawSocket — Send() ──────────────────────────────────────────────────────── + +TEST(RawSocketTest, Send_SendFails_ReturnsNegativeOne) +{ + FakeOsSyscalls fake; + fake.send_result = -1; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + const std::uint8_t data[14] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 14, hwts), -1); +} + +TEST(RawSocketTest, Send_Success_PollNoTxTs_ReturnsSentBytes) +{ + FakeOsSyscalls fake; + fake.send_result = 14; + fake.poll_result = 0; // no TX-timestamp event + RawSocket sock{&fake}; + OpenSocket(sock, fake); + const std::uint8_t data[14] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 14, hwts), 14); + EXPECT_EQ(hwts.tv_sec, 0); + EXPECT_EQ(hwts.tv_nsec, 0); +} + +TEST(RawSocketTest, Send_Success_TxTimestampCmsg_ExtractsHwts) +{ + // poll returns POLLERR → MSG_ERRQUEUE recvmsg fills SO_TIMESTAMPING cmsg. + FakeOsSyscalls fake; + fake.send_result = 14; + fake.poll_result = 1; + fake.poll_revents = POLLERR; + fake.tx_result = 14; // TX recvmsg succeeds + fake.tx_fill_hwts = true; // inject ts[2]={1,500_000_000} + RawSocket sock{&fake}; + OpenSocket(sock, fake); + const std::uint8_t data[14] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 14, hwts), 14); + EXPECT_EQ(hwts.tv_sec, 1); + EXPECT_EQ(hwts.tv_nsec, 500'000'000L); +} + +TEST(RawSocketTest, Send_DrainErrQueue_WhileBodyExecuted) +{ + // recvmsg_drain_limit=1 → first MSG_ERRQUEUE call returns 1 (while body runs), + // second returns -1 → loop exits. Covers DrainErrQueue's while body. + FakeOsSyscalls fake; + fake.send_result = 14; + fake.poll_result = 0; + fake.errqueue_drain_count = 1; + RawSocket sock{&fake}; + OpenSocket(sock, fake); + const std::uint8_t data[14] = {}; + ::timespec hwts{}; + EXPECT_EQ(sock.Send(data, 14, hwts), 14); + // recvmsg was called: 1 DrainErrQueue call (returns 1) + 1 call (returns -1) + EXPECT_GE(fake.recvmsg_call_count, 2); +} + // ── NetworkIdentity ─────────────────────────────────────────────────────────── TEST(NetworkIdentityTest, GetClockIdentity_BeforeResolve_ReturnsZeroIdentity) diff --git a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp index cd0387e..5a71e73 100644 --- a/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp +++ b/score/TimeSlave/code/gptp/platform/linux/raw_socket.cpp @@ -35,7 +35,7 @@ namespace details namespace { -void DrainErrQueue(int fd) noexcept +void DrainErrQueue(int fd, IOsSyscalls& sys) noexcept { char buf[2048]; ::iovec iov{buf, sizeof(buf)}; @@ -46,13 +46,18 @@ void DrainErrQueue(int fd) noexcept msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); - while (::recvmsg(fd, &msg, MSG_ERRQUEUE) > 0) + while (sys.recvmsg_call(fd, &msg, MSG_ERRQUEUE) > 0) { } } } // namespace +RawSocket::RawSocket(IOsSyscalls* sys) noexcept + : sys_{sys != nullptr ? sys : &RealOsSyscalls::Instance()} +{ +} + RawSocket::~RawSocket() { Close(); @@ -62,16 +67,16 @@ bool RawSocket::Open(const std::string& iface) { Close(); - const int fd = ::socket(AF_PACKET, SOCK_RAW, htons(ETH_P_1588)); + const int fd = sys_->socket_call(AF_PACKET, SOCK_RAW, htons(ETH_P_1588)); if (fd < 0) return false; ::ifreq ifr{}; std::strncpy(ifr.ifr_name, iface.c_str(), IFNAMSIZ - 1); ifr.ifr_name[IFNAMSIZ - 1] = '\0'; - if (::ioctl(fd, SIOCGIFINDEX, &ifr) < 0) + if (sys_->ioctl_call(fd, SIOCGIFINDEX, &ifr) < 0) { - ::close(fd); + sys_->close_call(fd); return false; } @@ -79,14 +84,15 @@ bool RawSocket::Open(const std::string& iface) sa.sll_family = AF_PACKET; sa.sll_protocol = htons(ETH_P_1588); sa.sll_ifindex = ifr.ifr_ifindex; - if (::bind(fd, reinterpret_cast<::sockaddr*>(&sa), sizeof(sa)) < 0) + if (sys_->bind_call(fd, reinterpret_cast<::sockaddr*>(&sa), sizeof(sa)) < 0) { - ::close(fd); + sys_->close_call(fd); return false; } // SO_BINDTODEVICE: best-effort, don't fail if it doesn't work - (void)::setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), static_cast(iface.size())); + (void)sys_->setsockopt_call( + fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), static_cast(iface.size())); fd_.store(fd, std::memory_order_release); iface_ = iface; @@ -108,15 +114,16 @@ bool RawSocket::EnableHwTimestamping() cfg.tx_type = HWTSTAMP_TX_ON; cfg.rx_filter = HWTSTAMP_FILTER_ALL; - if (::ioctl(fd, SIOCSHWTSTAMP, &ifr) < 0) + if (sys_->ioctl_call(fd, SIOCSHWTSTAMP, &ifr) < 0) { // Fall back to PTP-only filter cfg.rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT; - (void)::ioctl(fd, SIOCSHWTSTAMP, &ifr); + (void)sys_->ioctl_call(fd, SIOCSHWTSTAMP, &ifr); } - const int ts_opts = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; - if (::setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &ts_opts, sizeof(ts_opts)) < 0) + const int ts_opts = + SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; + if (sys_->setsockopt_call(fd, SOL_SOCKET, SO_TIMESTAMPING, &ts_opts, sizeof(ts_opts)) < 0) { return false; } @@ -127,7 +134,7 @@ void RawSocket::Close() { const int fd = fd_.exchange(-1, std::memory_order_acq_rel); if (fd >= 0) - ::close(fd); + sys_->close_call(fd); iface_.clear(); } @@ -139,7 +146,7 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, in // Poll with caller-specified timeout ::pollfd pfd{fd, POLLIN, 0}; - const int pr = ::poll(&pfd, 1, timeout_ms); + const int pr = sys_->poll_call(&pfd, 1, timeout_ms); if (pr == 0) return 0; // timeout if (pr < 0) @@ -153,7 +160,7 @@ int RawSocket::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts, in msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); - const int len = static_cast(::recvmsg(fd, &msg, 0)); + const int len = static_cast(sys_->recvmsg_call(fd, &msg, 0)); if (len < 0) return -1; @@ -178,16 +185,16 @@ int RawSocket::Send(const void* buf, int len, ::timespec& hwts) if (fd < 0 || buf == nullptr || len <= 0) return -1; - DrainErrQueue(fd); + DrainErrQueue(fd, *sys_); - const int sent = static_cast(::send(fd, buf, static_cast(len), 0)); + const int sent = static_cast(sys_->send_call(fd, buf, static_cast(len), 0)); if (sent < 0) return -1; constexpr int kTxTsTimeoutMs = 50; ::pollfd pfd{fd, POLLERR, 0}; std::memset(&hwts, 0, sizeof(hwts)); - if (::poll(&pfd, 1, kTxTsTimeoutMs) > 0 && (pfd.revents & POLLERR) != 0) + if (sys_->poll_call(&pfd, 1, kTxTsTimeoutMs) > 0 && (pfd.revents & POLLERR) != 0) { std::uint8_t tmp[2048]; ::iovec iov{tmp, sizeof(tmp)}; @@ -198,7 +205,7 @@ int RawSocket::Send(const void* buf, int len, ::timespec& hwts) msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); - if (::recvmsg(fd, &msg, MSG_ERRQUEUE) >= 0) + if (sys_->recvmsg_call(fd, &msg, MSG_ERRQUEUE) >= 0) { for (::cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; cm = CMSG_NXTHDR(&msg, cm)) { diff --git a/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp index a970708..5a9aea1 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/raw_socket.cpp @@ -34,6 +34,8 @@ namespace ts namespace details { +RawSocket::RawSocket(IOsSyscalls* /*sys*/) noexcept {} + RawSocket::~RawSocket() { Close(); From 648de4fd68928299a209b079c4b9d04484190c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 10 Apr 2026 14:29:30 +0800 Subject: [PATCH 07/12] Incorporate review feedback --- .../{ => gptp_engine}/gptp_engine_class.puml | 86 ++- .../{ => gptp_engine}/gptp_threading.puml | 0 docs/TimeSlave/_assets/ipc_sequence.puml | 46 -- .../{ => libtsclient}/ipc_channel.puml | 36 +- .../_assets/libtsclient/ipc_sequence.puml | 52 ++ .../shm_ptp_engine/shm_ptp_engine_class.puml | 105 ++++ .../shm_ptp_engine_init_seq.puml | 82 +++ .../shm_ptp_engine_read_seq.puml | 73 +++ docs/TimeSlave/_assets/timeslave_class.puml | 83 +-- .../_assets/timeslave_data_flow.puml | 15 +- .../_assets/timeslave_deployment.puml | 73 +-- .../_assets/gptp_engine_class.puml | 95 ---- .../gptp_engine/_assets/gptp_threading.puml | 53 -- docs/TimeSlave/gptp_engine/index.rst | 227 -------- docs/TimeSlave/index.rst | 493 +++++++++++++++--- .../libTSClient/_assets/ipc_channel.puml | 46 -- .../libTSClient/_assets/ipc_sequence.puml | 46 -- docs/TimeSlave/libTSClient/index.rst | 175 ------- score/TimeDaemon/code/common/BUILD | 2 +- score/TimeDaemon/code/common/data_types/BUILD | 2 +- score/TimeDaemon/code/ptp_machine/real/BUILD | 4 +- .../code/ptp_machine/real/details/BUILD | 14 +- .../real/details/real_ptp_engine.cpp | 32 +- .../real/details/shm_ptp_engine.cpp | 99 ++++ .../ptp_machine/real/details/shm_ptp_engine.h | 62 +++ .../real/details/shm_ptp_engine_test.cpp | 216 ++++++++ .../code/ptp_machine/real/gptp_real_machine.h | 6 +- score/TimeSlave/BUILD | 11 + score/TimeSlave/code/application/BUILD | 1 - .../TimeSlave/code/application/time_slave.cpp | 40 +- .../TimeSlave/code/common/logging_contexts.h | 4 + score/TimeSlave/code/gptp/BUILD | 5 +- score/TimeSlave/code/gptp/details/BUILD | 5 +- .../code/gptp/details/pdelay_measurer.cpp | 2 +- .../code/gptp/details/pdelay_measurer.h | 4 +- .../gptp/details/pdelay_measurer_test.cpp | 2 +- score/TimeSlave/code/gptp/details/ptp_types.h | 2 +- .../code/gptp/details/sync_state_machine.cpp | 2 +- .../code/gptp/details/sync_state_machine.h | 4 +- .../gptp/details/sync_state_machine_test.cpp | 2 +- score/TimeSlave/code/gptp/gptp_engine.cpp | 74 +-- score/TimeSlave/code/gptp/gptp_engine.h | 26 +- .../TimeSlave/code/gptp/gptp_engine_test.cpp | 101 ++-- score/TimeSlave/code/gptp/instrument/BUILD | 2 +- .../TimeSlave/code/gptp/instrument/probe.cpp | 4 +- .../code/gptp/platform/qnx/qnx_raw_shim.cpp | 9 +- score/libTSClient/BUILD | 47 +- score/libTSClient/gptp_ipc_channel.h | 4 +- score/libTSClient/gptp_ipc_data.h | 90 ++++ score/libTSClient/gptp_ipc_publisher.cpp | 8 +- score/libTSClient/gptp_ipc_publisher.h | 4 +- score/libTSClient/gptp_ipc_publisher_test.cpp | 68 +++ score/libTSClient/gptp_ipc_receiver.cpp | 6 +- score/libTSClient/gptp_ipc_receiver.h | 4 +- score/libTSClient/gptp_ipc_receiver_test.cpp | 89 ++++ score/libTSClient/gptp_ipc_roundtrip_test.cpp | 218 ++++++++ score/libTSClient/gptp_ipc_test_utils.h | 82 +++ 57 files changed, 2101 insertions(+), 1042 deletions(-) rename docs/TimeSlave/_assets/{ => gptp_engine}/gptp_engine_class.puml (51%) rename docs/TimeSlave/_assets/{ => gptp_engine}/gptp_threading.puml (100%) delete mode 100644 docs/TimeSlave/_assets/ipc_sequence.puml rename docs/TimeSlave/_assets/{ => libtsclient}/ipc_channel.puml (50%) create mode 100644 docs/TimeSlave/_assets/libtsclient/ipc_sequence.puml create mode 100644 docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml create mode 100644 docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml create mode 100644 docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml delete mode 100644 docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml delete mode 100644 docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml delete mode 100644 docs/TimeSlave/gptp_engine/index.rst delete mode 100644 docs/TimeSlave/libTSClient/_assets/ipc_channel.puml delete mode 100644 docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml delete mode 100644 docs/TimeSlave/libTSClient/index.rst create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h create mode 100644 score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp create mode 100644 score/libTSClient/gptp_ipc_data.h create mode 100644 score/libTSClient/gptp_ipc_publisher_test.cpp create mode 100644 score/libTSClient/gptp_ipc_receiver_test.cpp create mode 100644 score/libTSClient/gptp_ipc_roundtrip_test.cpp create mode 100644 score/libTSClient/gptp_ipc_test_utils.h diff --git a/docs/TimeSlave/_assets/gptp_engine_class.puml b/docs/TimeSlave/_assets/gptp_engine/gptp_engine_class.puml similarity index 51% rename from docs/TimeSlave/_assets/gptp_engine_class.puml rename to docs/TimeSlave/_assets/gptp_engine/gptp_engine_class.puml index c29842d..f079879 100644 --- a/docs/TimeSlave/_assets/gptp_engine_class.puml +++ b/docs/TimeSlave/_assets/gptp_engine/gptp_engine_class.puml @@ -12,32 +12,33 @@ legend top left | <#Beige> | Instrumentation | endlegend -package "score::ts::gptp" { +package "score::ts::details" { class GptpEngine #LightSalmon { - - options_ : GptpEngineOptions + - opts_ : GptpEngineOptions - rx_thread_ : std::thread - pdelay_thread_ : std::thread - - socket_ : std::unique_ptr + - socket_ : unique_ptr + - identity_ : unique_ptr - codec_ : FrameCodec - - parser_ : MessageParser + - parser_ : GptpMessageParser - sync_sm_ : SyncStateMachine - pdelay_ : PeerDelayMeasurer - - phc_ : PhcAdjuster - - probe_mgr_ : ProbeManager - - recorder_ : Recorder + - phc_adjuster_ : PhcAdjuster - snapshot_mutex_ : std::mutex - - latest_snapshot_ : PtpTimeInfo + - pending_snapshot_ : GptpIpcData + - current_snapshot_ : GptpIpcData + Initialize() : bool - + Deinitialize() : void - + ReadPTPSnapshot() : PtpTimeInfo + + Deinitialize() : bool + + FinalizeSnapshot() : void + + ReadPTPSnapshot(data : GptpIpcData&) : bool } interface IRawSocket #LightSkyBlue { + Open(iface) : bool + EnableHwTimestamping() : bool + Recv(buf, timeout_ms) : RecvResult - + Send(buf, hw_ts) : bool + + Send(buf, len) : bool + GetFd() : int + Close() : void } @@ -53,29 +54,65 @@ package "score::ts::gptp" { interface INetworkIdentity #LightSkyBlue { + Resolve(iface) : bool + GetClockIdentity() : ClockIdentity + + GetMac() : MacAddress } class NetworkIdentity #LightSkyBlue { Derives EUI-64 from MAC\n(inserts 0xFF 0xFE) } + class FrameCodec #Wheat { + + ParseEthernetHeader(buf) : EthernetHeader + + AddEthernetHeader(buf, dst_mac, src_mac) : void + } + + class GptpMessageParser #Wheat { + + Parse(payload, len, msg_out) : bool + } + + class SyncStateMachine #Wheat { + - timeout_ : atomic + + OnSync(msg) : void + + OnFollowUp(msg) : optional + + IsTimeout() : bool + + GetNeighborRateRatio() : double + } + + class PeerDelayMeasurer #Wheat { + - mutex_ : std::mutex + - result_ : PDelayResult + + SendRequest(socket) : void + + OnResponse(msg) : void + + OnResponseFollowUp(msg) : void + + GetResult() : PDelayResult + } + + class PhcAdjuster #Lavender { + - cfg_ : PhcConfig + - phc_fd_ : int + + IsEnabled() : bool + + AdjustOffset(offset_ns) : void + + AdjustFrequency(rate_ratio) : void + } + + struct PhcConfig #Lavender { + + enabled : bool = false + + device : string + + step_threshold_ns : int64_t = 100000000 + } + IRawSocket <|.. LinuxSocket IRawSocket <|.. QnxSocket INetworkIdentity <|.. NetworkIdentity + PhcAdjuster *-- PhcConfig GptpEngine *-- IRawSocket GptpEngine *-- INetworkIdentity -} - -package "score::ts::gptp::details" { - class FrameCodec #Wheat - class MessageParser #Wheat - class SyncStateMachine #Wheat - class PeerDelayMeasurer #Wheat -} - -package "score::ts::gptp::phc" { - class PhcAdjuster #Lavender + GptpEngine *-- FrameCodec + GptpEngine *-- GptpMessageParser + GptpEngine *-- SyncStateMachine + GptpEngine *-- PeerDelayMeasurer + GptpEngine *-- PhcAdjuster } package "score::ts::gptp::instrument" { @@ -94,11 +131,6 @@ package "score::ts::gptp::instrument" { ProbeManager --> Recorder } -GptpEngine *-- FrameCodec -GptpEngine *-- MessageParser -GptpEngine *-- SyncStateMachine -GptpEngine *-- PeerDelayMeasurer -GptpEngine *-- PhcAdjuster GptpEngine *-- ProbeManager @enduml diff --git a/docs/TimeSlave/_assets/gptp_threading.puml b/docs/TimeSlave/_assets/gptp_engine/gptp_threading.puml similarity index 100% rename from docs/TimeSlave/_assets/gptp_threading.puml rename to docs/TimeSlave/_assets/gptp_engine/gptp_threading.puml diff --git a/docs/TimeSlave/_assets/ipc_sequence.puml b/docs/TimeSlave/_assets/ipc_sequence.puml deleted file mode 100644 index 7e7bda3..0000000 --- a/docs/TimeSlave/_assets/ipc_sequence.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -!theme plain - -title libTSClient Seqlock IPC Protocol - -participant "TimeSlave\n(GptpIpcPublisher)" as PUB #LightPink -participant "SharedMemory\n(GptpIpcRegion)" as SHM #LightCyan -participant "TimeDaemon\n(GptpIpcReceiver)" as RCV #LightPink - -== Initialization == - -PUB -> SHM : shm_open("/gptp_ptp_info", O_CREAT | O_RDWR) -PUB -> SHM : ftruncate(sizeof(GptpIpcRegion)) -PUB -> SHM : mmap(PROT_READ | PROT_WRITE) -PUB -> SHM : write magic = 0x47505440 - -... - -RCV -> SHM : shm_open("/gptp_ptp_info", O_RDONLY) -RCV -> SHM : mmap(PROT_READ) -RCV -> SHM : verify magic == 0x47505440 - -== Publish (Writer Side) == - -PUB -> SHM : seq.fetch_add(1, release) // seq becomes odd -PUB -> SHM : memcpy(data, &info, sizeof) -PUB -> SHM : seq.fetch_add(1, release) // seq becomes even - -== Receive (Reader Side) == - -loop up to 20 retries - RCV -> SHM : s1 = seq.load(acquire) - alt s1 is odd (write in progress) - RCV -> RCV : retry - else s1 is even - RCV -> SHM : memcpy(&local, data, sizeof) - RCV -> SHM : s2 = seq.load(acquire) - alt s1 == s2 - RCV --> RCV : return PtpTimeInfo - else s1 != s2 (torn read) - RCV -> RCV : retry - end - end -end - -@enduml diff --git a/docs/TimeSlave/_assets/ipc_channel.puml b/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml similarity index 50% rename from docs/TimeSlave/_assets/ipc_channel.puml rename to docs/TimeSlave/_assets/libtsclient/ipc_channel.puml index bc7d9b4..846732e 100644 --- a/docs/TimeSlave/_assets/ipc_channel.puml +++ b/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml @@ -7,23 +7,25 @@ legend top left |= Color |= Description | | <#LightPink> | IPC components | | <#LightCyan> | Shared memory region | + | <#LightSalmon> | TimeDaemon adapter | endlegend package "TimeSlave Process" { class GptpIpcPublisher #LightPink { - region_ : GptpIpcRegion* - - fd_ : int + - shm_fd_ : int + Init(name) : bool - + Publish(info) : void + + Publish(data : GptpIpcData) : void + Destroy() : void } } package "Shared Memory" { class GptpIpcRegion <> #LightCyan { - + magic : uint32_t = 0x47505440 - + seq : std::atomic - + data : PtpTimeInfo + + magic : atomic = 0x47505450 + + seq : atomic + + data : GptpIpcData + + seq_confirm : atomic -- 64-byte aligned for\ncache line efficiency } @@ -32,21 +34,37 @@ package "Shared Memory" { package "TimeDaemon Process" { class GptpIpcReceiver #LightPink { - region_ : const GptpIpcRegion* - - fd_ : int + - shm_fd_ : int + Init(name) : bool - + Receive() : std::optional + + Receive() : std::optional + Close() : void } + + class ShmPTPEngine #LightSalmon { + - receiver_ : GptpIpcReceiver + - ipc_name_ : string + + Initialize() : bool + + Deinitialize() : bool + + ReadPTPSnapshot(info : PtpTimeInfo&) : bool + } } GptpIpcPublisher --> GptpIpcRegion : "shm_open(O_CREAT)\nmmap(PROT_WRITE)" GptpIpcReceiver --> GptpIpcRegion : "shm_open(O_RDONLY)\nmmap(PROT_READ)" +ShmPTPEngine *-- GptpIpcReceiver +ShmPTPEngine ..> "PtpTimeInfo" : converts to note right of GptpIpcRegion **Seqlock Protocol:** - Writer: seq++ → memcpy → seq++ - Reader: read seq (even) → memcpy → check seq + Writer: seq++ (odd) → fence → memcpy → seq_confirm++, seq++ (even) + Reader: read seq1 (even) → memcpy → fence → read seq2, seq3 + retry if seq1 != seq2 or seq1 != seq3 Retry up to 20 times on torn read end note +note bottom of ShmPTPEngine + Maps GptpIpcData fields to PtpTimeInfo. + Instantiated as GPTPRealMachine via CreateGPTPRealMachine(). +end note + @enduml diff --git a/docs/TimeSlave/_assets/libtsclient/ipc_sequence.puml b/docs/TimeSlave/_assets/libtsclient/ipc_sequence.puml new file mode 100644 index 0000000..4139f95 --- /dev/null +++ b/docs/TimeSlave/_assets/libtsclient/ipc_sequence.puml @@ -0,0 +1,52 @@ +@startuml +!theme plain + +title libTSClient Seqlock IPC Protocol + +participant "TimeSlave\n(GptpIpcPublisher)" as PUB #LightPink +participant "SharedMemory\n(GptpIpcRegion)" as SHM #LightCyan +participant "TimeDaemon\n(GptpIpcReceiver)" as RCV #LightPink + +== Initialization == + +PUB -> SHM : shm_open("/gptp_ptp_info", O_CREAT | O_RDWR) +PUB -> SHM : ftruncate(sizeof(GptpIpcRegion)) +PUB -> SHM : mmap(PROT_READ | PROT_WRITE) +PUB -> SHM : write magic = 0x47505450 ('GPTP') + +... + +RCV -> SHM : shm_open("/gptp_ptp_info", O_RDONLY) +RCV -> SHM : mmap(PROT_READ) +RCV -> SHM : verify magic == 0x47505450 + +== Publish (Writer Side) == + +PUB -> SHM : seq.fetch_add(1, relaxed) // seq becomes odd (write in progress) +PUB -> SHM : atomic_thread_fence(release) +PUB -> SHM : memcpy(&data, &src, sizeof(GptpIpcData)) +PUB -> SHM : seq_confirm.store(seq+1, release) // seq_confirm becomes even +PUB -> SHM : seq.store(seq+1, release) // seq becomes even (write done) + +== Receive (Reader Side) == + +loop up to 20 retries + RCV -> SHM : seq1 = seq.load(acquire) + alt seq1 is odd (write in progress) + RCV -> RCV : retry + else seq1 is even + RCV -> SHM : memcpy(&local, &data, sizeof(GptpIpcData)) + RCV -> SHM : atomic_thread_fence(acq_rel) + RCV -> SHM : seq2 = seq_confirm.load(acquire) + RCV -> SHM : seq3 = seq.load(acquire) + alt seq1 == seq2 && seq1 == seq3 + RCV --> RCV : return GptpIpcData (consistent) + else torn read (new write started) + RCV -> RCV : retry + end + end +end + +RCV --> RCV : return std::nullopt (exhausted retries) + +@enduml diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml new file mode 100644 index 0000000..8e47add --- /dev/null +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml @@ -0,0 +1,105 @@ +@startuml shm_ptp_engine_class_diagram +!theme plain + +title ShmPTPEngine: Class Diagram + +legend top left + |= Color |= Description | + | <#Beige> | Base classes / data types | + | <#Wheat> | PTPMachine / ShmPTPEngine | + | <#LightPink> | libTSClient IPC | +endlegend + +package "score::td" { + class "GPTPRealMachine" as real_machine #Wheat { + type alias for PTPMachine + -- + Constructed via CreateGPTPRealMachine() + } +} + +package "score::td (base classes)" { + abstract class "BaseMachine" as base_machine #Beige { + + GetName() : string + + Init() : bool + } + + abstract class "ProactiveMachine" as proactive_machine #Beige { + + Start() : void + + Stop() : void + } + + abstract class "PeriodicMachine" as periodic_machine #Beige { + # PeriodicTask() : void + } + + abstract class "Producer" as producer #Beige { + + SetPublishCallback(cb) : void + # Publish(data : T) : void + } + + class "PTPMachine" as ptp_machine #Wheat { + - engine_ : PTPEngine + + Init() : bool + + SetPublishCallback(cb) : void + # PeriodicTask() : void + } + + base_machine <|-- proactive_machine + proactive_machine <|-- periodic_machine + periodic_machine <|-- ptp_machine + producer <|.. ptp_machine +} + +package "score::td::details" { + class ShmPTPEngine #Wheat { + - ipc_name_ : string + - receiver_ : GptpIpcReceiver + - initialized_ : bool + + ShmPTPEngine(ipc_name : string) + + Initialize() : bool + + Deinitialize() : bool + + ReadPTPSnapshot(info : PtpTimeInfo&) : bool + } +} + +package "score::ts::details" { + class GptpIpcReceiver #LightPink { + + Init(name : string) : bool + + Receive() : optional + + Close() : void + } +} + +package "Data Types" { + class GptpIpcData #Beige { + + ptp_assumed_time : chrono::nanoseconds + + local_time : chrono::nanoseconds + + rate_deviation : double + + status : GptpIpcStatus + + sync_fup_data : GptpIpcSyncFupData + + pdelay_data : GptpIpcPDelayData + } + + class PtpTimeInfo #Beige { + + ptp_assumed_time + + local_time + + rate_deviation + + status + + sync_fup_data + + pdelay_data + } +} + +ptp_machine *-- ShmPTPEngine : PTPEngine = ShmPTPEngine +ShmPTPEngine *-- GptpIpcReceiver +ShmPTPEngine ..> GptpIpcData : reads +ShmPTPEngine ..> PtpTimeInfo : produces +real_machine --|> ptp_machine : alias + +note right of ShmPTPEngine + Maps GptpIpcData → PtpTimeInfo + on every ReadPTPSnapshot() call. +end note + +@enduml diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml new file mode 100644 index 0000000..d57892a --- /dev/null +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml @@ -0,0 +1,82 @@ +@startuml shm_ptp_engine_init_seq +!theme plain + +title ShmPTPEngine: Initialization Sequence + +hide footbox +autonumber "[00]" + +legend top left + |= Color |= Description | + | <#LightCyan> | TimeDaemon | + | <#Wheat> | GPTPRealMachine | + | <#LightPink> | libTSClient IPC | +endlegend + +participant "TimeBaseHandler" as tb #LightCyan +participant "GPTPRealMachine\n(PTPMachine)" as machine #Wheat +participant "ShmPTPEngine" as engine #Wheat +participant "GptpIpcReceiver" as receiver #LightPink +participant "MessageBroker" as broker #LightCyan + +== Construction == + +tb -> machine ** : CreateGPTPRealMachine("real", "/gptp_ptp_info") +activate tb +activate machine +machine -> engine ** : ShmPTPEngine("/gptp_ptp_info") +engine -> receiver ** : GptpIpcReceiver() +machine --> tb +deactivate machine +deactivate tb + +== Initialization == + +tb -> machine : Init() +activate tb +activate machine +machine -> engine : Initialize() +activate engine +engine -> receiver : Init("/gptp_ptp_info") +activate receiver +note right of receiver + shm_open(O_RDONLY) + mmap(PROT_READ) + verify magic == 0x47505450 +end note +receiver --> engine : true / false +deactivate receiver +engine --> machine +deactivate engine +machine --> tb +deactivate machine +deactivate tb + +== Setup Producer == + +tb -> broker : subscribe machine to "raw_ptp_data" topic +activate tb +activate broker +broker -> machine : SetPublishCallback(broker::OnNewData) +activate machine +machine --> broker +deactivate machine +broker --> tb +deactivate broker +deactivate tb + +== Start Periodic Operation == + +tb -> machine : Start() +activate tb +activate machine +machine -> machine : start periodic thread +note right + Begin periodic IPC reads + from shared memory +end note +machine --> tb +deactivate machine +deactivate tb + +@enduml diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml new file mode 100644 index 0000000..edfe97d --- /dev/null +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml @@ -0,0 +1,73 @@ +@startuml shm_ptp_engine_read_seq +!theme plain + +title ShmPTPEngine: Periodic Read and Publish Workflow + +hide footbox +autonumber "[00]" + +legend top left + |= Color |= Description | + | <#Wheat> | GPTPRealMachine | + | <#LightPink> | libTSClient IPC | + | <#LightCyan> | Shared Memory | + | <#PaleTurquoise> | MessageBroker | + | <#LightBlue> | ControlFlowDivider | +endlegend + +participant "PTPMachine\n(PeriodicTask)" as machine #Wheat +participant "ShmPTPEngine" as engine #Wheat +participant "GptpIpcReceiver" as receiver #LightPink +participant "SharedMemory\n(GptpIpcRegion)" as shm #LightCyan +participant "MessageBroker" as broker #PaleTurquoise +participant "ControlFlowDivider" as cfd #LightBlue + +loop periodic (e.g., every 50 ms) + activate machine + machine -> machine : PeriodicTask() + + machine -> engine : ReadPTPSnapshot(info) + activate engine + engine -> receiver : Receive() + activate receiver + receiver -> shm : seqlock read (up to 20 retries) + activate shm + note right of shm + 1. read seq1 (acquire, must be even) + 2. memcpy GptpIpcData + 3. fence, read seq_confirm + seq + 4. verify seq1 == seq2 == seq3 + end note + shm --> receiver : GptpIpcData or contention + deactivate shm + receiver --> engine : optional + deactivate receiver + + alt data available + engine -> engine : map GptpIpcData → PtpTimeInfo + note right + status, ptp_assumed_time, + local_time, rate_deviation, + sync_fup_data, pdelay_data + end note + engine --> machine : true (PtpTimeInfo filled) + else no data (nullopt) + engine --> machine : false + end + deactivate engine + + alt ReadPTPSnapshot returned true + machine -> machine : Publish(PtpTimeInfo) + machine -> broker : publish_callback_(PtpTimeInfo) + activate broker + broker -> cfd : subscription.callback_(PtpTimeInfo) + activate cfd + cfd --> broker + deactivate cfd + broker --> machine + deactivate broker + end + deactivate machine +end + +@enduml diff --git a/docs/TimeSlave/_assets/timeslave_class.puml b/docs/TimeSlave/_assets/timeslave_class.puml index e612d6f..c90ec1e 100644 --- a/docs/TimeSlave/_assets/timeslave_class.puml +++ b/docs/TimeSlave/_assets/timeslave_class.puml @@ -16,63 +16,58 @@ endlegend package "score::ts" { class TimeSlave #LightCyan { - - engine_ : GptpEngine + - opts_ : GptpEngineOptions + - engine_ : unique_ptr - publisher_ : GptpIpcPublisher - - clock_ : HighPrecisionLocalSteadyClock - + Initialize() : score::cpp::expected - + Run(stop_token) : score::cpp::expected - + Deinitialize() : score::cpp::expected + + Initialize(ctx) : int32_t + + Run(stop_token) : int32_t } class GptpEngine #LightSalmon { - - options_ : GptpEngineOptions + - opts_ : GptpEngineOptions - rx_thread_ : std::thread - pdelay_thread_ : std::thread - sync_sm_ : SyncStateMachine - pdelay_ : PeerDelayMeasurer - - socket_ : IRawSocket + - phc_adjuster_ : PhcAdjuster + - socket_ : unique_ptr + - identity_ : unique_ptr - codec_ : FrameCodec - - parser_ : MessageParser - - phc_ : PhcAdjuster + - parser_ : GptpMessageParser - snapshot_mutex_ : std::mutex - - latest_snapshot_ : PtpTimeInfo + - pending_snapshot_ : GptpIpcData + - current_snapshot_ : GptpIpcData + Initialize() : bool - + Deinitialize() : void - + ReadPTPSnapshot() : PtpTimeInfo - - RxThreadFunc(stop_token) : void - - PdelayThreadFunc(stop_token) : void + + Deinitialize() : bool + + FinalizeSnapshot() : void + + ReadPTPSnapshot(data : GptpIpcData&) : bool } struct GptpEngineOptions #Beige { - + interface_name : std::string - + pdelay_interval_ms : uint32_t - + sync_timeout_ms : uint32_t - + time_jump_forward_ns : int64_t - + time_jump_backward_ns : int64_t - + phc_config : PhcConfig + + iface_name : string = "eth0" + + pdelay_interval_ms : int = 1000 + + pdelay_warmup_ms : int = 2000 + + sync_timeout_ms : int = 3300 + + jump_future_threshold_ns : int64_t = 500000000 } TimeSlave *-- GptpEngine - TimeSlave *-- "1" GptpIpcPublisher } -package "score::ts::gptp::details" { +package "score::ts::details" { class FrameCodec #Wheat { + ParseEthernetHeader(buf) : EthernetHeader + AddEthernetHeader(buf, dst_mac, src_mac) : void } - class MessageParser #Wheat { - + Parse(payload, hw_ts) : std::optional + class GptpMessageParser #Wheat { + + Parse(payload, len, msg_out) : bool } class SyncStateMachine #Wheat { - - last_sync_ : PTPMessage - - last_offset_ns_ : int64_t - - neighbor_rate_ratio_ : double - - timeout_ : std::atomic + - timeout_ : atomic + OnSync(msg) : void - + OnFollowUp(msg) : std::optional + + OnFollowUp(msg) : optional + IsTimeout() : bool + GetNeighborRateRatio() : double } @@ -89,37 +84,45 @@ package "score::ts::gptp::details" { struct SyncResult #Beige { + master_ns : int64_t + offset_ns : int64_t - + sync_fup_data : SyncFupData + + sync_fup_data : GptpIpcSyncFupData + time_jump_forward : bool + time_jump_backward : bool } struct PDelayResult #Beige { - + path_delay_ns : int64_t + + pdelay_data : GptpIpcPDelayData + valid : bool } -} -package "score::ts::gptp::phc" { + class GptpIpcPublisher #LightPink { + - region_ : GptpIpcRegion* + - shm_fd_ : int + + Init(name) : bool + + Publish(data : GptpIpcData) : void + + Destroy() : void + } + class PhcAdjuster #Lavender { - - config_ : PhcConfig - - fd_ : int + - cfg_ : PhcConfig + - phc_fd_ : int + IsEnabled() : bool + AdjustOffset(offset_ns) : void - + AdjustFrequency(ppb) : void + + AdjustFrequency(rate_ratio) : void } struct PhcConfig #Beige { - + enabled : bool - + device_path : std::string - + step_threshold_ns : int64_t + + enabled : bool = false + + device : string + + step_threshold_ns : int64_t = 100000000 } } GptpEngine *-- FrameCodec -GptpEngine *-- MessageParser +GptpEngine *-- GptpMessageParser GptpEngine *-- SyncStateMachine GptpEngine *-- PeerDelayMeasurer GptpEngine *-- PhcAdjuster +PhcAdjuster *-- PhcConfig +TimeSlave *-- "1" GptpIpcPublisher @enduml diff --git a/docs/TimeSlave/_assets/timeslave_data_flow.puml b/docs/TimeSlave/_assets/timeslave_data_flow.puml index d7c4b15..3981db4 100644 --- a/docs/TimeSlave/_assets/timeslave_data_flow.puml +++ b/docs/TimeSlave/_assets/timeslave_data_flow.puml @@ -13,6 +13,7 @@ participant "PhcAdjuster" as PHC #Lavender participant "GptpEngine" as GE #LightSalmon participant "GptpIpcPublisher" as PUB #LightPink participant "SharedMemory" as SHM #LightPink +participant "TimeSlave\n(main thread)" as TS #LightCyan == RxThread — Sync/FollowUp Processing == @@ -27,9 +28,8 @@ SOCK -> FC : raw buffer + HW timestamp FC -> MP : Ethernet payload MP -> SSM : OnFollowUp(PTPMessage) SSM -> SSM : compute offset & neighborRateRatio -SSM --> MP : SyncResult{master_ns, offset_ns,\ntime_jump_flags} - -MP --> GE : update latest_snapshot_\n(mutex protected) +SSM --> GE : SyncResult{master_ns, offset_ns,\ntime_jump_flags} +GE -> GE : update pending_snapshot_\n(mutex protected) == PdelayThread — Peer Delay Measurement == @@ -54,8 +54,11 @@ PHC -> PHC : step or frequency slew == Periodic Publish to Shared Memory == -GE -> GE : ReadPTPSnapshot() -GE -> PUB : Publish(PtpTimeInfo) -PUB -> SHM : seqlock write\n(atomic seq++, memcpy, seq++) +TS -> GE : FinalizeSnapshot() +GE -> GE : check timeout, commit\npending_snapshot_ → current_snapshot_ +TS -> GE : ReadPTPSnapshot(data) +GE --> TS : GptpIpcData +TS -> PUB : Publish(GptpIpcData) +PUB -> SHM : seqlock write\n(seq odd → fence → memcpy → seq_confirm, seq even) @enduml diff --git a/docs/TimeSlave/_assets/timeslave_deployment.puml b/docs/TimeSlave/_assets/timeslave_deployment.puml index e70de49..844967d 100644 --- a/docs/TimeSlave/_assets/timeslave_deployment.puml +++ b/docs/TimeSlave/_assets/timeslave_deployment.puml @@ -1,59 +1,60 @@ @startuml !theme plain +skinparam nodesep 40 +skinparam ranksep 50 +skinparam componentStyle rectangle title TimeSlave Deployment View -node "ECU" { - package "TimeSlave Process" as TSP { - component [GptpEngine] as GE #LightSalmon - component [GptpIpcPublisher] as PUB #LightPink - - package "RxThread" as RXT { - component [FrameCodec] as FC #Wheat - component [MessageParser] as MP #Wheat - component [SyncStateMachine] as SSM #Wheat - } +node "Time Master" as TM { + component [PTP Grandmaster\nClock] as GMC +} - package "PdelayThread" as PDT { - component [PeerDelayMeasurer] as PDM #Wheat - } +node "ECU" as ECU { + package "TimeSlave Process" as TSP { + component [GptpEngine\n«RxThread + PdelayThread»] as GE #LightSalmon component [PhcAdjuster] as PHC #Lavender - component [ProbeManager] as PM #Beige - component [Recorder] as REC #Beige + component [GptpIpcPublisher] as PUB #LightPink + component [ProbeManager + Recorder] as INST #Beige } + component [«hw clock»\nPHC Device] as PHCDEV + + database "Shared Memory\n/gptp_ptp_info" as SHM #LightCyan + package "TimeDaemon Process" as TDP { + component [ShmPTPEngine] as SHME #LightSalmon component [GptpIpcReceiver] as RCV #LightPink } - - database "Shared Memory\n/gptp_ptp_info" as SHM - - interface "Raw Socket\n(AF_PACKET)" as SOCK - interface "PHC Device\n(/dev/ptpN)" as PHCDEV } -cloud "Network" as NET - -GE --> RXT -GE --> PDT -GE --> PHC -GE --> PUB +cloud "Network\n(Ethernet)" as NET -FC --> MP -MP --> SSM -MP --> PDM +' Top-level flow +GMC -down[#purple]-> GE : PTP sync messages\n(EtherType 0x88F7) +GE -down[#blue]-> NET : PDelayReq / PDelayResp +NET -up[#blue]-> GE -PUB -[#green]-> SHM : seqlock write -RCV -[#green]-> SHM : seqlock read +' Hardware clock adjustment +GE -down-> PHC : offset / rate ratio +PHC -down[#orange]-> PHCDEV : clock_adjtime /\nEMAC ioctl -RXT -[#blue]-> SOCK : recv -PDT -[#blue]-> SOCK : send/recv +' Shared memory IPC +GE -down-> PUB +PUB -down[#green]-> SHM : seqlock write\n(GptpIpcData) +RCV -up[#green]-> SHM : seqlock read\n(GptpIpcData) -PHC -[#orange]-> PHCDEV : clock_adjtime +' TimeDaemon internal +SHME -down-* RCV -SOCK -[#blue]-> NET : gPTP frames\nEtherType 0x88F7 +' Instrumentation +GE .down.> INST : probe events -PM --> REC : probe events +note right of TDP + ShmPTPEngine wraps GptpIpcReceiver. + Converts GptpIpcData → PtpTimeInfo. + Instantiated as GPTPRealMachine. +end note @enduml diff --git a/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml b/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml deleted file mode 100644 index f4550ad..0000000 --- a/docs/TimeSlave/gptp_engine/_assets/gptp_engine_class.puml +++ /dev/null @@ -1,95 +0,0 @@ -@startuml -!theme plain - -title gPTP Engine Internal Class Diagram - -package "score::ts::gptp" { - - class GptpEngine { - - options_ : GptpEngineOptions - - rx_thread_ : std::thread - - pdelay_thread_ : std::thread - - socket_ : std::unique_ptr - - codec_ : FrameCodec - - parser_ : MessageParser - - sync_sm_ : SyncStateMachine - - pdelay_ : PeerDelayMeasurer - - phc_ : PhcAdjuster - - probe_mgr_ : ProbeManager - - recorder_ : Recorder - - snapshot_mutex_ : std::mutex - - latest_snapshot_ : PtpTimeInfo - + Initialize() : bool - + Deinitialize() : void - + ReadPTPSnapshot() : PtpTimeInfo - } - - interface IRawSocket { - + Open(iface) : bool - + EnableHwTimestamping() : bool - + Recv(buf, timeout_ms) : RecvResult - + Send(buf, hw_ts) : bool - + GetFd() : int - + Close() : void - } - - class "RawSocket\n<>" as LinuxSocket { - AF_PACKET + SO_TIMESTAMPING - } - - class "RawSocket\n<>" as QnxSocket { - QNX raw-socket shim - } - - interface INetworkIdentity { - + Resolve(iface) : bool - + GetClockIdentity() : ClockIdentity - } - - class NetworkIdentity { - Derives EUI-64 from MAC\n(inserts 0xFF 0xFE) - } - - IRawSocket <|.. LinuxSocket - IRawSocket <|.. QnxSocket - INetworkIdentity <|.. NetworkIdentity - - GptpEngine *-- IRawSocket - GptpEngine *-- INetworkIdentity -} - -package "score::ts::gptp::details" { - class FrameCodec - class MessageParser - class SyncStateMachine - class PeerDelayMeasurer -} - -package "score::ts::gptp::phc" { - class PhcAdjuster -} - -package "score::ts::gptp::instrument" { - class ProbeManager { - + {static} Instance() : ProbeManager& - + Record(point, data) : void - + SetRecorder(recorder) : void - } - - class Recorder { - - file_ : std::ofstream - - mutex_ : std::mutex - + Record(entry) : void - } - - ProbeManager --> Recorder -} - -GptpEngine *-- FrameCodec -GptpEngine *-- MessageParser -GptpEngine *-- SyncStateMachine -GptpEngine *-- PeerDelayMeasurer -GptpEngine *-- PhcAdjuster -GptpEngine *-- ProbeManager - -@enduml diff --git a/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml b/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml deleted file mode 100644 index 93921e2..0000000 --- a/docs/TimeSlave/gptp_engine/_assets/gptp_threading.puml +++ /dev/null @@ -1,53 +0,0 @@ -@startuml -!theme plain - -title gPTP Engine Threading Model - -concise "RxThread" as RX -concise "PdelayThread" as PD -concise "Main Thread\n(TimeSlave)" as MAIN - -@0 -MAIN is "Initialize" - -@50 -RX is "Waiting" -PD is "Waiting" -MAIN is "Running" - -@100 -RX is "Recv Sync" -PD is "Sleep\n(interval)" - -@150 -RX is "Parse + SSM" - -@200 -RX is "Recv FollowUp" - -@250 -RX is "Parse + SSM\ncompute offset" - -@300 -RX is "Update snapshot" -PD is "SendRequest" - -@350 -RX is "Waiting" -PD is "Recv Resp" - -@400 -PD is "Recv RespFUp\ncompute delay" - -@450 -PD is "Update result" - -@500 -MAIN is "ReadPTPSnapshot\n→ Publish IPC" -RX is "Waiting" -PD is "Sleep\n(interval)" - -@550 -MAIN is "Running" - -@enduml diff --git a/docs/TimeSlave/gptp_engine/index.rst b/docs/TimeSlave/gptp_engine/index.rst deleted file mode 100644 index e734c5e..0000000 --- a/docs/TimeSlave/gptp_engine/index.rst +++ /dev/null @@ -1,227 +0,0 @@ -.. - # ******************************************************************************* - # Copyright (c) 2026 Contributors to the Eclipse Foundation - # - # See the NOTICE file(s) distributed with this work for additional - # information regarding copyright ownership. - # - # This program and the accompanying materials are made available under the - # terms of the Apache License Version 2.0 which is available at - # https://www.apache.org/licenses/LICENSE-2.0 - # - # SPDX-License-Identifier: Apache-2.0 - # ******************************************************************************* - -.. _gptp_engine_design: - -############################ -gPTP Engine Design -############################ - -.. contents:: Table of Contents - :depth: 3 - :local: - -Overview -======== - -The ``GptpEngine`` is the core protocol engine of TimeSlave. It implements the IEEE 802.1AS -gPTP slave clock functionality by managing two dedicated threads for network I/O and peer -delay measurement. - -Class view -========== - -.. raw:: html - -
- -.. uml:: _assets/gptp_engine_class.puml - :alt: gPTP Engine Class Diagram - -.. raw:: html - -
- -Threading model -=============== - -The GptpEngine operates with two background threads: - -.. raw:: html - -
- -.. uml:: _assets/gptp_threading.puml - :alt: gPTP Engine Threading Model - -.. raw:: html - -
- -RxThread --------- - -The RxThread is the primary receive path. It runs a continuous loop: - -1. **Recv** — Blocks on ``IRawSocket::Recv()`` with a configurable timeout, receiving raw - Ethernet frames with hardware timestamps from the NIC. - -2. **Decode** — ``FrameCodec::ParseEthernetHeader()`` strips the Ethernet header (with VLAN - support) and validates the EtherType (``0x88F7``). - -3. **Parse** — ``MessageParser::Parse()`` decodes the PTP payload into a ``PTPMessage`` - structure, identifying the message type (Sync, FollowUp, PdelayResp, PdelayRespFollowUp). - -4. **Dispatch** — Based on message type: - - - **Sync** → ``SyncStateMachine::OnSync()`` stores the Sync timestamp - - **FollowUp** → ``SyncStateMachine::OnFollowUp()`` correlates with the preceding Sync, - computes ``offset_ns`` and ``neighborRateRatio``, and detects time jumps - - **PdelayResp** → ``PeerDelayMeasurer::OnResponse()`` - - **PdelayRespFollowUp** → ``PeerDelayMeasurer::OnResponseFollowUp()`` - -5. **Snapshot** — After processing, the latest ``PtpTimeInfo`` snapshot is updated under - mutex protection. - -PdelayThread ------------- - -The PdelayThread performs IEEE 802.1AS peer delay measurement on a periodic interval -(configurable via ``GptpEngineOptions::pdelay_interval_ms``): - -1. **Send** — ``PeerDelayMeasurer::SendRequest()`` transmits a ``PDelayReq`` frame via the - raw socket, capturing the hardware transmit timestamp (``t1``). - -2. **Receive** — The RxThread dispatches incoming ``PDelayResp`` (providing ``t2``) and - ``PDelayRespFollowUp`` (providing ``t3c``) to the ``PeerDelayMeasurer``. - -3. **Compute** — The peer delay is computed using the IEEE 802.1AS formula: - - .. code-block:: text - - path_delay = ((t2 - t1) + (t4 - t3c)) / 2 - - where ``t4`` is the local hardware receive timestamp of the PDelayResp frame. - -Thread safety is ensured via a mutex in ``PeerDelayMeasurer``, as ``SendRequest()`` runs on -the PdelayThread while ``OnResponse()``/``OnResponseFollowUp()`` are called from the -RxThread. - -Core components -=============== - -FrameCodec ----------- - -Handles raw Ethernet frame encoding and decoding: - -- ``ParseEthernetHeader()`` — Parses source/destination MAC, handles 802.1Q VLAN tags, - extracts EtherType and payload offset. -- ``AddEthernetHeader()`` — Constructs Ethernet headers for outgoing PDelayReq frames using - the standard PTP multicast destination MAC (``01:80:C2:00:00:0E``). - -MessageParser -------------- - -Parses the PTP wire format (IEEE 1588-v2) from raw payload bytes: - -- Validates the PTP header (version, domain, message length). -- Decodes message-type-specific bodies: ``SyncBody``, ``FollowUpBody``, ``PdelayReqBody``, - ``PdelayRespBody``, ``PdelayRespFollowUpBody``. -- All wire structures are packed (``__attribute__((packed))``) for direct memory mapping. - -SyncStateMachine ----------------- - -Implements the two-step Sync/FollowUp correlation logic: - -- **OnSync(msg)** — Stores the Sync message and its hardware receive timestamp. -- **OnFollowUp(msg)** — Matches with the preceding Sync by sequence ID, then computes: - - - ``offset_ns`` = master_time - slave_receive_time - path_delay - - ``neighborRateRatio`` from successive Sync intervals (master vs. slave clock progression) - - Time jump detection (forward/backward) against configurable thresholds - -- **Timeout detection** — Uses ``std::atomic`` for thread-safe timeout status, - set when no Sync is received within ``sync_timeout_ms``. - -PeerDelayMeasurer ------------------ - -Implements the IEEE 802.1AS two-step peer delay measurement protocol: - -- Manages the four timestamps (``t1``, ``t2``, ``t3c``, ``t4``) across two threads. -- ``SendRequest()`` — Builds and sends a PDelayReq frame, records ``t1`` from the - hardware transmit timestamp. -- ``OnResponse()`` / ``OnResponseFollowUp()`` — Records ``t2``, ``t3c``, ``t4`` and - computes the path delay when all timestamps are available. -- Returns ``PDelayResult`` with ``path_delay_ns`` and a ``valid`` flag. - -PhcAdjuster ------------ - -Synchronizes the PTP Hardware Clock (PHC) on the NIC: - -- **Step correction** — For large offsets exceeding ``step_threshold_ns``, applies an - immediate time step to the PHC. -- **Frequency slew** — For smaller offsets, adjusts the clock frequency (in ppb) for - smooth convergence. -- Platform-specific: Linux uses ``clock_adjtime()``, QNX uses EMAC PTP ioctls. -- Configured via ``PhcConfig`` (device path, step threshold, enable/disable flag). - -Instrumentation -=============== - -ProbeManager ------------- - -A singleton that records probe events at key processing points. Probe points include: - -- ``RxPacketReceived`` — Raw frame received from socket -- ``SyncFrameParsed`` — Sync message successfully parsed -- ``FollowUpProcessed`` — Offset computed from Sync/FollowUp pair -- ``OffsetComputed`` — Final offset value available -- ``PdelayReqSent`` — PDelayReq frame transmitted -- ``PdelayCompleted`` — Peer delay measurement completed -- ``PhcAdjusted`` — PHC adjustment applied - -The ``GPTP_PROBE()`` macro provides zero-overhead when probing is disabled. - -Recorder --------- - -Thread-safe CSV file writer that persists probe events and other diagnostic data. Each -``RecordEntry`` contains a timestamp, event type, offset, peer delay, sequence ID, and -status flags. - -Configuration -============= - -The ``GptpEngineOptions`` struct provides all configurable parameters: - -.. list-table:: - :header-rows: 1 - :widths: 30 15 55 - - * - Parameter - - Type - - Description - * - ``interface_name`` - - string - - Network interface for gPTP frames (e.g., ``eth0``) - * - ``pdelay_interval_ms`` - - uint32_t - - Interval between PDelayReq transmissions - * - ``sync_timeout_ms`` - - uint32_t - - Timeout for Sync message reception before declaring timeout state - * - ``time_jump_forward_ns`` - - int64_t - - Threshold for forward time jump detection - * - ``time_jump_backward_ns`` - - int64_t - - Threshold for backward time jump detection - * - ``phc_config`` - - PhcConfig - - PHC device path, step threshold, and enable flag diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 923f349..1696ff4 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -18,7 +18,7 @@ More precisely we can specify the following use cases for the TimeSlave: 1. Receiving gPTP Sync/FollowUp messages from a Time Master on the Ethernet network 2. Measuring peer delay via the IEEE 802.1AS PDelayReq/PDelayResp exchange 3. Optionally adjusting the PTP Hardware Clock (PHC) on the NIC -4. Publishing the resulting ``PtpTimeInfo`` to shared memory for consumption by the TimeDaemon +4. Publishing the resulting ``GptpIpcData`` to shared memory for consumption by the TimeDaemon The raw architectural diagram is represented below. @@ -46,6 +46,7 @@ The design consists of several sw components: 6. `PeerDelayMeasurer <#peerdelaymeasurer-sw-component>`_ 7. `PhcAdjuster <#phcadjuster-sw-component>`_ 8. `libTSClient <#libtsclient-sw-component>`_ +9. `ShmPTPEngine <#shmptpengine-sw-component>`_ Class view ~~~~~~~~~~ @@ -116,9 +117,9 @@ Main thread (periodic publish) scope This control flow is responsible for the: -1. periodically call ``GptpEngine::ReadPTPSnapshot()`` to get the latest time measurement -2. enrich the snapshot with the local clock timestamp from ``HighPrecisionLocalSteadyClock`` -3. publish to shared memory via ``GptpIpcPublisher::Publish()`` +1. periodically call ``GptpEngine::FinalizeSnapshot()`` to check timeout and commit the pending snapshot +2. call ``GptpEngine::ReadPTPSnapshot(data)`` to copy the latest ``GptpIpcData`` into a local variable +3. publish to shared memory via ``GptpIpcPublisher::Publish(data)`` Data types or events ^^^^^^^^^^^^^^^^^^^^ @@ -143,7 +144,7 @@ PDelayResult PtpTimeInfo '''''''''''' -``PtpTimeInfo`` is the aggregated snapshot that combines PTP status flags, Sync/FollowUp data, peer delay data, and a local clock reference. This is the data published to shared memory for the TimeDaemon. +``PtpTimeInfo`` is the TimeDaemon-internal aggregated snapshot. It is **not** the shared memory type; it is produced by ``ShmPTPEngine::ReadPTPSnapshot()`` by field-mapping from ``GptpIpcData`` into the format expected by the TimeDaemon pipeline. SW Components decomposition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -158,10 +159,10 @@ Component requirements The ``TimeSlave Application`` has the following requirements: -- The ``TimeSlave Application`` shall implement the ``Initialize()`` method to create the ``GptpEngine`` with configured options, initialize the ``GptpIpcPublisher`` (creates the shared memory segment), and prepare the ``HighPrecisionLocalSteadyClock`` for local time reference -- The ``TimeSlave Application`` shall implement the ``Run()`` method to start the GptpEngine, enter a periodic publish loop, and monitor the ``stop_token`` for graceful shutdown -- The ``TimeSlave Application`` shall implement the ``Deinitialize()`` method to stop the GptpEngine threads and destroy the shared memory segment -- The ``TimeSlave Application`` shall periodically read the latest ``PtpTimeInfo`` snapshot, enrich it with the local clock timestamp, and publish it via ``GptpIpcPublisher`` +- The ``TimeSlave Application`` shall implement the ``Initialize()`` method to create the ``GptpEngine`` with configured options, initialize the ``GptpIpcPublisher`` (creates the shared memory segment), and create the ``HighPrecisionLocalSteadyClock`` for the engine +- The ``TimeSlave Application`` shall implement the ``Run()`` method to enter a periodic publish loop (50 ms interval) and monitor the ``stop_token`` for graceful shutdown +- On each loop iteration, ``TimeSlave Application`` shall call ``GptpEngine::FinalizeSnapshot()``, then ``GptpEngine::ReadPTPSnapshot(data)``, and publish the resulting ``GptpIpcData`` via ``GptpIpcPublisher::Publish(data)`` +- The ``TimeSlave Application`` shall call ``GptpEngine::Deinitialize()`` and ``GptpIpcPublisher::Destroy()`` after the ``stop_token`` is set GptpEngine SW component ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -175,8 +176,9 @@ The ``GptpEngine`` has the following requirements: - The ``GptpEngine`` shall manage an RxThread for receiving and parsing gPTP frames from raw Ethernet sockets - The ``GptpEngine`` shall manage a PdelayThread for periodic peer delay measurement -- The ``GptpEngine`` shall provide a thread-safe ``ReadPTPSnapshot()`` method that returns the latest ``PtpTimeInfo`` -- The ``GptpEngine`` shall support configurable parameters via ``GptpEngineOptions`` (interface name, PDelay interval, sync timeout, time jump thresholds, PHC configuration) +- The ``GptpEngine`` shall provide a ``FinalizeSnapshot()`` method that checks for sync timeout, applies status flags, and commits the pending snapshot to the current snapshot; this must be called before ``ReadPTPSnapshot()`` +- The ``GptpEngine`` shall provide a ``ReadPTPSnapshot(GptpIpcData&)`` method that copies the latest committed snapshot into the caller's buffer and returns false only if the engine is not initialized +- The ``GptpEngine`` shall support configurable parameters via ``GptpEngineOptions`` (interface name, PDelay interval, PDelay warmup, sync timeout, time-jump threshold) - The ``GptpEngine`` shall support exchangeability of the raw socket implementation for different platforms (Linux, QNX) Class view @@ -188,7 +190,7 @@ The Class Diagram is presented below:
-.. uml:: _assets/gptp_engine_class.puml +.. uml:: _assets/gptp_engine/gptp_engine_class.puml :alt: Class Diagram .. raw:: html @@ -204,7 +206,7 @@ The GptpEngine operates with two background threads. The threading model is repr
-.. uml:: _assets/gptp_threading.puml +.. uml:: _assets/gptp_engine/gptp_threading.puml :alt: Threading Model .. raw:: html @@ -216,10 +218,43 @@ Concurrency aspects The ``GptpEngine`` uses the following synchronization mechanisms: -- A ``std::mutex`` protects the ``latest_snapshot_`` field, shared between the RxThread (writer) and the main thread (reader via ``ReadPTPSnapshot()``) +- A ``std::mutex`` protects the ``pending_snapshot_`` and ``current_snapshot_`` fields (both ``GptpIpcData``): the RxThread writes ``pending_snapshot_``; the main thread calls ``FinalizeSnapshot()`` (commits pending to current) and ``ReadPTPSnapshot()`` (reads current) - The ``PeerDelayMeasurer`` uses its own ``std::mutex`` to synchronize between the PdelayThread (``SendRequest()``) and the RxThread (``OnResponse()``, ``OnResponseFollowUp()``) - The ``SyncStateMachine`` uses ``std::atomic`` for the timeout flag, which is read from the main thread and written from the RxThread +Hardware timestamping fallback +''''''''''''''''''''''''''''''' + +During ``Initialize()``, ``GptpEngine`` calls ``IRawSocket::EnableHwTimestamping()`` to request NIC-level receive timestamps (``SO_TIMESTAMPING`` on Linux). If the NIC does not support hardware timestamping, the call returns ``false`` and a warning is logged: + +.. code-block:: none + + GptpEngine: HW timestamping not available on , falling back to SW timestamps + +The engine continues to run normally. The difference between the two modes: + +.. list-table:: + :header-rows: 1 + :widths: 30 35 35 + + * - Field + - HW timestamping available + - SW timestamping fallback + * - ``recvHardwareTS`` (Sync receive time) + - NIC hardware timestamp (nanosecond precision, captured at wire level) + - Software timestamp (captured at socket receive, higher jitter) + * - ``sync_fup_data.reference_local_timestamp`` + - Derived from NIC hardware timestamp + - Derived from software timestamp + * - ``GptpIpcData.local_time`` + - Always ``CLOCK_MONOTONIC`` (unaffected) + - Always ``CLOCK_MONOTONIC`` (unaffected) + * - Clock offset accuracy + - High (sub-microsecond typical) + - Reduced (jitter depends on OS scheduling latency) + +The fallback does not affect protocol correctness — Sync/FollowUp correlation and peer delay measurement continue to work — but the computed clock offset will be less accurate due to higher receive timestamp jitter. + FrameCodec SW component ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -268,6 +303,40 @@ PeerDelayMeasurer SW component The ``PeerDelayMeasurer`` component implements the IEEE 802.1AS two-step peer delay measurement protocol. It manages the four timestamps (``t1``, ``t2``, ``t3c``, ``t4``) across two threads. +Timestamp definitions +''''''''''''''''''''' + +.. list-table:: Peer Delay Timestamps (IEEE 802.1AS) + :header-rows: 1 + :widths: 10 20 30 40 + + * - Symbol + - Message + - Captured by + - Meaning + * - ``t1`` + - PDelayReq (TX) + - Slave (PdelayThread) + - HW transmit timestamp of the PDelayReq frame leaving the slave NIC + * - ``t2`` + - PDelayResp (RX) + - Master → carried in PDelayResp body + - HW receive timestamp of the PDelayReq frame arriving at the master NIC + * - ``t3c`` + - PDelayRespFollowUp + - Master → carried in PDelayRespFollowUp body + - HW transmit timestamp of the PDelayResp frame leaving the master NIC ("corrected" because it includes the master's turnaround correction) + * - ``t4`` + - PDelayResp (RX) + - Slave (RxThread) + - HW receive timestamp of the PDelayResp frame arriving at the slave NIC + +The peer delay formula is: ``path_delay = ((t2 - t1) + (t4 - t3c)) / 2`` + +- ``(t2 - t1)`` = propagation time from slave → master +- ``(t4 - t3c)`` = propagation time from master → slave +- The average of the two gives the one-way link delay + Component requirements '''''''''''''''''''''' @@ -293,6 +362,21 @@ The ``PhcAdjuster`` has the following requirements: - The ``PhcAdjuster`` shall support platform-specific implementations: ``clock_adjtime()`` on Linux, EMAC PTP ioctls on QNX - The ``PhcAdjuster`` shall be configurable via ``PhcConfig`` (device path, step threshold, enable/disable flag) +Fallback behavior when PHC is unavailable +'''''''''''''''''''''''''''''''''''''''''' + +The ``PhcAdjuster`` degrades gracefully in two scenarios: + +1. **PHC disabled** (``PhcConfig.enabled = false``, the default): ``AdjustOffset()`` and ``AdjustFrequency()`` are no-ops. The gPTP protocol pipeline (Sync/FollowUp reception, peer-delay measurement, ``GptpIpcData`` publishing) is completely unaffected. The hardware clock is not touched. + +2. **PHC enabled but device inaccessible** (e.g., ``/dev/ptp0`` does not exist on Linux, or the EMAC interface name is wrong on QNX): + + - **Linux**: the constructor calls ``open(device, O_RDWR)``; on failure ``phc_fd_`` stays at ``-1``. Both ``AdjustOffset()`` and ``AdjustFrequency()`` guard against ``phc_fd_ < 0`` and return immediately — a true silent skip with no system call. + + - **QNX**: ``qnx_phc_open()`` always returns ``0`` and never fails — it only stores the device name in a thread-local context. There is no ``phc_fd_ < 0`` guard. The adjustment methods always call ``qnx_phc_adjtime_step()`` / ``qnx_phc_adjfreq_ppb()``, which internally create a UDP socket and issue ``SIOCGDRVSPEC`` / ``SIOCSDRVSPEC`` ioctls. If the socket or ioctl fails (e.g., wrong interface name, unsupported hardware), the function returns ``-1``, but the caller discards it with a ``(void)`` cast. There is no explicit skip — the call is always attempted and errors are silently absorbed. + +In both scenarios TimeSlave continues to track the master clock and publish accurate ``GptpIpcData`` snapshots (including offset and status flags) to shared memory. The downstream TimeDaemon and any applications consuming time are unaffected — only the NIC hardware clock itself will drift relative to PTP time. + libTSClient SW component ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -305,10 +389,10 @@ Component requirements The ``libTSClient`` has the following requirements: -- The ``libTSClient`` shall define a shared memory layout (``GptpIpcRegion``) with a magic number for validation, an atomic seqlock counter, and a ``PtpTimeInfo`` data payload +- The ``libTSClient`` shall define a shared memory layout (``GptpIpcRegion``) with a magic number (``0x47505450`` = 'GPTP') for validation, an atomic seqlock counter (``seq``), a confirmation counter (``seq_confirm``), and a ``GptpIpcData`` data payload - The ``libTSClient`` shall align the shared memory region to 64 bytes (cache line size) to prevent false sharing -- The ``libTSClient`` shall provide a ``GptpIpcPublisher`` component that creates and manages the POSIX shared memory segment and writes ``PtpTimeInfo`` using the seqlock protocol -- The ``libTSClient`` shall provide a ``GptpIpcReceiver`` component that opens the shared memory segment read-only and reads ``PtpTimeInfo`` with up to 20 seqlock retries +- The ``libTSClient`` shall provide a ``GptpIpcPublisher`` component (in ``score::ts::details``) that creates and manages the POSIX shared memory segment and writes ``GptpIpcData`` using the seqlock protocol +- The ``libTSClient`` shall provide a ``GptpIpcReceiver`` component (in ``score::ts::details``) that opens the shared memory segment read-only and reads ``GptpIpcData`` with up to 20 seqlock retries - The ``libTSClient`` shall use the POSIX shared memory name ``/gptp_ptp_info`` by default Class view @@ -320,7 +404,7 @@ The Class Diagram is presented below:
-.. uml:: _assets/ipc_channel.puml +.. uml:: _assets/libtsclient/ipc_channel.puml :alt: Class Diagram .. raw:: html @@ -330,21 +414,22 @@ The Class Diagram is presented below: Publish new data '''''''''''''''' -When ``TimeSlave Application`` has a new ``PtpTimeInfo`` snapshot, it publishes to the shared memory via the seqlock protocol: +When ``TimeSlave Application`` has a new ``GptpIpcData`` snapshot, it publishes to the shared memory via the seqlock protocol: -1. Increment ``seq`` (becomes odd — signals write in progress) -2. ``memcpy`` the data -3. Increment ``seq`` (becomes even — signals write complete) +1. Increment ``seq`` (becomes odd — signals write in progress); a release fence is applied +2. ``memcpy`` the ``GptpIpcData`` +3. Store ``seq_confirm = seq + 1`` and increment ``seq`` (both become even — signals write complete) Receive data '''''''''''' From TimeDaemon side, the receiver reads from the shared memory using the seqlock protocol with bounded retry: -1. Read ``seq`` (must be even, otherwise retry) -2. ``memcpy`` the data -3. Read ``seq`` again (must match step 1, otherwise retry — torn read detected) -4. Return ``std::optional`` (empty if all 20 retries exhausted) +1. Read ``seq1`` with acquire ordering (must be even, otherwise retry — write in progress) +2. ``memcpy`` the ``GptpIpcData`` +3. Apply an acquire-release fence; read ``seq_confirm`` as ``seq2`` and re-read ``seq`` as ``seq3`` +4. If ``seq1 == seq2 == seq3``, the read is consistent; otherwise retry — torn read detected +5. Return ``std::optional`` (empty if all 20 retries exhausted) The seqlock protocol workflow is presented in the following sequence diagram: @@ -352,7 +437,7 @@ The seqlock protocol workflow is presented in the following sequence diagram:
-.. uml:: _assets/ipc_sequence.puml +.. uml:: _assets/libtsclient/ipc_sequence.puml :alt: Seqlock Protocol .. raw:: html @@ -366,23 +451,23 @@ TimeSlave supports two target platforms with platform-specific implementations s .. list-table:: Platform Implementations :header-rows: 1 - :widths: 25 35 40 + :widths: 25 37 38 * - Component - Linux - QNX * - Raw Socket - - ``AF_PACKET`` with ``SO_TIMESTAMPING`` - - QNX raw-socket shim + - ``AF_PACKET`` + ``SO_TIMESTAMPING``; HW RX timestamp via ``recvmsg`` ``SCM_TIMESTAMPING`` + - BPF (``/dev/bpf``); HW RX timestamp via ``bpf_xhdr.bh_tstamp`` (``BIOCSTSTAMP BPF_T_PTP|BPF_T_BINTIME``); TX timestamp via ``BIOCGTSTAMPID`` + loopback fd (``BIOCSSEESENT``); fallback to ``CLOCK_REALTIME`` * - Network Identity - - ``ioctl(SIOCGIFHWADDR)`` - - QNX-specific MAC resolution + - ``ioctl(SIOCGIFHWADDR)`` → EUI-48 → EUI-64 + - ``getifaddrs()`` + ``AF_LINK`` / ``sockaddr_dl`` (``LLADDR``) → EUI-48/64 * - PHC Adjuster - - ``clock_adjtime()`` - - EMAC PTP ioctls + - ``clock_adjtime()`` (``SYS_clock_adjtime`` syscall); step via ``ADJ_SETOFFSET|ADJ_NANO``; slew via ``ADJ_FREQUENCY`` (scaled-ppm) + - ``SIOCGDRVSPEC`` / ``SIOCSDRVSPEC`` on UDP socket; step via ``PTP_GET_TIME`` (0x102) + ``PTP_SET_TIME`` (0x103); slew via ``EMAC_PTP_ADJ_FREQ_PPM`` (0x200) in ppm * - HighPrecisionLocalSteadyClock - - ``std::chrono`` system clock - - QTIME clock API + - ``CLOCK_MONOTONIC`` via ``clock_gettime()`` + - QNX ``ClockCycles()`` CPU instruction (reads hardware performance counter directly, equivalent to ``RDTSC`` on x86 / ``CNTVCT`` on ARM64), converted to nanoseconds via cycles-per-second calibration. Used instead of ``clock_gettime()`` because QNX ``CLOCK_MONOTONIC`` resolution is limited to microsecond level, whereas ``ClockCycles()`` provides nanosecond-level precision with no syscall overhead. The ``IRawSocket`` and ``INetworkIdentity`` interfaces provide the abstraction boundary. Platform-specific source files are organized under ``score/TimeSlave/code/gptp/platform/linux/`` and ``score/TimeSlave/code/gptp/platform/qnx/``. @@ -392,22 +477,120 @@ Instrumentation ProbeManager ^^^^^^^^^^^^ -The ``ProbeManager`` is a singleton that records probe events at key processing points in the gPTP engine. Probe points include: +The ``ProbeManager`` is a singleton that traces probe events at key processing points in the gPTP engine. It emits a ``LogDebug`` entry on every ``Trace()`` call and forwards the event to a linked ``Recorder`` (if set and enabled). Probing is controlled at runtime via ``SetEnabled()``; the ``GPTP_PROBE()`` macro provides zero overhead when disabled. -- ``RxPacketReceived`` — Raw frame received from socket -- ``SyncFrameParsed`` — Sync message successfully parsed -- ``FollowUpProcessed`` — Offset computed from Sync/FollowUp pair -- ``OffsetComputed`` — Final offset value available -- ``PdelayReqSent`` — PDelayReq frame transmitted -- ``PdelayCompleted`` — Peer delay measurement completed -- ``PhcAdjusted`` — PHC adjustment applied +Supported probe points (``ProbePoint`` enum): -The ``GPTP_PROBE()`` macro provides zero-overhead when probing is disabled. +.. list-table:: ProbePoint Events + :header-rows: 1 + :widths: 10 30 60 + + * - Value + - Enumerator + - Trigger + * - 0 + - ``kRxPacketReceived`` + - Raw Ethernet frame received from socket (RxThread) + * - 1 + - ``kSyncFrameParsed`` + - Sync message successfully decoded by ``GptpMessageParser`` + * - 2 + - ``kFollowUpProcessed`` + - FollowUp received; ``SyncStateMachine::OnFollowUp()`` returned a ``SyncResult`` + * - 3 + - ``kOffsetComputed`` + - Final clock offset value available after Sync/FollowUp correlation + * - 4 + - ``kPdelayReqSent`` + - PDelayReq frame transmitted by ``PeerDelayMeasurer`` + * - 5 + - ``kPdelayCompleted`` + - Peer delay computation finished (all four timestamps collected) + * - 6 + - ``kPhcAdjusted`` + - ``PhcAdjuster`` applied a step or frequency correction + +When a probe event is forwarded to the ``Recorder``, it is written with ``RecordEvent::kProbe`` and the ``ProbePoint`` value stored in the ``status_flags`` field of the CSV row. Recorder ^^^^^^^^^ -Thread-safe CSV file writer that persists probe events and other diagnostic data. Each ``RecordEntry`` contains a timestamp, event type, offset, peer delay, sequence ID, and status flags. +Thread-safe CSV file writer. When enabled, appends one row per event to the configured file. The file is opened in append mode (``ios::app``); a CSV header is written only if the file is newly created (size == 0). + +**Status model:** the ``Recorder`` starts in the state determined by ``Config.enabled``. If a write error occurs (``file_.good()`` fails after a flush), ``enabled_`` is atomically set to ``false`` and all subsequent ``Record()`` calls become no-ops. The file is never re-opened after an error. + +Configuration (``Recorder::Config``): + +.. list-table:: Recorder Configuration + :header-rows: 1 + :widths: 30 15 55 + + * - Parameter + - Type + - Description + * - ``enabled`` + - bool + - Enable or disable recording; default: ``false`` + * - ``file_path`` + - string + - Output CSV file path; default: ``/var/log/gptp_record.csv`` + * - ``offset_threshold_ns`` + - int64_t + - Reserved for ``kOffsetThreshold`` events (threshold above which offsets are logged); default: ``1 000 000`` (1 ms) + * - ``flush_interval`` + - uint32_t + - Number of rows between explicit ``file_.flush()`` calls; default: ``8`` + +CSV output format:: + + mono_ns,event,offset_ns,pdelay_ns,seq_id,status_flags + +Supported ``RecordEvent`` values written to the ``event`` column: + +.. list-table:: RecordEvent Values + :header-rows: 1 + :widths: 10 30 60 + + * - Value + - Enumerator + - Description + * - 0 + - ``kSyncReceived`` + - A Sync message was received and processed + * - 1 + - ``kPdelayCompleted`` + - A full peer delay measurement cycle completed + * - 2 + - ``kClockJump`` + - A forward or backward time jump was detected + * - 3 + - ``kOffsetThreshold`` + - Clock offset exceeded ``offset_threshold_ns`` + * - 4 + - ``kProbe`` + - Forwarded from ``ProbeManager::Trace()``; ``status_flags`` column carries the ``ProbePoint`` value + +Logging configuration +~~~~~~~~~~~~~~~~~~~~~ + +The TimeSlave and its TimeDaemon-side adapter use the following logging contexts: + +.. list-table:: Logging Contexts + :header-rows: 1 + :widths: 35 20 45 + + * - Component + - Context ID + - Comments + * - TimeSlave Application + - TS_APP + - **T**\ ime\ **S**\ lave **App**\ lication lifecycle (Initialize / Run) + * - gPTP Engine (RxThread / PdelayThread) + - GPTP_SLAVE + - **GPTP** **SLAVE** engine — low-level protocol processing + * - ShmPTPEngine (TimeDaemon side) + - GPTP + - TimeDaemon **GPTP** machine adapter (Initialize / ReadPTPSnapshot) Variability ~~~~~~~~~~~ @@ -424,26 +607,23 @@ The ``GptpEngineOptions`` struct provides all configurable parameters for the gP * - Parameter - Type - Description - * - ``interface_name`` + * - ``iface_name`` - string - - Network interface for gPTP frames (e.g., ``eth0``) + - Network interface for gPTP frames (e.g., ``eth0``); default: ``"eth0"`` * - ``pdelay_interval_ms`` - - uint32_t - - Interval between PDelayReq transmissions + - int + - Interval between PDelayReq transmissions (ms); default: ``1000`` + * - ``pdelay_warmup_ms`` + - int + - Delay before the first PDelayReq is sent (ms); default: ``2000`` * - ``sync_timeout_ms`` - - uint32_t - - Timeout for Sync message reception before declaring timeout state - * - ``time_jump_forward_ns`` + - int + - Timeout for Sync message reception before declaring timeout state (ms); default: ``3300`` + * - ``jump_future_threshold_ns`` - int64_t - - Threshold for forward time jump detection - * - ``time_jump_backward_ns`` - - int64_t - - Threshold for backward time jump detection - * - ``phc_config`` - - PhcConfig - - PHC device path, step threshold, and enable flag + - Threshold above which a positive clock offset is flagged as a forward time jump (ns); default: ``500 000 000`` -The ``PhcConfig`` struct additionally contains: +The ``PhcConfig`` struct (used by ``PhcAdjuster``, configured independently) contains: .. list-table:: PhcAdjuster Configuration :header-rows: 1 @@ -454,17 +634,190 @@ The ``PhcConfig`` struct additionally contains: - Description * - ``enabled`` - bool - - Enable or disable PHC adjustment - * - ``device_path`` + - Enable or disable PHC adjustment; default: ``false`` + * - ``device`` - string - - Path to the PHC device (e.g., ``/dev/ptp0``) + - PHC device identifier: ``/dev/ptp0`` on Linux, ``emac0`` on QNX * - ``step_threshold_ns`` - int64_t - - Offset threshold above which a step correction is applied instead of frequency slew + - Offset threshold above which a step correction is applied instead of frequency slew (ns); default: ``100 000 000`` + +Scalability +^^^^^^^^^^^ + +The TimeSlave architecture supports the following extensibility points: + +Platform extensibility +'''''''''''''''''''''' + +1. New target platforms can be supported by implementing the ``IRawSocket`` and ``INetworkIdentity`` interfaces under a new ``platform//`` directory and selecting the implementation via ``Bazel select()`` +2. The ``PhcAdjuster`` platform implementations (``clock_adjtime`` on Linux, EMAC ioctls on QNX) can be extended for additional hardware without changing protocol logic + +Protocol extensibility +'''''''''''''''''''''' + +1. The ``GptpEngine`` accepts injected ``IRawSocket`` and ``INetworkIdentity`` dependencies, making it straightforward to test or replace individual platform abstractions +2. The shared memory IPC channel name is configurable (``GptpIpcPublisher::Init(name)`` / ``GptpIpcReceiver::Init(name)``), allowing multiple gPTP instances per ECU if needed + +TimeDaemon integration extensibility +'''''''''''''''''''''''''''''''''''''' + +1. The ``ShmPTPEngine`` implements the same ``PTPEngine`` concept as other ``PTPMachine`` backends, making it transparently exchangeable with any other engine implementation +2. Alternative IPC mechanisms (e.g., socket-based) can be introduced by implementing a new engine class without modifying the ``PTPMachine`` template or downstream components + +ShmPTPEngine SW component +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``ShmPTPEngine`` component (in ``score::td::details``) is the TimeDaemon-side adapter that reads ``GptpIpcData`` from the shared memory channel written by TimeSlave and converts it into the ``PtpTimeInfo`` structure expected by the TimeDaemon pipeline. + +It is instantiated as ``GPTPRealMachine`` — a type alias for ``PTPMachine`` — which connects ``ShmPTPEngine`` to the TimeDaemon's internal ``MessageBroker``. + +Component requirements +'''''''''''''''''''''' + +The ``ShmPTPEngine`` has the following requirements: + +- The ``ShmPTPEngine`` shall call ``GptpIpcReceiver::Init(ipc_name)`` during ``Initialize()`` to open the shared memory channel +- The ``ShmPTPEngine`` shall call ``GptpIpcReceiver::Receive()`` in ``ReadPTPSnapshot()`` to fetch the latest ``GptpIpcData`` +- The ``ShmPTPEngine`` shall map all fields of ``GptpIpcData`` to the corresponding fields of ``PtpTimeInfo`` (status flags, Sync/FollowUp data, peer-delay data, time references) +- The ``ShmPTPEngine`` shall call ``GptpIpcReceiver::Close()`` during ``Deinitialize()`` +- The ``ShmPTPEngine`` shall be instantiatable with a configurable IPC channel name (default: ``/gptp_ptp_info``) + +Class view +'''''''''' + +The Class Diagram is presented below: + +.. raw:: html + +
+ +.. uml:: _assets/shm_ptp_engine/shm_ptp_engine_class.puml + :alt: Class Diagram + +.. raw:: html + +
+ +Component initialization +'''''''''''''''''''''''' + +During initialization the ``ShmPTPEngine`` shall open the shared memory channel to be able to read from it. + +The initialization workflow is represented in the following sequence diagram: + +.. raw:: html + +
+ +.. uml:: _assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml + :alt: Initialization workflow + +.. raw:: html + +
+ +Read PTP snapshot +''''''''''''''''' + +After ``ShmPTPEngine`` reads the latest ``GptpIpcData`` from shared memory, it maps it to ``PtpTimeInfo`` and publishes via the ``MessageBroker``. + +The periodic read and publish workflow is described below: + +.. raw:: html + +
+ +.. uml:: _assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml + :alt: Periodic read and publish workflow + +.. raw:: html + +
+ +Data mapping +'''''''''''' + +``ShmPTPEngine::ReadPTPSnapshot()`` performs a field-by-field mapping from ``GptpIpcData`` to ``PtpTimeInfo``: + +.. list-table:: GptpIpcData → PtpTimeInfo Mapping + :header-rows: 1 + :widths: 50 50 + + * - ``GptpIpcData`` field + - ``PtpTimeInfo`` field + * - ``ptp_assumed_time`` + - ``ptp_assumed_time`` + * - ``local_time`` + - ``local_time`` (wrapped in ``ReferenceClock::time_point``) + * - ``rate_deviation`` + - ``rate_deviation`` + * - ``status.is_synchronized`` + - ``status.is_synchronized`` + * - ``status.is_timeout`` + - ``status.is_timeout`` + * - ``status.is_time_jump_future`` + - ``status.is_time_jump_future`` + * - ``status.is_time_jump_past`` + - ``status.is_time_jump_past`` + * - ``status.is_correct`` + - ``status.is_correct`` + * - ``sync_fup_data.*`` (9 fields) + - ``sync_fup_data.*`` (direct copy) + * - ``pdelay_data.*`` (12 fields) + - ``pdelay_data.*`` (direct copy) + +Factory +''''''' + +``CreateGPTPRealMachine(name, ipc_name)`` is a convenience factory function in ``score::td`` that creates a configured ``GPTPRealMachine`` (``shared_ptr``) backed by ``ShmPTPEngine``: + +.. code-block:: cpp + + auto machine = CreateGPTPRealMachine("real", "/gptp_ptp_info"); + +Using in test environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using in ITF +^^^^^^^^^^^^ + +Normal behavior is expected. TimeSlave runs as a standalone process, communicates over real Ethernet, and writes to ``/gptp_ptp_info`` shared memory as in production. + +Using in Component Tests on the host +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Overview +'''''''' + +The ``TimeSlave`` and its constituent components can be tested on an x86 Linux host without PTP hardware or a real network. The key platform-dependent abstractions all have test-injectable counterparts: + +.. list-table:: Testable Abstractions + :header-rows: 1 + :widths: 30 35 35 + + * - Abstraction + - Production implementation + - Test replacement + * - ``IRawSocket`` + - ``RawSocket`` (AF_PACKET) + - ``FakeSocket`` (push-based frame queue) + * - ``INetworkIdentity`` + - ``NetworkIdentity`` (ioctl) + - ``FakeIdentity`` (fixed clock identity) + * - ``HighPrecisionLocalSteadyClock`` + - Platform clock (Linux / QNX) + - ``FakeClock`` (returns fixed timestamp) + +The ``GptpEngine`` provides a dedicated test constructor that accepts injected implementations: + +.. code-block:: cpp + + GptpEngine engine(opts, + std::make_unique(), + std::make_unique(), + std::make_unique()); -.. toctree:: - :maxdepth: 2 - :hidden: +This allows complete white-box testing of the Sync/FollowUp correlation, peer-delay measurement, timeout detection, and time-jump flagging logic by pushing crafted PTP frames directly into the ``FakeSocket`` queue. - gptp_engine/index - libTSClient/index +The ``GptpIpcPublisher`` and ``GptpIpcReceiver`` rely on POSIX shared memory (``shm_open``), which works on any Linux host, so ``ShmPTPEngine`` component tests can run end-to-end using real IPC without modification. diff --git a/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml b/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml deleted file mode 100644 index 69e04b4..0000000 --- a/docs/TimeSlave/libTSClient/_assets/ipc_channel.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -!theme plain - -title libTSClient Shared Memory IPC - -package "TimeSlave Process" { - class GptpIpcPublisher { - - region_ : GptpIpcRegion* - - fd_ : int - + Init(name) : bool - + Publish(info) : void - + Destroy() : void - } -} - -package "Shared Memory" { - class GptpIpcRegion <> { - + magic : uint32_t = 0x47505440 - + seq : std::atomic - + data : PtpTimeInfo - -- - 64-byte aligned for\ncache line efficiency - } -} - -package "TimeDaemon Process" { - class GptpIpcReceiver { - - region_ : const GptpIpcRegion* - - fd_ : int - + Init(name) : bool - + Receive() : std::optional - + Close() : void - } -} - -GptpIpcPublisher --> GptpIpcRegion : "shm_open(O_CREAT)\nmmap(PROT_WRITE)" -GptpIpcReceiver --> GptpIpcRegion : "shm_open(O_RDONLY)\nmmap(PROT_READ)" - -note right of GptpIpcRegion - **Seqlock Protocol:** - Writer: seq++ → memcpy → seq++ - Reader: read seq (even) → memcpy → check seq - Retry up to 20 times on torn read -end note - -@enduml diff --git a/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml b/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml deleted file mode 100644 index 46fa582..0000000 --- a/docs/TimeSlave/libTSClient/_assets/ipc_sequence.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -!theme plain - -title libTSClient Seqlock IPC Protocol - -participant "TimeSlave\n(GptpIpcPublisher)" as PUB -participant "SharedMemory\n(GptpIpcRegion)" as SHM -participant "TimeDaemon\n(GptpIpcReceiver)" as RCV - -== Initialization == - -PUB -> SHM : shm_open("/gptp_ptp_info", O_CREAT | O_RDWR) -PUB -> SHM : ftruncate(sizeof(GptpIpcRegion)) -PUB -> SHM : mmap(PROT_READ | PROT_WRITE) -PUB -> SHM : write magic = 0x47505440 - -... - -RCV -> SHM : shm_open("/gptp_ptp_info", O_RDONLY) -RCV -> SHM : mmap(PROT_READ) -RCV -> SHM : verify magic == 0x47505440 - -== Publish (Writer Side) == - -PUB -> SHM : seq.fetch_add(1, release) // seq becomes odd -PUB -> SHM : memcpy(data, &info, sizeof) -PUB -> SHM : seq.fetch_add(1, release) // seq becomes even - -== Receive (Reader Side) == - -loop up to 20 retries - RCV -> SHM : s1 = seq.load(acquire) - alt s1 is odd (write in progress) - RCV -> RCV : retry - else s1 is even - RCV -> SHM : memcpy(&local, data, sizeof) - RCV -> SHM : s2 = seq.load(acquire) - alt s1 == s2 - RCV --> RCV : return PtpTimeInfo - else s1 != s2 (torn read) - RCV -> RCV : retry - end - end -end - -@enduml diff --git a/docs/TimeSlave/libTSClient/index.rst b/docs/TimeSlave/libTSClient/index.rst deleted file mode 100644 index b843b5c..0000000 --- a/docs/TimeSlave/libTSClient/index.rst +++ /dev/null @@ -1,175 +0,0 @@ -.. - # ******************************************************************************* - # Copyright (c) 2026 Contributors to the Eclipse Foundation - # - # See the NOTICE file(s) distributed with this work for additional - # information regarding copyright ownership. - # - # This program and the accompanying materials are made available under the - # terms of the Apache License Version 2.0 which is available at - # https://www.apache.org/licenses/LICENSE-2.0 - # - # SPDX-License-Identifier: Apache-2.0 - # ******************************************************************************* - -.. _libtsclient_design: - -############################ -libTSClient Design -############################ - -.. contents:: Table of Contents - :depth: 3 - :local: - -Overview -======== - -**libTSClient** is the shared memory IPC library that connects the TimeSlave process to the -TimeDaemon process. It provides a lock-free, single-writer/multi-reader communication -channel using a **seqlock protocol** over POSIX shared memory. - -The library is intentionally minimal — it consists of three headers and two source files — -to keep the IPC boundary simple, auditable, and suitable for safety-critical deployments. - -Architecture -============ - -.. raw:: html - -
- -.. uml:: _assets/ipc_channel.puml - :alt: libTSClient IPC Architecture - -.. raw:: html - -
- -Components -========== - -GptpIpcChannel --------------- - -Defines the shared memory layout as the ``GptpIpcRegion`` structure: - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - Field - - Type - - Purpose - * - ``magic`` - - ``uint32_t`` - - Validation constant (``0x47505440``). Used by the receiver to confirm the shared - memory segment is valid and initialized. - * - ``seq`` - - ``std::atomic`` - - Seqlock counter. Odd values indicate a write in progress; even values indicate - a consistent state. - * - ``data`` - - ``PtpTimeInfo`` - - The actual time synchronization payload (PTP status, Sync/FollowUp data, - peer delay data, local clock reference). - -The structure is aligned to 64 bytes (cache line size) to prevent false sharing between -the writer and reader processes. - -The default POSIX shared memory name is ``/gptp_ptp_info`` (defined as ``kGptpIpcName``). - -GptpIpcPublisher ----------------- - -The **single-writer** component, used by TimeSlave: - -- ``Init(name)`` — Creates the POSIX shared memory segment via ``shm_open(O_CREAT | O_RDWR)``, - sizes it with ``ftruncate()``, maps it with ``mmap(PROT_READ | PROT_WRITE)``, and writes - the magic number. - -- ``Publish(info)`` — Writes a ``PtpTimeInfo`` using the seqlock protocol: - - 1. Increment ``seq`` (becomes odd — signals write in progress) - 2. ``memcpy`` the data - 3. Increment ``seq`` (becomes even — signals write complete) - -- ``Destroy()`` — Unmaps and unlinks the shared memory segment. - -GptpIpcReceiver ---------------- - -The **multi-reader** component, used by the TimeDaemon (via ``RealPTPEngine``): - -- ``Init(name)`` — Opens the existing shared memory segment via ``shm_open(O_RDONLY)`` and - maps it with ``mmap(PROT_READ)``. Validates the magic number. - -- ``Receive()`` — Reads ``PtpTimeInfo`` using the seqlock protocol with up to 20 retries: - - 1. Read ``seq`` (must be even, otherwise retry) - 2. ``memcpy`` the data - 3. Read ``seq`` again (must match step 1, otherwise retry — torn read detected) - 4. Return ``std::optional`` (empty if all retries exhausted) - -- ``Close()`` — Unmaps the shared memory. - -Seqlock protocol -================ - -.. raw:: html - -
- -.. uml:: _assets/ipc_sequence.puml - :alt: Seqlock IPC Protocol Sequence - -.. raw:: html - -
- -The seqlock provides the following properties: - -- **Lock-free for readers** — Readers never block the writer. A torn read is detected and - retried transparently. -- **Single writer** — Only one process (TimeSlave) writes to the shared memory. No - writer-writer contention. -- **Bounded retry** — The receiver retries up to 20 times. Under normal operation, - retries are rare because the write is a single ``memcpy`` of a small struct. -- **Cache-line alignment** — The 64-byte alignment of ``GptpIpcRegion`` prevents false - sharing, which is critical for cross-process shared memory performance. - -Data type -========= - -The ``PtpTimeInfo`` structure (defined in ``score/TimeDaemon/code/common/data_types/ptp_time_info.h``) -is the payload transferred through the IPC channel. It contains: - -.. list-table:: - :header-rows: 1 - :widths: 25 75 - - * - Field - - Content - * - ``PtpStatus`` - - Synchronization flags (synchronized, timeout, time leap indicators) - * - ``SyncFupData`` - - Sync and FollowUp message timestamps and correction fields - * - ``PDelayData`` - - Peer delay measurement results - * - Local clock value - - Reference timestamp from ``HighPrecisionLocalSteadyClock`` - -Build integration -================= - -The library is built as a Bazel ``cc_library`` target: - -.. code-block:: text - - //score/libTSClient:gptp_ipc - -It links against ``-lrt`` for POSIX shared memory (``shm_open``, ``shm_unlink``) and -depends on the ``PtpTimeInfo`` data type from the TimeDaemon common module. - -Both TimeSlave and TimeDaemon link against ``libTSClient`` — the publisher side in -TimeSlave, the receiver side in TimeDaemon. diff --git a/score/TimeDaemon/code/common/BUILD b/score/TimeDaemon/code/common/BUILD index fe5fa19..ea4d4fd 100644 --- a/score/TimeDaemon/code/common/BUILD +++ b/score/TimeDaemon/code/common/BUILD @@ -22,7 +22,7 @@ cc_library( ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], - visibility = ["//score:__subpackages__"], + visibility = ["//score/TimeDaemon:__subpackages__"], deps = [], ) diff --git a/score/TimeDaemon/code/common/data_types/BUILD b/score/TimeDaemon/code/common/data_types/BUILD index d7a7468..e6b718d 100644 --- a/score/TimeDaemon/code/common/data_types/BUILD +++ b/score/TimeDaemon/code/common/data_types/BUILD @@ -22,7 +22,7 @@ cc_library( ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], - visibility = ["//score:__subpackages__"], + visibility = ["//score/TimeDaemon:__subpackages__"], deps = ["//score/time/HighPrecisionLocalSteadyClock:interface"], ) diff --git a/score/TimeDaemon/code/ptp_machine/real/BUILD b/score/TimeDaemon/code/ptp_machine/real/BUILD index 55ec5df..5738111 100644 --- a/score/TimeDaemon/code/ptp_machine/real/BUILD +++ b/score/TimeDaemon/code/ptp_machine/real/BUILD @@ -28,7 +28,7 @@ cc_library( visibility = ["//score/TimeDaemon:__subpackages__"], deps = [ "//score/TimeDaemon/code/ptp_machine/core:ptp_machine", - "//score/TimeDaemon/code/ptp_machine/real/details:real_ptp_engine", + "//score/TimeDaemon/code/ptp_machine/real/details:shm_ptp_engine", "//score/libTSClient:gptp_ipc", ], ) @@ -50,7 +50,7 @@ cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", cc_unit_tests = [":gptp_real_machine_test"], test_suites_from_sub_packages = [ - "//score/TimeDaemon/code/ptp_machine/real/details:unit_test_suite", + "//score/TimeDaemon/code/ptp_machine/real/details:unit_test_suite", # shm_ptp_engine ], visibility = ["//score/TimeDaemon:__subpackages__"], ) diff --git a/score/TimeDaemon/code/ptp_machine/real/details/BUILD b/score/TimeDaemon/code/ptp_machine/real/details/BUILD index 71588d2..86750eb 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/BUILD +++ b/score/TimeDaemon/code/ptp_machine/real/details/BUILD @@ -15,12 +15,12 @@ load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") cc_library( - name = "real_ptp_engine", + name = "shm_ptp_engine", srcs = [ - "real_ptp_engine.cpp", + "shm_ptp_engine.cpp", ], hdrs = [ - "real_ptp_engine.h", + "shm_ptp_engine.h", ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], @@ -34,11 +34,11 @@ cc_library( ) cc_test( - name = "real_ptp_engine_test", - srcs = ["real_ptp_engine_test.cpp"], + name = "shm_ptp_engine_test", + srcs = ["shm_ptp_engine_test.cpp"], tags = ["unit"], deps = [ - ":real_ptp_engine", + ":shm_ptp_engine", "//score/libTSClient:gptp_ipc", "@googletest//:gtest", "@googletest//:gtest_main", @@ -48,7 +48,7 @@ cc_test( cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", - cc_unit_tests = [":real_ptp_engine_test"], + cc_unit_tests = [":shm_ptp_engine_test"], test_suites_from_sub_packages = [], visibility = ["//score/TimeDaemon:__subpackages__"], ) diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp index 6d94fd0..b348caf 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp @@ -13,6 +13,7 @@ #include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" #include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/libTSClient/gptp_ipc_data.h" #include "score/mw/log/logging.h" namespace score @@ -60,7 +61,36 @@ bool RealPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) if (!result.has_value()) return false; - info = result.value(); + const score::ts::GptpIpcData& d = result.value(); + info.ptp_assumed_time = d.ptp_assumed_time; + info.local_time = score::td::PtpTimeInfo::ReferenceClock::time_point{d.local_time}; + info.rate_deviation = d.rate_deviation; + info.status.is_synchronized = d.status.is_synchronized; + info.status.is_timeout = d.status.is_timeout; + info.status.is_time_jump_future = d.status.is_time_jump_future; + info.status.is_time_jump_past = d.status.is_time_jump_past; + info.status.is_correct = d.status.is_correct; + info.sync_fup_data.precise_origin_timestamp = d.sync_fup_data.precise_origin_timestamp; + info.sync_fup_data.reference_global_timestamp = d.sync_fup_data.reference_global_timestamp; + info.sync_fup_data.reference_local_timestamp = d.sync_fup_data.reference_local_timestamp; + info.sync_fup_data.sync_ingress_timestamp = d.sync_fup_data.sync_ingress_timestamp; + info.sync_fup_data.correction_field = d.sync_fup_data.correction_field; + info.sync_fup_data.sequence_id = d.sync_fup_data.sequence_id; + info.sync_fup_data.pdelay = d.sync_fup_data.pdelay; + info.sync_fup_data.port_number = d.sync_fup_data.port_number; + info.sync_fup_data.clock_identity = d.sync_fup_data.clock_identity; + info.pdelay_data.request_origin_timestamp = d.pdelay_data.request_origin_timestamp; + info.pdelay_data.request_receipt_timestamp = d.pdelay_data.request_receipt_timestamp; + info.pdelay_data.response_origin_timestamp = d.pdelay_data.response_origin_timestamp; + info.pdelay_data.response_receipt_timestamp = d.pdelay_data.response_receipt_timestamp; + info.pdelay_data.reference_global_timestamp = d.pdelay_data.reference_global_timestamp; + info.pdelay_data.reference_local_timestamp = d.pdelay_data.reference_local_timestamp; + info.pdelay_data.sequence_id = d.pdelay_data.sequence_id; + info.pdelay_data.pdelay = d.pdelay_data.pdelay; + info.pdelay_data.req_port_number = d.pdelay_data.req_port_number; + info.pdelay_data.req_clock_identity = d.pdelay_data.req_clock_identity; + info.pdelay_data.resp_port_number = d.pdelay_data.resp_port_number; + info.pdelay_data.resp_clock_identity = d.pdelay_data.resp_clock_identity; return true; } diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp new file mode 100644 index 0000000..75e9580 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp @@ -0,0 +1,99 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" + +#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/libTSClient/gptp_ipc_data.h" +#include "score/mw/log/logging.h" + +namespace score +{ +namespace td +{ +namespace details +{ + +ShmPTPEngine::ShmPTPEngine(std::string ipc_name) noexcept : ipc_name_{std::move(ipc_name)} {} + +bool ShmPTPEngine::Initialize() +{ + if (initialized_) + return true; + + initialized_ = receiver_.Init(ipc_name_); + if (initialized_) + { + score::mw::log::LogInfo(kGPtpMachineContext) << "ShmPTPEngine: connected to IPC channel " << ipc_name_; + } + else + { + score::mw::log::LogError(kGPtpMachineContext) << "ShmPTPEngine: failed to open IPC channel " << ipc_name_; + } + return initialized_; +} + +bool ShmPTPEngine::Deinitialize() +{ + if (initialized_) + { + receiver_.Close(); + initialized_ = false; + } + return true; +} + +bool ShmPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) +{ + if (!initialized_) + return false; + + auto result = receiver_.Receive(); + if (!result.has_value()) + return false; + + const score::ts::GptpIpcData& d = result.value(); + info.ptp_assumed_time = d.ptp_assumed_time; + info.local_time = PtpTimeInfo::ReferenceClock::time_point{d.local_time}; + info.rate_deviation = d.rate_deviation; + info.status.is_synchronized = d.status.is_synchronized; + info.status.is_timeout = d.status.is_timeout; + info.status.is_time_jump_future = d.status.is_time_jump_future; + info.status.is_time_jump_past = d.status.is_time_jump_past; + info.status.is_correct = d.status.is_correct; + info.sync_fup_data.precise_origin_timestamp = d.sync_fup_data.precise_origin_timestamp; + info.sync_fup_data.reference_global_timestamp = d.sync_fup_data.reference_global_timestamp; + info.sync_fup_data.reference_local_timestamp = d.sync_fup_data.reference_local_timestamp; + info.sync_fup_data.sync_ingress_timestamp = d.sync_fup_data.sync_ingress_timestamp; + info.sync_fup_data.correction_field = d.sync_fup_data.correction_field; + info.sync_fup_data.sequence_id = d.sync_fup_data.sequence_id; + info.sync_fup_data.pdelay = d.sync_fup_data.pdelay; + info.sync_fup_data.port_number = d.sync_fup_data.port_number; + info.sync_fup_data.clock_identity = d.sync_fup_data.clock_identity; + info.pdelay_data.request_origin_timestamp = d.pdelay_data.request_origin_timestamp; + info.pdelay_data.request_receipt_timestamp = d.pdelay_data.request_receipt_timestamp; + info.pdelay_data.response_origin_timestamp = d.pdelay_data.response_origin_timestamp; + info.pdelay_data.response_receipt_timestamp = d.pdelay_data.response_receipt_timestamp; + info.pdelay_data.reference_global_timestamp = d.pdelay_data.reference_global_timestamp; + info.pdelay_data.reference_local_timestamp = d.pdelay_data.reference_local_timestamp; + info.pdelay_data.sequence_id = d.pdelay_data.sequence_id; + info.pdelay_data.pdelay = d.pdelay_data.pdelay; + info.pdelay_data.req_port_number = d.pdelay_data.req_port_number; + info.pdelay_data.req_clock_identity = d.pdelay_data.req_clock_identity; + info.pdelay_data.resp_port_number = d.pdelay_data.resp_port_number; + info.pdelay_data.resp_clock_identity = d.pdelay_data.resp_clock_identity; + return true; +} + +} // namespace details +} // namespace td +} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h new file mode 100644 index 0000000..b455da7 --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H + +#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_receiver.h" + +#include + +namespace score +{ +namespace td +{ +namespace details +{ + +/** + * @brief PTP engine that reads time data from the shared-memory IPC channel + * written by TimeSlave via GptpIpcPublisher. + * + * Converts the libTSClient-internal GptpIpcData to the TimeDaemon PtpTimeInfo + * data model. + */ +class ShmPTPEngine final +{ + public: + explicit ShmPTPEngine(std::string ipc_name = score::ts::details::kGptpIpcName) noexcept; + ~ShmPTPEngine() noexcept = default; + + ShmPTPEngine(const ShmPTPEngine&) = delete; + ShmPTPEngine& operator=(const ShmPTPEngine&) = delete; + ShmPTPEngine(ShmPTPEngine&&) = delete; + ShmPTPEngine& operator=(ShmPTPEngine&&) = delete; + + bool Initialize(); + + bool Deinitialize(); + + bool ReadPTPSnapshot(PtpTimeInfo& info); + + private: + std::string ipc_name_; + score::ts::details::GptpIpcReceiver receiver_; + bool initialized_{false}; +}; + +} // namespace details +} // namespace td +} // namespace score + +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp new file mode 100644 index 0000000..b89243a --- /dev/null +++ b/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp @@ -0,0 +1,216 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" + +#include "score/libTSClient/gptp_ipc_publisher.h" + +#include + +#include +#include + +namespace score +{ +namespace td +{ +namespace details +{ + +namespace +{ + +std::string UniqueShmName() +{ + static std::atomic counter{0}; + return "/gptp_shm_ut_" + std::to_string(::getpid()) + "_" + + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +/// Build a fully-populated GptpIpcData for roundtrip verification. +score::ts::GptpIpcData MakeTestIpcData() +{ + score::ts::GptpIpcData d{}; + d.ptp_assumed_time = std::chrono::nanoseconds{9'876'543'210LL}; + d.local_time = std::chrono::nanoseconds{42'000'000'000LL}; + d.rate_deviation = -0.25; + + d.status.is_synchronized = true; + d.status.is_correct = true; + d.status.is_timeout = false; + d.status.is_time_jump_future = false; + d.status.is_time_jump_past = false; + + d.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + d.sync_fup_data.reference_global_timestamp = 100'000'000'500ULL; + d.sync_fup_data.reference_local_timestamp = 100'000'001'000ULL; + d.sync_fup_data.sync_ingress_timestamp = 100'000'001'000ULL; + d.sync_fup_data.correction_field = 8U; + d.sync_fup_data.sequence_id = 55; + d.sync_fup_data.pdelay = 4'000U; + d.sync_fup_data.port_number = 1; + d.sync_fup_data.clock_identity = 0xCAFEBABEDEAD0001ULL; + + d.pdelay_data.request_origin_timestamp = 200'000'000'000ULL; + d.pdelay_data.request_receipt_timestamp = 200'000'001'000ULL; + d.pdelay_data.response_origin_timestamp = 200'000'001'000ULL; + d.pdelay_data.response_receipt_timestamp = 200'000'002'000ULL; + d.pdelay_data.pdelay = 1'000U; + d.pdelay_data.req_port_number = 2; + d.pdelay_data.resp_port_number = 3; + d.pdelay_data.req_clock_identity = 0x0102030405060708ULL; + d.pdelay_data.resp_clock_identity = 0x0807060504030201ULL; + return d; +} + +} // namespace + +class ShmPTPEngineTest : public ::testing::Test +{ + protected: + void SetUp() override + { + name_ = UniqueShmName(); + engine_ = std::make_unique(name_); + } + + void TearDown() override + { + engine_->Deinitialize(); + pub_.Destroy(); + } + + std::string name_; + score::ts::details::GptpIpcPublisher pub_; + std::unique_ptr engine_; +}; + +// ── Lifecycle ──────────────────────────────────────────────────────────────── + +TEST_F(ShmPTPEngineTest, Initialize_WhenShmNotExist_ReturnsFalse) +{ + EXPECT_FALSE(engine_->Initialize()); +} + +TEST_F(ShmPTPEngineTest, Initialize_WhenShmExists_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + EXPECT_TRUE(engine_->Initialize()); +} + +TEST_F(ShmPTPEngineTest, Initialize_CalledTwiceWhenInitialized_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Initialize()); // idempotent +} + +TEST_F(ShmPTPEngineTest, Deinitialize_WhenNotInitialized_ReturnsTrue) +{ + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(ShmPTPEngineTest, Deinitialize_AfterInitialize_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(ShmPTPEngineTest, Deinitialize_CalledTwice_BothReturnTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + EXPECT_TRUE(engine_->Deinitialize()); + EXPECT_TRUE(engine_->Deinitialize()); +} + +TEST_F(ShmPTPEngineTest, ReInitialize_AfterDeinitialize_Succeeds) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(engine_->Initialize()); + ASSERT_TRUE(engine_->Deinitialize()); + EXPECT_TRUE(engine_->Initialize()); +} + +// ── ReadPTPSnapshot ─────────────────────────────────────────────────────────── + +TEST_F(ShmPTPEngineTest, ReadPTPSnapshot_WhenNotInitialized_ReturnsFalse) +{ + PtpTimeInfo info{}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); +} + +TEST_F(ShmPTPEngineTest, ReadPTPSnapshot_WithPublishedData_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + pub_.Publish(MakeTestIpcData()); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + EXPECT_TRUE(engine_->ReadPTPSnapshot(result)); +} + +TEST_F(ShmPTPEngineTest, ReadPTPSnapshot_CopiesTimeAndStatusCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const score::ts::GptpIpcData src = MakeTestIpcData(); + pub_.Publish(src); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.ptp_assumed_time, src.ptp_assumed_time); + EXPECT_DOUBLE_EQ(result.rate_deviation, src.rate_deviation); + EXPECT_EQ(result.status.is_synchronized, src.status.is_synchronized); + EXPECT_EQ(result.status.is_correct, src.status.is_correct); + EXPECT_EQ(result.status.is_timeout, src.status.is_timeout); +} + +TEST_F(ShmPTPEngineTest, ReadPTPSnapshot_CopiesSyncFupDataCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const score::ts::GptpIpcData src = MakeTestIpcData(); + pub_.Publish(src); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.sync_fup_data.precise_origin_timestamp, src.sync_fup_data.precise_origin_timestamp); + EXPECT_EQ(result.sync_fup_data.reference_global_timestamp, src.sync_fup_data.reference_global_timestamp); + EXPECT_EQ(result.sync_fup_data.sequence_id, src.sync_fup_data.sequence_id); + EXPECT_EQ(result.sync_fup_data.pdelay, src.sync_fup_data.pdelay); + EXPECT_EQ(result.sync_fup_data.clock_identity, src.sync_fup_data.clock_identity); +} + +TEST_F(ShmPTPEngineTest, ReadPTPSnapshot_CopiesPDelayDataCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + const score::ts::GptpIpcData src = MakeTestIpcData(); + pub_.Publish(src); + ASSERT_TRUE(engine_->Initialize()); + + PtpTimeInfo result{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); + + EXPECT_EQ(result.pdelay_data.pdelay, src.pdelay_data.pdelay); + EXPECT_EQ(result.pdelay_data.req_port_number, src.pdelay_data.req_port_number); + EXPECT_EQ(result.pdelay_data.resp_port_number, src.pdelay_data.resp_port_number); + EXPECT_EQ(result.pdelay_data.req_clock_identity, src.pdelay_data.req_clock_identity); + EXPECT_EQ(result.pdelay_data.resp_clock_identity, src.pdelay_data.resp_clock_identity); +} + +} // namespace details +} // namespace td +} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h index 3860ba0..c8013e7 100644 --- a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h +++ b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h @@ -14,14 +14,14 @@ #define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H #include "score/TimeDaemon/code/ptp_machine/core/ptp_machine.h" -#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" +#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" namespace score { namespace td { -/// @brief PTPMachine instantiated with the real gPTP engine. +/// @brief PTPMachine instantiated with the shared-memory gPTP engine. /// /// Reads PtpTimeInfo snapshots written by TimeSlave via the IPC channel. /// Construct via CreateGPTPRealMachine() (see factory.h) or directly: @@ -30,7 +30,7 @@ namespace td /// auto machine = std::make_shared( /// "real", std::chrono::milliseconds{50}, "/gptp_ptp_info"); /// @endcode -using GPTPRealMachine = PTPMachine; +using GPTPRealMachine = PTPMachine; } // namespace td } // namespace score diff --git a/score/TimeSlave/BUILD b/score/TimeSlave/BUILD index ca5de74..9075524 100644 --- a/score/TimeSlave/BUILD +++ b/score/TimeSlave/BUILD @@ -10,3 +10,14 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + +load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and_qnx") + +cc_unit_test_suites_for_host_and_qnx( + name = "unit_test_suite", + cc_unit_tests = [], + test_suites_from_sub_packages = [ + "//score/TimeSlave/code/gptp:unit_test_suite", + ], + visibility = ["//score:__subpackages__"], +) diff --git a/score/TimeSlave/code/application/BUILD b/score/TimeSlave/code/application/BUILD index 4f7eab4..1361a31 100644 --- a/score/TimeSlave/code/application/BUILD +++ b/score/TimeSlave/code/application/BUILD @@ -26,7 +26,6 @@ cc_binary( "//score/TimeSlave/code/common:logging_contexts", "//score/TimeSlave/code/gptp:gptp_engine", "//score/libTSClient:gptp_ipc", - "//score/time/HighPrecisionLocalSteadyClock", "@score_baselibs//score/mw/log:console_only_backend", "@score_lifecycle_health//src/lifecycle_client_lib", ], diff --git a/score/TimeSlave/code/application/time_slave.cpp b/score/TimeSlave/code/application/time_slave.cpp index c4e8d3e..a4345de 100644 --- a/score/TimeSlave/code/application/time_slave.cpp +++ b/score/TimeSlave/code/application/time_slave.cpp @@ -14,7 +14,6 @@ #include "score/TimeSlave/code/common/logging_contexts.h" #include "score/mw/log/logging.h" -#include "score/time/HighPrecisionLocalSteadyClock/details/factory_impl.h" #include @@ -23,44 +22,49 @@ namespace score namespace ts { +namespace +{ + +constexpr std::int32_t kInitSuccess = 0; +constexpr std::int32_t kInitFailure = -1; + +} // namespace + TimeSlave::TimeSlave() = default; std::int32_t TimeSlave::Initialize(const score::mw::lifecycle::ApplicationContext& /*context*/) { - // Create the high-precision local clock for the gPTP engine - score::time::HighPrecisionLocalSteadyClock::FactoryImpl clock_factory{}; - auto clock = clock_factory.CreateHighPrecisionLocalSteadyClock(); - - engine_ = std::make_unique(opts_, std::move(clock)); + engine_ = std::make_unique(opts_); if (!engine_->Initialize()) { - score::mw::log::LogError(kGPtpMachineContext) << "TimeSlave: GptpEngine initialization failed"; - return -1; + score::mw::log::LogError(kTimeSlaveAppContext) << "TimeSlave: GptpEngine initialization failed"; + return kInitFailure; } if (!publisher_.Init()) { - score::mw::log::LogError(kGPtpMachineContext) << "TimeSlave: shared memory publisher initialization failed"; - return -1; + score::mw::log::LogError(kTimeSlaveAppContext) << "TimeSlave: shared memory publisher initialization failed"; + return kInitFailure; } - score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave initialized"; - return 0; + score::mw::log::LogInfo(kTimeSlaveAppContext) << "TimeSlave initialized"; + return kInitSuccess; } std::int32_t TimeSlave::Run(const score::cpp::stop_token& token) { constexpr auto kPublishInterval = std::chrono::milliseconds{50}; - score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave running"; + score::mw::log::LogInfo(kTimeSlaveAppContext) << "TimeSlave running"; while (!token.stop_requested()) { - score::td::PtpTimeInfo info{}; - if (engine_->ReadPTPSnapshot(info)) + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + if (engine_->ReadPTPSnapshot(data)) { - publisher_.Publish(info); + publisher_.Publish(data); } std::this_thread::sleep_for(kPublishInterval); @@ -69,8 +73,8 @@ std::int32_t TimeSlave::Run(const score::cpp::stop_token& token) engine_->Deinitialize(); publisher_.Destroy(); - score::mw::log::LogInfo(kGPtpMachineContext) << "TimeSlave stopped"; - return 0; + score::mw::log::LogInfo(kTimeSlaveAppContext) << "TimeSlave stopped"; + return kInitSuccess; } } // namespace ts diff --git a/score/TimeSlave/code/common/logging_contexts.h b/score/TimeSlave/code/common/logging_contexts.h index 7a12c23..57ccf91 100644 --- a/score/TimeSlave/code/common/logging_contexts.h +++ b/score/TimeSlave/code/common/logging_contexts.h @@ -18,8 +18,12 @@ namespace score namespace ts { +/// Logging context for the gPTP protocol engine (RxThread / PdelayThread). constexpr auto kGPtpMachineContext = "GPTP_SLAVE"; +/// Logging context for the TimeSlave application lifecycle (Initialize / Run). +constexpr auto kTimeSlaveAppContext = "TS_APP"; + } // namespace ts } // namespace score diff --git a/score/TimeSlave/code/gptp/BUILD b/score/TimeSlave/code/gptp/BUILD index ca025a0..a215d22 100644 --- a/score/TimeSlave/code/gptp/BUILD +++ b/score/TimeSlave/code/gptp/BUILD @@ -29,10 +29,9 @@ cc_library( tags = ["QM"], visibility = ["//score:__subpackages__"], deps = [ - "//score/TimeDaemon/code/common:logging_contexts", - "//score/TimeDaemon/code/common/data_types:ptp_time_info", + "//score/TimeSlave/code/common:logging_contexts", "//score/TimeSlave/code/gptp/details:gptp_details", - "//score/time/HighPrecisionLocalSteadyClock:interface", + "//score/libTSClient:gptp_ipc", "@score_baselibs//score/mw/log:frontend", ], ) diff --git a/score/TimeSlave/code/gptp/details/BUILD b/score/TimeSlave/code/gptp/details/BUILD index 634117e..5c1f331 100644 --- a/score/TimeSlave/code/gptp/details/BUILD +++ b/score/TimeSlave/code/gptp/details/BUILD @@ -109,7 +109,7 @@ cc_library( deps = [ ":clock_util", ":ptp_types", - "//score/TimeDaemon/code/common/data_types:ptp_time_info", + "//score/libTSClient:gptp_ipc", ], ) @@ -124,7 +124,7 @@ cc_library( ":frame_codec", ":i_raw_socket", ":ptp_types", - "//score/TimeDaemon/code/common/data_types:ptp_time_info", + "//score/libTSClient:gptp_ipc", ], ) @@ -166,6 +166,7 @@ cc_test( name = "raw_socket_test", srcs = ["raw_socket_test.cpp"], tags = ["unit"], + target_compatible_with = ["@platforms//os:linux"], deps = [ ":network_identity", ":raw_socket", diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp index 4b2dbb1..b755447 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -144,7 +144,7 @@ void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept r.path_delay_ns = delay; r.valid = true; - score::td::PDelayData& d = r.pdelay_data; + score::ts::GptpIpcPDelayData& d = r.pdelay_data; d.request_origin_timestamp = static_cast(t1.ns); d.request_receipt_timestamp = static_cast(t2.ns); d.response_origin_timestamp = static_cast(t3.ns); diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.h b/score/TimeSlave/code/gptp/details/pdelay_measurer.h index 1b8d882..8715ba6 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.h +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.h @@ -13,7 +13,7 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_PDELAY_MEASURER_H -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_data.h" #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" #include "score/TimeSlave/code/gptp/details/ptp_types.h" @@ -31,7 +31,7 @@ namespace details struct PDelayResult { std::int64_t path_delay_ns{0}; - score::td::PDelayData pdelay_data{}; + score::ts::GptpIpcPDelayData pdelay_data{}; bool valid{false}; }; diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp index f0362f3..362c289 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer_test.cpp @@ -134,7 +134,7 @@ TEST_F(PeerDelayMeasurerTest, PDelayData_TimestampFields_PopulatedCorrectly) measurer_.OnResponse(MakeResp(0U, /*t2=*/100LL, /*t4=*/180LL)); measurer_.OnResponseFollowUp(MakeResp(0U, /*t3=*/80LL)); - const score::td::PDelayData& d = measurer_.GetResult().pdelay_data; + const score::ts::GptpIpcPDelayData& d = measurer_.GetResult().pdelay_data; EXPECT_EQ(d.request_origin_timestamp, 0ULL); // t1 EXPECT_EQ(d.request_receipt_timestamp, 100ULL); // t2 EXPECT_EQ(d.response_origin_timestamp, 80ULL); // t3 diff --git a/score/TimeSlave/code/gptp/details/ptp_types.h b/score/TimeSlave/code/gptp/details/ptp_types.h index 9d14d28..ec4c4ac 100644 --- a/score/TimeSlave/code/gptp/details/ptp_types.h +++ b/score/TimeSlave/code/gptp/details/ptp_types.h @@ -18,7 +18,7 @@ #include #include -#ifndef _QNX_PLAT +#ifndef __QNXNTO__ #include #else // Minimal ethhdr definition for QNX diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp index 92fd2cb..3c78b02 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp @@ -122,7 +122,7 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag return v >= 0 ? static_cast(v) : 0U; }; - score::td::SyncFupData& d = r.sync_fup_data; + score::ts::GptpIpcSyncFupData& d = r.sync_fup_data; d.precise_origin_timestamp = to_u64(fup_ts.ns); d.reference_global_timestamp = to_u64(master_ns); d.reference_local_timestamp = to_u64(sync.recvHardwareTS.ns); diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.h b/score/TimeSlave/code/gptp/details/sync_state_machine.h index 84072e3..e073045 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.h +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.h @@ -13,7 +13,7 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H #define SCORE_TIMESLAVE_CODE_GPTP_DETAILS_SYNC_STATE_MACHINE_H -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_data.h" #include "score/TimeSlave/code/gptp/details/ptp_types.h" #include @@ -32,7 +32,7 @@ struct SyncResult { std::int64_t master_ns{0}; ///< Grandmaster time (ns since epoch) std::int64_t offset_ns{0}; ///< local hw_ts − master_ns - score::td::SyncFupData sync_fup_data{}; ///< Ready to copy into PtpTimeInfo (pdelay field filled by engine) + score::ts::GptpIpcSyncFupData sync_fup_data{}; ///< Ready to copy into GptpIpcData (pdelay field filled by engine) bool is_time_jump_future{false}; bool is_time_jump_past{false}; }; diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp index ed7e2e8..157e3d0 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine_test.cpp @@ -145,7 +145,7 @@ TEST_F(SyncStateMachineTest, JumpPast_Detected_OnSecondPair) DeliverPair(ssm_, 1U, 2'100'000'000LL, 2'000'000'000LL); // Second pair: master_ns goes backward → is_time_jump_past - auto result = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); // no Sync preceding this on new seqId + std::ignore = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); // no Sync preceding this on new seqId ssm_.OnSync(MakeSync(2U, 3'000'000'000LL)); auto r2 = ssm_.OnFollowUp(MakeFollowUp(2U, 1'000'000'000LL)); diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp index ea2cdd1..148f4d6 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -15,10 +15,11 @@ #include "score/TimeSlave/code/gptp/details/network_identity.h" #include "score/TimeSlave/code/gptp/details/raw_socket.h" -#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/TimeSlave/code/common/logging_contexts.h" #include "score/mw/log/logging.h" #include +#include namespace score { @@ -35,10 +36,8 @@ constexpr int kRxBufferSize = 2048; } // namespace -GptpEngine::GptpEngine(GptpEngineOptions opts, - std::unique_ptr local_clock) noexcept +GptpEngine::GptpEngine(GptpEngineOptions opts) noexcept : opts_{std::move(opts)}, - local_clock_{std::move(local_clock)}, socket_{std::make_unique()}, identity_{std::make_unique()}, codec_{}, @@ -49,11 +48,9 @@ GptpEngine::GptpEngine(GptpEngineOptions opts, } GptpEngine::GptpEngine(GptpEngineOptions opts, - std::unique_ptr local_clock, std::unique_ptr socket, std::unique_ptr identity) noexcept : opts_{std::move(opts)}, - local_clock_{std::move(local_clock)}, socket_{std::move(socket)}, identity_{std::move(identity)}, codec_{}, @@ -75,7 +72,7 @@ bool GptpEngine::Initialize() if (!identity_->Resolve(opts_.iface_name)) { - score::mw::log::LogError(score::td::kGPtpMachineContext) + score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to resolve ClockIdentity for " << opts_.iface_name; return false; } @@ -84,14 +81,14 @@ bool GptpEngine::Initialize() if (!socket_->Open(opts_.iface_name)) { - score::mw::log::LogError(score::td::kGPtpMachineContext) + score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to open raw socket on " << opts_.iface_name; return false; } if (!socket_->EnableHwTimestamping()) { - score::mw::log::LogWarn(score::td::kGPtpMachineContext) + score::mw::log::LogWarn(kGPtpMachineContext) << "GptpEngine: HW timestamping not available on " << opts_.iface_name << ", falling back to SW timestamps"; } @@ -99,11 +96,13 @@ bool GptpEngine::Initialize() try { + // std::thread constructor throws std::system_error if the OS cannot + // create a new thread (e.g. EAGAIN — thread limit reached). rx_thread_ = std::thread([this]() noexcept { RxLoop(); }); } catch (const std::system_error& e) { - score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create RxThread: " << e.what(); + score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to create RxThread: " << std::string_view{e.what()}; running_.store(false, std::memory_order_release); socket_->Close(); return false; @@ -115,12 +114,12 @@ bool GptpEngine::Initialize() } catch (const std::system_error& e) { - score::mw::log::LogError(score::td::kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread: " << e.what(); + score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread: " << std::string_view{e.what()}; Deinitialize(); return false; } - score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine initialized on " << opts_.iface_name; + score::mw::log::LogInfo(kGPtpMachineContext) << "GptpEngine initialized on " << opts_.iface_name; return true; } @@ -136,28 +135,36 @@ bool GptpEngine::Deinitialize() if (pdelay_thread_.joinable()) pdelay_thread_.join(); - score::mw::log::LogInfo(score::td::kGPtpMachineContext) << "GptpEngine deinitialized"; + score::mw::log::LogInfo(kGPtpMachineContext) << "GptpEngine deinitialized"; return true; } -bool GptpEngine::ReadPTPSnapshot(score::td::PtpTimeInfo& info) +void GptpEngine::FinalizeSnapshot() noexcept { if (!running_.load(std::memory_order_acquire)) - return false; + return; const std::int64_t mono_now = MonoNs(); const std::int64_t timeout_ns = static_cast(opts_.sync_timeout_ms) * 1'000'000LL; std::lock_guard lk(snapshot_mutex_); const bool timed_out = sync_sm_.IsTimeout(mono_now, timeout_ns); - snapshot_.local_time = local_clock_->Now(); + current_snapshot_ = pending_snapshot_; if (timed_out) { - snapshot_.status.is_synchronized = false; - snapshot_.status.is_timeout = true; - snapshot_.status.is_correct = false; + current_snapshot_.status.is_synchronized = false; + current_snapshot_.status.is_timeout = true; + current_snapshot_.status.is_correct = false; } - info = snapshot_; +} + +bool GptpEngine::ReadPTPSnapshot(score::ts::GptpIpcData& data) const noexcept +{ + if (!running_.load(std::memory_order_acquire)) + return false; + + std::lock_guard lk(snapshot_mutex_); + data = current_snapshot_; return true; } @@ -181,7 +188,7 @@ void GptpEngine::PdelayLoop() noexcept ::timespec next{}; if (::clock_gettime(CLOCK_MONOTONIC, &next) != 0) { - score::mw::log::LogError(score::td::kGPtpMachineContext) + score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: clock_gettime failed in PdelayLoop, thread exiting"; return; } @@ -292,18 +299,19 @@ void GptpEngine::UpdateSnapshot(const SyncResult& sync, const PDelayResult& pdel std::lock_guard lk(snapshot_mutex_); const std::int64_t local_rx_ns = static_cast(sync.sync_fup_data.reference_local_timestamp); - snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; - snapshot_.local_time = local_clock_->Now(); - snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); - - snapshot_.status.is_synchronized = true; - snapshot_.status.is_timeout = false; - snapshot_.status.is_time_jump_future = sync.is_time_jump_future; - snapshot_.status.is_time_jump_past = sync.is_time_jump_past; - snapshot_.status.is_correct = !sync.is_time_jump_future && !sync.is_time_jump_past; - - snapshot_.sync_fup_data = sync.sync_fup_data; - snapshot_.pdelay_data = pdelay.pdelay_data; + pending_snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; + // Capture local_time as close as possible to Sync frame handling to minimise jitter. + pending_snapshot_.local_time = std::chrono::nanoseconds{MonoNs()}; + pending_snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); + + pending_snapshot_.status.is_synchronized = true; + pending_snapshot_.status.is_timeout = false; + pending_snapshot_.status.is_time_jump_future = sync.is_time_jump_future; + pending_snapshot_.status.is_time_jump_past = sync.is_time_jump_past; + pending_snapshot_.status.is_correct = !sync.is_time_jump_future && !sync.is_time_jump_past; + + pending_snapshot_.sync_fup_data = sync.sync_fup_data; + pending_snapshot_.pdelay_data = pdelay.pdelay_data; } } // namespace details diff --git a/score/TimeSlave/code/gptp/gptp_engine.h b/score/TimeSlave/code/gptp/gptp_engine.h index d63f4a9..54ab678 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.h +++ b/score/TimeSlave/code/gptp/gptp_engine.h @@ -13,7 +13,7 @@ #ifndef SCORE_TIMESLAVE_CODE_GPTP_GPTP_ENGINE_H #define SCORE_TIMESLAVE_CODE_GPTP_GPTP_ENGINE_H -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_data.h" #include "score/TimeSlave/code/gptp/details/frame_codec.h" #include "score/TimeSlave/code/gptp/details/i_network_identity.h" #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" @@ -53,17 +53,21 @@ struct GptpEngineOptions * Runs two POSIX threads: RxThread (receive/parse PTP frames) and * PdelayThread (periodic Pdelay_Req transmission). * - * ReadPTPSnapshot() is thread-safe once Initialize() returns true. + * Dual-snapshot design: + * - pending_snapshot_: filled by the RxThread on every Sync+FollowUp + * - current_snapshot_: a committed, fully-flagged snapshot + * + * Callers should: + * 1. Call FinalizeSnapshot() to check timeout and commit pending to current. + * 2. Call ReadPTPSnapshot() (const) to retrieve the current snapshot. */ class GptpEngine final { public: - explicit GptpEngine(GptpEngineOptions opts, - std::unique_ptr local_clock) noexcept; + explicit GptpEngine(GptpEngineOptions opts) noexcept; /// Constructor for testing: inject fake socket and identity. GptpEngine(GptpEngineOptions opts, - std::unique_ptr local_clock, std::unique_ptr socket, std::unique_ptr identity) noexcept; @@ -83,9 +87,13 @@ class GptpEngine final /// @return true (always succeeds). bool Deinitialize(); - /// Copy the latest measurement snapshot into @p info. + /// Check for sync timeout, apply status flags, and commit pending_snapshot_ + /// to current_snapshot_. Must be called periodically before ReadPTPSnapshot(). + void FinalizeSnapshot() noexcept; + + /// Copy the latest committed snapshot into @p data. /// Non-blocking; returns false only if the engine is not initialized. - bool ReadPTPSnapshot(score::td::PtpTimeInfo& info); + bool ReadPTPSnapshot(score::ts::GptpIpcData& data) const noexcept; private: void RxLoop() noexcept; @@ -96,7 +104,6 @@ class GptpEngine final GptpEngineOptions opts_; - std::unique_ptr local_clock_; std::unique_ptr socket_; std::unique_ptr identity_; FrameCodec codec_; @@ -105,7 +112,8 @@ class GptpEngine final std::unique_ptr pdelay_; mutable std::mutex snapshot_mutex_; - score::td::PtpTimeInfo snapshot_{}; + score::ts::GptpIpcData pending_snapshot_{}; ///< Filled by RxThread on Sync+FollowUp + score::ts::GptpIpcData current_snapshot_{}; ///< Committed by FinalizeSnapshot() std::atomic running_{false}; std::thread rx_thread_; diff --git a/score/TimeSlave/code/gptp/gptp_engine_test.cpp b/score/TimeSlave/code/gptp/gptp_engine_test.cpp index 0f07c07..6579d5c 100644 --- a/score/TimeSlave/code/gptp/gptp_engine_test.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine_test.cpp @@ -13,7 +13,6 @@ #include "score/TimeSlave/code/gptp/gptp_engine.h" #include "score/TimeSlave/code/gptp/details/i_network_identity.h" #include "score/TimeSlave/code/gptp/details/i_raw_socket.h" -#include "score/time/HighPrecisionLocalSteadyClock/high_precision_local_steady_clock.h" #include @@ -37,17 +36,6 @@ namespace details namespace { -// ── FakeClock ───────────────────────────────────────────────────────────────── - -class FakeClock final : public score::time::HighPrecisionLocalSteadyClock -{ - public: - score::time::HighPrecisionLocalSteadyClock::time_point Now() noexcept override - { - return score::time::HighPrecisionLocalSteadyClock::time_point{std::chrono::nanoseconds{42'000'000'000LL}}; - } -}; - // ── FakeSocket ──────────────────────────────────────────────────────────────── class FakeSocket final : public IRawSocket @@ -264,9 +252,10 @@ bool WaitForSync(GptpEngine& eng, int max_ms = 500) { for (int i = 0; i < max_ms / 10; ++i) { - score::td::PtpTimeInfo info{}; - eng.ReadPTPSnapshot(info); - if (info.status.is_synchronized) + score::ts::GptpIpcData data{}; + eng.FinalizeSnapshot(); + eng.ReadPTPSnapshot(data); + if (data.status.is_synchronized) return true; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -281,7 +270,7 @@ class GptpEngineTest : public ::testing::Test protected: void SetUp() override { - engine_ = std::make_unique(FastOptions(), std::make_unique()); + engine_ = std::make_unique(FastOptions()); } void TearDown() override @@ -302,7 +291,7 @@ class GptpEngineFakeTest : public ::testing::Test auto identity = std::make_unique(); socket_raw_ = sock.get(); engine_ = std::make_unique( - FastOptions(), std::make_unique(), std::move(sock), std::move(identity)); + FastOptions(), std::move(sock), std::move(identity)); } void TearDown() override @@ -331,16 +320,16 @@ TEST_F(GptpEngineTest, Deinitialize_CalledTwice_BothReturnTrue) TEST_F(GptpEngineTest, ReadPTPSnapshot_WhenNotInitialized_ReturnsFalse) { - score::td::PtpTimeInfo info{}; - EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); + score::ts::GptpIpcData data{}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(data)); } TEST_F(GptpEngineTest, ReadPTPSnapshot_InfoUnchanged_WhenNotInitialized) { - score::td::PtpTimeInfo info{}; - info.ptp_assumed_time = std::chrono::nanoseconds{999LL}; - EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); - EXPECT_EQ(info.ptp_assumed_time, std::chrono::nanoseconds{999LL}); + score::ts::GptpIpcData data{}; + data.ptp_assumed_time = std::chrono::nanoseconds{999LL}; + EXPECT_FALSE(engine_->ReadPTPSnapshot(data)); + EXPECT_EQ(data.ptp_assumed_time, std::chrono::nanoseconds{999LL}); } // ── GptpEngineFakeTest — Initialize / Deinitialize ─────────────────────────── @@ -365,16 +354,18 @@ TEST_F(GptpEngineFakeTest, Deinitialize_AfterInitialize_ReturnsTrue) TEST_F(GptpEngineFakeTest, ReadPTPSnapshot_AfterInitialize_ReturnsTrue) { ASSERT_TRUE(engine_->Initialize()); - score::td::PtpTimeInfo info{}; - EXPECT_TRUE(engine_->ReadPTPSnapshot(info)); + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + EXPECT_TRUE(engine_->ReadPTPSnapshot(data)); } TEST_F(GptpEngineFakeTest, ReadPTPSnapshot_NotSynchronized_BeforeAnySync) { ASSERT_TRUE(engine_->Initialize()); - score::td::PtpTimeInfo info{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); - EXPECT_FALSE(info.status.is_synchronized); + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(data)); + EXPECT_FALSE(data.status.is_synchronized); } // ── GptpEngineFakeTest — identity failure ───────────────────────────────────── @@ -383,7 +374,7 @@ TEST(GptpEngineIdentityFailTest, Initialize_IdentityResolveFails_ReturnsFalse) { auto sock = std::make_unique(); auto identity = std::make_unique(/*resolve_ok=*/false); - GptpEngine eng{FastOptions(), std::make_unique(), std::move(sock), std::move(identity)}; + GptpEngine eng{FastOptions(), std::move(sock), std::move(identity)}; EXPECT_FALSE(eng.Initialize()); EXPECT_TRUE(eng.Deinitialize()); } @@ -410,10 +401,11 @@ TEST_F(GptpEngineFakeTest, HandlePacket_SyncFollowUp_SnapshotBecomesSync) socket_raw_->Push(MakeFollowUpFrame(1U, /*sec=*/2, /*ns=*/0)); EXPECT_TRUE(WaitForSync(*engine_)); - score::td::PtpTimeInfo info{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); - EXPECT_TRUE(info.status.is_synchronized); - EXPECT_FALSE(info.status.is_timeout); + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(data)); + EXPECT_TRUE(data.status.is_synchronized); + EXPECT_FALSE(data.status.is_timeout); } TEST_F(GptpEngineFakeTest, HandlePacket_MultipleSyncFup_SnapshotUpdated) @@ -472,7 +464,7 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) auto identity = std::make_unique(); FakeSocket* raw_sock = sock.get(); - GptpEngine eng{opts, std::make_unique(), std::move(sock), std::move(identity)}; + GptpEngine eng{opts, std::move(sock), std::move(identity)}; ASSERT_TRUE(eng.Initialize()); // First receive a Sync+FUP so the state machine records a timestamp. @@ -485,7 +477,8 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) bool got_sync = false; for (int i = 0; i < 50; ++i) { - score::td::PtpTimeInfo tmp{}; + score::ts::GptpIpcData tmp{}; + eng.FinalizeSnapshot(); eng.ReadPTPSnapshot(tmp); if (tmp.status.is_synchronized) { @@ -499,7 +492,8 @@ TEST(GptpEngineTimeoutTest, ReadPTPSnapshot_TimeoutPath_IsTimeoutSet) // Now wait longer than sync_timeout_ms for the timeout to trigger. std::this_thread::sleep_for(std::chrono::milliseconds(150)); - score::td::PtpTimeInfo info{}; + score::ts::GptpIpcData info{}; + eng.FinalizeSnapshot(); ASSERT_TRUE(eng.ReadPTPSnapshot(info)); EXPECT_TRUE(info.status.is_timeout); EXPECT_FALSE(info.status.is_synchronized); @@ -513,7 +507,7 @@ TEST(GptpEngineRealSocketTest, Initialize_NonExistentInterface_ReturnsFalse) GptpEngineOptions opts; opts.iface_name = "nonexistent_iface_xyz"; opts.pdelay_warmup_ms = 0; - GptpEngine eng{opts, std::make_unique()}; + GptpEngine eng{opts}; EXPECT_FALSE(eng.Initialize()); EXPECT_TRUE(eng.Deinitialize()); } @@ -525,7 +519,7 @@ TEST(GptpEngineSocketFailTest, Initialize_SocketOpenFails_ReturnsFalse) auto sock = std::make_unique(); auto identity = std::make_unique(); sock->SetOpenOk(false); - GptpEngine eng{FastOptions(), std::make_unique(), std::move(sock), std::move(identity)}; + GptpEngine eng{FastOptions(), std::move(sock), std::move(identity)}; EXPECT_FALSE(eng.Initialize()); EXPECT_TRUE(eng.Deinitialize()); } @@ -535,13 +529,14 @@ TEST(GptpEngineSocketFailTest, Initialize_SocketOpenFails_ReturnsFalse) namespace { -bool WaitForFlag(GptpEngine& eng, bool (*pred)(const score::td::PtpTimeInfo&), int max_ms = 1000) +bool WaitForFlag(GptpEngine& eng, bool (*pred)(const score::ts::GptpIpcData&), int max_ms = 1000) { for (int i = 0; i < max_ms / 10; ++i) { - score::td::PtpTimeInfo info{}; - eng.ReadPTPSnapshot(info); - if (pred(info)) + score::ts::GptpIpcData data{}; + eng.FinalizeSnapshot(); + eng.ReadPTPSnapshot(data); + if (pred(data)) return true; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -565,13 +560,14 @@ TEST_F(GptpEngineFakeTest, HandlePacket_TwoSyncFup_TimeJumpFuture_Detected) socket_raw_->Push(MakeFollowUpFrame(2U, /*sec=*/3U, /*ns=*/0U)); const bool got = - WaitForFlag(*engine_, [](const score::td::PtpTimeInfo& i) { return i.status.is_time_jump_future; }); + WaitForFlag(*engine_, [](const score::ts::GptpIpcData& d) { return d.status.is_time_jump_future; }); EXPECT_TRUE(got); - score::td::PtpTimeInfo info{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); - EXPECT_TRUE(info.status.is_time_jump_future); - EXPECT_FALSE(info.status.is_correct); + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(data)); + EXPECT_TRUE(data.status.is_time_jump_future); + EXPECT_FALSE(data.status.is_correct); } TEST_F(GptpEngineFakeTest, HandlePacket_TwoSyncFup_TimeJumpPast_Detected) @@ -589,13 +585,14 @@ TEST_F(GptpEngineFakeTest, HandlePacket_TwoSyncFup_TimeJumpPast_Detected) socket_raw_->Push(MakeFollowUpFrame(2U, /*sec=*/2U, /*ns=*/0U)); const bool got = - WaitForFlag(*engine_, [](const score::td::PtpTimeInfo& i) { return i.status.is_time_jump_past; }); + WaitForFlag(*engine_, [](const score::ts::GptpIpcData& d) { return d.status.is_time_jump_past; }); EXPECT_TRUE(got); - score::td::PtpTimeInfo info{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(info)); - EXPECT_TRUE(info.status.is_time_jump_past); - EXPECT_FALSE(info.status.is_correct); + engine_->FinalizeSnapshot(); + score::ts::GptpIpcData data{}; + ASSERT_TRUE(engine_->ReadPTPSnapshot(data)); + EXPECT_TRUE(data.status.is_time_jump_past); + EXPECT_FALSE(data.status.is_correct); } } // namespace details diff --git a/score/TimeSlave/code/gptp/instrument/BUILD b/score/TimeSlave/code/gptp/instrument/BUILD index 48ca897..0c63e06 100644 --- a/score/TimeSlave/code/gptp/instrument/BUILD +++ b/score/TimeSlave/code/gptp/instrument/BUILD @@ -22,7 +22,7 @@ cc_library( tags = ["QM"], visibility = ["//score:__subpackages__"], deps = [ - "//score/TimeDaemon/code/common:logging_contexts", + "//score/TimeSlave/code/common:logging_contexts", "//score/TimeSlave/code/gptp/record:recorder", "@score_baselibs//score/mw/log:frontend", ], diff --git a/score/TimeSlave/code/gptp/instrument/probe.cpp b/score/TimeSlave/code/gptp/instrument/probe.cpp index 3bc0e6b..70bb781 100644 --- a/score/TimeSlave/code/gptp/instrument/probe.cpp +++ b/score/TimeSlave/code/gptp/instrument/probe.cpp @@ -12,7 +12,7 @@ ********************************************************************************/ #include "score/TimeSlave/code/gptp/instrument/probe.h" -#include "score/TimeDaemon/code/common/logging_contexts.h" +#include "score/TimeSlave/code/common/logging_contexts.h" #include "score/mw/log/logging.h" #include @@ -32,7 +32,7 @@ ProbeManager& ProbeManager::Instance() void ProbeManager::Trace(ProbePoint point, const ProbeData& data) { - score::mw::log::LogDebug(score::td::kGPtpMachineContext) + score::mw::log::LogDebug(score::ts::kGPtpMachineContext) << "PROBE point=" << static_cast(point) << " ts=" << data.ts_mono_ns << " val=" << data.value_ns << " seq=" << data.seq_id; diff --git a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp index 44436fd..daee590 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp @@ -22,8 +22,11 @@ #include #include #include +#include #include +#include #include +#include #include // QNX SDP 8.0: PTP API constants (from io-sock/ptp.h, inlined to avoid @@ -219,7 +222,7 @@ extern "C" int qnx_raw_open(const char* ifname) return -1; } - std::strlcpy(g_qnx_ctx.iface_name, ifname, sizeof(g_qnx_ctx.iface_name)); + ::strlcpy(g_qnx_ctx.iface_name, ifname, sizeof(g_qnx_ctx.iface_name)); char devpath[256]{}; const char* sock_env = std::getenv("SOCK"); @@ -233,7 +236,7 @@ extern "C" int qnx_raw_open(const char* ifname) return -1; ::ifreq ifr{}; - std::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); if (::ioctl(fd, BIOCSETIF, &ifr) < 0) { ::close(fd); @@ -475,7 +478,7 @@ extern "C" int qnx_raw_send(int fd, const void* buf, int len, timespec* hwts) extern "C" int qnx_phc_open(const char* phc_dev) { if (phc_dev != nullptr && phc_dev[0] != '\0' && phc_dev[0] != '/') - std::strlcpy(g_qnx_ctx.iface_name, phc_dev, sizeof(g_qnx_ctx.iface_name)); + ::strlcpy(g_qnx_ctx.iface_name, phc_dev, sizeof(g_qnx_ctx.iface_name)); return 0; } diff --git a/score/libTSClient/BUILD b/score/libTSClient/BUILD index 1807bcc..445363d 100644 --- a/score/libTSClient/BUILD +++ b/score/libTSClient/BUILD @@ -23,21 +23,54 @@ cc_library( hdrs = [ "gptp_ipc.h", "gptp_ipc_channel.h", + "gptp_ipc_data.h", "gptp_ipc_publisher.h", "gptp_ipc_receiver.h", ], features = COMPILER_WARNING_FEATURES, - linkopts = ["-lrt"], + linkopts = select({ + "@platforms//os:qnx": [], + "//conditions:default": ["-lrt"], + }), tags = ["QM"], visibility = ["//score:__subpackages__"], + deps = [], +) + +cc_test( + name = "gptp_ipc_publisher_test", + srcs = [ + "gptp_ipc_publisher_test.cpp", + "gptp_ipc_test_utils.h", + ], + tags = ["unit"], deps = [ - "//score/TimeDaemon/code/common/data_types:ptp_time_info", + ":gptp_ipc", + "@googletest//:gtest", + "@googletest//:gtest_main", ], ) cc_test( - name = "gptp_ipc_test", - srcs = ["gptp_ipc_test.cpp"], + name = "gptp_ipc_receiver_test", + srcs = [ + "gptp_ipc_receiver_test.cpp", + "gptp_ipc_test_utils.h", + ], + tags = ["unit"], + deps = [ + ":gptp_ipc", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "gptp_ipc_roundtrip_test", + srcs = [ + "gptp_ipc_roundtrip_test.cpp", + "gptp_ipc_test_utils.h", + ], tags = ["unit"], deps = [ ":gptp_ipc", @@ -48,7 +81,11 @@ cc_test( cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", - cc_unit_tests = [":gptp_ipc_test"], + cc_unit_tests = [ + ":gptp_ipc_publisher_test", + ":gptp_ipc_receiver_test", + ":gptp_ipc_roundtrip_test", + ], test_suites_from_sub_packages = [], visibility = ["//score:__subpackages__"], ) diff --git a/score/libTSClient/gptp_ipc_channel.h b/score/libTSClient/gptp_ipc_channel.h index 999d2a1..428a42d 100644 --- a/score/libTSClient/gptp_ipc_channel.h +++ b/score/libTSClient/gptp_ipc_channel.h @@ -13,7 +13,7 @@ #ifndef SCORE_LIBTSCLIENT_GPTP_IPC_CHANNEL_H #define SCORE_LIBTSCLIENT_GPTP_IPC_CHANNEL_H -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" +#include "score/libTSClient/gptp_ipc_data.h" #include #include @@ -45,7 +45,7 @@ struct alignas(64) GptpIpcRegion { std::atomic magic{kGptpIpcMagic}; std::atomic seq{0}; - score::td::PtpTimeInfo data{}; + score::ts::GptpIpcData data{}; std::atomic seq_confirm{1}; }; diff --git a/score/libTSClient/gptp_ipc_data.h b/score/libTSClient/gptp_ipc_data.h new file mode 100644 index 0000000..3774193 --- /dev/null +++ b/score/libTSClient/gptp_ipc_data.h @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_DATA_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_DATA_H + +#include +#include + +namespace score +{ +namespace ts +{ + +/** + * @brief IPC-layer status flags transmitted from TimeSlave to TimeDaemon. + */ +struct GptpIpcStatus +{ + bool is_synchronized; + bool is_timeout; + bool is_time_jump_future; + bool is_time_jump_past; + bool is_correct; +}; + +/** + * @brief IPC-layer Sync+FollowUp measurement data. + */ +struct GptpIpcSyncFupData +{ + std::uint64_t precise_origin_timestamp; + std::uint64_t reference_global_timestamp; + std::uint64_t reference_local_timestamp; + std::uint64_t sync_ingress_timestamp; + std::uint64_t correction_field; + std::uint16_t sequence_id; + std::uint64_t pdelay; + std::uint32_t port_number; + std::uint64_t clock_identity; +}; + +/** + * @brief IPC-layer peer-delay measurement data. + */ +struct GptpIpcPDelayData +{ + std::uint64_t request_origin_timestamp; + std::uint64_t request_receipt_timestamp; + std::uint64_t response_origin_timestamp; + std::uint64_t response_receipt_timestamp; + std::uint64_t reference_global_timestamp; + std::uint64_t reference_local_timestamp; + std::uint16_t sequence_id; + std::uint64_t pdelay; + std::uint32_t req_port_number; + std::uint64_t req_clock_identity; + std::uint32_t resp_port_number; + std::uint64_t resp_clock_identity; +}; + +/** + * @brief IPC data snapshot written by TimeSlave and read by TimeDaemon. + * + * This type is internal to libTSClient and intentionally decoupled from + * score::td::PtpTimeInfo. Callers are responsible for mapping between the two. + */ +struct GptpIpcData +{ + std::chrono::nanoseconds ptp_assumed_time; + std::chrono::nanoseconds local_time; ///< Local monotonic time of the last Sync frame + double rate_deviation; + GptpIpcStatus status; + GptpIpcSyncFupData sync_fup_data; + GptpIpcPDelayData pdelay_data; +}; + +} // namespace ts +} // namespace score + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_DATA_H diff --git a/score/libTSClient/gptp_ipc_publisher.cpp b/score/libTSClient/gptp_ipc_publisher.cpp index 7b84f9c..3d153ce 100644 --- a/score/libTSClient/gptp_ipc_publisher.cpp +++ b/score/libTSClient/gptp_ipc_publisher.cpp @@ -18,8 +18,8 @@ #include #include -static_assert(std::is_trivially_copyable::value, - "PtpTimeInfo must be trivially copyable for seqlock memcpy to be valid"); +static_assert(std::is_trivially_copyable::value, + "GptpIpcData must be trivially copyable for seqlock memcpy to be valid"); namespace score { @@ -65,7 +65,7 @@ bool GptpIpcPublisher::Init(const std::string& ipc_name) return true; } -void GptpIpcPublisher::Publish(const score::td::PtpTimeInfo& info) +void GptpIpcPublisher::Publish(const score::ts::GptpIpcData& data) { if (region_ == nullptr) return; @@ -77,7 +77,7 @@ void GptpIpcPublisher::Publish(const score::td::PtpTimeInfo& info) // half of acq_rel is unnecessary for a seqlock writer; release suffices here. std::atomic_thread_fence(std::memory_order_release); - std::memcpy(®ion_->data, &info, sizeof(score::td::PtpTimeInfo)); + std::memcpy(®ion_->data, &data, sizeof(score::ts::GptpIpcData)); region_->seq_confirm.store(next + 1U, std::memory_order_release); region_->seq.store(next + 1U, std::memory_order_release); diff --git a/score/libTSClient/gptp_ipc_publisher.h b/score/libTSClient/gptp_ipc_publisher.h index b0f4509..3e20711 100644 --- a/score/libTSClient/gptp_ipc_publisher.h +++ b/score/libTSClient/gptp_ipc_publisher.h @@ -43,8 +43,8 @@ class GptpIpcPublisher final /// @return true on success. bool Init(const std::string& ipc_name = kGptpIpcName); - /// Publish a PtpTimeInfo snapshot using seqlock. - void Publish(const score::td::PtpTimeInfo& info); + /// Publish a GptpIpcData snapshot using seqlock. + void Publish(const score::ts::GptpIpcData& data); /// Unmap and unlink the shared memory segment. void Destroy(); diff --git a/score/libTSClient/gptp_ipc_publisher_test.cpp b/score/libTSClient/gptp_ipc_publisher_test.cpp new file mode 100644 index 0000000..43a106a --- /dev/null +++ b/score/libTSClient/gptp_ipc_publisher_test.cpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_test_utils.h" + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +class GptpIpcPublisherTest : public ::testing::Test +{ + protected: + void TearDown() override + { + pub_.Destroy(); + } + + GptpIpcPublisher pub_; +}; + +TEST_F(GptpIpcPublisherTest, Init_ValidName_ReturnsTrue) +{ + EXPECT_TRUE(pub_.Init(UniqueShmName())); +} + +TEST_F(GptpIpcPublisherTest, Publish_WithoutInit_DoesNotCrash) +{ + score::ts::GptpIpcData data{}; + EXPECT_NO_THROW(pub_.Publish(data)); +} + +TEST_F(GptpIpcPublisherTest, Destroy_CalledTwice_DoesNotCrash) +{ + ASSERT_TRUE(pub_.Init(UniqueShmName())); + pub_.Destroy(); + EXPECT_NO_THROW(pub_.Destroy()); +} + +TEST_F(GptpIpcPublisherTest, Destroy_WithoutInit_DoesNotCrash) +{ + EXPECT_NO_THROW(pub_.Destroy()); +} + +TEST_F(GptpIpcPublisherTest, Init_CalledTwice_ReturnsTrueOnSecondCall) +{ + // region_ != nullptr after first Init → second call returns true immediately. + ASSERT_TRUE(pub_.Init(UniqueShmName())); + EXPECT_TRUE(pub_.Init(UniqueShmName())); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/gptp_ipc_receiver.cpp b/score/libTSClient/gptp_ipc_receiver.cpp index 8d5e1c1..710e054 100644 --- a/score/libTSClient/gptp_ipc_receiver.cpp +++ b/score/libTSClient/gptp_ipc_receiver.cpp @@ -70,7 +70,7 @@ bool GptpIpcReceiver::Init(const std::string& ipc_name) return true; } -std::optional GptpIpcReceiver::Receive() +std::optional GptpIpcReceiver::Receive() { if (region_ == nullptr) return std::nullopt; @@ -82,8 +82,8 @@ std::optional GptpIpcReceiver::Receive() if ((seq1 & 1U) != 0U) continue; // write in progress, retry - score::td::PtpTimeInfo data{}; - std::memcpy(&data, ®ion_->data, sizeof(score::td::PtpTimeInfo)); + score::ts::GptpIpcData data{}; + std::memcpy(&data, ®ion_->data, sizeof(score::ts::GptpIpcData)); // acq_rel fence: prevents data reads from floating past the consistency checks below // (release half prevents memcpy reordering after the fence on ARM64), and prevents diff --git a/score/libTSClient/gptp_ipc_receiver.h b/score/libTSClient/gptp_ipc_receiver.h index 4d4dc49..4b54d01 100644 --- a/score/libTSClient/gptp_ipc_receiver.h +++ b/score/libTSClient/gptp_ipc_receiver.h @@ -44,9 +44,9 @@ class GptpIpcReceiver final /// @return true on success. bool Init(const std::string& ipc_name = kGptpIpcName); - /// Read a PtpTimeInfo snapshot using seqlock (up to 20 retries). + /// Read a GptpIpcData snapshot using seqlock (up to 20 retries). /// @return The data if consistent, or std::nullopt on contention failure. - std::optional Receive(); + std::optional Receive(); /// Unmap the shared memory segment. void Close(); diff --git a/score/libTSClient/gptp_ipc_receiver_test.cpp b/score/libTSClient/gptp_ipc_receiver_test.cpp new file mode 100644 index 0000000..694d378 --- /dev/null +++ b/score/libTSClient/gptp_ipc_receiver_test.cpp @@ -0,0 +1,89 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_receiver.h" +#include "score/libTSClient/gptp_ipc_test_utils.h" + +#include + +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +class GptpIpcReceiverTest : public ::testing::Test +{ + protected: + void TearDown() override + { + rx_.Close(); + } + + GptpIpcReceiver rx_; +}; + +TEST_F(GptpIpcReceiverTest, Init_ShmNotExist_ReturnsFalse) +{ + EXPECT_FALSE(rx_.Init("/gptp_nonexistent_" + std::to_string(::getpid()))); +} + +TEST_F(GptpIpcReceiverTest, Close_WithoutInit_DoesNotCrash) +{ + EXPECT_NO_THROW(rx_.Close()); +} + +TEST_F(GptpIpcReceiverTest, Close_CalledTwice_DoesNotCrash) +{ + EXPECT_NO_THROW(rx_.Close()); + EXPECT_NO_THROW(rx_.Close()); +} + +TEST_F(GptpIpcReceiverTest, Receive_WithoutInit_ReturnsNullopt) +{ + EXPECT_FALSE(rx_.Receive().has_value()); +} + +TEST_F(GptpIpcReceiverTest, Init_CalledTwice_ReturnsTrueOnSecondCall) +{ + // region_ != nullptr after first Init → second call returns true immediately. + GptpIpcPublisher pub; + const std::string name = UniqueShmName(); + ASSERT_TRUE(pub.Init(name)); + ASSERT_TRUE(rx_.Init(name)); + EXPECT_TRUE(rx_.Init(name)); + pub.Destroy(); +} + +TEST_F(GptpIpcReceiverTest, Init_TooSmallShm_ReturnsFalse) +{ + // Create a shm segment smaller than GptpIpcRegion so the fstat size check fails. + const std::string name = UniqueShmName(); + const int fd = ::shm_open(name.c_str(), O_CREAT | O_RDWR, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(::ftruncate(fd, 1), 0); + ::close(fd); + + EXPECT_FALSE(rx_.Init(name)); + + ::shm_unlink(name.c_str()); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/gptp_ipc_roundtrip_test.cpp b/score/libTSClient/gptp_ipc_roundtrip_test.cpp new file mode 100644 index 0000000..37508eb --- /dev/null +++ b/score/libTSClient/gptp_ipc_roundtrip_test.cpp @@ -0,0 +1,218 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_receiver.h" +#include "score/libTSClient/gptp_ipc_test_utils.h" + +#include + +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +class GptpIpcRoundtripTest : public ::testing::Test +{ + protected: + void SetUp() override + { + name_ = UniqueShmName(); + } + void TearDown() override + { + rx_.Close(); + pub_.Destroy(); + } + + std::string name_; + GptpIpcPublisher pub_; + GptpIpcReceiver rx_; +}; + +TEST_F(GptpIpcRoundtripTest, ReceiverInit_AfterPublisherInit_ReturnsTrue) +{ + ASSERT_TRUE(pub_.Init(name_)); + EXPECT_TRUE(rx_.Init(name_)); +} + +TEST_F(GptpIpcRoundtripTest, ReceiverReceive_BeforeAnyPublish_ReturnsNullopt) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + // seq_confirm is initialised to 1 (≠ seq=0) by GptpIpcRegion's constructor, + // so the seqlock always mismatches before the first Publish() call. + EXPECT_FALSE(rx_.Receive().has_value()); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_BasicFields_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::ts::GptpIpcData data{}; + data.ptp_assumed_time = std::chrono::nanoseconds{1'234'567'890LL}; + data.rate_deviation = 0.75; + data.status.is_synchronized = true; + data.status.is_correct = true; + + pub_.Publish(data); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->ptp_assumed_time, data.ptp_assumed_time); + EXPECT_DOUBLE_EQ(result->rate_deviation, data.rate_deviation); + EXPECT_TRUE(result->status.is_synchronized); + EXPECT_TRUE(result->status.is_correct); + EXPECT_FALSE(result->status.is_timeout); + EXPECT_FALSE(result->status.is_time_jump_future); + EXPECT_FALSE(result->status.is_time_jump_past); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_StatusFlags_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::ts::GptpIpcData data{}; + data.status.is_timeout = true; + data.status.is_time_jump_future = true; + data.status.is_synchronized = false; + data.status.is_correct = false; + + pub_.Publish(data); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result->status.is_timeout); + EXPECT_TRUE(result->status.is_time_jump_future); + EXPECT_FALSE(result->status.is_time_jump_past); + EXPECT_FALSE(result->status.is_synchronized); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_SyncFupData_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::ts::GptpIpcData data{}; + data.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; + data.sync_fup_data.reference_global_timestamp = 100'000'001'000ULL; + data.sync_fup_data.reference_local_timestamp = 100'000'001'500ULL; + data.sync_fup_data.sync_ingress_timestamp = 100'000'001'500ULL; + data.sync_fup_data.correction_field = 42U; + data.sync_fup_data.sequence_id = 77; + data.sync_fup_data.pdelay = 3'000U; + data.sync_fup_data.port_number = 1; + data.sync_fup_data.clock_identity = 0xAABBCCDDEEFF0011ULL; + + pub_.Publish(data); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->sync_fup_data.precise_origin_timestamp, 100'000'000'000ULL); + EXPECT_EQ(result->sync_fup_data.reference_global_timestamp, 100'000'001'000ULL); + EXPECT_EQ(result->sync_fup_data.sequence_id, 77); + EXPECT_EQ(result->sync_fup_data.pdelay, 3'000U); + EXPECT_EQ(result->sync_fup_data.clock_identity, 0xAABBCCDDEEFF0011ULL); +} + +TEST_F(GptpIpcRoundtripTest, PublishReceive_PDelayData_RoundtripCorrectly) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + score::ts::GptpIpcData data{}; + data.pdelay_data.request_origin_timestamp = 1'000'000'000ULL; + data.pdelay_data.request_receipt_timestamp = 1'000'001'000ULL; + data.pdelay_data.response_origin_timestamp = 1'000'001'000ULL; + data.pdelay_data.response_receipt_timestamp = 1'000'002'000ULL; + data.pdelay_data.pdelay = 1'000U; + data.pdelay_data.req_port_number = 1; + data.pdelay_data.resp_port_number = 2; + data.pdelay_data.req_clock_identity = 0x1122334455667788ULL; + + pub_.Publish(data); + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->pdelay_data.request_origin_timestamp, 1'000'000'000ULL); + EXPECT_EQ(result->pdelay_data.pdelay, 1'000U); + EXPECT_EQ(result->pdelay_data.req_port_number, 1); + EXPECT_EQ(result->pdelay_data.resp_port_number, 2); + EXPECT_EQ(result->pdelay_data.req_clock_identity, 0x1122334455667788ULL); +} + +TEST_F(GptpIpcRoundtripTest, MultiplePublish_LastValueIsVisible) +{ + ASSERT_TRUE(pub_.Init(name_)); + ASSERT_TRUE(rx_.Init(name_)); + + for (int i = 1; i <= 5; ++i) + { + score::ts::GptpIpcData data{}; + data.ptp_assumed_time = std::chrono::nanoseconds{static_cast(i) * 1'000'000'000LL}; + pub_.Publish(data); + } + + const auto result = rx_.Receive(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->ptp_assumed_time, std::chrono::nanoseconds{5'000'000'000LL}); +} + +// ── Edge cases via ManualShm ────────────────────────────────────────────────── + +TEST_F(GptpIpcRoundtripTest, ReceiverInit_WrongMagic_ReturnsFalse) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + new (shm.Region()) GptpIpcRegion{}; + const std::uint32_t bad = 0xDEADBEEFU; + std::memcpy(shm.ptr, &bad, sizeof(bad)); + + EXPECT_FALSE(rx_.Init(name_)); +} + +TEST_F(GptpIpcRoundtripTest, Receive_PersistentOddSeq_ExhaustsRetriesAndReturnsNullopt) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + auto* region = new (shm.Region()) GptpIpcRegion{}; + region->seq.store(1U, std::memory_order_relaxed); + region->seq_confirm.store(0U, std::memory_order_relaxed); + + ASSERT_TRUE(rx_.Init(name_)); + EXPECT_FALSE(rx_.Receive().has_value()); +} + +TEST_F(GptpIpcRoundtripTest, Receive_SeqConfirmMismatch_ExhaustsRetriesAndReturnsNullopt) +{ + ManualShm shm{name_}; + ASSERT_TRUE(shm.Valid()); + + auto* region = new (shm.Region()) GptpIpcRegion{}; + region->seq.store(4U, std::memory_order_relaxed); + region->seq_confirm.store(2U, std::memory_order_relaxed); + + ASSERT_TRUE(rx_.Init(name_)); + EXPECT_FALSE(rx_.Receive().has_value()); +} + +} // namespace details +} // namespace ts +} // namespace score diff --git a/score/libTSClient/gptp_ipc_test_utils.h b/score/libTSClient/gptp_ipc_test_utils.h new file mode 100644 index 0000000..d838dee --- /dev/null +++ b/score/libTSClient/gptp_ipc_test_utils.h @@ -0,0 +1,82 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIBTSCLIENT_GPTP_IPC_TEST_UTILS_H +#define SCORE_LIBTSCLIENT_GPTP_IPC_TEST_UTILS_H + +#include "score/libTSClient/gptp_ipc_channel.h" + +#include +#include +#include +#include +#include + +namespace score +{ +namespace ts +{ +namespace details +{ + +/// Generate a unique POSIX shm name per invocation (avoids cross-test pollution). +inline std::string UniqueShmName() +{ + static std::atomic counter{0}; + return "/gptp_ipc_ut_" + std::to_string(::getpid()) + "_" + + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} + +/// RAII helper: creates shm manually (without GptpIpcPublisher) for edge-case +/// testing; cleans up in destructor. +struct ManualShm +{ + std::string name; + void* ptr = MAP_FAILED; + std::size_t size = sizeof(GptpIpcRegion); + + explicit ManualShm(const std::string& n) : name{n} + { + const int fd = ::shm_open(name.c_str(), O_CREAT | O_RDWR, 0666); + if (fd < 0) + return; + if (::ftruncate(fd, static_cast(size)) != 0) + { + ::close(fd); + return; + } + ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ::close(fd); + } + + ~ManualShm() + { + if (ptr != MAP_FAILED) + ::munmap(ptr, size); + ::shm_unlink(name.c_str()); + } + + bool Valid() const + { + return ptr != MAP_FAILED; + } + GptpIpcRegion* Region() + { + return static_cast(ptr); + } +}; + +} // namespace details +} // namespace ts +} // namespace score + +#endif // SCORE_LIBTSCLIENT_GPTP_IPC_TEST_UTILS_H From 40ede65b4476495ba69d898d3e9cef5a48bb2071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Fri, 10 Apr 2026 15:01:33 +0800 Subject: [PATCH 08/12] [ECARX][TimeSlave]fix bazel build failed --- .../code/ptp_machine/real/gptp_real_machine_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp index 705c40c..9511fe6 100644 --- a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp @@ -36,9 +36,9 @@ std::string UniqueShmName() std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); } -score::td::PtpTimeInfo MakePublishedInfo() +score::ts::GptpIpcData MakePublishedInfo() { - score::td::PtpTimeInfo info{}; + score::ts::GptpIpcData info{}; info.ptp_assumed_time = std::chrono::nanoseconds{5'000'000'000LL}; info.rate_deviation = 0.5; info.status.is_synchronized = true; From 77f241c6c455dd1e649650c195b9e82cf46d528f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Mon, 20 Apr 2026 17:07:52 +0800 Subject: [PATCH 09/12] [ECARX][TimeSlave]Fix the PHC clock adjust issue --- docs/TimeSlave/index.rst | 11 +- score/TimeSlave/code/gptp/BUILD | 1 + .../code/gptp/details/pdelay_measurer.cpp | 4 +- score/TimeSlave/code/gptp/gptp_engine.cpp | 58 +- score/TimeSlave/code/gptp/gptp_engine.h | 5 +- .../code/gptp/platform/qnx/qnx_raw_shim.cpp | 585 ++++++++++-------- 6 files changed, 379 insertions(+), 285 deletions(-) diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 1696ff4..75af0ec 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -178,7 +178,7 @@ The ``GptpEngine`` has the following requirements: - The ``GptpEngine`` shall manage a PdelayThread for periodic peer delay measurement - The ``GptpEngine`` shall provide a ``FinalizeSnapshot()`` method that checks for sync timeout, applies status flags, and commits the pending snapshot to the current snapshot; this must be called before ``ReadPTPSnapshot()`` - The ``GptpEngine`` shall provide a ``ReadPTPSnapshot(GptpIpcData&)`` method that copies the latest committed snapshot into the caller's buffer and returns false only if the engine is not initialized -- The ``GptpEngine`` shall support configurable parameters via ``GptpEngineOptions`` (interface name, PDelay interval, PDelay warmup, sync timeout, time-jump threshold) +- The ``GptpEngine`` shall support configurable parameters via ``GptpEngineOptions`` (interface name, PDelay interval, PDelay warmup, sync timeout, time-jump threshold, PHC configuration) - The ``GptpEngine`` shall support exchangeability of the raw socket implementation for different platforms (Linux, QNX) Class view @@ -458,7 +458,7 @@ TimeSlave supports two target platforms with platform-specific implementations s - QNX * - Raw Socket - ``AF_PACKET`` + ``SO_TIMESTAMPING``; HW RX timestamp via ``recvmsg`` ``SCM_TIMESTAMPING`` - - BPF (``/dev/bpf``); HW RX timestamp via ``bpf_xhdr.bh_tstamp`` (``BIOCSTSTAMP BPF_T_PTP|BPF_T_BINTIME``); TX timestamp via ``BIOCGTSTAMPID`` + loopback fd (``BIOCSSEESENT``); fallback to ``CLOCK_REALTIME`` + - BPF (``/dev/bpf``); HW RX timestamp via ``bpf_xhdr.bh_tstamp`` (``BIOCSTSTAMP BPF_T_BINTIME|BPF_T_PTP``); TX PHC timestamp via dedicated TX loopback fd (``BIOCSSEESENT``), filtered to Pdelay_Req frames only (BPF message-type 0x02); single static context (not thread-local) * - Network Identity - ``ioctl(SIOCGIFHWADDR)`` → EUI-48 → EUI-64 - ``getifaddrs()`` + ``AF_LINK`` / ``sockaddr_dl`` (``LLADDR``) → EUI-48/64 @@ -609,7 +609,7 @@ The ``GptpEngineOptions`` struct provides all configurable parameters for the gP - Description * - ``iface_name`` - string - - Network interface for gPTP frames (e.g., ``eth0``); default: ``"eth0"`` + - Network interface for gPTP frames (e.g., ``emac0``); default: ``"emac0"`` * - ``pdelay_interval_ms`` - int - Interval between PDelayReq transmissions (ms); default: ``1000`` @@ -622,8 +622,11 @@ The ``GptpEngineOptions`` struct provides all configurable parameters for the gP * - ``jump_future_threshold_ns`` - int64_t - Threshold above which a positive clock offset is flagged as a forward time jump (ns); default: ``500 000 000`` + * - ``phc_config`` + - PhcConfig + - PHC hardware clock adjustment settings (see ``PhcConfig`` table below); disabled by default -The ``PhcConfig`` struct (used by ``PhcAdjuster``, configured independently) contains: +The ``PhcConfig`` struct (embedded in ``GptpEngineOptions``) contains: .. list-table:: PhcAdjuster Configuration :header-rows: 1 diff --git a/score/TimeSlave/code/gptp/BUILD b/score/TimeSlave/code/gptp/BUILD index a215d22..9a19ed8 100644 --- a/score/TimeSlave/code/gptp/BUILD +++ b/score/TimeSlave/code/gptp/BUILD @@ -31,6 +31,7 @@ cc_library( deps = [ "//score/TimeSlave/code/common:logging_contexts", "//score/TimeSlave/code/gptp/details:gptp_details", + "//score/TimeSlave/code/gptp/phc:phc_adjuster", "//score/libTSClient:gptp_ipc", "@score_baselibs//score/mw/log:frontend", ], diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp index b755447..ae93a9f 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -123,7 +123,7 @@ void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept sizeof(PortIdentity)) != 0) return; - // t1 = HW send timestamp of our Pdelay_Req + // t1 = BPF_T_BINTIME (PHC) send timestamp of our Pdelay_Req (TX loopback fd) const TmvT t1 = req_.sendHardwareTS; // t2 = remote receipt time (from Pdelay_Resp body: requestReceiptTimestamp) const TmvT t2 = resp_.parseMessageTs; @@ -132,7 +132,7 @@ void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept const TmvT c1 = CorrectionToTmv(resp_.ptpHdr.correctionField); const TmvT c2 = CorrectionToTmv(resp_fup_.ptpHdr.correctionField); const TmvT t3c = TmvT{t3.ns + c1.ns + c2.ns}; - // t4 = local HW receive timestamp of Pdelay_Resp + // t4 = BPF_T_BINTIME (PHC) receive timestamp of Pdelay_Resp (main BPF fd) const TmvT t4 = resp_.recvHardwareTS; const std::int64_t delay = ((t2.ns - t1.ns) + (t4.ns - t3c.ns)) / 2LL; diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp index 148f4d6..915e18b 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -43,7 +43,8 @@ GptpEngine::GptpEngine(GptpEngineOptions opts) noexcept codec_{}, parser_{}, sync_sm_{opts_.jump_future_threshold_ns}, - pdelay_{nullptr} + pdelay_{nullptr}, + phc_{opts_.phc_config} { } @@ -56,7 +57,8 @@ GptpEngine::GptpEngine(GptpEngineOptions opts, codec_{}, parser_{}, sync_sm_{opts_.jump_future_threshold_ns}, - pdelay_{nullptr} + pdelay_{nullptr}, + phc_{opts_.phc_config} { } @@ -296,22 +298,48 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp void GptpEngine::UpdateSnapshot(const SyncResult& sync, const PDelayResult& pdelay) noexcept { - std::lock_guard lk(snapshot_mutex_); + const double rate_ratio = sync_sm_.GetNeighborRateRatio(); - const std::int64_t local_rx_ns = static_cast(sync.sync_fup_data.reference_local_timestamp); - pending_snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; - // Capture local_time as close as possible to Sync frame handling to minimise jitter. - pending_snapshot_.local_time = std::chrono::nanoseconds{MonoNs()}; - pending_snapshot_.rate_deviation = sync_sm_.GetNeighborRateRatio(); + { + std::lock_guard lk(snapshot_mutex_); + + const std::int64_t local_rx_ns = static_cast(sync.sync_fup_data.reference_local_timestamp); + pending_snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; + // Capture local_time as close as possible to Sync frame handling to minimise jitter. + pending_snapshot_.local_time = std::chrono::nanoseconds{MonoNs()}; + pending_snapshot_.rate_deviation = rate_ratio; + + pending_snapshot_.status.is_synchronized = true; + pending_snapshot_.status.is_timeout = false; + pending_snapshot_.status.is_time_jump_future = sync.is_time_jump_future; + pending_snapshot_.status.is_time_jump_past = sync.is_time_jump_past; + pending_snapshot_.status.is_correct = !sync.is_time_jump_future && !sync.is_time_jump_past; + + pending_snapshot_.sync_fup_data = sync.sync_fup_data; + pending_snapshot_.pdelay_data = pdelay.pdelay_data; + } + + if (phc_.IsEnabled()) + { + const bool is_step = + (sync.offset_ns >= opts_.phc_config.step_threshold_ns) || + (sync.offset_ns <= -opts_.phc_config.step_threshold_ns); - pending_snapshot_.status.is_synchronized = true; - pending_snapshot_.status.is_timeout = false; - pending_snapshot_.status.is_time_jump_future = sync.is_time_jump_future; - pending_snapshot_.status.is_time_jump_past = sync.is_time_jump_past; - pending_snapshot_.status.is_correct = !sync.is_time_jump_future && !sync.is_time_jump_past; + phc_.AdjustOffset(sync.offset_ns); + phc_.AdjustFrequency(rate_ratio); - pending_snapshot_.sync_fup_data = sync.sync_fup_data; - pending_snapshot_.pdelay_data = pdelay.pdelay_data; + if (is_step) + { + score::mw::log::LogInfo(kGPtpMachineContext) + << "PHC step applied: offset=" << sync.offset_ns << " ns"; + } + else + { + score::mw::log::LogInfo(kGPtpMachineContext) + << "PHC slew: offset=" << sync.offset_ns << " ns" + << " rate_ratio=" << rate_ratio; + } + } } } // namespace details diff --git a/score/TimeSlave/code/gptp/gptp_engine.h b/score/TimeSlave/code/gptp/gptp_engine.h index 54ab678..c30e2f2 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.h +++ b/score/TimeSlave/code/gptp/gptp_engine.h @@ -21,6 +21,7 @@ #include "score/TimeSlave/code/gptp/details/pdelay_measurer.h" #include "score/TimeSlave/code/gptp/details/ptp_types.h" #include "score/TimeSlave/code/gptp/details/sync_state_machine.h" +#include "score/TimeSlave/code/gptp/phc/phc_adjuster.h" #include #include @@ -40,11 +41,12 @@ namespace details /// Configuration for GptpEngine. struct GptpEngineOptions { - std::string iface_name = "eth0"; ///< Network interface for gPTP + std::string iface_name = "emac0"; ///< Network interface for gPTP int pdelay_interval_ms = 1000; ///< Period between Pdelay_Req transmissions (ms) int pdelay_warmup_ms = 2000; ///< Delay before first Pdelay_Req (ms) int sync_timeout_ms = 3300; ///< Declare timeout after this many ms without Sync std::int64_t jump_future_threshold_ns = 500'000'000LL; ///< 500 ms + PhcConfig phc_config{}; ///< PHC hardware clock adjustment (disabled by default) }; /** @@ -110,6 +112,7 @@ class GptpEngine final GptpMessageParser parser_; SyncStateMachine sync_sm_; std::unique_ptr pdelay_; + PhcAdjuster phc_; mutable std::mutex snapshot_mutex_; score::ts::GptpIpcData pending_snapshot_{}; ///< Filled by RxThread on Sync+FollowUp diff --git a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp index daee590..6110cb3 100644 --- a/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp +++ b/score/TimeSlave/code/gptp/platform/qnx/qnx_raw_shim.cpp @@ -10,44 +10,46 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ - -// QNX BPF-based raw socket shim for gPTP frame capture and transmission. -// Provides qnx_raw_open / qnx_raw_recv / qnx_raw_send / qnx_phc_* symbols -// declared in raw_socket.cpp (extern "C"). - #include #include #include #include #include +#include #include #include #include +#include #include #include #include #include #include -// QNX SDP 8.0: PTP API constants (from io-sock/ptp.h, inlined to avoid -// struct PortIdentity redefinition conflict with details/ptp_types.h). #define PTP_GET_TIME 0x102 #define PTP_SET_TIME 0x103 +// EMAC_PTP_ADJ_FREQ_PPM: Qualcomm BSP (hw/iosock/emac_ioctl.h), ptp_ppm_t = int +// Positive ppm = speed up PHC, negative = slow down. +static constexpr unsigned long kEmacPtpAdjFreqPpm = 52UL; struct ptp_time { int64_t sec; int32_t nsec; }; -// Inlined ptp_tstmp (from io-sock/ptp.h) — avoids PortIdentity name collision. -// A TX loopback frame contains an Ethernet header followed by this struct. -struct PtpTstmp +struct ptp_tstmp { - uint32_t uid; - ptp_time time; + struct + { + std::int64_t sec; // EMAC PHC TX hardware timestamp seconds, offset 0 + std::int32_t nsec; // EMAC PHC TX hardware timestamp nanoseconds, offset 8 + // implicit 4-byte trailing pad: sizeof(this struct) = 16 + } time; + std::uint32_t uid; // per-TX frame matching uid (BIOCGTSTAMPID), offset 16 + // implicit 4-byte trailing pad: sizeof(ptp_tstmp) = 24 }; -// ── EtherType constants ─────────────────────────────────────────────────────── + #ifndef ETH_P_8021Q #define ETH_P_8021Q 0x8100U #endif @@ -55,120 +57,134 @@ struct PtpTstmp #define ETH_P_1588 0x88F7U #endif -// ── Self-contained ethernet header layout ──────────────────────────────────── struct GptpEthHdr { unsigned char h_dest[6]; unsigned char h_source[6]; - uint16_t h_proto; + uint16_t h_proto; }; -static constexpr int64_t kNsPerSec = 1'000'000'000LL; +static constexpr int64_t kNsPerSec = 1'000'000'000LL; static constexpr std::size_t kMaxBpfBufSz = 65536U; -static constexpr int kMaxTxScanTries = 8; - -// Caplen of a BPF TX loopback frame injected by the PTP driver: -// Ethernet header (14 B) + ptp_tstmp payload (4 + 12 = 16 B) = 30 B -static constexpr int kTxLoopbackCaplen = static_cast(sizeof(GptpEthHdr) + sizeof(PtpTstmp)); - -// ── BPF kernel filter: pass only IEEE 802.1AS (ETH_P_1588) frames ──────────── -// BPF_LD H ABS 12 — load EtherType (bytes 12-13) -// BPF_JEQ ETH_P_1588 — jump if match -// BPF_RET (u_int)-1 — keep entire packet -// BPF_RET 0 — drop -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + +// PHC frequency adjustment state (PI controller). +// g_skip_freq_after_step: skip N cycles after a step correction so the +// clock can stabilise before re-applying rate-ratio based slewing. +// g_smoothed_comp_ppb: P term — EMA of raw_ppb (α=0.2), fast convergence. +// g_integral_ppb: I term — slow integrator of P, eliminates the E/2 +// steady-state error that a pure P/EMA controller leaves behind. +static int g_skip_freq_after_step = 0; +static double g_smoothed_comp_ppb = 0.0; // P term: EMA of raw_ppb (ppb) +static double g_integral_ppb = 0.0; // I term: integrator (ppb) + +static_assert(sizeof(ptp_tstmp) == 24U, "ptp_tstmp: time{sec:8+nsec:4+pad:4}=16 + uid:4 + pad:4 = 24"); +static constexpr int kTxLoopbackCaplen = static_cast(sizeof(GptpEthHdr) + sizeof(ptp_tstmp)); + static struct bpf_insn kPtp1588FilterInsns[] = { BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETH_P_1588, 0, 1), BPF_STMT(BPF_RET + BPF_K, static_cast(-1)), BPF_STMT(BPF_RET + BPF_K, 0), }; -static const u_int kPtp1588FilterLen = static_cast(sizeof(kPtp1588FilterInsns) / sizeof(kPtp1588FilterInsns[0])); +static const u_int kPtp1588FilterLen = + static_cast(sizeof(kPtp1588FilterInsns) / sizeof(kPtp1588FilterInsns[0])); + +static struct bpf_insn kPdelayReqFilterInsns[] = { + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), // load EtherType + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETH_P_1588, 0, 4), // != 0x88F7 → FAIL + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 14), // load PTP tsmt byte + BPF_STMT(BPF_ALU + BPF_AND + BPF_K, 0x0FU), // mask message type + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x02U, 0, 1), // != Pdelay_Req → FAIL + BPF_STMT(BPF_RET + BPF_K, static_cast(-1)), // PASS + BPF_STMT(BPF_RET + BPF_K, 0U), // FAIL +}; +static const u_int kPdelayReqFilterLen = + static_cast(sizeof(kPdelayReqFilterInsns) / sizeof(kPdelayReqFilterInsns[0])); -// ── Per-thread BPF context ─────────────────────────────────────────────────── struct QnxRawContext { - int bpf_fd = -1; - u_int bpf_buflen = 0; - char iface_name[IFNAMSIZ]{}; + int bpf_fd = -1; + u_int bpf_buflen = 0; + char iface_name[IFNAMSIZ]{}; unsigned char bpf_buf[kMaxBpfBufSz]{}; - ssize_t bpf_n = 0; - ssize_t bpf_off = 0; - bool initialized = false; + ssize_t bpf_n = 0; + ssize_t bpf_off = 0; + bool initialized = false; unsigned char tx_frame[ETHER_HDR_LEN + 1500]{}; - // Secondary BPF fd with BIOCSSEESENT=1 for reading TX loopback timestamps. - // Lazily opened on first qnx_raw_send() call. - int tx_loopback_fd = -1; - u_int tx_loopback_buflen = 0; - unsigned char tx_loopback_buf[kMaxBpfBufSz]{}; + int promisc_sock = -1; + + int tx_lb_fd = -1; + u_int tx_lb_buflen = 0; + unsigned char tx_lb_buf[kMaxBpfBufSz]{}; + + std::atomic inject_t1_ns{-1LL}; ~QnxRawContext() { - if (bpf_fd >= 0) - { - ::close(bpf_fd); - bpf_fd = -1; - } - if (tx_loopback_fd >= 0) - { - ::close(tx_loopback_fd); - tx_loopback_fd = -1; - } + if (bpf_fd >= 0) { ::close(bpf_fd); bpf_fd = -1; } + if (tx_lb_fd >= 0) { ::close(tx_lb_fd); tx_lb_fd = -1; } + if (promisc_sock >= 0) { ::close(promisc_sock); promisc_sock = -1; } } }; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -thread_local QnxRawContext g_qnx_ctx; - -// ── Internal helpers ────────────────────────────────────────────────────────── +static QnxRawContext g_qnx_ctx; -// Convert a bpf_xhdr hardware timestamp to timespec. -// bpf_ts::bt_sec — seconds (int64_t) -// bpf_ts::bt_frac — binary fraction of a second (uint64_t, unit = 2^-64 s) -// This is equivalent to bintime2timespec() from . static void bpf_ts_to_timespec(const bpf_xhdr* bh, struct timespec* ts) noexcept { - ts->tv_sec = static_cast(bh->bh_tstamp.bt_sec); + ts->tv_sec = static_cast(bh->bh_tstamp.bt_sec); const uint64_t top32 = bh->bh_tstamp.bt_frac >> 32U; ts->tv_nsec = static_cast((top32 * 1'000'000'000ULL) >> 32U); } -// Parse an Ethernet/VLAN frame; return byte offset of PTP payload or -1. static int ptp_payload_offset(const unsigned char* frame, int caplen) { if (caplen < static_cast(sizeof(GptpEthHdr))) return -1; - GptpEthHdr eth{}; std::memcpy(ð, frame, sizeof(GptpEthHdr)); uint16_t etype = ntohs(eth.h_proto); int offset = static_cast(sizeof(GptpEthHdr)); - if (etype == ETH_P_8021Q) { - if (caplen < offset + 4) - return -1; + if (caplen < offset + 4) return -1; uint16_t inner{}; std::memcpy(&inner, frame + offset + 2, sizeof(uint16_t)); etype = ntohs(inner); offset += 4; } - return (etype == ETH_P_1588) ? offset : -1; } -// Open a secondary BPF fd on the same interface as main_fd, with -// BIOCSSEESENT=1 so our own TX frames appear as loopback records. -// Stores the resulting buffer length in g_qnx_ctx.tx_loopback_buflen. -// Returns the new fd or -1. -static int open_tx_loopback_fd(int main_fd) noexcept +static void join_eth_multicast(const char* ifname, const unsigned char mac[6]) noexcept { - // Retrieve interface name from the already-bound main fd. + int s = ::socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) return; ::ifreq ifr{}; - if (::ioctl(main_fd, BIOCGETIF, &ifr) < 0) - return -1; + ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + ifr.ifr_addr.sa_len = static_cast(1U + 1U + ETHER_ADDR_LEN); + ifr.ifr_addr.sa_family = AF_UNSPEC; + std::memcpy(ifr.ifr_addr.sa_data, mac, 6); + (void)::ioctl(s, SIOCADDMULTI, &ifr); + ::close(s); +} +static int set_iface_promisc(const char* ifname) noexcept +{ + int s = ::socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) return -1; + ::ifreq ifr{}; + ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + if (::ioctl(s, SIOCGIFFLAGS, &ifr) == 0) + { + ifr.ifr_flags |= IFF_PROMISC | IFF_ALLMULTI; + (void)::ioctl(s, SIOCSIFFLAGS, &ifr); + } + return s; // keep open — closed in ~QnxRawContext() +} + +static int open_tx_loopback_fd(const char* ifname) noexcept +{ char devpath[256]{}; const char* sock_env = std::getenv("SOCK"); if (sock_env != nullptr && sock_env[0] != '\0') @@ -176,51 +192,36 @@ static int open_tx_loopback_fd(int main_fd) noexcept else std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); - int lfd = ::open(devpath, O_RDWR); - if (lfd < 0) - return -1; + const int fd = ::open(devpath, O_RDWR); + if (fd < 0) return -1; - if (::ioctl(lfd, BIOCSETIF, &ifr) < 0) - { - ::close(lfd); - return -1; - } + ::ifreq ifr{}; + ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + if (::ioctl(fd, BIOCSETIF, &ifr) < 0) { ::close(fd); return -1; } - // Enable loopback so our sent frames are visible on this fd. u_int one = 1U; - (void)::ioctl(lfd, BIOCSSEESENT, &one); - (void)::ioctl(lfd, BIOCIMMEDIATE, &one); + (void)::ioctl(fd, BIOCSSEESENT, &one); // capture TX frames + (void)::ioctl(fd, BIOCIMMEDIATE, &one); // no batching delay - // Request PTP hardware timestamps in bpf_xhdr format. - u_int bpf_ts = BPF_T_PTP | BPF_T_BINTIME; - (void)::ioctl(lfd, BIOCSTSTAMP, &bpf_ts); + u_int bpf_ts = BPF_T_BINTIME | BPF_T_PTP; + (void)::ioctl(fd, BIOCSTSTAMP, &bpf_ts); - // Apply the same ETH_P_1588 kernel filter. - struct bpf_program prog - { - kPtp1588FilterLen, kPtp1588FilterInsns - }; - (void)::ioctl(lfd, BIOCSETF, &prog); + struct bpf_program prog{kPdelayReqFilterLen, kPdelayReqFilterInsns}; + if (::ioctl(fd, BIOCSETF, &prog) < 0) { ::close(fd); return -1; } u_int buflen = 0U; - if (::ioctl(lfd, BIOCGBLEN, &buflen) < 0 || buflen == 0U || buflen > kMaxBpfBufSz) + if (::ioctl(fd, BIOCGBLEN, &buflen) < 0 || buflen > kMaxBpfBufSz) { - ::close(lfd); + ::close(fd); return -1; } - g_qnx_ctx.tx_loopback_buflen = buflen; - return lfd; + g_qnx_ctx.tx_lb_buflen = buflen; + return fd; } -// ── Public C interface ──────────────────────────────────────────────────────── - extern "C" int qnx_raw_open(const char* ifname) { - if (ifname == nullptr) - { - errno = EINVAL; - return -1; - } + if (ifname == nullptr) { errno = EINVAL; return -1; } ::strlcpy(g_qnx_ctx.iface_name, ifname, sizeof(g_qnx_ctx.iface_name)); @@ -232,37 +233,26 @@ extern "C" int qnx_raw_open(const char* ifname) std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); int fd = ::open(devpath, O_RDWR); - if (fd < 0) - return -1; + if (fd < 0) return -1; ::ifreq ifr{}; ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); - if (::ioctl(fd, BIOCSETIF, &ifr) < 0) - { - ::close(fd); - return -1; - } + if (::ioctl(fd, BIOCSETIF, &ifr) < 0) { ::close(fd); return -1; } - // Do NOT see our own TX frames on the main fd; use tx_loopback_fd instead. - int zero = 0; - (void)::ioctl(fd, BIOCSSEESENT, &zero); + u_int seesent = 0U; + (void)::ioctl(fd, BIOCSSEESENT, &seesent); u_int yes = 1U; (void)::ioctl(fd, BIOCIMMEDIATE, &yes); + + g_qnx_ctx.promisc_sock = set_iface_promisc(ifname); (void)::ioctl(fd, BIOCPROMISC, &yes); - // Request PTP hardware timestamps in bpf_xhdr format (IEEE 1588 clock). - // Falls back gracefully: if unsupported, timestamps will be zero and - // qnx_raw_recv() will fall back to CLOCK_REALTIME. - u_int bpf_ts = BPF_T_PTP | BPF_T_BINTIME; + u_int bpf_ts = BPF_T_BINTIME | BPF_T_PTP; (void)::ioctl(fd, BIOCSTSTAMP, &bpf_ts); - // Install kernel BPF filter: discard all non-ETH_P_1588 frames early. - struct bpf_program prog - { - kPtp1588FilterLen, kPtp1588FilterInsns - }; - (void)::ioctl(fd, BIOCSETF, &prog); // best-effort; userspace filter still runs + struct bpf_program prog{kPtp1588FilterLen, kPtp1588FilterInsns}; + if (::ioctl(fd, BIOCSETF, &prog) < 0) { ::close(fd); return -1; } if (::ioctl(fd, BIOCGBLEN, &g_qnx_ctx.bpf_buflen) < 0) { @@ -276,8 +266,16 @@ extern "C" int qnx_raw_open(const char* ifname) return -1; } - g_qnx_ctx.bpf_fd = fd; + g_qnx_ctx.bpf_fd = fd; g_qnx_ctx.initialized = true; + + g_qnx_ctx.tx_lb_fd = open_tx_loopback_fd(ifname); + + static const unsigned char kPtpP2PMac[6] = {0x01U, 0x80U, 0xC2U, 0x00U, 0x00U, 0x0EU}; + static const unsigned char kPtp1588Mac[6] = {0x01U, 0x1BU, 0x19U, 0x00U, 0x00U, 0x00U}; + join_eth_multicast(ifname, kPtpP2PMac); + join_eth_multicast(ifname, kPtp1588Mac); + return fd; } @@ -303,87 +301,133 @@ extern "C" int qnx_raw_recv(int fd, void* buf, int buf_len, timespec* hwts, int for (;;) { - // Refill BPF read buffer when exhausted. if (g_qnx_ctx.bpf_off >= g_qnx_ctx.bpf_n) { - ssize_t n = ::read(fd, g_qnx_ctx.bpf_buf, g_qnx_ctx.bpf_buflen); - if (n < 0) - return -1; + if (nonblock == 0) + { + struct pollfd pfd{fd, POLLIN, 0}; + const int pr = ::poll(&pfd, 1, 100); + if (pr < 0) return -1; + if (pr == 0) { errno = ETIMEDOUT; return -1; } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) return -1; + } + + const ssize_t n = ::read(fd, g_qnx_ctx.bpf_buf, g_qnx_ctx.bpf_buflen); + if (n < 0) return -1; if (n == 0) { - if (nonblock != 0) - { - errno = EAGAIN; - return -1; - } + if (nonblock != 0) { errno = EAGAIN; return -1; } continue; } - g_qnx_ctx.bpf_n = n; + g_qnx_ctx.bpf_n = n; g_qnx_ctx.bpf_off = 0; } - // Need at least sizeof(bpf_xhdr) bytes for the header. - if (g_qnx_ctx.bpf_off + static_cast(sizeof(bpf_xhdr)) > g_qnx_ctx.bpf_n) - { - g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; - continue; - } + static constexpr ssize_t kBhHdrMinBytes = + static_cast(offsetof(bpf_xhdr, bh_hdrlen)) + + static_cast(sizeof(u_short)); // = 26 - // Verify 8-byte alignment required by bpf_xhdr. - const auto ptr_val = reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); - if (ptr_val % alignof(bpf_xhdr) != 0U) + if (g_qnx_ctx.bpf_off + kBhHdrMinBytes > g_qnx_ctx.bpf_n) { g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; continue; } - const auto* bh = reinterpret_cast(g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off); + const unsigned char* bh_raw = g_qnx_ctx.bpf_buf + g_qnx_ctx.bpf_off; + bpf_u_int32 bh_caplen = 0; + u_short bh_hdrlen = 0; + std::memcpy(&bh_caplen, bh_raw + offsetof(bpf_xhdr, bh_caplen), sizeof(bpf_u_int32)); + std::memcpy(&bh_hdrlen, bh_raw + offsetof(bpf_xhdr, bh_hdrlen), sizeof(u_short)); - // Bounds checks. - if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || - bh->bh_caplen > static_cast(g_qnx_ctx.bpf_n) || - g_qnx_ctx.bpf_off + static_cast(bh->bh_hdrlen) + static_cast(bh->bh_caplen) > - g_qnx_ctx.bpf_n) + if (bh_hdrlen < static_cast(kBhHdrMinBytes) || + bh_caplen > static_cast(g_qnx_ctx.bpf_n) || + g_qnx_ctx.bpf_off + static_cast(bh_hdrlen) + + static_cast(bh_caplen) > g_qnx_ctx.bpf_n) { g_qnx_ctx.bpf_off = g_qnx_ctx.bpf_n; continue; } - const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; - const int caplen = static_cast(bh->bh_caplen); - const ssize_t next_off = g_qnx_ctx.bpf_off + static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); + const unsigned char* pkt = bh_raw + bh_hdrlen; + const int caplen = static_cast(bh_caplen); + const ssize_t next_off = + g_qnx_ctx.bpf_off + static_cast(BPF_WORDALIGN(bh_hdrlen + bh_caplen)); - // Skip TX loopback frames (BIOCSSEESENT=0 should prevent them on the - // main fd, but guard defensively: a loopback frame has a fixed small - // caplen equal to ETH header + ptp_tstmp, not a valid PTP message). if (caplen == kTxLoopbackCaplen) { + ptp_tstmp tstmp{}; + std::memcpy(&tstmp, pkt + sizeof(GptpEthHdr), sizeof(ptp_tstmp)); + const std::int64_t t1_ns = + tstmp.time.sec * kNsPerSec + static_cast(tstmp.time.nsec); + if (t1_ns > 0) + { + g_qnx_ctx.inject_t1_ns.store(t1_ns, std::memory_order_release); + std::fprintf(stderr, "[t1-inject] uid=%u ts=%lld.%09d\n", + tstmp.uid, + static_cast(tstmp.time.sec), + tstmp.time.nsec); + } g_qnx_ctx.bpf_off = next_off; continue; } const int ptp_off = ptp_payload_offset(pkt, caplen); - if (ptp_off >= 0) + if (ptp_off < 0) { - // Use PTP hardware RX timestamp from bpf_xhdr. - // bt_sec==0 && bt_frac==0 means the driver did not provide a HW - // timestamp; fall back to CLOCK_REALTIME in that case. - if (bh->bh_tstamp.bt_sec != 0 || bh->bh_tstamp.bt_frac != 0) + g_qnx_ctx.bpf_off = next_off; + continue; + } + + const uint8_t msgtype = static_cast(pkt[ptp_off]) & 0x0Fu; + const auto* bh = reinterpret_cast(bh_raw); + bool t4_set = false; + if (bh->bh_tstamp.bt_sec != 0 || bh->bh_tstamp.bt_frac != 0) + { + bpf_ts_to_timespec(bh, hwts); + t4_set = true; + if (msgtype == 0x03u) { - bpf_ts_to_timespec(bh, hwts); + std::fprintf(stderr, "[t4] bpf_phc ts=%lld.%09ld\n", + static_cast(hwts->tv_sec), + hwts->tv_nsec); } - else + } + // PTP_GET_TIME fallback: use PHC hardware time when no BPF timestamp + // is available, so rate_ratio reflects ΔPHC / ΔCLOCK_REALTIME. + if (!t4_set && g_qnx_ctx.promisc_sock >= 0) + { + struct + { + struct ifdrv ifd; + struct ptp_time tm; + } cmd{}; + std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, + sizeof(cmd.ifd.ifd_name) - 1U); + cmd.ifd.ifd_len = sizeof(cmd.tm); + cmd.ifd.ifd_data = &cmd.tm; + cmd.ifd.ifd_cmd = PTP_GET_TIME; + if (::ioctl(g_qnx_ctx.promisc_sock, SIOCGDRVSPEC, &cmd) == 0) { - (void)::clock_gettime(CLOCK_REALTIME, hwts); + hwts->tv_sec = static_cast(cmd.tm.sec); + hwts->tv_nsec = static_cast(cmd.tm.nsec); + t4_set = true; + if (msgtype == 0x03u) + { + std::fprintf(stderr, "[t4] PTP_GET_TIME ts=%lld.%09ld\n", + static_cast(cmd.tm.sec), + static_cast(cmd.tm.nsec)); + } } - - const int frame_len = std::min(caplen, buf_len); - std::memcpy(buf, pkt, static_cast(frame_len)); - g_qnx_ctx.bpf_off = next_off; - return frame_len; + } + if (!t4_set) + { + (void)::clock_gettime(CLOCK_REALTIME, hwts); } + const int frame_len = std::min(caplen, buf_len); + std::memcpy(buf, pkt, static_cast(frame_len)); g_qnx_ctx.bpf_off = next_off; + return frame_len; } } @@ -401,80 +445,57 @@ extern "C" int qnx_raw_send(int fd, const void* buf, int len, timespec* hwts) } std::memcpy(g_qnx_ctx.tx_frame, buf, static_cast(len)); - ssize_t n = ::write(fd, g_qnx_ctx.tx_frame, static_cast(len)); - if (n < 0) + + g_qnx_ctx.inject_t1_ns.store(-1LL, std::memory_order_relaxed); + + if (::write(fd, g_qnx_ctx.tx_frame, static_cast(len)) < 0) return -1; - // Attempt to obtain a hardware TX timestamp via the BPF loopback mechanism: - // 1. BIOCGTSTAMPID returns the UID assigned to the just-sent frame. - // 2. The driver inserts a loopback record on fds with BIOCSSEESENT=1; - // its payload is a ptp_tstmp struct carrying the actual HW timestamp. - // 3. We scan the secondary loopback fd for a record whose uid matches. - // If any step fails, we fall back to a CLOCK_REALTIME software timestamp. - uint32_t tx_uid = 0U; - if (::ioctl(fd, BIOCGTSTAMPID, &tx_uid) == 0) + for (int i = 0; i < 100; ++i) { - // Lazy-open the secondary fd (needs BIOCGETIF to recover iface name). - if (g_qnx_ctx.tx_loopback_fd < 0) - g_qnx_ctx.tx_loopback_fd = open_tx_loopback_fd(fd); - - if (g_qnx_ctx.tx_loopback_fd >= 0 && g_qnx_ctx.tx_loopback_buflen > 0) + const std::int64_t t1 = g_qnx_ctx.inject_t1_ns.load(std::memory_order_acquire); + if (t1 > 0) { - const int lfd = g_qnx_ctx.tx_loopback_fd; - - // Non-blocking scan: the loopback frame typically arrives within - // a few microseconds; we try kMaxTxScanTries reads. - int flags = ::fcntl(lfd, F_GETFL, 0); - (void)::fcntl(lfd, F_SETFL, (flags >= 0 ? flags : 0) | O_NONBLOCK); - - for (int tries = 0; tries < kMaxTxScanTries; ++tries) - { - ssize_t nr = ::read(lfd, g_qnx_ctx.tx_loopback_buf, g_qnx_ctx.tx_loopback_buflen); - if (nr <= 0) - break; + hwts->tv_sec = static_cast(t1 / kNsPerSec); + hwts->tv_nsec = static_cast(t1 % kNsPerSec); + std::fprintf(stderr, "[t1] inject ts=%lld.%09ld\n", + static_cast(hwts->tv_sec), + hwts->tv_nsec); + return len; + } + ::usleep(100U); // 100 µs + } - ssize_t off = 0; - while (off + static_cast(sizeof(bpf_xhdr)) <= nr) - { - const auto pv = reinterpret_cast(g_qnx_ctx.tx_loopback_buf + off); - if (pv % alignof(bpf_xhdr) != 0U) - break; - - const auto* bh = reinterpret_cast(g_qnx_ctx.tx_loopback_buf + off); - - if (bh->bh_hdrlen < static_cast(sizeof(bpf_xhdr)) || - off + static_cast(bh->bh_hdrlen) + static_cast(bh->bh_caplen) > nr) - break; - - const unsigned char* pkt = reinterpret_cast(bh) + bh->bh_hdrlen; - const int caplen = static_cast(bh->bh_caplen); - const ssize_t next = off + static_cast(BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen)); - - // A TX loopback record has a fixed caplen and contains a - // ptp_tstmp payload right after the Ethernet header. - if (caplen == kTxLoopbackCaplen) - { - const auto* tstmp = reinterpret_cast(pkt + sizeof(GptpEthHdr)); - if (tstmp->uid == tx_uid) - { - hwts->tv_sec = static_cast(tstmp->time.sec); - hwts->tv_nsec = static_cast(tstmp->time.nsec); - return static_cast(len); - } - } - off = next; - } - } + if (g_qnx_ctx.promisc_sock >= 0) + { + struct + { + struct ifdrv ifd; + struct ptp_time tm; + } cmd{}; + std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, + sizeof(cmd.ifd.ifd_name) - 1U); + cmd.ifd.ifd_len = sizeof(cmd.tm); + cmd.ifd.ifd_data = &cmd.tm; + cmd.ifd.ifd_cmd = PTP_GET_TIME; + if (::ioctl(g_qnx_ctx.promisc_sock, SIOCGDRVSPEC, &cmd) == 0) + { + hwts->tv_sec = static_cast(cmd.tm.sec); + hwts->tv_nsec = static_cast(cmd.tm.nsec); + std::fprintf(stderr, "[t1] PTP_GET ts=%lld.%09ld (inject timeout)\n", + static_cast(hwts->tv_sec), + hwts->tv_nsec); + return len; } } - // Fallback: software TX timestamp. (void)::clock_gettime(CLOCK_REALTIME, hwts); - return static_cast(len); + std::fprintf(stderr, "[t1] CLOCK_RT ts=%lld.%09ld (fallback)\n", + static_cast(hwts->tv_sec), + static_cast(hwts->tv_nsec)); + return len; } -// ── PHC clock adjustment (QNX SDP 8.0 io-sock/ptp.h ioctl path) ────────────── - extern "C" int qnx_phc_open(const char* phc_dev) { if (phc_dev != nullptr && phc_dev[0] != '\0' && phc_dev[0] != '/') @@ -482,74 +503,112 @@ extern "C" int qnx_phc_open(const char* phc_dev) return 0; } + extern "C" int qnx_phc_adjtime_step(int /*phc_fd*/, long long offset_ns) { - if (offset_ns == 0) - return 0; + if (offset_ns == 0) return 0; const int s = ::socket(AF_INET, SOCK_DGRAM, 0); - if (s < 0) - return -1; + if (s < 0) return -1; struct { - struct ifdrv ifd; + struct ifdrv ifd; struct ptp_time tm; } cmd{}; - std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); - cmd.ifd.ifd_len = sizeof(cmd.tm); + cmd.ifd.ifd_len = sizeof(cmd.tm); cmd.ifd.ifd_data = &cmd.tm; - cmd.ifd.ifd_cmd = PTP_GET_TIME; + cmd.ifd.ifd_cmd = PTP_GET_TIME; - if (::ioctl(s, SIOCGDRVSPEC, &cmd) == -1) - { - ::close(s); - return -1; - } + if (::ioctl(s, SIOCGDRVSPEC, &cmd) == -1) { ::close(s); return -1; } const int64_t cur_ns = cmd.tm.sec * kNsPerSec + static_cast(cmd.tm.nsec); - const int64_t new_ns = cur_ns + static_cast(offset_ns); - - cmd.tm.sec = new_ns / kNsPerSec; + const int64_t new_ns = cur_ns - static_cast(offset_ns); + cmd.tm.sec = new_ns / kNsPerSec; cmd.tm.nsec = static_cast(new_ns % kNsPerSec); if (cmd.tm.nsec < 0) { cmd.tm.nsec += static_cast(kNsPerSec); - cmd.tm.sec -= 1; + cmd.tm.sec -= 1; } cmd.ifd.ifd_cmd = PTP_SET_TIME; - const int r = ::ioctl(s, SIOCSDRVSPEC, &cmd); + const int r = ::ioctl(s, SIOCGDRVSPEC, &cmd); + if (r == 0) + { + std::fprintf(stderr, "[phc-step] offset=%lld ns new=%lld.%09d\n", + static_cast(offset_ns), + static_cast(cmd.tm.sec), + cmd.tm.nsec); + // After a hard step, skip 3 frequency-adjustment cycles and reset + // the smoothed estimate so stale rate data doesn't corrupt slewing. + g_skip_freq_after_step = 3; + g_smoothed_comp_ppb = 0.0; + g_integral_ppb = 0.0; + } + else + { + std::fprintf(stderr, "[phc-step] PTP_SET_TIME failed errno=%d\n", errno); + } ::close(s); return r; } extern "C" int qnx_phc_adjfreq_ppb(int /*phc_fd*/, long long freq_ppb) { - if (freq_ppb == 0) + // Skip a few cycles immediately after a step correction. + if (g_skip_freq_after_step > 0) + { + --g_skip_freq_after_step; return 0; + } - const int s = ::socket(AF_INET, SOCK_DGRAM, 0); - if (s < 0) - return -1; + constexpr double kAlpha = 0.2; + constexpr double kKi = 0.002; + constexpr double kICap = 300'000.0; // I term anti-windup: ±300 ppm + constexpr double kTotCap = 400'000.0; // combined output cap: ±400 ppm + + // --- P term: EMA of raw_ppb --- + g_smoothed_comp_ppb = kAlpha * static_cast(freq_ppb) + + (1.0 - kAlpha) * g_smoothed_comp_ppb; + + // --- I term: slow integrator of P; clamp to prevent wind-up --- + g_integral_ppb += kKi * g_smoothed_comp_ppb; + if (g_integral_ppb > kICap) g_integral_ppb = kICap; + if (g_integral_ppb < -kICap) g_integral_ppb = -kICap; + + // --- Combined PI output --- + double combined = g_smoothed_comp_ppb + g_integral_ppb; + if (combined > kTotCap) combined = kTotCap; + if (combined < -kTotCap) combined = -kTotCap; - // Convert ppb to ppm (EMAC_PTP_ADJ_FREQ_PPM expects ppm) - int ppm = static_cast(freq_ppb / 1000LL); + // ppb → ppm with sign flip: + // positive error = slave running fast → apply negative adj_ppm to slow PHC down + const int adj_ppm = -static_cast(combined / 1000.0); + if (adj_ppm == 0) return 0; // below 1 ppm resolution, skip ioctl + + const int s = ::socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) return -1; struct { struct ifdrv ifd; - int adj_ppm; + int ppm; } cmd{}; - std::strncpy(cmd.ifd.ifd_name, g_qnx_ctx.iface_name, sizeof(cmd.ifd.ifd_name) - 1U); - cmd.ifd.ifd_len = sizeof(cmd.adj_ppm); - cmd.ifd.ifd_data = &cmd.adj_ppm; - cmd.ifd.ifd_cmd = 0x200; // EMAC_PTP_ADJ_FREQ_PPM - cmd.adj_ppm = ppm; + cmd.ifd.ifd_cmd = kEmacPtpAdjFreqPpm; + cmd.ifd.ifd_len = sizeof(int); + cmd.ifd.ifd_data = &cmd.ppm; + cmd.ppm = adj_ppm; const int r = ::ioctl(s, SIOCGDRVSPEC, &cmd); + std::fprintf(stderr, "[phc-freq] raw_ppb=%lld P=%.0f I=%.0f adj_ppm=%d r=%d%s\n", + static_cast(freq_ppb), + g_smoothed_comp_ppb, + g_integral_ppb, + adj_ppm, r, + r != 0 ? " (FAILED)" : ""); ::close(s); return r; } From 8c014e9f41bb400f656bb4f5ea8feeec7231d296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Tue, 21 Apr 2026 14:21:05 +0800 Subject: [PATCH 10/12] [ECARX][TimeSlave]Revise the comments raised by the reviewer --- .../_assets/libtsclient/ipc_channel.puml | 2 +- .../shm_ptp_engine/shm_ptp_engine_class.puml | 4 +- .../shm_ptp_engine_init_seq.puml | 6 +- .../shm_ptp_engine_read_seq.puml | 2 +- .../_assets/timeslave_deployment.puml | 2 +- docs/TimeSlave/index.rst | 10 +- score/TimeDaemon/code/ptp_machine/BUILD | 2 +- .../real/details/real_ptp_engine.cpp | 99 ------ .../real/details/real_ptp_engine.h | 58 ---- .../real/details/real_ptp_engine_test.cpp | 217 ------------ .../code/ptp_machine/{real => shm}/BUILD | 16 +- .../ptp_machine/{real => shm}/details/BUILD | 0 .../{real => shm}/details/shm_ptp_engine.cpp | 2 +- .../{real => shm}/details/shm_ptp_engine.h | 6 +- .../details/shm_ptp_engine_test.cpp | 2 +- .../ptp_machine/{real => shm}/factory.cpp | 6 +- .../code/ptp_machine/{real => shm}/factory.h | 16 +- .../gptp_shm_machine.h} | 16 +- .../gptp_shm_machine_test.cpp} | 24 +- .../TimeSlave/code/common/logging_contexts.h | 4 +- score/TimeSlave/code/gptp/details/ptp_types.h | 1 + .../code/gptp/details/sync_state_machine.cpp | 1 + .../code/gptp/details/sync_state_machine.h | 1 + score/TimeSlave/code/gptp/gptp_engine.cpp | 18 +- score/examples/BUILD | 47 +++ score/examples/gptp_master.cpp | 324 ++++++++++++++++++ score/examples/time_reader.cpp | 159 +++++++++ score/examples/timeslave_standalone.cpp | 122 +++++++ score/libTSClient/gptp_ipc_channel.h | 2 +- score/libTSClient/gptp_ipc_receiver.h | 2 +- 30 files changed, 726 insertions(+), 445 deletions(-) delete mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp delete mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h delete mode 100644 score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp rename score/TimeDaemon/code/ptp_machine/{real => shm}/BUILD (79%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/details/BUILD (100%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/details/shm_ptp_engine.cpp (98%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/details/shm_ptp_engine.h (88%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/details/shm_ptp_engine_test.cpp (98%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/factory.cpp (74%) rename score/TimeDaemon/code/ptp_machine/{real => shm}/factory.h (68%) rename score/TimeDaemon/code/ptp_machine/{real/gptp_real_machine.h => shm/gptp_shm_machine.h} (62%) rename score/TimeDaemon/code/ptp_machine/{real/gptp_real_machine_test.cpp => shm/gptp_shm_machine_test.cpp} (77%) create mode 100644 score/examples/BUILD create mode 100644 score/examples/gptp_master.cpp create mode 100644 score/examples/time_reader.cpp create mode 100644 score/examples/timeslave_standalone.cpp diff --git a/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml b/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml index 846732e..ead16c4 100644 --- a/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml +++ b/docs/TimeSlave/_assets/libtsclient/ipc_channel.puml @@ -64,7 +64,7 @@ end note note bottom of ShmPTPEngine Maps GptpIpcData fields to PtpTimeInfo. - Instantiated as GPTPRealMachine via CreateGPTPRealMachine(). + Instantiated as GPTPShmMachine via CreateGPTPShmMachine(). end note @enduml diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml index 8e47add..1b5486c 100644 --- a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_class.puml @@ -11,10 +11,10 @@ legend top left endlegend package "score::td" { - class "GPTPRealMachine" as real_machine #Wheat { + class "GPTPShmMachine" as real_machine #Wheat { type alias for PTPMachine -- - Constructed via CreateGPTPRealMachine() + Constructed via CreateGPTPShmMachine() } } diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml index d57892a..1a3d2f6 100644 --- a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_init_seq.puml @@ -9,19 +9,19 @@ autonumber "[00]" legend top left |= Color |= Description | | <#LightCyan> | TimeDaemon | - | <#Wheat> | GPTPRealMachine | + | <#Wheat> | GPTPShmMachine | | <#LightPink> | libTSClient IPC | endlegend participant "TimeBaseHandler" as tb #LightCyan -participant "GPTPRealMachine\n(PTPMachine)" as machine #Wheat +participant "GPTPShmMachine\n(PTPMachine)" as machine #Wheat participant "ShmPTPEngine" as engine #Wheat participant "GptpIpcReceiver" as receiver #LightPink participant "MessageBroker" as broker #LightCyan == Construction == -tb -> machine ** : CreateGPTPRealMachine("real", "/gptp_ptp_info") +tb -> machine ** : CreateGPTPShmMachine("shm", "/gptp_ptp_info") activate tb activate machine machine -> engine ** : ShmPTPEngine("/gptp_ptp_info") diff --git a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml index edfe97d..0a6a914 100644 --- a/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml +++ b/docs/TimeSlave/_assets/shm_ptp_engine/shm_ptp_engine_read_seq.puml @@ -8,7 +8,7 @@ autonumber "[00]" legend top left |= Color |= Description | - | <#Wheat> | GPTPRealMachine | + | <#Wheat> | GPTPShmMachine | | <#LightPink> | libTSClient IPC | | <#LightCyan> | Shared Memory | | <#PaleTurquoise> | MessageBroker | diff --git a/docs/TimeSlave/_assets/timeslave_deployment.puml b/docs/TimeSlave/_assets/timeslave_deployment.puml index 844967d..2e922ca 100644 --- a/docs/TimeSlave/_assets/timeslave_deployment.puml +++ b/docs/TimeSlave/_assets/timeslave_deployment.puml @@ -54,7 +54,7 @@ GE .down.> INST : probe events note right of TDP ShmPTPEngine wraps GptpIpcReceiver. Converts GptpIpcData → PtpTimeInfo. - Instantiated as GPTPRealMachine. + Instantiated as GPTPShmMachine. end note @enduml diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 75af0ec..14a510a 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -583,10 +583,10 @@ The TimeSlave and its TimeDaemon-side adapter use the following logging contexts - Context ID - Comments * - TimeSlave Application - - TS_APP + - TSAP - **T**\ ime\ **S**\ lave **App**\ lication lifecycle (Initialize / Run) * - gPTP Engine (RxThread / PdelayThread) - - GPTP_SLAVE + - GTPS - **GPTP** **SLAVE** engine — low-level protocol processing * - ShmPTPEngine (TimeDaemon side) - GPTP @@ -673,7 +673,7 @@ ShmPTPEngine SW component The ``ShmPTPEngine`` component (in ``score::td::details``) is the TimeDaemon-side adapter that reads ``GptpIpcData`` from the shared memory channel written by TimeSlave and converts it into the ``PtpTimeInfo`` structure expected by the TimeDaemon pipeline. -It is instantiated as ``GPTPRealMachine`` — a type alias for ``PTPMachine`` — which connects ``ShmPTPEngine`` to the TimeDaemon's internal ``MessageBroker``. +It is instantiated as ``GPTPShmMachine`` — a type alias for ``PTPMachine`` — which connects ``ShmPTPEngine`` to the TimeDaemon's internal ``MessageBroker``. Component requirements '''''''''''''''''''''' @@ -773,11 +773,11 @@ Data mapping Factory ''''''' -``CreateGPTPRealMachine(name, ipc_name)`` is a convenience factory function in ``score::td`` that creates a configured ``GPTPRealMachine`` (``shared_ptr``) backed by ``ShmPTPEngine``: +``CreateGPTPShmMachine(name, ipc_name)`` is a convenience factory function in ``score::td`` that creates a configured ``GPTPShmMachine`` (``shared_ptr``) backed by ``ShmPTPEngine``: .. code-block:: cpp - auto machine = CreateGPTPRealMachine("real", "/gptp_ptp_info"); + auto machine = CreateGPTPShmMachine("shm", "/gptp_ptp_info"); Using in test environment ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/score/TimeDaemon/code/ptp_machine/BUILD b/score/TimeDaemon/code/ptp_machine/BUILD index 2e05898..6111572 100644 --- a/score/TimeDaemon/code/ptp_machine/BUILD +++ b/score/TimeDaemon/code/ptp_machine/BUILD @@ -23,7 +23,7 @@ cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", test_suites_from_sub_packages = [ "//score/TimeDaemon/code/ptp_machine/core:unit_test_suite", - "//score/TimeDaemon/code/ptp_machine/real:unit_test_suite", + "//score/TimeDaemon/code/ptp_machine/shm:unit_test_suite", "//score/TimeDaemon/code/ptp_machine/stub:unit_test_suite", ], visibility = ["//score/TimeDaemon:__subpackages__"], diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp deleted file mode 100644 index b348caf..0000000 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" - -#include "score/TimeDaemon/code/common/logging_contexts.h" -#include "score/libTSClient/gptp_ipc_data.h" -#include "score/mw/log/logging.h" - -namespace score -{ -namespace td -{ -namespace details -{ - -RealPTPEngine::RealPTPEngine(std::string ipc_name) noexcept : ipc_name_{std::move(ipc_name)} {} - -bool RealPTPEngine::Initialize() -{ - if (initialized_) - return true; - - initialized_ = receiver_.Init(ipc_name_); - if (initialized_) - { - score::mw::log::LogInfo(kGPtpMachineContext) << "RealPTPEngine: connected to IPC channel " << ipc_name_; - } - else - { - score::mw::log::LogError(kGPtpMachineContext) << "RealPTPEngine: failed to open IPC channel " << ipc_name_; - } - return initialized_; -} - -bool RealPTPEngine::Deinitialize() -{ - if (initialized_) - { - receiver_.Close(); - initialized_ = false; - } - return true; -} - -bool RealPTPEngine::ReadPTPSnapshot(PtpTimeInfo& info) -{ - if (!initialized_) - return false; - - auto result = receiver_.Receive(); - if (!result.has_value()) - return false; - - const score::ts::GptpIpcData& d = result.value(); - info.ptp_assumed_time = d.ptp_assumed_time; - info.local_time = score::td::PtpTimeInfo::ReferenceClock::time_point{d.local_time}; - info.rate_deviation = d.rate_deviation; - info.status.is_synchronized = d.status.is_synchronized; - info.status.is_timeout = d.status.is_timeout; - info.status.is_time_jump_future = d.status.is_time_jump_future; - info.status.is_time_jump_past = d.status.is_time_jump_past; - info.status.is_correct = d.status.is_correct; - info.sync_fup_data.precise_origin_timestamp = d.sync_fup_data.precise_origin_timestamp; - info.sync_fup_data.reference_global_timestamp = d.sync_fup_data.reference_global_timestamp; - info.sync_fup_data.reference_local_timestamp = d.sync_fup_data.reference_local_timestamp; - info.sync_fup_data.sync_ingress_timestamp = d.sync_fup_data.sync_ingress_timestamp; - info.sync_fup_data.correction_field = d.sync_fup_data.correction_field; - info.sync_fup_data.sequence_id = d.sync_fup_data.sequence_id; - info.sync_fup_data.pdelay = d.sync_fup_data.pdelay; - info.sync_fup_data.port_number = d.sync_fup_data.port_number; - info.sync_fup_data.clock_identity = d.sync_fup_data.clock_identity; - info.pdelay_data.request_origin_timestamp = d.pdelay_data.request_origin_timestamp; - info.pdelay_data.request_receipt_timestamp = d.pdelay_data.request_receipt_timestamp; - info.pdelay_data.response_origin_timestamp = d.pdelay_data.response_origin_timestamp; - info.pdelay_data.response_receipt_timestamp = d.pdelay_data.response_receipt_timestamp; - info.pdelay_data.reference_global_timestamp = d.pdelay_data.reference_global_timestamp; - info.pdelay_data.reference_local_timestamp = d.pdelay_data.reference_local_timestamp; - info.pdelay_data.sequence_id = d.pdelay_data.sequence_id; - info.pdelay_data.pdelay = d.pdelay_data.pdelay; - info.pdelay_data.req_port_number = d.pdelay_data.req_port_number; - info.pdelay_data.req_clock_identity = d.pdelay_data.req_clock_identity; - info.pdelay_data.resp_port_number = d.pdelay_data.resp_port_number; - info.pdelay_data.resp_clock_identity = d.pdelay_data.resp_clock_identity; - return true; -} - -} // namespace details -} // namespace td -} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h deleted file mode 100644 index fd47403..0000000 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ -#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H -#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H - -#include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" -#include "score/libTSClient/gptp_ipc_receiver.h" - -#include - -namespace score -{ -namespace td -{ -namespace details -{ - -/** - * @brief PTP engine that reads time data from the IPC channel written by TimeSlave. - */ -class RealPTPEngine final -{ - public: - explicit RealPTPEngine(std::string ipc_name = score::ts::details::kGptpIpcName) noexcept; - ~RealPTPEngine() noexcept = default; - - RealPTPEngine(const RealPTPEngine&) = delete; - RealPTPEngine& operator=(const RealPTPEngine&) = delete; - RealPTPEngine(RealPTPEngine&&) = delete; - RealPTPEngine& operator=(RealPTPEngine&&) = delete; - - bool Initialize(); - - bool Deinitialize(); - - bool ReadPTPSnapshot(PtpTimeInfo& info); - - private: - std::string ipc_name_; - score::ts::details::GptpIpcReceiver receiver_; - bool initialized_{false}; -}; - -} // namespace details -} // namespace td -} // namespace score - -#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_REAL_PTP_ENGINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp deleted file mode 100644 index 5ab42b4..0000000 --- a/score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine_test.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/details/real_ptp_engine.h" - -#include "score/libTSClient/gptp_ipc_publisher.h" - -#include - -#include -#include - -namespace score -{ -namespace td -{ -namespace details -{ - -namespace -{ - -std::string UniqueShmName() -{ - static std::atomic counter{0}; - return "/gptp_rpe_ut_" + std::to_string(::getpid()) + "_" + - std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); -} - -// Build a fully-populated PtpTimeInfo for roundtrip verification. -PtpTimeInfo MakeTestInfo() -{ - PtpTimeInfo info{}; - info.ptp_assumed_time = std::chrono::nanoseconds{9'876'543'210LL}; - info.rate_deviation = -0.25; - - info.status.is_synchronized = true; - info.status.is_correct = true; - info.status.is_timeout = false; - info.status.is_time_jump_future = false; - info.status.is_time_jump_past = false; - - info.sync_fup_data.precise_origin_timestamp = 100'000'000'000ULL; - info.sync_fup_data.reference_global_timestamp = 100'000'000'500ULL; - info.sync_fup_data.reference_local_timestamp = 100'000'001'000ULL; - info.sync_fup_data.sync_ingress_timestamp = 100'000'001'000ULL; - info.sync_fup_data.correction_field = 8U; - info.sync_fup_data.sequence_id = 55; - info.sync_fup_data.pdelay = 4'000U; - info.sync_fup_data.port_number = 1; - info.sync_fup_data.clock_identity = 0xCAFEBABEDEAD0001ULL; - - info.pdelay_data.request_origin_timestamp = 200'000'000'000ULL; - info.pdelay_data.request_receipt_timestamp = 200'000'001'000ULL; - info.pdelay_data.response_origin_timestamp = 200'000'001'000ULL; - info.pdelay_data.response_receipt_timestamp = 200'000'002'000ULL; - info.pdelay_data.pdelay = 1'000U; - info.pdelay_data.req_port_number = 2; - info.pdelay_data.resp_port_number = 3; - info.pdelay_data.req_clock_identity = 0x0102030405060708ULL; - info.pdelay_data.resp_clock_identity = 0x0807060504030201ULL; - return info; -} - -} // namespace - -class RealPTPEngineTest : public ::testing::Test -{ - protected: - void SetUp() override - { - name_ = UniqueShmName(); - engine_ = std::make_unique(name_); - } - - void TearDown() override - { - engine_->Deinitialize(); - pub_.Destroy(); - } - - std::string name_; - score::ts::details::GptpIpcPublisher pub_; - std::unique_ptr engine_; -}; - -// ── Lifecycle ──────────────────────────────────────────────────────────────── - -TEST_F(RealPTPEngineTest, Initialize_WhenShmNotExist_ReturnsFalse) -{ - // No publisher → shm doesn't exist. - EXPECT_FALSE(engine_->Initialize()); -} - -TEST_F(RealPTPEngineTest, Initialize_WhenShmExists_ReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - EXPECT_TRUE(engine_->Initialize()); -} - -TEST_F(RealPTPEngineTest, Initialize_CalledTwiceWhenInitialized_ReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - ASSERT_TRUE(engine_->Initialize()); - EXPECT_TRUE(engine_->Initialize()); // idempotent -} - -TEST_F(RealPTPEngineTest, Deinitialize_WhenNotInitialized_ReturnsTrue) -{ - EXPECT_TRUE(engine_->Deinitialize()); -} - -TEST_F(RealPTPEngineTest, Deinitialize_AfterInitialize_ReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - ASSERT_TRUE(engine_->Initialize()); - EXPECT_TRUE(engine_->Deinitialize()); -} - -TEST_F(RealPTPEngineTest, Deinitialize_CalledTwice_BothReturnTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - ASSERT_TRUE(engine_->Initialize()); - EXPECT_TRUE(engine_->Deinitialize()); - EXPECT_TRUE(engine_->Deinitialize()); -} - -TEST_F(RealPTPEngineTest, ReInitialize_AfterDeinitialize_Succeeds) -{ - ASSERT_TRUE(pub_.Init(name_)); - ASSERT_TRUE(engine_->Initialize()); - ASSERT_TRUE(engine_->Deinitialize()); - EXPECT_TRUE(engine_->Initialize()); -} - -// ── ReadPTPSnapshot ─────────────────────────────────────────────────────────── - -TEST_F(RealPTPEngineTest, ReadPTPSnapshot_WhenNotInitialized_ReturnsFalse) -{ - PtpTimeInfo info{}; - EXPECT_FALSE(engine_->ReadPTPSnapshot(info)); -} - -TEST_F(RealPTPEngineTest, ReadPTPSnapshot_WithPublishedData_ReturnsTrue) -{ - ASSERT_TRUE(pub_.Init(name_)); - pub_.Publish(MakeTestInfo()); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo result{}; - EXPECT_TRUE(engine_->ReadPTPSnapshot(result)); -} - -TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesTimeAndStatusCorrectly) -{ - ASSERT_TRUE(pub_.Init(name_)); - const PtpTimeInfo expected = MakeTestInfo(); - pub_.Publish(expected); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo result{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); - - EXPECT_EQ(result.ptp_assumed_time, expected.ptp_assumed_time); - EXPECT_DOUBLE_EQ(result.rate_deviation, expected.rate_deviation); - EXPECT_EQ(result.status.is_synchronized, expected.status.is_synchronized); - EXPECT_EQ(result.status.is_correct, expected.status.is_correct); - EXPECT_EQ(result.status.is_timeout, expected.status.is_timeout); -} - -TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesSyncFupDataCorrectly) -{ - ASSERT_TRUE(pub_.Init(name_)); - const PtpTimeInfo expected = MakeTestInfo(); - pub_.Publish(expected); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo result{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); - - EXPECT_EQ(result.sync_fup_data.precise_origin_timestamp, expected.sync_fup_data.precise_origin_timestamp); - EXPECT_EQ(result.sync_fup_data.reference_global_timestamp, expected.sync_fup_data.reference_global_timestamp); - EXPECT_EQ(result.sync_fup_data.sequence_id, expected.sync_fup_data.sequence_id); - EXPECT_EQ(result.sync_fup_data.pdelay, expected.sync_fup_data.pdelay); - EXPECT_EQ(result.sync_fup_data.clock_identity, expected.sync_fup_data.clock_identity); -} - -TEST_F(RealPTPEngineTest, ReadPTPSnapshot_CopiesPDelayDataCorrectly) -{ - ASSERT_TRUE(pub_.Init(name_)); - const PtpTimeInfo expected = MakeTestInfo(); - pub_.Publish(expected); - ASSERT_TRUE(engine_->Initialize()); - - PtpTimeInfo result{}; - ASSERT_TRUE(engine_->ReadPTPSnapshot(result)); - - EXPECT_EQ(result.pdelay_data.pdelay, expected.pdelay_data.pdelay); - EXPECT_EQ(result.pdelay_data.req_port_number, expected.pdelay_data.req_port_number); - EXPECT_EQ(result.pdelay_data.resp_port_number, expected.pdelay_data.resp_port_number); - EXPECT_EQ(result.pdelay_data.req_clock_identity, expected.pdelay_data.req_clock_identity); - EXPECT_EQ(result.pdelay_data.resp_clock_identity, expected.pdelay_data.resp_clock_identity); -} - - -} // namespace details -} // namespace td -} // namespace score diff --git a/score/TimeDaemon/code/ptp_machine/real/BUILD b/score/TimeDaemon/code/ptp_machine/shm/BUILD similarity index 79% rename from score/TimeDaemon/code/ptp_machine/real/BUILD rename to score/TimeDaemon/code/ptp_machine/shm/BUILD index 5738111..96fbc14 100644 --- a/score/TimeDaemon/code/ptp_machine/real/BUILD +++ b/score/TimeDaemon/code/ptp_machine/shm/BUILD @@ -15,30 +15,30 @@ load("@score_baselibs//:bazel/unit_tests.bzl", "cc_unit_test_suites_for_host_and load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") cc_library( - name = "gptp_real_machine", + name = "gptp_shm_machine", srcs = [ "factory.cpp", ], hdrs = [ "factory.h", - "gptp_real_machine.h", + "gptp_shm_machine.h", ], features = COMPILER_WARNING_FEATURES, tags = ["QM"], visibility = ["//score/TimeDaemon:__subpackages__"], deps = [ "//score/TimeDaemon/code/ptp_machine/core:ptp_machine", - "//score/TimeDaemon/code/ptp_machine/real/details:shm_ptp_engine", + "//score/TimeDaemon/code/ptp_machine/shm/details:shm_ptp_engine", "//score/libTSClient:gptp_ipc", ], ) cc_test( - name = "gptp_real_machine_test", - srcs = ["gptp_real_machine_test.cpp"], + name = "gptp_shm_machine_test", + srcs = ["gptp_shm_machine_test.cpp"], tags = ["unit"], deps = [ - ":gptp_real_machine", + ":gptp_shm_machine", "//score/libTSClient:gptp_ipc", "@googletest//:gtest", "@googletest//:gtest_main", @@ -48,9 +48,9 @@ cc_test( cc_unit_test_suites_for_host_and_qnx( name = "unit_test_suite", - cc_unit_tests = [":gptp_real_machine_test"], + cc_unit_tests = [":gptp_shm_machine_test"], test_suites_from_sub_packages = [ - "//score/TimeDaemon/code/ptp_machine/real/details:unit_test_suite", # shm_ptp_engine + "//score/TimeDaemon/code/ptp_machine/shm/details:unit_test_suite", ], visibility = ["//score/TimeDaemon:__subpackages__"], ) diff --git a/score/TimeDaemon/code/ptp_machine/real/details/BUILD b/score/TimeDaemon/code/ptp_machine/shm/details/BUILD similarity index 100% rename from score/TimeDaemon/code/ptp_machine/real/details/BUILD rename to score/TimeDaemon/code/ptp_machine/shm/details/BUILD diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.cpp similarity index 98% rename from score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp rename to score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.cpp index 75e9580..e202c15 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.cpp +++ b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.cpp @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" +#include "score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h" #include "score/TimeDaemon/code/common/logging_contexts.h" #include "score/libTSClient/gptp_ipc_data.h" diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h similarity index 88% rename from score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h rename to score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h index b455da7..9a9055c 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h +++ b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h @@ -10,8 +10,8 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H -#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_DETAILS_SHM_PTP_ENGINE_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_DETAILS_SHM_PTP_ENGINE_H #include "score/TimeDaemon/code/common/data_types/ptp_time_info.h" #include "score/libTSClient/gptp_ipc_receiver.h" @@ -59,4 +59,4 @@ class ShmPTPEngine final } // namespace td } // namespace score -#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_DETAILS_SHM_PTP_ENGINE_H +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_DETAILS_SHM_PTP_ENGINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine_test.cpp similarity index 98% rename from score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp rename to score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine_test.cpp index b89243a..e67324b 100644 --- a/score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine_test.cpp @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" +#include "score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h" #include "score/libTSClient/gptp_ipc_publisher.h" diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.cpp b/score/TimeDaemon/code/ptp_machine/shm/factory.cpp similarity index 74% rename from score/TimeDaemon/code/ptp_machine/real/factory.cpp rename to score/TimeDaemon/code/ptp_machine/shm/factory.cpp index a9e9027..d863159 100644 --- a/score/TimeDaemon/code/ptp_machine/real/factory.cpp +++ b/score/TimeDaemon/code/ptp_machine/shm/factory.cpp @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/factory.h" +#include "score/TimeDaemon/code/ptp_machine/shm/factory.h" namespace score { namespace td { -std::shared_ptr CreateGPTPRealMachine(const std::string& name, const std::string& ipc_name) +std::shared_ptr CreateGPTPShmMachine(const std::string& name, const std::string& ipc_name) { constexpr std::chrono::milliseconds updateInterval(50); - return std::make_shared(name, updateInterval, ipc_name); + return std::make_shared(name, updateInterval, ipc_name); } } // namespace td diff --git a/score/TimeDaemon/code/ptp_machine/real/factory.h b/score/TimeDaemon/code/ptp_machine/shm/factory.h similarity index 68% rename from score/TimeDaemon/code/ptp_machine/real/factory.h rename to score/TimeDaemon/code/ptp_machine/shm/factory.h index d32bbaf..65a5f4c 100644 --- a/score/TimeDaemon/code/ptp_machine/real/factory.h +++ b/score/TimeDaemon/code/ptp_machine/shm/factory.h @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H -#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_FACTORY_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_FACTORY_H -#include "score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h" +#include "score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine.h" #include "score/libTSClient/gptp_ipc_channel.h" #include @@ -25,20 +25,20 @@ namespace td { /** - * @brief Factory function to create a configured GPTPRealMachine. + * @brief Factory function to create a configured GPTPShmMachine. * - * Creates a GPTPRealMachine backed by the real gPTP engine. + * Creates a GPTPShmMachine backed by the shared-memory gPTP engine. * The engine reads PtpTimeInfo snapshots published by TimeSlave via * the IPC channel named @p ipc_name. * * @param name Logical name for the machine instance. * @param ipc_name IPC channel name (default: kGptpIpcName). - * @return A fully configured GPTPRealMachine instance. + * @return A fully configured GPTPShmMachine instance. */ -std::shared_ptr CreateGPTPRealMachine(const std::string& name, +std::shared_ptr CreateGPTPShmMachine(const std::string& name, const std::string& ipc_name = score::ts::details::kGptpIpcName); } // namespace td } // namespace score -#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_FACTORY_H +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_FACTORY_H diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h b/score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine.h similarity index 62% rename from score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h rename to score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine.h index c8013e7..0da9f41 100644 --- a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h +++ b/score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine.h @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H -#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H +#ifndef SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_GPTP_SHM_MACHINE_H +#define SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_GPTP_SHM_MACHINE_H #include "score/TimeDaemon/code/ptp_machine/core/ptp_machine.h" -#include "score/TimeDaemon/code/ptp_machine/real/details/shm_ptp_engine.h" +#include "score/TimeDaemon/code/ptp_machine/shm/details/shm_ptp_engine.h" namespace score { @@ -24,15 +24,15 @@ namespace td /// @brief PTPMachine instantiated with the shared-memory gPTP engine. /// /// Reads PtpTimeInfo snapshots written by TimeSlave via the IPC channel. -/// Construct via CreateGPTPRealMachine() (see factory.h) or directly: +/// Construct via CreateGPTPShmMachine() (see factory.h) or directly: /// /// @code -/// auto machine = std::make_shared( -/// "real", std::chrono::milliseconds{50}, "/gptp_ptp_info"); +/// auto machine = std::make_shared( +/// "shm", std::chrono::milliseconds{50}, "/gptp_ptp_info"); /// @endcode -using GPTPRealMachine = PTPMachine; +using GPTPShmMachine = PTPMachine; } // namespace td } // namespace score -#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_REAL_GPTP_REAL_MACHINE_H +#endif // SCORE_TIMEDAEMON_CODE_PTP_MACHINE_SHM_GPTP_SHM_MACHINE_H diff --git a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp b/score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine_test.cpp similarity index 77% rename from score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp rename to score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine_test.cpp index 9511fe6..0d96d07 100644 --- a/score/TimeDaemon/code/ptp_machine/real/gptp_real_machine_test.cpp +++ b/score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine_test.cpp @@ -10,8 +10,8 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/TimeDaemon/code/ptp_machine/real/gptp_real_machine.h" -#include "score/TimeDaemon/code/ptp_machine/real/factory.h" +#include "score/TimeDaemon/code/ptp_machine/shm/gptp_shm_machine.h" +#include "score/TimeDaemon/code/ptp_machine/shm/factory.h" #include "score/libTSClient/gptp_ipc_publisher.h" #include @@ -50,7 +50,7 @@ score::ts::GptpIpcData MakePublishedInfo() } // namespace -class GPTPRealMachineIntegrationTest : public ::testing::Test +class GPTPShmMachineIntegrationTest : public ::testing::Test { protected: void SetUp() override @@ -59,7 +59,7 @@ class GPTPRealMachineIntegrationTest : public ::testing::Test ASSERT_TRUE(pub_.Init(name_)); pub_.Publish(MakePublishedInfo()); - machine_ = CreateGPTPRealMachine("RealPTPMachine", name_); + machine_ = CreateGPTPShmMachine("ShmPTPMachine", name_); machine_->SetPublishCallback([this](const PtpTimeInfo& data) { { std::lock_guard lk(mu_); @@ -78,29 +78,29 @@ class GPTPRealMachineIntegrationTest : public ::testing::Test std::string name_; score::ts::details::GptpIpcPublisher pub_; - std::shared_ptr machine_; + std::shared_ptr machine_; std::promise promise_; PtpTimeInfo published_{}; std::mutex mu_; }; -TEST_F(GPTPRealMachineIntegrationTest, GetName_ReturnsConstructionName) +TEST_F(GPTPShmMachineIntegrationTest, GetName_ReturnsConstructionName) { - EXPECT_EQ(machine_->GetName(), "RealPTPMachine"); + EXPECT_EQ(machine_->GetName(), "ShmPTPMachine"); } -TEST_F(GPTPRealMachineIntegrationTest, Init_WhenShmExists_ReturnsTrue) +TEST_F(GPTPShmMachineIntegrationTest, Init_WhenShmExists_ReturnsTrue) { EXPECT_TRUE(machine_->Init()); } -TEST_F(GPTPRealMachineIntegrationTest, Init_WhenShmMissing_ReturnsFalse) +TEST_F(GPTPShmMachineIntegrationTest, Init_WhenShmMissing_ReturnsFalse) { - auto m = CreateGPTPRealMachine("NoShm", "/gptp_nosuchshm_xyz"); + auto m = CreateGPTPShmMachine("NoShm", "/gptp_nosuchshm_xyz"); EXPECT_FALSE(m->Init()); } -TEST_F(GPTPRealMachineIntegrationTest, Start_DeliversPublishedData_ViaCallback) +TEST_F(GPTPShmMachineIntegrationTest, Start_DeliversPublishedData_ViaCallback) { ASSERT_TRUE(machine_->Init()); machine_->Start(); @@ -117,7 +117,7 @@ TEST_F(GPTPRealMachineIntegrationTest, Start_DeliversPublishedData_ViaCallback) EXPECT_EQ(published_.sync_fup_data.pdelay, 1'000U); } -TEST_F(GPTPRealMachineIntegrationTest, Init_CalledTwice_SecondCallReturnsSameResult) +TEST_F(GPTPShmMachineIntegrationTest, Init_CalledTwice_SecondCallReturnsSameResult) { ASSERT_TRUE(machine_->Init()); EXPECT_TRUE(machine_->Init()); diff --git a/score/TimeSlave/code/common/logging_contexts.h b/score/TimeSlave/code/common/logging_contexts.h index 57ccf91..e432223 100644 --- a/score/TimeSlave/code/common/logging_contexts.h +++ b/score/TimeSlave/code/common/logging_contexts.h @@ -19,10 +19,10 @@ namespace ts { /// Logging context for the gPTP protocol engine (RxThread / PdelayThread). -constexpr auto kGPtpMachineContext = "GPTP_SLAVE"; +constexpr auto kGPtpMachineContext = "GTPS"; /// Logging context for the TimeSlave application lifecycle (Initialize / Run). -constexpr auto kTimeSlaveAppContext = "TS_APP"; +constexpr auto kTimeSlaveAppContext = "TSAP"; } // namespace ts } // namespace score diff --git a/score/TimeSlave/code/gptp/details/ptp_types.h b/score/TimeSlave/code/gptp/details/ptp_types.h index ec4c4ac..bbdbcb8 100644 --- a/score/TimeSlave/code/gptp/details/ptp_types.h +++ b/score/TimeSlave/code/gptp/details/ptp_types.h @@ -175,6 +175,7 @@ struct PTPMessage TmvT sendHardwareTS{}; TmvT parseMessageTs{}; TmvT recvHardwareTS{}; + std::int64_t recvMonoNs{0}; // CLOCK_MONOTONIC at packet reception; set for Sync only }; static_assert(sizeof(PTPMessage) <= 1600, "PTPMessage too large"); diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp index 3c78b02..e3cd0dc 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.cpp +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.cpp @@ -108,6 +108,7 @@ SyncResult SyncStateMachine::BuildResult(const PTPMessage& sync, const PTPMessag SyncResult r{}; r.master_ns = master_ns; r.offset_ns = offset_ns; + r.sync_mono_ns = sync.recvMonoNs; if (has_previous_master_) { diff --git a/score/TimeSlave/code/gptp/details/sync_state_machine.h b/score/TimeSlave/code/gptp/details/sync_state_machine.h index e073045..4a0d479 100644 --- a/score/TimeSlave/code/gptp/details/sync_state_machine.h +++ b/score/TimeSlave/code/gptp/details/sync_state_machine.h @@ -32,6 +32,7 @@ struct SyncResult { std::int64_t master_ns{0}; ///< Grandmaster time (ns since epoch) std::int64_t offset_ns{0}; ///< local hw_ts − master_ns + std::int64_t sync_mono_ns{0}; ///< CLOCK_MONOTONIC when the Sync frame was received score::ts::GptpIpcSyncFupData sync_fup_data{}; ///< Ready to copy into GptpIpcData (pdelay field filled by engine) bool is_time_jump_future{false}; bool is_time_jump_past{false}; diff --git a/score/TimeSlave/code/gptp/gptp_engine.cpp b/score/TimeSlave/code/gptp/gptp_engine.cpp index 915e18b..782725c 100644 --- a/score/TimeSlave/code/gptp/gptp_engine.cpp +++ b/score/TimeSlave/code/gptp/gptp_engine.cpp @@ -74,7 +74,7 @@ bool GptpEngine::Initialize() if (!identity_->Resolve(opts_.iface_name)) { - score::mw::log::LogError(kGPtpMachineContext) + score::mw::log::LogError(kTimeSlaveAppContext) << "GptpEngine: failed to resolve ClockIdentity for " << opts_.iface_name; return false; } @@ -83,14 +83,14 @@ bool GptpEngine::Initialize() if (!socket_->Open(opts_.iface_name)) { - score::mw::log::LogError(kGPtpMachineContext) + score::mw::log::LogError(kTimeSlaveAppContext) << "GptpEngine: failed to open raw socket on " << opts_.iface_name; return false; } if (!socket_->EnableHwTimestamping()) { - score::mw::log::LogWarn(kGPtpMachineContext) + score::mw::log::LogWarn(kTimeSlaveAppContext) << "GptpEngine: HW timestamping not available on " << opts_.iface_name << ", falling back to SW timestamps"; } @@ -104,7 +104,7 @@ bool GptpEngine::Initialize() } catch (const std::system_error& e) { - score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to create RxThread: " << std::string_view{e.what()}; + score::mw::log::LogError(kTimeSlaveAppContext) << "GptpEngine: failed to create RxThread: " << std::string_view{e.what()}; running_.store(false, std::memory_order_release); socket_->Close(); return false; @@ -116,12 +116,12 @@ bool GptpEngine::Initialize() } catch (const std::system_error& e) { - score::mw::log::LogError(kGPtpMachineContext) << "GptpEngine: failed to create PdelayThread: " << std::string_view{e.what()}; + score::mw::log::LogError(kTimeSlaveAppContext) << "GptpEngine: failed to create PdelayThread: " << std::string_view{e.what()}; Deinitialize(); return false; } - score::mw::log::LogInfo(kGPtpMachineContext) << "GptpEngine initialized on " << opts_.iface_name; + score::mw::log::LogInfo(kTimeSlaveAppContext) << "GptpEngine initialized on " << opts_.iface_name; return true; } @@ -137,7 +137,7 @@ bool GptpEngine::Deinitialize() if (pdelay_thread_.joinable()) pdelay_thread_.join(); - score::mw::log::LogInfo(kGPtpMachineContext) << "GptpEngine deinitialized"; + score::mw::log::LogInfo(kTimeSlaveAppContext) << "GptpEngine deinitialized"; return true; } @@ -253,6 +253,7 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp { case kPtpMsgtypeSync: msg.recvHardwareTS = hw_ts; + msg.recvMonoNs = MonoNs(); sync_sm_.OnSync(msg); break; @@ -305,8 +306,7 @@ void GptpEngine::UpdateSnapshot(const SyncResult& sync, const PDelayResult& pdel const std::int64_t local_rx_ns = static_cast(sync.sync_fup_data.reference_local_timestamp); pending_snapshot_.ptp_assumed_time = std::chrono::nanoseconds{local_rx_ns - sync.offset_ns}; - // Capture local_time as close as possible to Sync frame handling to minimise jitter. - pending_snapshot_.local_time = std::chrono::nanoseconds{MonoNs()}; + pending_snapshot_.local_time = std::chrono::nanoseconds{sync.sync_mono_ns}; pending_snapshot_.rate_deviation = rate_ratio; pending_snapshot_.status.is_synchronized = true; diff --git a/score/examples/BUILD b/score/examples/BUILD new file mode 100644 index 0000000..e77bde2 --- /dev/null +++ b/score/examples/BUILD @@ -0,0 +1,47 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Demo programs for board-level functional verification of TimeSlave + libTSClient. +# +# Typical workflow (three terminals on the board): +# Terminal 1: ./gptp_master [iface] — test gPTP master +# Terminal 2: ./timeslave_standalone [iface] — gPTP slave + shared-memory publisher +# Terminal 3: ./time_reader — read and display shared-memory data + +cc_binary( + name = "time_reader", + srcs = ["time_reader.cpp"], + deps = [ + "//score/libTSClient:gptp_ipc", + ], +) + +cc_binary( + name = "timeslave_standalone", + srcs = ["timeslave_standalone.cpp"], + deps = [ + "//score/TimeSlave/code/gptp:gptp_engine", + "//score/libTSClient:gptp_ipc", + "@score_baselibs//score/mw/log:console_only_backend", + ], +) + +cc_binary( + name = "gptp_master", + srcs = ["gptp_master.cpp"], + target_compatible_with = ["@platforms//os:qnx"], + linkopts = ["-lsocket"], + deps = [ + "//score/TimeSlave/code/gptp/details:ptp_types", + ], +) diff --git a/score/examples/gptp_master.cpp b/score/examples/gptp_master.cpp new file mode 100644 index 0000000..8ac9275 --- /dev/null +++ b/score/examples/gptp_master.cpp @@ -0,0 +1,324 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/** + * @brief Minimal gPTP master for single-board PHC-slewing tests — QNX / EMAC. + * + * Sends Sync + FollowUp (two-step) at 1 Hz on a raw BPF socket. + * The preciseOriginTimestamp in each FollowUp is taken from CLOCK_REALTIME + * immediately after the corresponding Sync write. The slave computes: + * + * rate_ratio = ΔtPHC_rx / ΔtREALTIME_origin + * + * and slews its PHC toward CLOCK_REALTIME. + * + * Same-board single-interface usage: + * The slave's BPF fd must be opened with BIOCSSEESENT=1 so that it sees + * outgoing frames from this process without a physical loopback cable. + * (qnx_raw_shim.cpp is already patched to set BIOCSSEESENT=1.) + * + * Terminal A: ./gptp_master emac0 + * Terminal B: ./timeslave_standalone emac0 + * + * Cross-board usage (standard gPTP topology): + * Board 1 (master): ./gptp_master emac0 + * Board 2 (slave): ./timeslave_standalone emac0 + * + * Usage: + * ./gptp_master [interface] + * ./gptp_master emac0 + */ + +#include "score/TimeSlave/code/gptp/details/ptp_types.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +volatile sig_atomic_t g_running = 1; // NOLINT + +void SigHandler(int /*sig*/) noexcept { g_running = 0; } + +// IEEE 802.1AS / PTP layer-2 multicast destination MAC. +constexpr std::array kPtpMcastMac{0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E}; + +// Minimal packed Ethernet header (avoid pulling in system ethhdr which may +// differ between QNX versions). +struct SCORE_TS_PACKED MasterEthHdr +{ + std::uint8_t dst[6]; + std::uint8_t src[6]; + std::uint16_t etype; +}; + +// --------------------------------------------------------------------------- +// Helper: write a network-byte-order Timestamp from a timespec. +// The parser (LoadTimestamp) applies ntohs/ntohl when reading, so we must +// write big-endian values here. +// --------------------------------------------------------------------------- +void EncodeNetTimestamp(score::ts::details::Timestamp& out, + const struct timespec& ts) noexcept +{ + const auto total_sec = static_cast(ts.tv_sec); + out.seconds_msb = htons(static_cast((total_sec >> 32U) & 0xFFFFU)); + out.seconds_lsb = htonl(static_cast(total_sec & 0xFFFFFFFFU)); + out.nanoseconds = htonl(static_cast(ts.tv_nsec)); +} + +// --------------------------------------------------------------------------- +// Helper: derive EUI-64 ClockIdentity from a 6-byte MAC (EUI-48 → EUI-64). +// --------------------------------------------------------------------------- +score::ts::details::ClockIdentity MakeClock( + const std::array& mac) noexcept +{ + score::ts::details::ClockIdentity id{}; + id.id[0] = mac[0]; + id.id[1] = mac[1]; + id.id[2] = mac[2]; + id.id[3] = 0xFFU; + id.id[4] = 0xFEU; + id.id[5] = mac[3]; + id.id[6] = mac[4]; + id.id[7] = mac[5]; + return id; +} + +// --------------------------------------------------------------------------- +// GetMac: read the hardware MAC of a network interface via getifaddrs. +// --------------------------------------------------------------------------- +bool GetMac(const char* ifname, + std::array& mac) noexcept +{ + struct ifaddrs* list = nullptr; + if (::getifaddrs(&list) != 0) + return false; + + bool found = false; + for (const struct ifaddrs* ifa = list; ifa != nullptr; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == nullptr) + continue; + if (ifa->ifa_addr->sa_family != AF_LINK) + continue; + if (std::strcmp(ifa->ifa_name, ifname) != 0) + continue; + const auto* dl = + reinterpret_cast(ifa->ifa_addr); + if (dl->sdl_alen >= static_cast(mac.size())) + { + std::memcpy(mac.data(), LLADDR(dl), mac.size()); + found = true; + } + break; + } + ::freeifaddrs(list); + return found; +} + +// --------------------------------------------------------------------------- +// OpenBpf: open a BPF fd bound to the given interface for raw TX. +// --------------------------------------------------------------------------- +int OpenBpf(const char* ifname) noexcept +{ + char devpath[256]{}; + const char* sock_env = std::getenv("SOCK"); + if (sock_env != nullptr && sock_env[0] != '\0') + std::snprintf(devpath, sizeof(devpath), "%s/dev/bpf0", sock_env); + else + std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); + + const int fd = ::open(devpath, O_RDWR); + if (fd < 0) + { + std::perror("[gptp_master] open /dev/bpf"); + return -1; + } + + ::ifreq ifr{}; + ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); + if (::ioctl(fd, BIOCSETIF, &ifr) < 0) + { + std::perror("[gptp_master] BIOCSETIF"); + ::close(fd); + return -1; + } + + // Immediate mode: no read buffering delay (not required for TX-only, but + // keeps the fd consistent if we ever add RX later). + u_int one = 1U; + (void)::ioctl(fd, BIOCIMMEDIATE, &one); + + return fd; +} + +// --------------------------------------------------------------------------- +// SendSync: build and write a two-step Sync frame; record send time in ts_out. +// --------------------------------------------------------------------------- +void SendSync(int bpf_fd, + const std::array& src_mac, + const score::ts::details::ClockIdentity& clock_id, + std::uint16_t seqnum, + struct timespec& ts_out) noexcept +{ + using namespace score::ts::details; + + // PTP payload + SyncBody sync{}; + sync.ptpHdr.tsmt = kPtpMsgtypeSync | kPtpTransportSpecific; + sync.ptpHdr.version = kPtpVersion; + sync.ptpHdr.messageLength = htons(static_cast(sizeof(SyncBody))); + sync.ptpHdr.flagField[0] = 0x02U; // twoStepFlag (two-step clock) + sync.ptpHdr.flagField[1] = 0x00U; + sync.ptpHdr.correctionField = 0; + sync.ptpHdr.reserved2 = 0; + sync.ptpHdr.sourcePortIdentity.clockIdentity = clock_id; + sync.ptpHdr.sourcePortIdentity.portNumber = htons(1U); + sync.ptpHdr.sequenceId = htons(seqnum); + sync.ptpHdr.controlField = static_cast(ControlField::kSync); + sync.ptpHdr.logMessageInterval = 0; // 1 s interval (2^0) + // originTimestamp = 0 (two-step; precise time comes in FollowUp) + + // Assemble Ethernet frame + constexpr std::size_t kFrameLen = sizeof(MasterEthHdr) + sizeof(SyncBody); + std::uint8_t frame[kFrameLen]{}; + auto* eth = reinterpret_cast(frame); + std::memcpy(eth->dst, kPtpMcastMac.data(), 6U); + std::memcpy(eth->src, src_mac.data(), 6U); + eth->etype = htons(0x88F7U); // ETH_P_1588 + std::memcpy(frame + sizeof(MasterEthHdr), &sync, sizeof(SyncBody)); + + // Send frame; record CLOCK_REALTIME after write as origin timestamp. + // Post-send timestamp is closer to actual wire-egress time than pre-send. + (void)::write(bpf_fd, frame, kFrameLen); + (void)::clock_gettime(CLOCK_REALTIME, &ts_out); +} + +// --------------------------------------------------------------------------- +// SendFollowUp: build and write a FollowUp carrying the Sync origin timestamp. +// --------------------------------------------------------------------------- +void SendFollowUp(int bpf_fd, + const std::array& src_mac, + const score::ts::details::ClockIdentity& clock_id, + std::uint16_t seqnum, + const struct timespec& origin_ts) noexcept +{ + using namespace score::ts::details; + + FollowUpBody fup{}; + fup.ptpHdr.tsmt = kPtpMsgtypeFollowUp | kPtpTransportSpecific; + fup.ptpHdr.version = kPtpVersion; + fup.ptpHdr.messageLength = htons(static_cast(sizeof(FollowUpBody))); + fup.ptpHdr.flagField[0] = 0x00U; + fup.ptpHdr.flagField[1] = 0x00U; + fup.ptpHdr.correctionField = 0; + fup.ptpHdr.reserved2 = 0; + fup.ptpHdr.sourcePortIdentity.clockIdentity = clock_id; + fup.ptpHdr.sourcePortIdentity.portNumber = htons(1U); + fup.ptpHdr.sequenceId = htons(seqnum); + fup.ptpHdr.controlField = static_cast(ControlField::kFollowUp); + fup.ptpHdr.logMessageInterval = 0; + + // Encode the Sync send time as the preciseOriginTimestamp (network byte order). + EncodeNetTimestamp(fup.preciseOriginTimestamp, origin_ts); + + constexpr std::size_t kFrameLen = sizeof(MasterEthHdr) + sizeof(FollowUpBody); + std::uint8_t frame[kFrameLen]{}; + auto* eth = reinterpret_cast(frame); + std::memcpy(eth->dst, kPtpMcastMac.data(), 6U); + std::memcpy(eth->src, src_mac.data(), 6U); + eth->etype = htons(0x88F7U); + std::memcpy(frame + sizeof(MasterEthHdr), &fup, sizeof(FollowUpBody)); + + (void)::write(bpf_fd, frame, kFrameLen); +} + +} // namespace + +int main(int argc, char* argv[]) +{ + std::signal(SIGINT, SigHandler); + std::signal(SIGTERM, SigHandler); + + const char* iface = (argc >= 2) ? argv[1] : "emac0"; + std::printf("[gptp_master] interface = %s\n", iface); + + // Resolve interface MAC address. + std::array src_mac{}; + if (!GetMac(iface, src_mac)) + { + std::fprintf(stderr, "[gptp_master] ERROR: cannot read MAC for %s\n", iface); + return 1; + } + std::printf("[gptp_master] MAC = %02X:%02X:%02X:%02X:%02X:%02X\n", + src_mac[0], src_mac[1], src_mac[2], + src_mac[3], src_mac[4], src_mac[5]); + + const score::ts::details::ClockIdentity clock_id = MakeClock(src_mac); + + const int bpf_fd = OpenBpf(iface); + if (bpf_fd < 0) + return 1; + + std::printf("[gptp_master] Sending Sync+FUP every 1 s — Ctrl+C to stop\n"); + std::printf("[gptp_master] preciseOriginTimestamp source: CLOCK_REALTIME\n\n"); + + std::uint16_t seqnum = 0U; + + // Anchor the send loop to a monotonic base so 1-second intervals are tight. + struct timespec next{}; + (void)::clock_gettime(CLOCK_MONOTONIC, &next); + + while (g_running != 0) + { + // --- Send Sync then immediately FollowUp --- + struct timespec origin_ts{}; + SendSync(bpf_fd, src_mac, clock_id, seqnum, origin_ts); + SendFollowUp(bpf_fd, src_mac, clock_id, seqnum, origin_ts); + + std::printf("[%5u] Sync+FUP origin=%lld.%09ld\n", + static_cast(seqnum), + static_cast(origin_ts.tv_sec), + origin_ts.tv_nsec); + + ++seqnum; // uint16_t: wraps naturally at 0xFFFF + + // --- Sleep until next 1-second tick --- + const std::int64_t next_ns = + static_cast(next.tv_sec) * 1'000'000'000LL + + next.tv_nsec + 1'000'000'000LL; + next.tv_sec = static_cast(next_ns / 1'000'000'000LL); + next.tv_nsec = static_cast(next_ns % 1'000'000'000LL); + (void)::clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, nullptr); + } + + std::printf("\n[gptp_master] Stopped after %u Sync pairs\n", + static_cast(seqnum)); + ::close(bpf_fd); + return 0; +} diff --git a/score/examples/time_reader.cpp b/score/examples/time_reader.cpp new file mode 100644 index 0000000..bdf9343 --- /dev/null +++ b/score/examples/time_reader.cpp @@ -0,0 +1,159 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/** + * @brief Demo: libTSClient GptpIpcReceiver consumer. + * + * Opens the shared memory segment written by TimeSlave (or time_publisher) + * and continuously reads + displays the GptpIpcData using the seqlock protocol. + * + * Usage: + * bazel run --config time-x86_64-linux //score/examples:time_reader + * + * Requires time_publisher (or real TimeSlave) to be running first. + */ + +#include "score/libTSClient/gptp_ipc_receiver.h" +#include "score/libTSClient/gptp_ipc_data.h" +#include "score/libTSClient/gptp_ipc_channel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +volatile sig_atomic_t g_running = 1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +void SignalHandler(int /*sig*/) noexcept +{ + g_running = 0; +} + +/// Return a formatted wall-clock timestamp string "HH:MM:SS.mmm". +std::string Timestamp() +{ + const auto now = std::chrono::system_clock::now(); + const auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + const std::time_t t = std::chrono::system_clock::to_time_t(now); + std::tm tm_buf{}; + localtime_r(&t, &tm_buf); + char buf[16]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm_buf); + std::ostringstream oss; + oss << buf << '.' << std::setw(3) << std::setfill('0') << ms.count(); + return oss.str(); +} + +void PrintData(const score::ts::GptpIpcData& d) +{ + const double ptp_sec = static_cast(d.ptp_assumed_time.count()) * 1e-9; + const double local_sec = static_cast(d.local_time.count()) * 1e-9; + + std::cout << " ptp_assumed_time : " << std::fixed << std::setprecision(9) << ptp_sec << " s\n"; + std::cout << " local_time : " << std::fixed << std::setprecision(9) << local_sec << " s\n"; + std::cout << " rate_deviation : " << std::scientific << std::setprecision(3) << d.rate_deviation << '\n'; + + // Status flags + std::cout << " status :" + << (d.status.is_synchronized ? " [SYNC]" : " [NO-SYNC]") + << (d.status.is_timeout ? " [TIMEOUT]" : "") + << (d.status.is_time_jump_future ? " [JUMP-FUTURE]" : "") + << (d.status.is_time_jump_past ? " [JUMP-PAST]" : "") + << (d.status.is_correct ? " [CORRECT]" : " [INCORRECT]") + << '\n'; + + // Sync+FollowUp data + const auto& s = d.sync_fup_data; + std::cout << " sync_fup.seq_id : " << s.sequence_id << '\n'; + std::cout << " sync_fup.pdelay : " << s.pdelay << " ns\n"; + std::cout << " sync_fup.port : " << s.port_number << '\n'; + std::cout << " sync_fup.clk_id : 0x" + << std::hex << std::setw(16) << std::setfill('0') << s.clock_identity + << std::dec << '\n'; + + // Peer delay data + const auto& p = d.pdelay_data; + std::cout << " pdelay.pdelay : " << p.pdelay << " ns\n"; + std::cout << " pdelay.req_port : " << p.req_port_number << '\n'; + std::cout << " pdelay.resp_port : " << p.resp_port_number << '\n'; +} + +} // namespace + +int main() +{ + std::signal(SIGINT, SignalHandler); + std::signal(SIGTERM, SignalHandler); + + score::ts::details::GptpIpcReceiver receiver; + + std::cout << "[time_reader] Waiting for shared memory '" + << score::ts::details::kGptpIpcName << "' ...\n"; + + // Retry Init until the publisher creates the shared memory segment + while (g_running != 0) + { + if (receiver.Init()) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds{200}); + } + + if (g_running == 0) + { + std::cout << "\n[time_reader] Interrupted before connecting.\n"; + return 0; + } + + std::cout << "[time_reader] Connected. Reading every 100 ms. Press Ctrl+C to stop.\n\n"; + + std::uint64_t read_count = 0U; + std::uint64_t nullopt_count = 0U; + + while (g_running != 0) + { + const auto result = receiver.Receive(); + + if (result.has_value()) + { + ++read_count; + // Print full detail every 10 reads (~1 s), summary otherwise + if (read_count % 10U == 1U) + { + std::cout << "--- [" << Timestamp() << "] read #" << read_count + << " (contention_misses=" << nullopt_count << ") ---\n"; + PrintData(*result); + std::cout << '\n'; + } + } + else + { + ++nullopt_count; + // nullopt means seqlock contention; just retry next cycle + } + + std::this_thread::sleep_for(std::chrono::milliseconds{100}); + } + + std::cout << "\n[time_reader] Stopping. total_reads=" << read_count + << " contention_misses=" << nullopt_count << '\n'; + receiver.Close(); + return 0; +} diff --git a/score/examples/timeslave_standalone.cpp b/score/examples/timeslave_standalone.cpp new file mode 100644 index 0000000..869dc88 --- /dev/null +++ b/score/examples/timeslave_standalone.cpp @@ -0,0 +1,122 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/** + * @brief Standalone TimeSlave — bypasses the lifecycle framework. + * + * Directly calls GptpEngine + GptpIpcPublisher without any lifecycle manager, + * suitable for board-level functional verification. + * + * Usage: + * ./timeslave_standalone [interface] + * ./timeslave_standalone emac0 + * + * Defaults to "emac0" if no argument is given. + */ + +#include "score/TimeSlave/code/gptp/gptp_engine.h" +#include "score/libTSClient/gptp_ipc_publisher.h" +#include "score/libTSClient/gptp_ipc_data.h" + +#include +#include +#include +#include +#include + +namespace +{ +volatile sig_atomic_t g_running = 1; // NOLINT + +void SignalHandler(int /*sig*/) noexcept { g_running = 0; } +} // namespace + +int main(int argc, char* argv[]) +{ + std::signal(SIGINT, SignalHandler); + std::signal(SIGTERM, SignalHandler); + + const std::string iface = (argc >= 2) ? argv[1] : "emac0"; + + std::cout << "[timeslave_standalone] interface = " << iface << '\n'; + + // --- Engine options --- + score::ts::details::GptpEngineOptions opts; + opts.iface_name = iface; + opts.pdelay_interval_ms = 1000; + opts.pdelay_warmup_ms = 2000; + opts.sync_timeout_ms = 3300; + opts.phc_config.enabled = true; + opts.phc_config.device = iface; + // On Qualcomm EMAC, PTP_SET_TIME only adjusts an offset register; BPF + // hardware timestamps are unaffected. A high threshold prevents the step + // path from blocking the PI frequency controller. + opts.phc_config.step_threshold_ns = 10'000'000'000LL; + + score::ts::details::GptpEngine engine{opts}; + + if (!engine.Initialize()) + { + std::cerr << "[timeslave_standalone] ERROR: GptpEngine::Initialize() failed\n" + << " Check: interface name correct? Running as root?\n"; + return 1; + } + std::cout << "[timeslave_standalone] GptpEngine initialized\n"; + + // --- Shared memory publisher --- + score::ts::details::GptpIpcPublisher publisher; + if (!publisher.Init()) + { + std::cerr << "[timeslave_standalone] ERROR: GptpIpcPublisher::Init() failed\n"; + engine.Deinitialize(); + return 1; + } + std::cout << "[timeslave_standalone] Shared memory ready: " + << score::ts::details::kGptpIpcName << '\n'; + std::cout << "[timeslave_standalone] Running — Ctrl+C to stop\n\n"; + + constexpr auto kPublishInterval = std::chrono::milliseconds{50}; + std::uint64_t publish_count = 0U; + + while (g_running != 0) + { + engine.FinalizeSnapshot(); + + score::ts::GptpIpcData data{}; + if (engine.ReadPTPSnapshot(data)) + { + publisher.Publish(data); + ++publish_count; + + // Print status every 2 seconds (40 publishes × 50 ms) + if (publish_count % 40U == 0U) + { + const double ptp_sec = static_cast(data.ptp_assumed_time.count()) * 1e-9; + std::cout << "[" << publish_count << "] " + << "ptp=" << ptp_sec << " s" + << " sync=" << std::boolalpha << data.status.is_synchronized + << " timeout=" << data.status.is_timeout + << " correct=" << data.status.is_correct + << " pdelay=" << data.sync_fup_data.pdelay << " ns" + << '\n'; + } + } + + std::this_thread::sleep_for(kPublishInterval); + } + + std::cout << "\n[timeslave_standalone] Stopping\n"; + engine.Deinitialize(); + publisher.Destroy(); + return 0; +} diff --git a/score/libTSClient/gptp_ipc_channel.h b/score/libTSClient/gptp_ipc_channel.h index 428a42d..1a95a04 100644 --- a/score/libTSClient/gptp_ipc_channel.h +++ b/score/libTSClient/gptp_ipc_channel.h @@ -34,7 +34,7 @@ inline constexpr std::uint32_t kGptpIpcMagic = 0x47505450U; /** * @brief Shared memory layout for gPTP IPC (seqlock protocol). * - * Single-writer (TimeSlave), multi-reader (TimeDaemon via RealPTPEngine). + * Single-writer (TimeSlave), multi-reader (TimeDaemon via ShmPTPEngine). * Aligned to 64 bytes (cache line) to avoid false sharing. * * Seqlock protocol: diff --git a/score/libTSClient/gptp_ipc_receiver.h b/score/libTSClient/gptp_ipc_receiver.h index 4b54d01..dd3d440 100644 --- a/score/libTSClient/gptp_ipc_receiver.h +++ b/score/libTSClient/gptp_ipc_receiver.h @@ -29,7 +29,7 @@ namespace details * @brief Multi-reader receiver for the gPTP IPC channel. * * Opens an existing POSIX shared memory segment (read-only) and reads - * PtpTimeInfo using the seqlock protocol. Used by RealPTPEngine. + * PtpTimeInfo using the seqlock protocol. Used by ShmPTPEngine. */ class GptpIpcReceiver final { From 201059d9004b8ad8713a476c014b958f6af382ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Tue, 21 Apr 2026 14:30:27 +0800 Subject: [PATCH 11/12] [ECARX][TimeSlave]Delete the local test demo --- score/examples/BUILD | 47 ---- score/examples/gptp_master.cpp | 324 ------------------------ score/examples/time_reader.cpp | 159 ------------ score/examples/timeslave_standalone.cpp | 122 --------- 4 files changed, 652 deletions(-) delete mode 100644 score/examples/BUILD delete mode 100644 score/examples/gptp_master.cpp delete mode 100644 score/examples/time_reader.cpp delete mode 100644 score/examples/timeslave_standalone.cpp diff --git a/score/examples/BUILD b/score/examples/BUILD deleted file mode 100644 index e77bde2..0000000 --- a/score/examples/BUILD +++ /dev/null @@ -1,47 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -# Demo programs for board-level functional verification of TimeSlave + libTSClient. -# -# Typical workflow (three terminals on the board): -# Terminal 1: ./gptp_master [iface] — test gPTP master -# Terminal 2: ./timeslave_standalone [iface] — gPTP slave + shared-memory publisher -# Terminal 3: ./time_reader — read and display shared-memory data - -cc_binary( - name = "time_reader", - srcs = ["time_reader.cpp"], - deps = [ - "//score/libTSClient:gptp_ipc", - ], -) - -cc_binary( - name = "timeslave_standalone", - srcs = ["timeslave_standalone.cpp"], - deps = [ - "//score/TimeSlave/code/gptp:gptp_engine", - "//score/libTSClient:gptp_ipc", - "@score_baselibs//score/mw/log:console_only_backend", - ], -) - -cc_binary( - name = "gptp_master", - srcs = ["gptp_master.cpp"], - target_compatible_with = ["@platforms//os:qnx"], - linkopts = ["-lsocket"], - deps = [ - "//score/TimeSlave/code/gptp/details:ptp_types", - ], -) diff --git a/score/examples/gptp_master.cpp b/score/examples/gptp_master.cpp deleted file mode 100644 index 8ac9275..0000000 --- a/score/examples/gptp_master.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ - -/** - * @brief Minimal gPTP master for single-board PHC-slewing tests — QNX / EMAC. - * - * Sends Sync + FollowUp (two-step) at 1 Hz on a raw BPF socket. - * The preciseOriginTimestamp in each FollowUp is taken from CLOCK_REALTIME - * immediately after the corresponding Sync write. The slave computes: - * - * rate_ratio = ΔtPHC_rx / ΔtREALTIME_origin - * - * and slews its PHC toward CLOCK_REALTIME. - * - * Same-board single-interface usage: - * The slave's BPF fd must be opened with BIOCSSEESENT=1 so that it sees - * outgoing frames from this process without a physical loopback cable. - * (qnx_raw_shim.cpp is already patched to set BIOCSSEESENT=1.) - * - * Terminal A: ./gptp_master emac0 - * Terminal B: ./timeslave_standalone emac0 - * - * Cross-board usage (standard gPTP topology): - * Board 1 (master): ./gptp_master emac0 - * Board 2 (slave): ./timeslave_standalone emac0 - * - * Usage: - * ./gptp_master [interface] - * ./gptp_master emac0 - */ - -#include "score/TimeSlave/code/gptp/details/ptp_types.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace -{ - -volatile sig_atomic_t g_running = 1; // NOLINT - -void SigHandler(int /*sig*/) noexcept { g_running = 0; } - -// IEEE 802.1AS / PTP layer-2 multicast destination MAC. -constexpr std::array kPtpMcastMac{0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E}; - -// Minimal packed Ethernet header (avoid pulling in system ethhdr which may -// differ between QNX versions). -struct SCORE_TS_PACKED MasterEthHdr -{ - std::uint8_t dst[6]; - std::uint8_t src[6]; - std::uint16_t etype; -}; - -// --------------------------------------------------------------------------- -// Helper: write a network-byte-order Timestamp from a timespec. -// The parser (LoadTimestamp) applies ntohs/ntohl when reading, so we must -// write big-endian values here. -// --------------------------------------------------------------------------- -void EncodeNetTimestamp(score::ts::details::Timestamp& out, - const struct timespec& ts) noexcept -{ - const auto total_sec = static_cast(ts.tv_sec); - out.seconds_msb = htons(static_cast((total_sec >> 32U) & 0xFFFFU)); - out.seconds_lsb = htonl(static_cast(total_sec & 0xFFFFFFFFU)); - out.nanoseconds = htonl(static_cast(ts.tv_nsec)); -} - -// --------------------------------------------------------------------------- -// Helper: derive EUI-64 ClockIdentity from a 6-byte MAC (EUI-48 → EUI-64). -// --------------------------------------------------------------------------- -score::ts::details::ClockIdentity MakeClock( - const std::array& mac) noexcept -{ - score::ts::details::ClockIdentity id{}; - id.id[0] = mac[0]; - id.id[1] = mac[1]; - id.id[2] = mac[2]; - id.id[3] = 0xFFU; - id.id[4] = 0xFEU; - id.id[5] = mac[3]; - id.id[6] = mac[4]; - id.id[7] = mac[5]; - return id; -} - -// --------------------------------------------------------------------------- -// GetMac: read the hardware MAC of a network interface via getifaddrs. -// --------------------------------------------------------------------------- -bool GetMac(const char* ifname, - std::array& mac) noexcept -{ - struct ifaddrs* list = nullptr; - if (::getifaddrs(&list) != 0) - return false; - - bool found = false; - for (const struct ifaddrs* ifa = list; ifa != nullptr; ifa = ifa->ifa_next) - { - if (ifa->ifa_addr == nullptr) - continue; - if (ifa->ifa_addr->sa_family != AF_LINK) - continue; - if (std::strcmp(ifa->ifa_name, ifname) != 0) - continue; - const auto* dl = - reinterpret_cast(ifa->ifa_addr); - if (dl->sdl_alen >= static_cast(mac.size())) - { - std::memcpy(mac.data(), LLADDR(dl), mac.size()); - found = true; - } - break; - } - ::freeifaddrs(list); - return found; -} - -// --------------------------------------------------------------------------- -// OpenBpf: open a BPF fd bound to the given interface for raw TX. -// --------------------------------------------------------------------------- -int OpenBpf(const char* ifname) noexcept -{ - char devpath[256]{}; - const char* sock_env = std::getenv("SOCK"); - if (sock_env != nullptr && sock_env[0] != '\0') - std::snprintf(devpath, sizeof(devpath), "%s/dev/bpf0", sock_env); - else - std::snprintf(devpath, sizeof(devpath), "/dev/bpf"); - - const int fd = ::open(devpath, O_RDWR); - if (fd < 0) - { - std::perror("[gptp_master] open /dev/bpf"); - return -1; - } - - ::ifreq ifr{}; - ::strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); - if (::ioctl(fd, BIOCSETIF, &ifr) < 0) - { - std::perror("[gptp_master] BIOCSETIF"); - ::close(fd); - return -1; - } - - // Immediate mode: no read buffering delay (not required for TX-only, but - // keeps the fd consistent if we ever add RX later). - u_int one = 1U; - (void)::ioctl(fd, BIOCIMMEDIATE, &one); - - return fd; -} - -// --------------------------------------------------------------------------- -// SendSync: build and write a two-step Sync frame; record send time in ts_out. -// --------------------------------------------------------------------------- -void SendSync(int bpf_fd, - const std::array& src_mac, - const score::ts::details::ClockIdentity& clock_id, - std::uint16_t seqnum, - struct timespec& ts_out) noexcept -{ - using namespace score::ts::details; - - // PTP payload - SyncBody sync{}; - sync.ptpHdr.tsmt = kPtpMsgtypeSync | kPtpTransportSpecific; - sync.ptpHdr.version = kPtpVersion; - sync.ptpHdr.messageLength = htons(static_cast(sizeof(SyncBody))); - sync.ptpHdr.flagField[0] = 0x02U; // twoStepFlag (two-step clock) - sync.ptpHdr.flagField[1] = 0x00U; - sync.ptpHdr.correctionField = 0; - sync.ptpHdr.reserved2 = 0; - sync.ptpHdr.sourcePortIdentity.clockIdentity = clock_id; - sync.ptpHdr.sourcePortIdentity.portNumber = htons(1U); - sync.ptpHdr.sequenceId = htons(seqnum); - sync.ptpHdr.controlField = static_cast(ControlField::kSync); - sync.ptpHdr.logMessageInterval = 0; // 1 s interval (2^0) - // originTimestamp = 0 (two-step; precise time comes in FollowUp) - - // Assemble Ethernet frame - constexpr std::size_t kFrameLen = sizeof(MasterEthHdr) + sizeof(SyncBody); - std::uint8_t frame[kFrameLen]{}; - auto* eth = reinterpret_cast(frame); - std::memcpy(eth->dst, kPtpMcastMac.data(), 6U); - std::memcpy(eth->src, src_mac.data(), 6U); - eth->etype = htons(0x88F7U); // ETH_P_1588 - std::memcpy(frame + sizeof(MasterEthHdr), &sync, sizeof(SyncBody)); - - // Send frame; record CLOCK_REALTIME after write as origin timestamp. - // Post-send timestamp is closer to actual wire-egress time than pre-send. - (void)::write(bpf_fd, frame, kFrameLen); - (void)::clock_gettime(CLOCK_REALTIME, &ts_out); -} - -// --------------------------------------------------------------------------- -// SendFollowUp: build and write a FollowUp carrying the Sync origin timestamp. -// --------------------------------------------------------------------------- -void SendFollowUp(int bpf_fd, - const std::array& src_mac, - const score::ts::details::ClockIdentity& clock_id, - std::uint16_t seqnum, - const struct timespec& origin_ts) noexcept -{ - using namespace score::ts::details; - - FollowUpBody fup{}; - fup.ptpHdr.tsmt = kPtpMsgtypeFollowUp | kPtpTransportSpecific; - fup.ptpHdr.version = kPtpVersion; - fup.ptpHdr.messageLength = htons(static_cast(sizeof(FollowUpBody))); - fup.ptpHdr.flagField[0] = 0x00U; - fup.ptpHdr.flagField[1] = 0x00U; - fup.ptpHdr.correctionField = 0; - fup.ptpHdr.reserved2 = 0; - fup.ptpHdr.sourcePortIdentity.clockIdentity = clock_id; - fup.ptpHdr.sourcePortIdentity.portNumber = htons(1U); - fup.ptpHdr.sequenceId = htons(seqnum); - fup.ptpHdr.controlField = static_cast(ControlField::kFollowUp); - fup.ptpHdr.logMessageInterval = 0; - - // Encode the Sync send time as the preciseOriginTimestamp (network byte order). - EncodeNetTimestamp(fup.preciseOriginTimestamp, origin_ts); - - constexpr std::size_t kFrameLen = sizeof(MasterEthHdr) + sizeof(FollowUpBody); - std::uint8_t frame[kFrameLen]{}; - auto* eth = reinterpret_cast(frame); - std::memcpy(eth->dst, kPtpMcastMac.data(), 6U); - std::memcpy(eth->src, src_mac.data(), 6U); - eth->etype = htons(0x88F7U); - std::memcpy(frame + sizeof(MasterEthHdr), &fup, sizeof(FollowUpBody)); - - (void)::write(bpf_fd, frame, kFrameLen); -} - -} // namespace - -int main(int argc, char* argv[]) -{ - std::signal(SIGINT, SigHandler); - std::signal(SIGTERM, SigHandler); - - const char* iface = (argc >= 2) ? argv[1] : "emac0"; - std::printf("[gptp_master] interface = %s\n", iface); - - // Resolve interface MAC address. - std::array src_mac{}; - if (!GetMac(iface, src_mac)) - { - std::fprintf(stderr, "[gptp_master] ERROR: cannot read MAC for %s\n", iface); - return 1; - } - std::printf("[gptp_master] MAC = %02X:%02X:%02X:%02X:%02X:%02X\n", - src_mac[0], src_mac[1], src_mac[2], - src_mac[3], src_mac[4], src_mac[5]); - - const score::ts::details::ClockIdentity clock_id = MakeClock(src_mac); - - const int bpf_fd = OpenBpf(iface); - if (bpf_fd < 0) - return 1; - - std::printf("[gptp_master] Sending Sync+FUP every 1 s — Ctrl+C to stop\n"); - std::printf("[gptp_master] preciseOriginTimestamp source: CLOCK_REALTIME\n\n"); - - std::uint16_t seqnum = 0U; - - // Anchor the send loop to a monotonic base so 1-second intervals are tight. - struct timespec next{}; - (void)::clock_gettime(CLOCK_MONOTONIC, &next); - - while (g_running != 0) - { - // --- Send Sync then immediately FollowUp --- - struct timespec origin_ts{}; - SendSync(bpf_fd, src_mac, clock_id, seqnum, origin_ts); - SendFollowUp(bpf_fd, src_mac, clock_id, seqnum, origin_ts); - - std::printf("[%5u] Sync+FUP origin=%lld.%09ld\n", - static_cast(seqnum), - static_cast(origin_ts.tv_sec), - origin_ts.tv_nsec); - - ++seqnum; // uint16_t: wraps naturally at 0xFFFF - - // --- Sleep until next 1-second tick --- - const std::int64_t next_ns = - static_cast(next.tv_sec) * 1'000'000'000LL + - next.tv_nsec + 1'000'000'000LL; - next.tv_sec = static_cast(next_ns / 1'000'000'000LL); - next.tv_nsec = static_cast(next_ns % 1'000'000'000LL); - (void)::clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, nullptr); - } - - std::printf("\n[gptp_master] Stopped after %u Sync pairs\n", - static_cast(seqnum)); - ::close(bpf_fd); - return 0; -} diff --git a/score/examples/time_reader.cpp b/score/examples/time_reader.cpp deleted file mode 100644 index bdf9343..0000000 --- a/score/examples/time_reader.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ - -/** - * @brief Demo: libTSClient GptpIpcReceiver consumer. - * - * Opens the shared memory segment written by TimeSlave (or time_publisher) - * and continuously reads + displays the GptpIpcData using the seqlock protocol. - * - * Usage: - * bazel run --config time-x86_64-linux //score/examples:time_reader - * - * Requires time_publisher (or real TimeSlave) to be running first. - */ - -#include "score/libTSClient/gptp_ipc_receiver.h" -#include "score/libTSClient/gptp_ipc_data.h" -#include "score/libTSClient/gptp_ipc_channel.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -volatile sig_atomic_t g_running = 1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void SignalHandler(int /*sig*/) noexcept -{ - g_running = 0; -} - -/// Return a formatted wall-clock timestamp string "HH:MM:SS.mmm". -std::string Timestamp() -{ - const auto now = std::chrono::system_clock::now(); - const auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; - const std::time_t t = std::chrono::system_clock::to_time_t(now); - std::tm tm_buf{}; - localtime_r(&t, &tm_buf); - char buf[16]; - std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm_buf); - std::ostringstream oss; - oss << buf << '.' << std::setw(3) << std::setfill('0') << ms.count(); - return oss.str(); -} - -void PrintData(const score::ts::GptpIpcData& d) -{ - const double ptp_sec = static_cast(d.ptp_assumed_time.count()) * 1e-9; - const double local_sec = static_cast(d.local_time.count()) * 1e-9; - - std::cout << " ptp_assumed_time : " << std::fixed << std::setprecision(9) << ptp_sec << " s\n"; - std::cout << " local_time : " << std::fixed << std::setprecision(9) << local_sec << " s\n"; - std::cout << " rate_deviation : " << std::scientific << std::setprecision(3) << d.rate_deviation << '\n'; - - // Status flags - std::cout << " status :" - << (d.status.is_synchronized ? " [SYNC]" : " [NO-SYNC]") - << (d.status.is_timeout ? " [TIMEOUT]" : "") - << (d.status.is_time_jump_future ? " [JUMP-FUTURE]" : "") - << (d.status.is_time_jump_past ? " [JUMP-PAST]" : "") - << (d.status.is_correct ? " [CORRECT]" : " [INCORRECT]") - << '\n'; - - // Sync+FollowUp data - const auto& s = d.sync_fup_data; - std::cout << " sync_fup.seq_id : " << s.sequence_id << '\n'; - std::cout << " sync_fup.pdelay : " << s.pdelay << " ns\n"; - std::cout << " sync_fup.port : " << s.port_number << '\n'; - std::cout << " sync_fup.clk_id : 0x" - << std::hex << std::setw(16) << std::setfill('0') << s.clock_identity - << std::dec << '\n'; - - // Peer delay data - const auto& p = d.pdelay_data; - std::cout << " pdelay.pdelay : " << p.pdelay << " ns\n"; - std::cout << " pdelay.req_port : " << p.req_port_number << '\n'; - std::cout << " pdelay.resp_port : " << p.resp_port_number << '\n'; -} - -} // namespace - -int main() -{ - std::signal(SIGINT, SignalHandler); - std::signal(SIGTERM, SignalHandler); - - score::ts::details::GptpIpcReceiver receiver; - - std::cout << "[time_reader] Waiting for shared memory '" - << score::ts::details::kGptpIpcName << "' ...\n"; - - // Retry Init until the publisher creates the shared memory segment - while (g_running != 0) - { - if (receiver.Init()) - { - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds{200}); - } - - if (g_running == 0) - { - std::cout << "\n[time_reader] Interrupted before connecting.\n"; - return 0; - } - - std::cout << "[time_reader] Connected. Reading every 100 ms. Press Ctrl+C to stop.\n\n"; - - std::uint64_t read_count = 0U; - std::uint64_t nullopt_count = 0U; - - while (g_running != 0) - { - const auto result = receiver.Receive(); - - if (result.has_value()) - { - ++read_count; - // Print full detail every 10 reads (~1 s), summary otherwise - if (read_count % 10U == 1U) - { - std::cout << "--- [" << Timestamp() << "] read #" << read_count - << " (contention_misses=" << nullopt_count << ") ---\n"; - PrintData(*result); - std::cout << '\n'; - } - } - else - { - ++nullopt_count; - // nullopt means seqlock contention; just retry next cycle - } - - std::this_thread::sleep_for(std::chrono::milliseconds{100}); - } - - std::cout << "\n[time_reader] Stopping. total_reads=" << read_count - << " contention_misses=" << nullopt_count << '\n'; - receiver.Close(); - return 0; -} diff --git a/score/examples/timeslave_standalone.cpp b/score/examples/timeslave_standalone.cpp deleted file mode 100644 index 869dc88..0000000 --- a/score/examples/timeslave_standalone.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ - -/** - * @brief Standalone TimeSlave — bypasses the lifecycle framework. - * - * Directly calls GptpEngine + GptpIpcPublisher without any lifecycle manager, - * suitable for board-level functional verification. - * - * Usage: - * ./timeslave_standalone [interface] - * ./timeslave_standalone emac0 - * - * Defaults to "emac0" if no argument is given. - */ - -#include "score/TimeSlave/code/gptp/gptp_engine.h" -#include "score/libTSClient/gptp_ipc_publisher.h" -#include "score/libTSClient/gptp_ipc_data.h" - -#include -#include -#include -#include -#include - -namespace -{ -volatile sig_atomic_t g_running = 1; // NOLINT - -void SignalHandler(int /*sig*/) noexcept { g_running = 0; } -} // namespace - -int main(int argc, char* argv[]) -{ - std::signal(SIGINT, SignalHandler); - std::signal(SIGTERM, SignalHandler); - - const std::string iface = (argc >= 2) ? argv[1] : "emac0"; - - std::cout << "[timeslave_standalone] interface = " << iface << '\n'; - - // --- Engine options --- - score::ts::details::GptpEngineOptions opts; - opts.iface_name = iface; - opts.pdelay_interval_ms = 1000; - opts.pdelay_warmup_ms = 2000; - opts.sync_timeout_ms = 3300; - opts.phc_config.enabled = true; - opts.phc_config.device = iface; - // On Qualcomm EMAC, PTP_SET_TIME only adjusts an offset register; BPF - // hardware timestamps are unaffected. A high threshold prevents the step - // path from blocking the PI frequency controller. - opts.phc_config.step_threshold_ns = 10'000'000'000LL; - - score::ts::details::GptpEngine engine{opts}; - - if (!engine.Initialize()) - { - std::cerr << "[timeslave_standalone] ERROR: GptpEngine::Initialize() failed\n" - << " Check: interface name correct? Running as root?\n"; - return 1; - } - std::cout << "[timeslave_standalone] GptpEngine initialized\n"; - - // --- Shared memory publisher --- - score::ts::details::GptpIpcPublisher publisher; - if (!publisher.Init()) - { - std::cerr << "[timeslave_standalone] ERROR: GptpIpcPublisher::Init() failed\n"; - engine.Deinitialize(); - return 1; - } - std::cout << "[timeslave_standalone] Shared memory ready: " - << score::ts::details::kGptpIpcName << '\n'; - std::cout << "[timeslave_standalone] Running — Ctrl+C to stop\n\n"; - - constexpr auto kPublishInterval = std::chrono::milliseconds{50}; - std::uint64_t publish_count = 0U; - - while (g_running != 0) - { - engine.FinalizeSnapshot(); - - score::ts::GptpIpcData data{}; - if (engine.ReadPTPSnapshot(data)) - { - publisher.Publish(data); - ++publish_count; - - // Print status every 2 seconds (40 publishes × 50 ms) - if (publish_count % 40U == 0U) - { - const double ptp_sec = static_cast(data.ptp_assumed_time.count()) * 1e-9; - std::cout << "[" << publish_count << "] " - << "ptp=" << ptp_sec << " s" - << " sync=" << std::boolalpha << data.status.is_synchronized - << " timeout=" << data.status.is_timeout - << " correct=" << data.status.is_correct - << " pdelay=" << data.sync_fup_data.pdelay << " ns" - << '\n'; - } - } - - std::this_thread::sleep_for(kPublishInterval); - } - - std::cout << "\n[timeslave_standalone] Stopping\n"; - engine.Deinitialize(); - publisher.Destroy(); - return 0; -} From 5c09177439830a446d39e9bdc621dd2e5d7bc92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gordon=20=5C=20Chenhao=20Gao=20=5C=20=E9=AB=98=E6=99=A8?= =?UTF-8?q?=E6=B5=A9?= Date: Tue, 28 Apr 2026 13:59:18 +0800 Subject: [PATCH 12/12] [ECARX][TimeSlave] Reject stale/duplicate PDelayResp messages --- docs/TimeSlave/_assets/timeslave_class.puml | 4 ++-- docs/TimeSlave/index.rst | 2 ++ score/TimeSlave/code/gptp/details/frame_codec.h | 7 ++++--- score/TimeSlave/code/gptp/details/pdelay_measurer.cpp | 10 +++++++++- score/TimeSlave/code/gptp/details/pdelay_measurer.h | 1 + 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/TimeSlave/_assets/timeslave_class.puml b/docs/TimeSlave/_assets/timeslave_class.puml index c90ec1e..588766b 100644 --- a/docs/TimeSlave/_assets/timeslave_class.puml +++ b/docs/TimeSlave/_assets/timeslave_class.puml @@ -56,8 +56,8 @@ package "score::ts" { package "score::ts::details" { class FrameCodec #Wheat { - + ParseEthernetHeader(buf) : EthernetHeader - + AddEthernetHeader(buf, dst_mac, src_mac) : void + + ParseEthernetHeader(frame, frame_len, ptp_offset) : bool + + AddEthernetHeader(buf, buf_len, src_mac, buf_capacity) : bool } class GptpMessageParser #Wheat { diff --git a/docs/TimeSlave/index.rst b/docs/TimeSlave/index.rst index 14a510a..29c56a4 100644 --- a/docs/TimeSlave/index.rst +++ b/docs/TimeSlave/index.rst @@ -345,6 +345,8 @@ The ``PeerDelayMeasurer`` has the following requirements: - The ``PeerDelayMeasurer`` shall transmit PDelayReq frames and capture the hardware transmit timestamp (``t1``) - The ``PeerDelayMeasurer`` shall receive PDelayResp (providing ``t2``, ``t4``) and PDelayRespFollowUp (providing ``t3c``) messages - The ``PeerDelayMeasurer`` shall compute the peer delay using the IEEE 802.1AS formula: ``path_delay = ((t2 - t1) + (t4 - t3c)) / 2`` +- The ``PeerDelayMeasurer`` shall discard PDelayResp and PDelayRespFollowUp messages whose sequence ID does not match the most recently transmitted PDelayReq +- The ``PeerDelayMeasurer`` shall suppress the path-delay result when more than one PDelayResp is received for a single PDelayReq (detection of non-time-aware bridges per IEEE 802.1AS) - The ``PeerDelayMeasurer`` shall provide thread-safe access to the ``PDelayResult`` via a mutex, as ``SendRequest()`` runs on the PdelayThread while response handlers are called from the RxThread PhcAdjuster SW component diff --git a/score/TimeSlave/code/gptp/details/frame_codec.h b/score/TimeSlave/code/gptp/details/frame_codec.h index 020146f..edcef5c 100644 --- a/score/TimeSlave/code/gptp/details/frame_codec.h +++ b/score/TimeSlave/code/gptp/details/frame_codec.h @@ -53,9 +53,10 @@ class FrameCodec final * Modifies @p buf in-place (shifts payload to make room) and increments * @p buf_len by the size of the added header. * - * @param buf Buffer large enough to hold existing payload plus header. - * @param buf_len In/out: payload length → frame length after prepend. - * @param src_mac Source MAC address (should be the port's own MAC). + * @param buf Buffer large enough to hold existing payload plus header. + * @param buf_len In/out: payload length → frame length after prepend. + * @param src_mac Source MAC address (should be the port's own MAC). + * @param buf_capacity Total allocated size of @p buf in bytes; used to detect overflow. * @return true on success, false if the buffer would overflow. */ bool AddEthernetHeader(std::uint8_t* buf, diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp index ae93a9f..20de210 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.cpp @@ -53,6 +53,7 @@ int PeerDelayMeasurer::SendRequest(IRawSocket& socket) req_.ptpHdr.sequenceId = seqnum_; req_.ptpHdr.sourcePortIdentity.portNumber = 0x0001U; // host byte order req_.sendHardwareTS = TmvT{-1}; // sentinel: TX timestamp pending + resp_count_ = 0U; ++seqnum_; // uint16_t: wraps naturally at 0xFFFF } @@ -89,19 +90,25 @@ int PeerDelayMeasurer::SendRequest(IRawSocket& socket) void PeerDelayMeasurer::OnResponse(const PTPMessage& msg) { std::lock_guard lk(mutex_); + if (msg.ptpHdr.sequenceId != req_.ptpHdr.sequenceId) + return; + ++resp_count_; resp_ = msg; } void PeerDelayMeasurer::OnResponseFollowUp(const PTPMessage& msg) { - std::lock_guard lk(mutex_); + if (msg.ptpHdr.sequenceId != req_.ptpHdr.sequenceId) + return; resp_fup_ = msg; ComputeAndStoreUnlocked(); } void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept { + if (resp_count_ > 1U) // multiple responses → non-time-aware bridge detected + return; if (req_.ptpHdr.sequenceId != resp_.ptpHdr.sequenceId) return; if (resp_.ptpHdr.sequenceId != resp_fup_.ptpHdr.sequenceId) @@ -159,6 +166,7 @@ void PeerDelayMeasurer::ComputeAndStoreUnlocked() noexcept d.resp_clock_identity = ClockIdentityToU64(resp_.ptpHdr.sourcePortIdentity.clockIdentity); result_ = r; + resp_count_ = 0U; } PDelayResult PeerDelayMeasurer::GetResult() const diff --git a/score/TimeSlave/code/gptp/details/pdelay_measurer.h b/score/TimeSlave/code/gptp/details/pdelay_measurer.h index 8715ba6..c9d3152 100644 --- a/score/TimeSlave/code/gptp/details/pdelay_measurer.h +++ b/score/TimeSlave/code/gptp/details/pdelay_measurer.h @@ -72,6 +72,7 @@ class PeerDelayMeasurer final mutable std::mutex mutex_; std::uint16_t seqnum_{0U}; + std::uint16_t resp_count_{0U}; // Pdelay_Resp messages received for the current request PTPMessage req_{}; PTPMessage resp_{}; PTPMessage resp_fup_{};