From 6c6db1fc728053185f3764f01baf3e218e40bc06 Mon Sep 17 00:00:00 2001 From: nuubik Date: Wed, 28 May 2025 15:54:32 +0300 Subject: [PATCH 1/3] Improve time sync accuracy --- README.rst | 4 ++++ index.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 62cf404..0c6f5a3 100644 --- a/README.rst +++ b/README.rst @@ -447,6 +447,10 @@ node.subscribeToValues(valueConsumer, fs, sampleRate) Subscribe to value changes on this node. On each value change valueConsumer function is called with value of the nodes value_type and UTC Unix timestamp in nanoseconds (nanoseconds from 01.01.1970). Timestamp refers to the time of value change in connected application on target controller. + If the client detects a time synchronization offset larger than 3 seconds, + incoming timestamps are automatically adjusted by that offset. The offset is + determined by sending three consecutive time-sync requests and using the + smallest offset reported. - Example diff --git a/index.js b/index.js index ba5dab8..fe388d8 100644 --- a/index.js +++ b/index.js @@ -725,6 +725,9 @@ studio.internal = (function(proto) { var onError; var onOpen; var reauthRequestPending = false; + var timeOffsetNs = 0; + var timeSyncTimer = null; + var timeSyncSamples = []; socket.binaryType = proto.BINARY_TYPE; nodeMap.set(systemNode.id(), systemNode); handler.onContainer = handleIncomingContainer; @@ -747,7 +750,10 @@ studio.internal = (function(proto) { onMessage = function(evt) { handler.handle(evt.data); }; onError = function (ev) { console.log("Socket error: " + ev.data); }; - onOpen = function() { appConnection.resubscribe(systemNode); }; + onOpen = function() { + appConnection.resubscribe(systemNode); + startTimeSync(); + }; onClosed = function (event) { var reason; @@ -781,6 +787,8 @@ studio.internal = (function(proto) { reason = "Unknown reason"; console.log("Socket close: " + reason); + stopTimeSync(); + timeOffsetNs = 0; if (autoConnect) { @@ -834,6 +842,53 @@ studio.internal = (function(proto) { requests = []; } + function makeCurrentTimeRequest() { + var msg = new proto.Container(); + msg.message_type = proto.ContainerType.eCurrentTimeRequest; + send(msg); + } + + function beginTimeSync() { + if (timeSyncSamples.length === 0) { + makeCurrentTimeRequest(); + } + } + + function parseCurrentTimeResponse(serverTime) { + var localNs = Date.now() * 1000000; + var offsetCandidate = serverTime - localNs; + timeSyncSamples.push(offsetCandidate); + if (timeSyncSamples.length < 3) { + makeCurrentTimeRequest(); + return; + } + var minOffset = Math.min.apply(null, timeSyncSamples); + timeSyncSamples = []; + if (Math.abs(minOffset) > 3000000000) { + timeOffsetNs = minOffset; + } + } + + function startTimeSync() { + if (!timeSyncTimer) { + beginTimeSync(); + timeSyncTimer = setInterval(beginTimeSync, 30000); + } + } + + function stopTimeSync() { + if (timeSyncTimer) { + clearInterval(timeSyncTimer); + timeSyncTimer = null; + } + } + + function applyTimeOffset(ts) { + if (ts === undefined || ts === null) + return ts; + return ts + timeOffsetNs; + } + this.makeStructureRequest = function(id) { var msg = new proto.Container(); msg.message_type = proto.ContainerType.eStructureRequest; @@ -990,7 +1045,7 @@ studio.internal = (function(proto) { var variantValue = protoResponse[i]; var node = nodeMap.get(variantValue.node_id); if (node) - node.receiveValue(proto.valueFromVariant(variantValue, node.info().value_type), variantValue.timestamp); + node.receiveValue(proto.valueFromVariant(variantValue, node.info().value_type), applyTimeOffset(variantValue.timestamp)); } } @@ -1014,7 +1069,7 @@ studio.internal = (function(proto) { sender: variantValue.sender, code: variantValue.code, status: variantValue.status, - timestamp: variantValue.timestamp, + timestamp: applyTimeOffset(variantValue.timestamp), data: variantValue.data }; node.receiveEvent(event); @@ -1054,6 +1109,7 @@ studio.internal = (function(proto) { } function handleIncomingContainer(protoContainer, metadata) { + startTimeSync(); switch(protoContainer.message_type){ case proto.ContainerType.eStructureResponse: parseStructureResponse(protoContainer.structure_response); @@ -1068,6 +1124,7 @@ studio.internal = (function(proto) { parseEventResponse(protoContainer.event_response); break; case proto.ContainerType.eCurrentTimeResponse: + parseCurrentTimeResponse(protoContainer.current_time_response); break; case proto.ContainerType.eReauthResponse: parseReauthResponse(protoContainer.re_auth_response, metadata); From ad0abad167dc39f719a7dab117ce98d0d19183b4 Mon Sep 17 00:00:00 2001 From: nuubik Date: Wed, 28 May 2025 16:12:05 +0300 Subject: [PATCH 2/3] Run time sync periodically --- index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index fe388d8..197f669 100644 --- a/index.js +++ b/index.js @@ -753,6 +753,9 @@ studio.internal = (function(proto) { onOpen = function() { appConnection.resubscribe(systemNode); startTimeSync(); + if (!timeSyncTimer) { + timeSyncTimer = setInterval(startTimeSync, 30000); + } }; onClosed = function (event) { var reason; @@ -870,10 +873,7 @@ studio.internal = (function(proto) { } function startTimeSync() { - if (!timeSyncTimer) { - beginTimeSync(); - timeSyncTimer = setInterval(beginTimeSync, 30000); - } + beginTimeSync(); } function stopTimeSync() { @@ -1109,7 +1109,6 @@ studio.internal = (function(proto) { } function handleIncomingContainer(protoContainer, metadata) { - startTimeSync(); switch(protoContainer.message_type){ case proto.ContainerType.eStructureResponse: parseStructureResponse(protoContainer.structure_response); From 44f493095eb42bb7c13db0971639b95438030309 Mon Sep 17 00:00:00 2001 From: nuubik Date: Wed, 28 May 2025 16:29:36 +0300 Subject: [PATCH 3/3] Share time offset across applications on same host (#17) --- index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 197f669..9d97ba7 100644 --- a/index.js +++ b/index.js @@ -322,6 +322,7 @@ studio.protocol.BINARY_TYPE = "arraybuffer"; */ studio.internal = (function(proto) { var obj = {}; + var hostTimeOffsets = new Map(); obj.structure = { REMOVE: 0, @@ -715,6 +716,7 @@ studio.internal = (function(proto) { var appName = ""; var appId = undefined; var appUrl = composeUrl(url); + var host = new URL(appUrl).hostname; var socket = new WebSocket(appUrl); var handler = new proto.Handler(socket, notificationListener); var requests = []; @@ -725,7 +727,7 @@ studio.internal = (function(proto) { var onError; var onOpen; var reauthRequestPending = false; - var timeOffsetNs = 0; + var timeOffsetNs = hostTimeOffsets.get(host) || 0; var timeSyncTimer = null; var timeSyncSamples = []; socket.binaryType = proto.BINARY_TYPE; @@ -791,7 +793,6 @@ studio.internal = (function(proto) { console.log("Socket close: " + reason); stopTimeSync(); - timeOffsetNs = 0; if (autoConnect) { @@ -851,6 +852,11 @@ studio.internal = (function(proto) { send(msg); } + function setTimeOffset(offset) { + timeOffsetNs = offset; + hostTimeOffsets.set(host, offset); + } + function beginTimeSync() { if (timeSyncSamples.length === 0) { makeCurrentTimeRequest(); @@ -868,7 +874,7 @@ studio.internal = (function(proto) { var minOffset = Math.min.apply(null, timeSyncSamples); timeSyncSamples = []; if (Math.abs(minOffset) > 3000000000) { - timeOffsetNs = minOffset; + setTimeOffset(minOffset); } } @@ -886,7 +892,7 @@ studio.internal = (function(proto) { function applyTimeOffset(ts) { if (ts === undefined || ts === null) return ts; - return ts + timeOffsetNs; + return ts + (hostTimeOffsets.get(host) || 0); } this.makeStructureRequest = function(id) {