[ntcore] Add method to get server time offset (#4847)

Also exposes this as an event signaled when the offset is updated due to
a ping response from the server.
This commit is contained in:
Peter Johnson
2022-12-30 20:15:57 -08:00
committed by GitHub
parent fe1b62647f
commit f1151d375f
29 changed files with 718 additions and 60 deletions

View File

@@ -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<const NT_Listener> handles,
unsigned int flags, int64_t serverTimeOffset,
int64_t rtt2, bool valid) = 0;
void Notify(std::span<const NT_Listener> handles, unsigned int flags,
const ConnectionInfo* info) {

View File

@@ -6,9 +6,7 @@
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "ntcore_cpp.h"

View File

@@ -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<NetworkClient>(
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<INetworkClient> 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<INetworkClient> InstanceImpl::GetClient() {
return m_networkClient;
}
std::optional<int64_t> 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

View File

@@ -6,6 +6,7 @@
#include <atomic>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
@@ -56,6 +57,9 @@ class InstanceImpl {
std::shared_ptr<NetworkServer> GetServer();
std::shared_ptr<INetworkClient> GetClient();
std::optional<int64_t> GetServerTimeOffset();
void AddTimeSyncListener(NT_Listener listener, unsigned int eventMask);
void Reset();
ListenerStorage listenerStorage;
@@ -77,6 +81,8 @@ class InstanceImpl {
std::shared_ptr<NetworkServer> m_networkServer;
std::shared_ptr<INetworkClient> m_networkClient;
std::vector<std::pair<std::string, unsigned int>> m_servers;
std::optional<int64_t> m_serverTimeOffset;
int64_t m_rtt2 = 0;
int m_inst;
};

View File

@@ -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<const NT_Listener> 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) {

View File

@@ -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<const NT_Listener> 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<ListenerData*> m_topicListeners;
VectorSet<ListenerData*> m_valueListeners;
VectorSet<ListenerData*> m_logListeners;
VectorSet<ListenerData*> m_timeSyncListeners;
class Thread;
wpi::SafeThreadOwner<Thread> m_thread;

View File

@@ -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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
m_timeSyncUpdated;
std::shared_ptr<net::WebSocketConnection> m_wire;
std::unique_ptr<net::ClientImpl> 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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
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<net::WebSocketConnection>(ws);
m_clientImpl = std::make_unique<net::ClientImpl>(
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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
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<Impl>(inst, id, localStorage, connList, logger)} {
}
NetworkClient::NetworkClient(
int inst, std::string_view id, net::ILocalStorage& localStorage,
IConnectionList& connList, wpi::Logger& logger,
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated)
: m_impl{std::make_unique<Impl>(inst, id, localStorage, connList, logger,
std::move(timeSyncUpdated))} {}
NetworkClient::~NetworkClient() {
m_impl->m_localStorage.ClearNetwork();

View File

@@ -4,7 +4,9 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
@@ -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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated);
~NetworkClient() final;
void SetServers(

View File

@@ -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<int64_t> 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<jlong>(*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<jint>(data.subentry), value.obj());
}
static jobject MakeJObject(JNIEnv* env, const nt::TimeSyncEventData& data) {
static jmethodID constructor =
env->GetMethodID(timeSyncEventDataCls, "<init>", "(JJZ)V");
return env->NewObject(timeSyncEventDataCls, constructor,
static_cast<jlong>(data.serverTimeOffset),
static_cast<jlong>(data.rtt2),
static_cast<jboolean>(data.valid));
}
static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::Event& event) {
static jmethodID constructor =
env->GetMethodID(eventCls, "<init>",
@@ -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<jobject> connInfo{env, nullptr};
JLocal<jobject> topicInfo{env, nullptr};
JLocal<jobject> valueData{env, nullptr};
JLocal<jobject> logMessage{env, nullptr};
JLocal<jobject> timeSyncData{env, nullptr};
if (auto v = event.GetConnectionInfo()) {
connInfo = JLocal<jobject>{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<jobject>{env, MakeJObject(env, inst, *v)};
} else if (auto v = event.GetLogMessage()) {
logMessage = JLocal<jobject>{env, MakeJObject(env, *v)};
} else if (auto v = event.GetTimeSyncEventData()) {
timeSyncData = JLocal<jobject>{env, MakeJObject(env, *v)};
}
return env->NewObject(eventCls, constructor, inst,
static_cast<jint>(event.listener),
static_cast<jint>(event.flags), connInfo.obj(),
topicInfo.obj(), valueData.obj(), logMessage.obj());
return env->NewObject(
eventCls, constructor, inst, static_cast<jint>(event.listener),
static_cast<jint>(event.flags), connInfo.obj(), topicInfo.obj(),
valueData.obj(), logMessage.obj(), timeSyncData.obj());
}
static jobjectArray MakeJObject(JNIEnv* env, std::span<const nt::Value> 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

View File

@@ -49,6 +49,8 @@ struct PublisherData {
class CImpl : public ServerMessageHandler {
public:
CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated,
std::function<void(uint32_t repeatMs)> setPeriodic);
void ProcessIncomingBinary(std::span<const uint8_t> data);
@@ -76,6 +78,8 @@ class CImpl : public ServerMessageHandler {
WireConnection& m_wire;
wpi::Logger& m_logger;
LocalInterface* m_local{nullptr};
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
m_timeSyncUpdated;
std::function<void(uint32_t repeatMs)> 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<void(uint32_t repeatMs)> setPeriodic)
CImpl::CImpl(
uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated,
std::function<void(uint32_t repeatMs)> 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<const uint8_t> 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<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated,
std::function<void(uint32_t repeatMs)> 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<void(uint32_t repeatMs)> setPeriodic)
ClientImpl::ClientImpl(
uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated,
std::function<void(uint32_t repeatMs)> setPeriodic)
: m_impl{std::make_unique<Impl>(curTimeMs, inst, wire, logger,
std::move(timeSyncUpdated),
std::move(setPeriodic))} {}
ClientImpl::~ClientImpl() = default;

View File

@@ -32,9 +32,11 @@ class WireConnection;
class ClientImpl {
public:
ClientImpl(uint64_t curTimeMs, int inst, WireConnection& wire,
wpi::Logger& logger,
std::function<void(uint32_t repeatMs)> setPeriodic);
ClientImpl(
uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
timeSyncUpdated,
std::function<void(uint32_t repeatMs)> setPeriodic);
~ClientImpl();
void ProcessIncomingText(std::string_view data);

View File

@@ -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<NT_ConnectionInfo>(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);
}

View File

@@ -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<int64_t> 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)) {