diff --git a/ntcore/src/generate/java/NetworkTableInstance.java.jinja b/ntcore/src/generate/java/NetworkTableInstance.java.jinja index d94787c429..ba5e33f841 100644 --- a/ntcore/src/generate/java/NetworkTableInstance.java.jinja +++ b/ntcore/src/generate/java/NetworkTableInstance.java.jinja @@ -10,6 +10,7 @@ import edu.wpi.first.util.datalog.DataLog; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.OptionalLong; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -594,6 +595,24 @@ public final class NetworkTableInstance implements AutoCloseable { return m_listeners.add(m_handle, eventKinds, listener); } + /** + * Add a time synchronization listener. The callback function is called asynchronously on a + * separate thread, so it's important to use synchronization or atomics when accessing any shared + * state from the callback function. + * + * @param immediateNotify Notify listener of current time synchronization value + * @param listener Listener to add + * @return Listener handle + */ + public int addTimeSyncListener( + boolean immediateNotify, Consumer listener) { + EnumSet eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync); + if (immediateNotify) { + eventKinds.add(NetworkTableEvent.Kind.kImmediate); + } + return m_listeners.add(m_handle, eventKinds, listener); + } + /** * Add a listener for changes on a particular topic. The callback function is called * asynchronously on a separate thread, so it's important to use synchronization or atomics when @@ -951,6 +970,19 @@ public final class NetworkTableInstance implements AutoCloseable { return NetworkTablesJNI.isConnected(m_handle); } + /** + * Get the time offset between server time and local time. Add this value to local time to get + * the estimated equivalent server time. In server mode, this always returns 0. In client mode, + * this returns the time offset only if the client and server are connected and have exchanged + * synchronization messages. Note the time offset may change over time as it is periodically + * updated; to receive updates as events, add a listener to the "time sync" event. + * + * @return Time offset in microseconds (optional) + */ + public OptionalLong getServerTimeOffset() { + return NetworkTablesJNI.getServerTimeOffset(m_handle); + } + /** * Starts logging entry changes to a DataLog. * diff --git a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja b/ntcore/src/generate/java/NetworkTablesJNI.java.jinja index 41ba45e7d8..2f119fe13f 100644 --- a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja +++ b/ntcore/src/generate/java/NetworkTablesJNI.java.jinja @@ -8,6 +8,7 @@ import edu.wpi.first.util.RuntimeLoader; import edu.wpi.first.util.datalog.DataLog; import java.io.IOException; import java.util.EnumSet; +import java.util.OptionalLong; import java.util.concurrent.atomic.AtomicBoolean; public final class NetworkTablesJNI { @@ -262,6 +263,8 @@ public final class NetworkTablesJNI { public static native boolean isConnected(int inst); + public static native OptionalLong getServerTimeOffset(int inst); + public static native long now(); private static native int startEntryDataLog(int inst, long log, String prefix, String logPrefix); diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java index f1cfd5ce84..97883c8c39 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java @@ -50,7 +50,10 @@ public final class NetworkTableEvent { kValueAll(0x0080 | 0x0040), /** Log message. */ - kLogMessage(0x0100); + kLogMessage(0x0100), + + /** Time synchronized with server. */ + kTimeSync(0x0200); private final int value; @@ -101,6 +104,9 @@ public final class NetworkTableEvent { /** Log message (for log message events). */ public final LogMessage logMessage; + /** Log message (for log message events). */ + public final TimeSyncEventData timeSyncData; + /** * Constructor. This should generally only be used internally to NetworkTables. * @@ -111,6 +117,7 @@ public final class NetworkTableEvent { * @param topicInfo Topic information * @param valueData Value data * @param logMessage Log message + * @param timeSyncData Time sync data */ public NetworkTableEvent( NetworkTableInstance inst, @@ -119,7 +126,8 @@ public final class NetworkTableEvent { ConnectionInfo connInfo, TopicInfo topicInfo, ValueEventData valueData, - LogMessage logMessage) { + LogMessage logMessage, + TimeSyncEventData timeSyncData) { this.m_inst = inst; this.listener = listener; this.m_flags = flags; @@ -127,6 +135,7 @@ public final class NetworkTableEvent { this.topicInfo = topicInfo; this.valueData = valueData; this.logMessage = logMessage; + this.timeSyncData = timeSyncData; } /* Network table instance. */ diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java index 5b61abf4e8..2568738c2a 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java @@ -109,6 +109,19 @@ public final class NetworkTableListener implements AutoCloseable { return new NetworkTableListener(inst, inst.addConnectionListener(immediateNotify, listener)); } + /** + * Create a time synchronization listener. + * + * @param inst instance + * @param immediateNotify notify listener of current time synchonization value + * @param listener listener function + * @return Listener + */ + public static NetworkTableListener createTimeSyncListener( + NetworkTableInstance inst, boolean immediateNotify, Consumer listener) { + return new NetworkTableListener(inst, inst.addTimeSyncListener(immediateNotify, listener)); + } + /** * Create a listener for log messages. By default, log messages are sent to stderr; this function * sends log messages with the specified levels to the provided callback function instead. The diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java index b88e805e85..3a504f3bf0 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java @@ -7,8 +7,8 @@ package edu.wpi.first.networktables; import java.util.EnumSet; /** - * Topic change listener. This queues topic change events matching the specified mask. Code using - * the listener must periodically call readQueue() to read the events. + * Event listener poller. This queues events matching the specified mask. Code using the listener + * must periodically call readQueue() to read the events. */ public final class NetworkTableListenerPoller implements AutoCloseable { /** @@ -94,6 +94,22 @@ public final class NetworkTableListenerPoller implements AutoCloseable { return NetworkTablesJNI.addListener(m_handle, m_inst.getHandle(), eventKinds); } + /** + * Add a time synchronization listener. The callback function is called asynchronously on a + * separate thread, so it's important to use synchronization or atomics when accessing any shared + * state from the callback function. + * + * @param immediateNotify notify listener of current time synchronization value + * @return Listener handle + */ + public int addTimeSyncListener(boolean immediateNotify) { + EnumSet eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync); + if (immediateNotify) { + eventKinds.add(NetworkTableEvent.Kind.kImmediate); + } + return NetworkTablesJNI.addListener(m_handle, m_inst.getHandle(), eventKinds); + } + /** * Add logger callback function. By default, log messages are sent to stderr; this function sends * log messages with the specified levels to the provided callback function instead. The callback diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java b/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java new file mode 100644 index 0000000000..f8710ad1c8 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java @@ -0,0 +1,37 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.networktables; + +/** NetworkTables time sync event data. */ +@SuppressWarnings("MemberName") +public final class TimeSyncEventData { + /** + * Offset between local time and server time, in microseconds. Add this value to local time to get + * the estimated equivalent server time. + */ + public final long serverTimeOffset; + + /** Measured round trip time divided by 2, in microseconds. */ + public final long rtt2; + + /** + * If serverTimeOffset and RTT are valid. An event with this set to false is sent when the client + * disconnects. + */ + public final boolean valid; + + /** + * Constructor. This should generally only be used internally to NetworkTables. + * + * @param serverTimeOffset Server time offset + * @param rtt2 Round trip time divided by 2 + * @param valid If other parameters are valid + */ + public TimeSyncEventData(long serverTimeOffset, long rtt2, boolean valid) { + this.serverTimeOffset = serverTimeOffset; + this.rtt2 = rtt2; + this.valid = valid; + } +} diff --git a/ntcore/src/main/native/cpp/IListenerStorage.h b/ntcore/src/main/native/cpp/IListenerStorage.h index 71018206be..82d743224e 100644 --- a/ntcore/src/main/native/cpp/IListenerStorage.h +++ b/ntcore/src/main/native/cpp/IListenerStorage.h @@ -35,6 +35,9 @@ class IListenerStorage { virtual void Notify(unsigned int flags, unsigned int level, std::string_view filename, unsigned int line, std::string_view message) = 0; + virtual void NotifyTimeSync(std::span handles, + unsigned int flags, int64_t serverTimeOffset, + int64_t rtt2, bool valid) = 0; void Notify(std::span handles, unsigned int flags, const ConnectionInfo* info) { diff --git a/ntcore/src/main/native/cpp/INetworkClient.h b/ntcore/src/main/native/cpp/INetworkClient.h index 2c14a1bbaa..986f43ab84 100644 --- a/ntcore/src/main/native/cpp/INetworkClient.h +++ b/ntcore/src/main/native/cpp/INetworkClient.h @@ -6,9 +6,7 @@ #include #include -#include #include -#include #include "ntcore_cpp.h" diff --git a/ntcore/src/main/native/cpp/InstanceImpl.cpp b/ntcore/src/main/native/cpp/InstanceImpl.cpp index 5ea1cad479..b34db7323d 100644 --- a/ntcore/src/main/native/cpp/InstanceImpl.cpp +++ b/ntcore/src/main/native/cpp/InstanceImpl.cpp @@ -110,6 +110,9 @@ void InstanceImpl::StartServer(std::string_view persistFilename, networkMode &= ~NT_NET_MODE_STARTING; }); networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_STARTING; + listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, true); + m_serverTimeOffset = 0; + m_rtt2 = 0; } void InstanceImpl::StopServer() { @@ -121,6 +124,9 @@ void InstanceImpl::StopServer() { } server = std::move(m_networkServer); networkMode = NT_NET_MODE_NONE; + listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, false); + m_serverTimeOffset.reset(); + m_rtt2 = 0; } } @@ -143,7 +149,19 @@ void InstanceImpl::StartClient4(std::string_view identity) { return; } m_networkClient = std::make_shared( - m_inst, identity, localStorage, connectionList, logger); + m_inst, identity, localStorage, connectionList, logger, + [this](int64_t serverTimeOffset, int64_t rtt2, bool valid) { + std::scoped_lock lock{m_mutex}; + listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, serverTimeOffset, + rtt2, valid); + if (valid) { + m_serverTimeOffset = serverTimeOffset; + m_rtt2 = rtt2; + } else { + m_serverTimeOffset.reset(); + m_rtt2 = 0; + } + }); if (!m_servers.empty()) { m_networkClient->SetServers(m_servers); } @@ -151,12 +169,22 @@ void InstanceImpl::StartClient4(std::string_view identity) { } void InstanceImpl::StopClient() { - std::scoped_lock lock{m_mutex}; - if ((networkMode & (NT_NET_MODE_CLIENT3 | NT_NET_MODE_CLIENT4)) == 0) { - return; + std::shared_ptr client; + { + std::scoped_lock lock{m_mutex}; + if ((networkMode & (NT_NET_MODE_CLIENT3 | NT_NET_MODE_CLIENT4)) == 0) { + return; + } + client = std::move(m_networkClient); + networkMode = NT_NET_MODE_NONE; + } + client.reset(); + { + std::scoped_lock lock{m_mutex}; + listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, false); + m_serverTimeOffset.reset(); + m_rtt2 = 0; } - m_networkClient.reset(); - networkMode = NT_NET_MODE_NONE; } void InstanceImpl::SetServers( @@ -178,12 +206,33 @@ std::shared_ptr InstanceImpl::GetClient() { return m_networkClient; } +std::optional InstanceImpl::GetServerTimeOffset() { + std::scoped_lock lock{m_mutex}; + return m_serverTimeOffset; +} + +void InstanceImpl::AddTimeSyncListener(NT_Listener listener, + unsigned int eventMask) { + std::scoped_lock lock{m_mutex}; + eventMask &= (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE); + listenerStorage.Activate(listener, eventMask); + if ((eventMask & (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE)) == + (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE) && + m_serverTimeOffset) { + listenerStorage.NotifyTimeSync({&listener, 1}, + NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE, + *m_serverTimeOffset, m_rtt2, true); + } +} + void InstanceImpl::Reset() { std::scoped_lock lock{m_mutex}; m_networkServer.reset(); m_networkClient.reset(); m_servers.clear(); networkMode = NT_NET_MODE_NONE; + m_serverTimeOffset.reset(); + m_rtt2 = 0; listenerStorage.Reset(); // connectionList should have been cleared by destroying networkClient/server diff --git a/ntcore/src/main/native/cpp/InstanceImpl.h b/ntcore/src/main/native/cpp/InstanceImpl.h index 6d250c54fe..a7b4742fa7 100644 --- a/ntcore/src/main/native/cpp/InstanceImpl.h +++ b/ntcore/src/main/native/cpp/InstanceImpl.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,9 @@ class InstanceImpl { std::shared_ptr GetServer(); std::shared_ptr GetClient(); + std::optional GetServerTimeOffset(); + void AddTimeSyncListener(NT_Listener listener, unsigned int eventMask); + void Reset(); ListenerStorage listenerStorage; @@ -77,6 +81,8 @@ class InstanceImpl { std::shared_ptr m_networkServer; std::shared_ptr m_networkClient; std::vector> m_servers; + std::optional m_serverTimeOffset; + int64_t m_rtt2 = 0; int m_inst; }; diff --git a/ntcore/src/main/native/cpp/ListenerStorage.cpp b/ntcore/src/main/native/cpp/ListenerStorage.cpp index 85f373c88c..fdbf03bc09 100644 --- a/ntcore/src/main/native/cpp/ListenerStorage.cpp +++ b/ntcore/src/main/native/cpp/ListenerStorage.cpp @@ -81,6 +81,9 @@ void ListenerStorage::Activate(NT_Listener listenerHandle, unsigned int mask, (deltaMask & 0x1ff0000) != 0) { m_logListeners.Add(listener); } + if ((deltaMask & NT_EVENT_TIMESYNC) != 0) { + m_timeSyncListeners.Add(listener); + } } } @@ -237,6 +240,42 @@ void ListenerStorage::Notify(unsigned int flags, unsigned int level, } } +void ListenerStorage::NotifyTimeSync(std::span handles, + unsigned int flags, + int64_t serverTimeOffset, int64_t rtt2, + bool valid) { + if (flags == 0) { + return; + } + std::scoped_lock lock{m_mutex}; + + auto doSignal = [&](ListenerData& listener) { + if ((flags & listener.eventMask) != 0) { + for (auto&& [finishEvent, mask] : listener.sources) { + if ((flags & mask) != 0) { + listener.poller->queue.emplace_back(listener.handle, flags, + serverTimeOffset, rtt2, valid); + // finishEvent is never set (see InstanceImpl) + } + } + listener.handle.Set(); + listener.poller->handle.Set(); + } + }; + + if (!handles.empty()) { + for (auto handle : handles) { + if (auto listener = m_listeners.Get(handle)) { + doSignal(*listener); + } + } + } else { + for (auto&& listener : m_timeSyncListeners) { + doSignal(*listener); + } + } +} + NT_Listener ListenerStorage::AddListener(ListenerCallback callback) { std::scoped_lock lock{m_mutex}; if (!m_thread) { diff --git a/ntcore/src/main/native/cpp/ListenerStorage.h b/ntcore/src/main/native/cpp/ListenerStorage.h index bbea074756..b291a12dfe 100644 --- a/ntcore/src/main/native/cpp/ListenerStorage.h +++ b/ntcore/src/main/native/cpp/ListenerStorage.h @@ -41,6 +41,8 @@ class ListenerStorage final : public IListenerStorage { NT_Topic topic, NT_Handle subentry, const Value& value) final; void Notify(unsigned int flags, unsigned int level, std::string_view filename, unsigned int line, std::string_view message) final; + void NotifyTimeSync(std::span handles, unsigned int flags, + int64_t serverTimeOffset, int64_t rtt2, bool valid) final; // user-facing functions NT_Listener AddListener(ListenerCallback callback); @@ -105,6 +107,7 @@ class ListenerStorage final : public IListenerStorage { VectorSet m_topicListeners; VectorSet m_valueListeners; VectorSet m_logListeners; + VectorSet m_timeSyncListeners; class Thread; wpi::SafeThreadOwner m_thread; diff --git a/ntcore/src/main/native/cpp/NetworkClient.cpp b/ntcore/src/main/native/cpp/NetworkClient.cpp index b227e10e9f..a329fb9dd9 100644 --- a/ntcore/src/main/native/cpp/NetworkClient.cpp +++ b/ntcore/src/main/native/cpp/NetworkClient.cpp @@ -107,8 +107,11 @@ class NCImpl3 : public NCImpl { class NCImpl4 : public NCImpl { public: - NCImpl4(int inst, std::string_view id, net::ILocalStorage& localStorage, - IConnectionList& connList, wpi::Logger& logger); + NCImpl4( + int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger, + std::function + timeSyncUpdated); ~NCImpl4() override; void HandleLocal(); @@ -116,6 +119,8 @@ class NCImpl4 : public NCImpl { void WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp); void Disconnect(std::string_view reason) override; + std::function + m_timeSyncUpdated; std::shared_ptr m_wire; std::unique_ptr m_clientImpl; }; @@ -325,10 +330,13 @@ void NCImpl3::Disconnect(std::string_view reason) { NCImpl::Disconnect(reason); } -NCImpl4::NCImpl4(int inst, std::string_view id, - net::ILocalStorage& localStorage, IConnectionList& connList, - wpi::Logger& logger) - : NCImpl{inst, id, localStorage, connList, logger} { +NCImpl4::NCImpl4( + int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger, + std::function + timeSyncUpdated) + : NCImpl{inst, id, localStorage, connList, logger}, + m_timeSyncUpdated{std::move(timeSyncUpdated)} { m_loopRunner.ExecAsync([this](uv::Loop& loop) { m_parallelConnect = wpi::ParallelTcpConnector::Create( loop, kReconnectRate, m_logger, @@ -421,7 +429,7 @@ void NCImpl4::WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp) { m_wire = std::make_shared(ws); m_clientImpl = std::make_unique( - m_loop.Now().count(), m_inst, *m_wire, m_logger, + m_loop.Now().count(), m_inst, *m_wire, m_logger, m_timeSyncUpdated, [this](uint32_t repeatMs) { DEBUG4("Setting periodic timer to {}", repeatMs); m_sendValuesTimer->Start(uv::Timer::Time{repeatMs}, @@ -453,20 +461,26 @@ void NCImpl4::Disconnect(std::string_view reason) { m_clientImpl.reset(); m_wire.reset(); NCImpl::Disconnect(reason); + m_timeSyncUpdated(0, 0, false); } class NetworkClient::Impl final : public NCImpl4 { public: Impl(int inst, std::string_view id, net::ILocalStorage& localStorage, - IConnectionList& connList, wpi::Logger& logger) - : NCImpl4{inst, id, localStorage, connList, logger} {} + IConnectionList& connList, wpi::Logger& logger, + std::function + timeSyncUpdated) + : NCImpl4{inst, id, localStorage, + connList, logger, std::move(timeSyncUpdated)} {} }; -NetworkClient::NetworkClient(int inst, std::string_view id, - net::ILocalStorage& localStorage, - IConnectionList& connList, wpi::Logger& logger) - : m_impl{std::make_unique(inst, id, localStorage, connList, logger)} { -} +NetworkClient::NetworkClient( + int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger, + std::function + timeSyncUpdated) + : m_impl{std::make_unique(inst, id, localStorage, connList, logger, + std::move(timeSyncUpdated))} {} NetworkClient::~NetworkClient() { m_impl->m_localStorage.ClearNetwork(); diff --git a/ntcore/src/main/native/cpp/NetworkClient.h b/ntcore/src/main/native/cpp/NetworkClient.h index af42f9854d..34bf37931d 100644 --- a/ntcore/src/main/native/cpp/NetworkClient.h +++ b/ntcore/src/main/native/cpp/NetworkClient.h @@ -4,7 +4,9 @@ #pragma once +#include #include +#include #include #include #include @@ -27,8 +29,11 @@ class IConnectionList; class NetworkClient final : public INetworkClient { public: - NetworkClient(int inst, std::string_view id, net::ILocalStorage& localStorage, - IConnectionList& connList, wpi::Logger& logger); + NetworkClient( + int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger, + std::function + timeSyncUpdated); ~NetworkClient() final; void SetServers( diff --git a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp index a1a1bf652c..b8686044cf 100644 --- a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp +++ b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp @@ -39,7 +39,9 @@ static JClass eventCls; static JClass floatCls; static JClass logMessageCls; static JClass longCls; +static JClass optionalLongCls; static JClass pubSubOptionsCls; +static JClass timeSyncEventDataCls; static JClass topicInfoCls; static JClass valueCls; static JClass valueEventDataCls; @@ -55,7 +57,9 @@ static const JClassInit classes[] = { {"java/lang/Float", &floatCls}, {"edu/wpi/first/networktables/LogMessage", &logMessageCls}, {"java/lang/Long", &longCls}, + {"java/util/OptionalLong", &optionalLongCls}, {"edu/wpi/first/networktables/PubSubOptions", &pubSubOptionsCls}, + {"edu/wpi/first/networktables/TimeSyncEventData", &timeSyncEventDataCls}, {"edu/wpi/first/networktables/TopicInfo", &topicInfoCls}, {"edu/wpi/first/networktables/NetworkTableValue", &valueCls}, {"edu/wpi/first/networktables/ValueEventData", &valueEventDataCls}}; @@ -164,6 +168,25 @@ static nt::PubSubOptions FromJavaPubSubOptions(JNIEnv* env, jobject joptions) { // Conversions from C++ to Java objects // +static jobject MakeJObject(JNIEnv* env, std::optional value) { + static jmethodID emptyMethod = nullptr; + static jmethodID ofMethod = nullptr; + if (!emptyMethod) { + emptyMethod = env->GetStaticMethodID(optionalLongCls, "empty", + "()Ljava/util/OptionalLong;"); + } + if (!ofMethod) { + ofMethod = env->GetStaticMethodID(optionalLongCls, "of", + "(J)Ljava/util/OptionalLong;"); + } + if (value) { + return env->CallStaticObjectMethod(optionalLongCls, ofMethod, + static_cast(*value)); + } else { + return env->CallStaticObjectMethod(optionalLongCls, emptyMethod); + } +} + static jobject MakeJObject(JNIEnv* env, const nt::Value& value) { static jmethodID booleanConstructor = nullptr; static jmethodID doubleConstructor = nullptr; @@ -275,6 +298,15 @@ static jobject MakeJObject(JNIEnv* env, jobject inst, static_cast(data.subentry), value.obj()); } +static jobject MakeJObject(JNIEnv* env, const nt::TimeSyncEventData& data) { + static jmethodID constructor = + env->GetMethodID(timeSyncEventDataCls, "", "(JJZ)V"); + return env->NewObject(timeSyncEventDataCls, constructor, + static_cast(data.serverTimeOffset), + static_cast(data.rtt2), + static_cast(data.valid)); +} + static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::Event& event) { static jmethodID constructor = env->GetMethodID(eventCls, "", @@ -282,11 +314,13 @@ static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::Event& event) { "Ledu/wpi/first/networktables/ConnectionInfo;" "Ledu/wpi/first/networktables/TopicInfo;" "Ledu/wpi/first/networktables/ValueEventData;" - "Ledu/wpi/first/networktables/LogMessage;)V"); + "Ledu/wpi/first/networktables/LogMessage;" + "Ledu/wpi/first/networktables/TimeSyncEventData;)V"); JLocal connInfo{env, nullptr}; JLocal topicInfo{env, nullptr}; JLocal valueData{env, nullptr}; JLocal logMessage{env, nullptr}; + JLocal timeSyncData{env, nullptr}; if (auto v = event.GetConnectionInfo()) { connInfo = JLocal{env, MakeJObject(env, *v)}; } else if (auto v = event.GetTopicInfo()) { @@ -295,11 +329,13 @@ static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::Event& event) { valueData = JLocal{env, MakeJObject(env, inst, *v)}; } else if (auto v = event.GetLogMessage()) { logMessage = JLocal{env, MakeJObject(env, *v)}; + } else if (auto v = event.GetTimeSyncEventData()) { + timeSyncData = JLocal{env, MakeJObject(env, *v)}; } - return env->NewObject(eventCls, constructor, inst, - static_cast(event.listener), - static_cast(event.flags), connInfo.obj(), - topicInfo.obj(), valueData.obj(), logMessage.obj()); + return env->NewObject( + eventCls, constructor, inst, static_cast(event.listener), + static_cast(event.flags), connInfo.obj(), topicInfo.obj(), + valueData.obj(), logMessage.obj(), timeSyncData.obj()); } static jobjectArray MakeJObject(JNIEnv* env, std::span arr) { @@ -1344,6 +1380,18 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_isConnected return nt::IsConnected(inst); } +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: getServerTimeOffset + * Signature: (I)Ljava/lang/Object; + */ +JNIEXPORT jobject JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_getServerTimeOffset + (JNIEnv* env, jclass, jint inst) +{ + return MakeJObject(env, nt::GetServerTimeOffset(inst)); +} + /* * Class: edu_wpi_first_networktables_NetworkTablesJNI * Method: now diff --git a/ntcore/src/main/native/cpp/net/ClientImpl.cpp b/ntcore/src/main/native/cpp/net/ClientImpl.cpp index 87fc6ac7d2..2efbb29691 100644 --- a/ntcore/src/main/native/cpp/net/ClientImpl.cpp +++ b/ntcore/src/main/native/cpp/net/ClientImpl.cpp @@ -49,6 +49,8 @@ struct PublisherData { class CImpl : public ServerMessageHandler { public: CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function + timeSyncUpdated, std::function setPeriodic); void ProcessIncomingBinary(std::span data); @@ -76,6 +78,8 @@ class CImpl : public ServerMessageHandler { WireConnection& m_wire; wpi::Logger& m_logger; LocalInterface* m_local{nullptr}; + std::function + m_timeSyncUpdated; std::function m_setPeriodic; // indexed by publisher index @@ -102,12 +106,15 @@ class CImpl : public ServerMessageHandler { } // namespace -CImpl::CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, - wpi::Logger& logger, - std::function setPeriodic) +CImpl::CImpl( + uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function + timeSyncUpdated, + std::function setPeriodic) : m_inst{inst}, m_wire{wire}, m_logger{logger}, + m_timeSyncUpdated{std::move(timeSyncUpdated)}, m_setPeriodic{std::move(setPeriodic)}, m_nextPingTimeMs{curTimeMs + kPingIntervalMs} { // immediately send RTT ping @@ -151,6 +158,7 @@ void CImpl::ProcessIncomingBinary(std::span data) { m_serverTimeOffsetUs = value.server_time() + rtt2 - now; DEBUG3("Time offset: {}", m_serverTimeOffsetUs); m_haveTimeOffset = true; + m_timeSyncUpdated(m_serverTimeOffsetUs, m_rtt2Us, true); } continue; } @@ -424,14 +432,24 @@ void CImpl::ServerPropertiesUpdate(std::string_view name, class ClientImpl::Impl final : public CImpl { public: Impl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function + timeSyncUpdated, std::function setPeriodic) - : CImpl{curTimeMs, inst, wire, logger, std::move(setPeriodic)} {} + : CImpl{curTimeMs, + inst, + wire, + logger, + std::move(timeSyncUpdated), + std::move(setPeriodic)} {} }; -ClientImpl::ClientImpl(uint64_t curTimeMs, int inst, WireConnection& wire, - wpi::Logger& logger, - std::function setPeriodic) +ClientImpl::ClientImpl( + uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function + timeSyncUpdated, + std::function setPeriodic) : m_impl{std::make_unique(curTimeMs, inst, wire, logger, + std::move(timeSyncUpdated), std::move(setPeriodic))} {} ClientImpl::~ClientImpl() = default; diff --git a/ntcore/src/main/native/cpp/net/ClientImpl.h b/ntcore/src/main/native/cpp/net/ClientImpl.h index 4116a54643..0e7fd4a00d 100644 --- a/ntcore/src/main/native/cpp/net/ClientImpl.h +++ b/ntcore/src/main/native/cpp/net/ClientImpl.h @@ -32,9 +32,11 @@ class WireConnection; class ClientImpl { public: - ClientImpl(uint64_t curTimeMs, int inst, WireConnection& wire, - wpi::Logger& logger, - std::function setPeriodic); + ClientImpl( + uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function + timeSyncUpdated, + std::function setPeriodic); ~ClientImpl(); void ProcessIncomingText(std::string_view data); diff --git a/ntcore/src/main/native/cpp/ntcore_c.cpp b/ntcore/src/main/native/cpp/ntcore_c.cpp index bca7266d6b..151e0f1c64 100644 --- a/ntcore/src/main/native/cpp/ntcore_c.cpp +++ b/ntcore/src/main/native/cpp/ntcore_c.cpp @@ -18,6 +18,7 @@ #include "Value_internal.h" #include "ntcore.h" +#include "ntcore_cpp.h" using namespace nt; @@ -52,6 +53,12 @@ static void ConvertToC(const LogMessage& in, NT_LogMessage* out) { ConvertToC(in.message, &out->message); } +static void ConvertToC(const TimeSyncEventData& in, NT_TimeSyncEventData* out) { + out->serverTimeOffset = in.serverTimeOffset; + out->rtt2 = in.rtt2; + out->valid = in.valid; +} + static void ConvertToC(const Event& in, NT_Event* out) { out->listener = in.listener; out->flags = in.flags; @@ -71,6 +78,10 @@ static void ConvertToC(const Event& in, NT_Event* out) { if (auto v = in.GetLogMessage()) { return ConvertToC(*v, &out->data.logMessage); } + } else if ((in.flags & NT_EVENT_TIMESYNC) != 0) { + if (auto v = in.GetTimeSyncEventData()) { + return ConvertToC(*v, &out->data.timeSyncData); + } } out->flags = NT_EVENT_NONE; // sanity to make sure we don't dispose } @@ -551,15 +562,25 @@ struct NT_ConnectionInfo* NT_GetConnections(NT_Inst inst, size_t* count) { return ConvertToC(conn_v, count); } +int64_t NT_GetServerTimeOffset(NT_Inst inst, NT_Bool* valid) { + if (auto v = nt::GetServerTimeOffset(inst)) { + *valid = true; + return *v; + } else { + *valid = false; + return 0; + } +} + /* * Utility Functions */ -uint64_t NT_Now(void) { +int64_t NT_Now(void) { return nt::Now(); } -void NT_SetNow(uint64_t timestamp) { +void NT_SetNow(int64_t timestamp) { nt::SetNow(timestamp); } diff --git a/ntcore/src/main/native/cpp/ntcore_cpp.cpp b/ntcore/src/main/native/cpp/ntcore_cpp.cpp index 2780bb2f33..cfb5af2014 100644 --- a/ntcore/src/main/native/cpp/ntcore_cpp.cpp +++ b/ntcore/src/main/native/cpp/ntcore_cpp.cpp @@ -422,6 +422,9 @@ static void DoAddListener(InstanceImpl& ii, NT_Listener listener, if ((mask & NT_EVENT_LOGMESSAGE) != 0) { ii.logger_impl.AddListener(listener, NT_LOG_INFO, UINT_MAX); } + if ((mask & NT_EVENT_TIMESYNC) != 0) { + ii.AddTimeSyncListener(listener, mask); + } } else if ((mask & (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL)) != 0) { ii.localStorage.AddListener(listener, handle, mask); } @@ -735,6 +738,14 @@ bool IsConnected(NT_Inst inst) { } } +std::optional GetServerTimeOffset(NT_Inst inst) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->GetServerTimeOffset(); + } else { + return {}; + } +} + NT_Listener AddLogger(NT_Inst inst, unsigned int minLevel, unsigned int maxLevel, ListenerCallback func) { if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h index b8555bf56c..fabc63421c 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -391,6 +392,20 @@ class NetworkTableInstance final { NT_Listener AddConnectionListener(bool immediate_notify, ListenerCallback callback) const; + /** + * Add a time synchronization listener. The callback function is called + * asynchronously on a separate thread, so it's important to use + * synchronization or atomics when accessing any shared state from the + * callback function. + * + * @param immediate_notify notify listener of current time synchronization + * value + * @param callback listener to add + * @return Listener handle + */ + NT_Listener AddTimeSyncListener(bool immediate_notify, + ListenerCallback callback) const; + /** * Add a listener for changes on a particular topic. The callback * function is called asynchronously on a separate thread, so it's important @@ -614,6 +629,19 @@ class NetworkTableInstance final { */ bool IsConnected() const; + /** + * Get the time offset between server time and local time. Add this value to + * local time to get the estimated equivalent server time. In server mode, + * this always returns 0. In client mode, this returns the time offset only if + * the client and server are connected and have exchanged synchronization + * messages. Note the time offset may change over time as it is periodically + * updated; to receive updates as events, add a listener to the "time sync" + * event. + * + * @return Time offset in microseconds (optional) + */ + std::optional GetServerTimeOffset() const; + /** @} */ /** diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc index 62d320c9a6..9b712ebc89 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc +++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc @@ -100,6 +100,13 @@ inline NT_Listener NetworkTableInstance::AddConnectionListener( std::move(callback)); } +inline NT_Listener NetworkTableInstance::AddTimeSyncListener( + bool immediate_notify, ListenerCallback callback) const { + return ::nt::AddListener( + m_handle, NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0), + std::move(callback)); +} + inline NT_Listener NetworkTableInstance::AddListener( std::span prefixes, int eventMask, ListenerCallback listener) { @@ -181,6 +188,11 @@ inline bool NetworkTableInstance::IsConnected() const { return ::nt::IsConnected(m_handle); } +inline std::optional NetworkTableInstance::GetServerTimeOffset() + const { + return ::nt::GetServerTimeOffset(m_handle); +} + inline NT_DataLogger NetworkTableInstance::StartEntryDataLog( wpi::log::DataLog& log, std::string_view prefix, std::string_view logPrefix) { diff --git a/ntcore/src/main/native/include/networktables/NetworkTableListener.h b/ntcore/src/main/native/include/networktables/NetworkTableListener.h index 3b51bda309..1a8cf1fa46 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableListener.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableListener.h @@ -106,6 +106,19 @@ class NetworkTableListener final { NetworkTableInstance inst, bool immediate_notify, ListenerCallback listener); + /** + * Create a time synchronization listener. + * + * @param inst instance + * @param immediate_notify notify listener of current time synchronization + * value + * @param listener listener function + * @return Listener + */ + static NetworkTableListener CreateTimeSyncListener(NetworkTableInstance inst, + bool immediate_notify, + ListenerCallback listener); + /** * Create a listener for log messages. By default, log messages are sent to * stderr; this function sends log messages with the specified levels to the @@ -251,6 +264,18 @@ class NetworkTableListenerPoller final { */ NT_Listener AddConnectionListener(bool immediate_notify); + /** + * Add a time synchronization listener. The callback function is called + * asynchronously on a separate thread, so it's important to use + * synchronization or atomics when accessing any shared state from the + * callback function. + * + * @param immediate_notify notify listener of current time synchronization + * value + * @return Listener handle + */ + NT_Listener AddTimeSyncListener(bool immediate_notify); + /** * Add logger callback function. By default, log messages are sent to stderr; * this function sends log messages with the specified levels to the provided diff --git a/ntcore/src/main/native/include/networktables/NetworkTableListener.inc b/ntcore/src/main/native/include/networktables/NetworkTableListener.inc index ed7006ff8e..5453d87584 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableListener.inc +++ b/ntcore/src/main/native/include/networktables/NetworkTableListener.inc @@ -58,6 +58,15 @@ inline NetworkTableListener NetworkTableListener::CreateConnectionListener( std::move(listener))}; } +inline NetworkTableListener NetworkTableListener::CreateTimeSyncListener( + NetworkTableInstance inst, bool immediate_notify, + ListenerCallback listener) { + return NetworkTableListener{::nt::AddListener( + inst.GetHandle(), + NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0), + std::move(listener))}; +} + inline NetworkTableListener NetworkTableListener::CreateLogger( NetworkTableInstance inst, unsigned int minLevel, unsigned int maxLevel, ListenerCallback listener) { @@ -152,6 +161,13 @@ inline NT_Listener NetworkTableListenerPoller::AddConnectionListener( NT_EVENT_CONNECTION | (immediate_notify ? NT_EVENT_IMMEDIATE : 0)); } +inline NT_Listener NetworkTableListenerPoller::AddTimeSyncListener( + bool immediate_notify) { + return ::nt::AddPolledListener( + m_handle, ::nt::GetInstanceFromHandle(m_handle), + NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0)); +} + inline NT_Listener NetworkTableListenerPoller::AddLogger( unsigned int minLevel, unsigned int maxLevel) { return ::nt::AddPolledLogger(m_handle, minLevel, maxLevel); diff --git a/ntcore/src/main/native/include/ntcore_c.h b/ntcore/src/main/native/include/ntcore_c.h index f00cb21225..74f9666163 100644 --- a/ntcore/src/main/native/include/ntcore_c.h +++ b/ntcore/src/main/native/include/ntcore_c.h @@ -115,6 +115,8 @@ enum NT_EventFlags { NT_EVENT_VALUE_ALL = NT_EVENT_VALUE_REMOTE | NT_EVENT_VALUE_LOCAL, /** Log message. */ NT_EVENT_LOGMESSAGE = 0x100, + /** Time synchronized with server. */ + NT_EVENT_TIMESYNC = 0x200, }; /* @@ -247,6 +249,24 @@ struct NT_LogMessage { char* message; }; +/** NetworkTables time sync event data. */ +struct NT_TimeSyncEventData { + /** + * Offset between local time and server time, in microseconds. Add this value + * to local time to get the estimated equivalent server time. + */ + int64_t serverTimeOffset; + + /** Measured round trip time divided by 2, in microseconds. */ + int64_t rtt2; + + /** + * If serverTimeOffset and RTT are valid. An event with this set to false is + * sent when the client disconnects. + */ + NT_Bool valid; +}; + /** NetworkTables event */ struct NT_Event { /** Listener that triggered this event. */ @@ -259,6 +279,7 @@ struct NT_Event { * - NT_EVENT_PUBLISH, NT_EVENT_UNPUBLISH, or NT_EVENT_PROPERTIES: topicInfo * - NT_EVENT_VALUE_REMOTE, NT_NOTIFY_VALUE_LOCAL: valueData * - NT_EVENT_LOGMESSAGE: logMessage + * - NT_EVENT_TIMESYNC: timeSyncData */ unsigned int flags; @@ -268,6 +289,7 @@ struct NT_Event { struct NT_TopicInfo topicInfo; struct NT_ValueEventData valueData; struct NT_LogMessage logMessage; + struct NT_TimeSyncEventData timeSyncData; } data; }; @@ -1190,6 +1212,22 @@ struct NT_ConnectionInfo* NT_GetConnections(NT_Inst inst, size_t* count); */ NT_Bool NT_IsConnected(NT_Inst inst); +/** + * Get the time offset between server time and local time. Add this value to + * local time to get the estimated equivalent server time. In server mode, this + * always returns a valid value of 0. In client mode, this returns the time + * offset only if the client and server are connected and have exchanged + * synchronization messages. Note the time offset may change over time as it is + * periodically updated; to receive updates as events, add a listener to the + * "time sync" event. + * + * @param inst instance handle + * @param valid set to true if the return value is valid, false otherwise + * (output) + * @return Time offset in microseconds (if valid is set to true) + */ +int64_t NT_GetServerTimeOffset(NT_Inst inst, NT_Bool* valid); + /** @} */ /** @@ -1286,7 +1324,7 @@ void NT_DisposeEvent(struct NT_Event* event); * * @return Timestamp */ -uint64_t NT_Now(void); +int64_t NT_Now(void); /** * Sets the current timestamp used for timestamping values that do not @@ -1297,7 +1335,7 @@ uint64_t NT_Now(void); * * @param timestamp timestamp (1 us increments) */ -void NT_SetNow(uint64_t timestamp); +void NT_SetNow(int64_t timestamp); /** @} */ diff --git a/ntcore/src/main/native/include/ntcore_cpp.h b/ntcore/src/main/native/include/ntcore_cpp.h index 279efe641b..40cfe5c2ac 100644 --- a/ntcore/src/main/native/include/ntcore_cpp.h +++ b/ntcore/src/main/native/include/ntcore_cpp.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,8 @@ struct EventFlags { static constexpr unsigned int kValueAll = kValueRemote | kValueLocal; /** Log message. */ static constexpr unsigned int kLogMessage = NT_EVENT_LOGMESSAGE; + /** Time synchronized with server. */ + static constexpr unsigned int kTimeSync = NT_EVENT_TIMESYNC; }; /** NetworkTables Topic Information */ @@ -186,6 +189,29 @@ class LogMessage { std::string message; }; +/** NetworkTables time sync event data. */ +class TimeSyncEventData { + public: + TimeSyncEventData() = default; + TimeSyncEventData(int64_t serverTimeOffset, int64_t rtt2, bool valid) + : serverTimeOffset{serverTimeOffset}, rtt2{rtt2}, valid{valid} {} + + /** + * Offset between local time and server time, in microseconds. Add this value + * to local time to get the estimated equivalent server time. + */ + int64_t serverTimeOffset; + + /** Measured round trip time divided by 2, in microseconds. */ + int64_t rtt2; + + /** + * If serverTimeOffset and RTT are valid. An event with this set to false is + * sent when the client disconnects. + */ + bool valid; +}; + /** NetworkTables event */ class Event { public: @@ -208,6 +234,11 @@ class Event { : listener{listener}, flags{flags}, data{LogMessage{level, filename, line, message}} {} + Event(NT_Listener listener, unsigned int flags, int64_t serverTimeOffset, + int64_t rtt2, bool valid) + : listener{listener}, + flags{flags}, + data{TimeSyncEventData{serverTimeOffset, rtt2, valid}} {} /** Listener that triggered this event. */ NT_Listener listener{0}; @@ -215,11 +246,12 @@ class Event { /** * Event flags (NT_EventFlags). Also indicates the data included with the * event: - * - NT_NOTIFY_CONNECTED or NT_NOTIFY_DISCONNECTED: GetConnectionInfo() - * - NT_NOTIFY_PUBLISH, NT_NOTIFY_UNPUBLISH, or NT_NOTIFY_PROPERTIES: + * - NT_EVENT_CONNECTED or NT_EVENT_DISCONNECTED: GetConnectionInfo() + * - NT_EVENT_PUBLISH, NT_EVENT_UNPUBLISH, or NT_EVENT_PROPERTIES: * GetTopicInfo() - * - NT_NOTIFY_VALUE, NT_NOTIFY_VALUE_LOCAL: GetValueData() - * - NT_NOTIFY_LOGMESSAGE: GetLogMessage() + * - NT_EVENT_VALUE, NT_EVENT_VALUE_LOCAL: GetValueData() + * - NT_EVENT_LOGMESSAGE: GetLogMessage() + * - NT_EVENT_TIMESYNC: GetTimeSyncEventData() */ unsigned int flags{0}; @@ -232,31 +264,40 @@ class Event { bool Is(unsigned int kind) const { return (flags & kind) != 0; } /** Event data; content depends on flags. */ - std::variant data; + std::variant + data; const ConnectionInfo* GetConnectionInfo() const { - return std::get_if(&data); + return std::get_if(&data); } ConnectionInfo* GetConnectionInfo() { - return std::get_if(&data); + return std::get_if(&data); } const TopicInfo* GetTopicInfo() const { - return std::get_if(&data); + return std::get_if(&data); } - TopicInfo* GetTopicInfo() { return std::get_if(&data); } + TopicInfo* GetTopicInfo() { return std::get_if(&data); } const ValueEventData* GetValueEventData() const { - return std::get_if(&data); + return std::get_if(&data); } ValueEventData* GetValueEventData() { - return std::get_if(&data); + return std::get_if(&data); } const LogMessage* GetLogMessage() const { - return std::get_if(&data); + return std::get_if(&data); + } + LogMessage* GetLogMessage() { return std::get_if(&data); } + + const TimeSyncEventData* GetTimeSyncEventData() const { + return std::get_if(&data); + } + TimeSyncEventData* GetTimeSyncEventData() { + return std::get_if(&data); } - LogMessage* GetLogMessage() { return std::get_if(&data); } }; /** NetworkTables publish/subscribe options. */ @@ -1107,6 +1148,19 @@ std::vector GetConnections(NT_Inst inst); */ bool IsConnected(NT_Inst inst); +/** + * Get the time offset between server time and local time. Add this value to + * local time to get the estimated equivalent server time. In server mode, this + * always returns 0. In client mode, this returns the time offset only if the + * client and server are connected and have exchanged synchronization messages. + * Note the time offset may change over time as it is periodically updated; to + * receive updates as events, add a listener to the "time sync" event. + * + * @param inst instance handle + * @return Time offset in microseconds (optional) + */ +std::optional GetServerTimeOffset(NT_Inst inst); + /** @} */ /** diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java new file mode 100644 index 0000000000..c539d2027d --- /dev/null +++ b/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java @@ -0,0 +1,83 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.networktables; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TimeSyncTest { + private NetworkTableInstance m_inst; + + @BeforeEach + void setUp() { + m_inst = NetworkTableInstance.create(); + } + + @AfterEach + void tearDown() { + m_inst.close(); + } + + @Test + void testLocal() { + var offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + } + + @Test + void testServer() { + var poller = new NetworkTableListenerPoller(m_inst); + poller.addTimeSyncListener(false); + + m_inst.startServer("timesynctest.json", "127.0.0.1", 0, 10030); + var offset = m_inst.getServerTimeOffset(); + assertTrue(offset.isPresent()); + assertEquals(0L, offset.getAsLong()); + + NetworkTableEvent[] events = poller.readQueue(); + assertEquals(1, events.length); + assertNotNull(events[0].timeSyncData); + assertTrue(events[0].timeSyncData.valid); + assertEquals(0L, events[0].timeSyncData.serverTimeOffset); + assertEquals(0L, events[0].timeSyncData.rtt2); + + m_inst.stopServer(); + offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + + events = poller.readQueue(); + assertEquals(1, events.length); + assertNotNull(events[0].timeSyncData); + assertFalse(events[0].timeSyncData.valid); + } + + @Test + void testClient3() { + m_inst.startClient3("client"); + var offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + + m_inst.stopClient(); + offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + } + + @Test + void testClient4() { + m_inst.startClient4("client"); + var offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + + m_inst.stopClient(); + offset = m_inst.getServerTimeOffset(); + assertFalse(offset.isPresent()); + } +} diff --git a/ntcore/src/test/native/cpp/MockListenerStorage.h b/ntcore/src/test/native/cpp/MockListenerStorage.h index e02d21f53e..6e1dc4ef9c 100644 --- a/ntcore/src/test/native/cpp/MockListenerStorage.h +++ b/ntcore/src/test/native/cpp/MockListenerStorage.h @@ -36,6 +36,10 @@ class MockListenerStorage : public IListenerStorage { std::string_view filename, unsigned int line, std::string_view message), (override)); + MOCK_METHOD(void, NotifyTimeSync, + (std::span handles, unsigned int flags, + int64_t serverTimeOffset, int64_t rtt2, bool valid), + (override)); }; } // namespace nt diff --git a/ntcore/src/test/native/cpp/TimeSyncTest.cpp b/ntcore/src/test/native/cpp/TimeSyncTest.cpp new file mode 100644 index 0000000000..54e1f7c8aa --- /dev/null +++ b/ntcore/src/test/native/cpp/TimeSyncTest.cpp @@ -0,0 +1,70 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "gtest/gtest.h" +#include "networktables/NetworkTableInstance.h" +#include "networktables/NetworkTableListener.h" + +class TimeSyncTest : public ::testing::Test { + public: + TimeSyncTest() : m_inst(nt::NetworkTableInstance::Create()) {} + + ~TimeSyncTest() override { nt::NetworkTableInstance::Destroy(m_inst); } + + protected: + nt::NetworkTableInstance m_inst; +}; + +TEST_F(TimeSyncTest, TestLocal) { + auto offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); +} + +TEST_F(TimeSyncTest, TestServer) { + nt::NetworkTableListenerPoller poller{m_inst}; + poller.AddTimeSyncListener(false); + + m_inst.StartServer("timesynctest.json", "127.0.0.1", 0, 10030); + auto offset = m_inst.GetServerTimeOffset(); + ASSERT_TRUE(offset); + ASSERT_EQ(0, *offset); + + auto events = poller.ReadQueue(); + ASSERT_EQ(1u, events.size()); + auto data = events[0].GetTimeSyncEventData(); + ASSERT_TRUE(data); + ASSERT_TRUE(data->valid); + ASSERT_EQ(0, data->serverTimeOffset); + ASSERT_EQ(0, data->rtt2); + + m_inst.StopServer(); + offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); + + events = poller.ReadQueue(); + ASSERT_EQ(1u, events.size()); + data = events[0].GetTimeSyncEventData(); + ASSERT_TRUE(data); + ASSERT_FALSE(data->valid); +} + +TEST_F(TimeSyncTest, TestClient3) { + m_inst.StartClient3("client"); + auto offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); + + m_inst.StopClient(); + offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); +} + +TEST_F(TimeSyncTest, TestClient4) { + m_inst.StartClient4("client"); + auto offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); + + m_inst.StopClient(); + offset = m_inst.GetServerTimeOffset(); + ASSERT_FALSE(offset); +} diff --git a/ntcoreffi/src/main/native/symbols.txt b/ntcoreffi/src/main/native/symbols.txt index f9c951a553..ddcf1143ef 100644 --- a/ntcoreffi/src/main/native/symbols.txt +++ b/ntcoreffi/src/main/native/symbols.txt @@ -89,6 +89,7 @@ NT_GetInteger NT_GetIntegerArray NT_GetNetworkMode NT_GetRaw +NT_GetServerTimeOffset NT_GetString NT_GetStringArray NT_GetStringForTesting