Skip to content

Commit 5cab95b

Browse files
committed
Add clock sync server and client, and clock sync client library
1 parent b0ee3a8 commit 5cab95b

12 files changed

Lines changed: 1268 additions & 0 deletions

src/core/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ add_subdirectory(retargeting_engine_ui)
3636
# Build TeleopSessionManager (pure Python module)
3737
add_subdirectory(teleop_session_manager)
3838

39+
# Build Synchronization utilities (clock sync, etc.)
40+
add_subdirectory(synchronization)
41+
3942
# Python wheel packaging (combines both modules)
4043
if(BUILD_PYTHON_BINDINGS)
4144
add_subdirectory(python)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.20)
5+
6+
add_subdirectory(clock_sync)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.20)
5+
6+
# ==============================================================================
7+
# Clock Sync - types (header-only) + client library + executables + Python
8+
# ==============================================================================
9+
10+
# Header-only library for shared types (clock_types.hpp, platform.hpp)
11+
add_library(clock_sync_types INTERFACE)
12+
13+
target_include_directories(clock_sync_types
14+
INTERFACE
15+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
16+
$<INSTALL_INTERFACE:include>
17+
)
18+
19+
# On Windows, link Winsock2 and the high-resolution timer API
20+
if(WIN32)
21+
target_link_libraries(clock_sync_types INTERFACE ws2_32 winmm)
22+
endif()
23+
24+
add_library(utilities::clock_sync_types ALIAS clock_sync_types)
25+
26+
# Static library for the ClockClient class
27+
add_library(clock_sync_client STATIC
28+
clock_client_lib.cpp
29+
)
30+
31+
target_link_libraries(clock_sync_client
32+
PUBLIC
33+
utilities::clock_sync_types
34+
)
35+
36+
target_include_directories(clock_sync_client
37+
PUBLIC
38+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
39+
$<INSTALL_INTERFACE:include>
40+
)
41+
42+
add_library(utilities::clock_sync_client ALIAS clock_sync_client)
43+
44+
# Clock server executable
45+
add_executable(clock_server clock_server.cpp)
46+
target_link_libraries(clock_server PRIVATE utilities::clock_sync_types)
47+
48+
# Clock client executable (monotonic timestamps, no XR)
49+
add_executable(clock_client clock_client.cpp)
50+
target_link_libraries(clock_client PRIVATE utilities::clock_sync_client)
51+
52+
# ------------------------------------------------------------------------------
53+
# Optional: XR time translator library + XR client executable
54+
# Requires oxr::oxr_core (OpenXR session management + time conversion utilities)
55+
# ------------------------------------------------------------------------------
56+
57+
set(CLOCK_SYNC_HAS_XR OFF)
58+
59+
if(TARGET oxr_core)
60+
add_library(clock_sync_xr STATIC
61+
xr_clock_translator.cpp
62+
)
63+
64+
target_link_libraries(clock_sync_xr
65+
PUBLIC
66+
utilities::clock_sync_types
67+
oxr::oxr_core
68+
)
69+
70+
target_include_directories(clock_sync_xr
71+
PUBLIC
72+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
73+
$<INSTALL_INTERFACE:include>
74+
)
75+
76+
add_library(utilities::clock_sync_xr ALIAS clock_sync_xr)
77+
78+
add_executable(clock_client_xr clock_client_xr.cpp)
79+
target_link_libraries(clock_client_xr
80+
PRIVATE
81+
utilities::clock_sync_client
82+
utilities::clock_sync_xr
83+
)
84+
85+
set(CLOCK_SYNC_HAS_XR ON)
86+
endif()
87+
88+
# Install
89+
set(_CLOCK_SYNC_INSTALL_TARGETS clock_server clock_client clock_sync_client)
90+
if(CLOCK_SYNC_HAS_XR)
91+
list(APPEND _CLOCK_SYNC_INSTALL_TARGETS clock_client_xr clock_sync_xr)
92+
endif()
93+
94+
install(TARGETS ${_CLOCK_SYNC_INSTALL_TARGETS}
95+
RUNTIME DESTINATION bin
96+
ARCHIVE DESTINATION lib
97+
INCLUDES DESTINATION include
98+
)
99+
100+
install(DIRECTORY inc/clock_sync
101+
DESTINATION include
102+
)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include "clock_sync/clock_client.hpp"
5+
6+
#include <condition_variable>
7+
#include <csignal>
8+
#include <cstdlib>
9+
#include <iomanip>
10+
#include <iostream>
11+
#include <mutex>
12+
#include <vector>
13+
14+
namespace
15+
{
16+
17+
volatile sig_atomic_t g_running = 1;
18+
19+
void signal_handler(int /*sig*/)
20+
{
21+
g_running = 0;
22+
}
23+
24+
void print_header()
25+
{
26+
std::cout << std::left << std::setw(8) << "seq" << std::setw(18) << "rtt_us" << std::setw(18) << "offset_us"
27+
<< std::setw(18) << "t1_ns" << std::setw(18) << "t2_ns" << std::setw(18) << "t3_ns" << std::setw(18)
28+
<< "t4_ns"
29+
<< "\n";
30+
std::cout << std::string(116, '-') << "\n";
31+
}
32+
33+
void print_measurement(uint64_t seq, const core::ClockMeasurement& m)
34+
{
35+
std::cout << std::left << std::setw(8) << seq << std::setw(18) << std::fixed << std::setprecision(3)
36+
<< core::ns_to_sec(m.rtt()) * 1e6 << std::setw(18) << core::ns_to_sec(m.offset()) * 1e6 << std::setw(18)
37+
<< m.t1 << std::setw(18) << m.t2 << std::setw(18) << m.t3 << std::setw(18) << m.t4 << "\n";
38+
}
39+
40+
void print_summary(const std::vector<core::ClockMeasurement>& measurements, uint64_t sent)
41+
{
42+
if (measurements.empty())
43+
return;
44+
45+
core::clock_ns_t min_rtt = measurements[0].rtt();
46+
core::clock_ns_t max_rtt = min_rtt;
47+
double sum_rtt = 0;
48+
double sum_offset = 0;
49+
50+
for (const auto& m : measurements)
51+
{
52+
core::clock_ns_t r = m.rtt();
53+
if (r < min_rtt)
54+
min_rtt = r;
55+
if (r > max_rtt)
56+
max_rtt = r;
57+
sum_rtt += static_cast<double>(r);
58+
sum_offset += static_cast<double>(m.offset());
59+
}
60+
61+
double avg_rtt = sum_rtt / static_cast<double>(measurements.size());
62+
double avg_offset = sum_offset / static_cast<double>(measurements.size());
63+
uint64_t lost = sent - measurements.size();
64+
65+
std::cout << "\n--- Summary ---\n";
66+
std::cout << "Packets: " << sent << " sent, " << measurements.size() << " received, " << lost << " lost\n";
67+
std::cout << std::fixed << std::setprecision(3);
68+
std::cout << "RTT (us): min=" << core::ns_to_sec(min_rtt) * 1e6 << " avg=" << avg_rtt / 1e3
69+
<< " max=" << core::ns_to_sec(max_rtt) * 1e6 << "\n";
70+
std::cout << "Offset (us): avg=" << avg_offset / 1e3 << "\n";
71+
}
72+
73+
} // namespace
74+
75+
int main(int argc, char* argv[])
76+
{
77+
if (argc < 2)
78+
{
79+
std::cerr << "Usage: clock_client <server_ip> [port] [interval_ms] [count]\n"
80+
<< " Client uses CLOCK_MONOTONIC; server uses CLOCK_MONOTONIC_RAW.\n";
81+
return 1;
82+
}
83+
84+
const char* server_ip = argv[1];
85+
uint16_t port = (argc > 2) ? static_cast<uint16_t>(std::atoi(argv[2])) : core::kDefaultClockPort;
86+
int interval_ms = (argc > 3) ? std::atoi(argv[3]) : 1000;
87+
int count = (argc > 4) ? std::atoi(argv[4]) : 0;
88+
89+
std::signal(SIGINT, signal_handler);
90+
std::signal(SIGTERM, signal_handler);
91+
92+
std::vector<core::ClockMeasurement> measurements;
93+
std::mutex mu;
94+
std::condition_variable cv;
95+
uint64_t seq = 0;
96+
97+
auto on_measurement = [&](const core::ClockMeasurement& m)
98+
{
99+
std::lock_guard<std::mutex> lock(mu);
100+
measurements.push_back(m);
101+
print_measurement(seq, m);
102+
++seq;
103+
cv.notify_one();
104+
};
105+
106+
std::shared_ptr<core::ClockClient> client;
107+
try
108+
{
109+
client = core::ClockClient::Create(server_ip, port, on_measurement);
110+
}
111+
catch (const std::exception& e)
112+
{
113+
std::cerr << e.what() << std::endl;
114+
return 1;
115+
}
116+
117+
std::cout << "[ClockClient] Pinging " << server_ip << ":" << port << " every " << interval_ms << " ms"
118+
<< " (client: CLOCK_MONOTONIC, server: CLOCK_MONOTONIC_RAW)\n\n";
119+
120+
print_header();
121+
122+
uint64_t sent = 0;
123+
124+
if (count > 0)
125+
{
126+
client->start(interval_ms, count);
127+
sent = static_cast<uint64_t>(count);
128+
129+
std::unique_lock<std::mutex> lock(mu);
130+
cv.wait_for(lock, std::chrono::milliseconds(count * interval_ms + 2000),
131+
[&] { return seq >= static_cast<uint64_t>(count) || !g_running; });
132+
}
133+
else
134+
{
135+
client->start(interval_ms, 0);
136+
137+
while (g_running)
138+
{
139+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
140+
}
141+
142+
client->stop();
143+
sent = seq + 1;
144+
}
145+
146+
{
147+
std::lock_guard<std::mutex> lock(mu);
148+
print_summary(measurements, sent);
149+
}
150+
151+
return 0;
152+
}

0 commit comments

Comments
 (0)