diff --git a/src/Timeserver/Nodered-Timeserver.json b/src/Timeserver/Nodered-Timeserver.json index 085b132d..0f324ba7 100644 --- a/src/Timeserver/Nodered-Timeserver.json +++ b/src/Timeserver/Nodered-Timeserver.json @@ -1,61 +1,4 @@ [ - { - "id": "9b4f492d.fbfd18", - "type": "change", - "z": "449c1517.e25f4c", - "name": "Payload", - "rules": [ - { - "t": "change", - "p": "topic", - "pt": "msg", - "from": "up", - "fromt": "str", - "to": "down", - "tot": "str" - }, - { - "t": "set", - "p": "payload.confirmed", - "pt": "msg", - "to": "false", - "tot": "bool" - }, - { - "t": "set", - "p": "payload.schedule", - "pt": "msg", - "to": "replace", - "tot": "str" - }, - { - "t": "move", - "p": "payload", - "pt": "msg", - "to": "payload.payload_raw", - "tot": "msg" - }, - { - "t": "move", - "p": "port", - "pt": "msg", - "to": "payload.port", - "tot": "msg" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 220, - "y": 520, - "wires": [ - [ - "53a85e2c.2728d" - ] - ] - }, { "id": "9c105726.613a58", "type": "mqtt in", @@ -72,19 +15,6 @@ ] ] }, - { - "id": "1c9a7438.6e38ec", - "type": "mqtt out", - "z": "449c1517.e25f4c", - "name": "send", - "topic": "", - "qos": "", - "retain": "", - "broker": "2a15ab6f.ab2244", - "x": 710, - "y": 520, - "wires": [] - }, { "id": "113ef524.57edeb", "type": "json", @@ -126,22 +56,6 @@ ] ] }, - { - "id": "90e76b02.6298f8", - "type": "json", - "z": "449c1517.e25f4c", - "name": "Convert", - "property": "payload", - "action": "", - "pretty": false, - "x": 560, - "y": 520, - "wires": [ - [ - "1c9a7438.6e38ec" - ] - ] - }, { "id": "d6f27e8e.93242", "type": "base64", @@ -158,28 +72,13 @@ ] ] }, - { - "id": "53a85e2c.2728d", - "type": "base64", - "z": "449c1517.e25f4c", - "name": "Encode", - "action": "", - "property": "payload.payload_raw", - "x": 400, - "y": 520, - "wires": [ - [ - "90e76b02.6298f8" - ] - ] - }, { "id": "15980d22.6f4663", "type": "comment", "z": "449c1517.e25f4c", - "name": "LoRaWAN Timeserver v1.21", + "name": "LoRaWAN Timeserver v1.3", "info": "PLEASE NOTE: There is a patent filed for the time sync algorithm used in the\ncode of this file. The shown implementation example is covered by the\nrepository's licencse, but you may not be eligible to deploy the applied\nalgorithm in applications without granted license by the patent holder.", - "x": 160, + "x": 150, "y": 47, "wires": [] }, @@ -188,7 +87,7 @@ "type": "function", "z": "449c1517.e25f4c", "name": "Timeserver Logic", - "func": "/* LoRaWAN Timeserver\n\nconstruct 7 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1 sequence number (taken from node's time_sync_req)\n2 timezone in 15 minutes steps\n3..6 current second (from epoch time 1970)\n7 1/250ths fractions of current second\n\n*/\n\nfunction timecompare(a, b) {\n \n const timeA = a.time;\n const timeB = b.time;\n\n let comparison = 0;\n if (timeA > timeB) {\n comparison = 1;\n } else if (timeA < timeB) {\n comparison = -1;\n }\n return comparison;\n}\n\nlet confidence = 2000; // max millisecond diff gateway time to server time\n\n// guess if we have received a valid time_sync_req command\nif (msg.payload.payload_raw.length != 1)\n return;\n\nvar deviceMsg = { payload: msg.payload.dev_id };\nvar seqNo = msg.payload.payload_raw[0];\nvar seqNoMsg = { payload: seqNo };\nvar gateway_list = msg.payload.metadata.gateways;\n\n// filter all gateway timestamps that have milliseconds part (which we assume have a \".\")\nvar gateways = gateway_list.filter(function (element) {\n return (element.time.includes(\".\"));\n});\n\nvar gateway_time = gateways.map(gw => {\n return {\n time: new Date(gw.time),\n eui: gw.gtw_id,\n }\n });\nvar server_time = new Date(msg.payload.metadata.time);\n\n// validate all gateway timestamps against lorawan server_time (which is assumed to be recent)\nvar gw_timestamps = gateway_time.filter(function (element) {\n return ((element.time > (server_time - confidence) && element.time <= server_time));\n});\n\n// if no timestamp left, we have no valid one and exit\nif (gw_timestamps.length === 0) {\n var notavailMsg = { payload: \"n/a\" };\n var notimeMsg = { payload: 0xff }; \n var buf2 = Buffer.alloc(1);\n msg.payload = new Buffer(buf2.fill(0xff));\n msg.port = 9; // Paxcounter TIMEPORT\n return [notavailMsg, notavailMsg, deviceMsg, seqNoMsg, msg];}\n\n// sort time array in ascending order to find most recent timestamp for time answer\ngw_timestamps.sort(timecompare);\n\nvar timestamp = gw_timestamps[0].time;\nvar eui = gw_timestamps[0].eui;\nvar offset = server_time - timestamp;\n\nvar seconds = Math.floor(timestamp/1000);\nvar fractions = (timestamp % 1000) / 4;\n\nlet buf = new ArrayBuffer(7);\nnew DataView(buf).setUint8(0, seqNo);\n// Timezone (in 15min steps)\nvar timezone = 8; // CET = UTC+2h\nnew DataView(buf).setUint8(1, timezone);\nnew DataView(buf).setUint32(2, seconds);\nnew DataView(buf).setUint8(6, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = 9; // Paxcounter TIMEPORT\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];", + "func": "/* LoRaWAN Timeserver\n\nVERSION: 1.3\n\nconstruct 6 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1 sequence number (taken from node's time_sync_req)\n2..5 current second (from GPS epoch starting 1980)\n6 1/250ths fractions of current second\n\n*/\n\nfunction timecompare(a, b) {\n \n const timeA = a.time;\n const timeB = b.time;\n\n let comparison = 0;\n if (timeA > timeB) {\n comparison = 1;\n } else if (timeA < timeB) {\n comparison = -1;\n }\n return comparison;\n}\n\nlet confidence = 1000; // max millisecond diff gateway time to server time\n\n// guess if we have received a valid time_sync_req command\nif (msg.payload.payload_raw.length != 1)\n return;\n\nvar deviceMsg = { payload: msg.payload.dev_id };\nvar seqNo = msg.payload.payload_raw[0];\nvar seqNoMsg = { payload: seqNo };\nvar gateway_list = msg.payload.metadata.gateways;\n\n// filter all gateway timestamps that have milliseconds part (which we assume have a \".\")\nvar gateways = gateway_list.filter(function (element) {\n return (element.time.includes(\".\"));\n});\n\nvar gateway_time = gateways.map(gw => {\n return {\n time: new Date(gw.time),\n eui: gw.gtw_id,\n }\n });\nvar server_time = new Date(msg.payload.metadata.time);\n\n// validate all gateway timestamps against lorawan server_time (which is assumed to be recent)\nvar gw_timestamps = gateway_time.filter(function (element) {\n return ((element.time > (server_time - confidence) && element.time <= server_time));\n});\n\n// if no timestamp left, we have no valid one and exit\nif (gw_timestamps.length === 0) {\n var notavailMsg = { payload: \"n/a\" };\n var notimeMsg = { payload: 0xff }; \n var buf2 = Buffer.alloc(1);\n msg.payload = new Buffer(buf2.fill(0xff));\n msg.port = 9; // Paxcounter TIMEPORT\n return [notavailMsg, notavailMsg, deviceMsg, seqNoMsg, msg];}\n\n// sort time array in ascending order to find most recent timestamp for time answer\ngw_timestamps.sort(timecompare);\n\nvar timestamp = gw_timestamps[0].time;\nvar eui = gw_timestamps[0].eui;\nvar offset = server_time - timestamp;\n\nvar seconds = Math.floor(timestamp/1000);\nvar fractions = (timestamp % 1000) / 4;\n\nlet buf = new ArrayBuffer(6);\nnew DataView(buf).setUint8(0, seqNo);\n// Timezone (in 15min steps) -> deprecated\n//var timezone = 8; // CET = UTC+2h\n//new DataView(buf).setUint8(1, timezone);\nnew DataView(buf).setUint32(2, seconds);\nnew DataView(buf).setUint8(6, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = 9; // Paxcounter TIMEPORT\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];", "outputs": 5, "noerr": 0, "x": 330, @@ -251,32 +150,6 @@ "y": 307, "wires": [] }, - { - "id": "de908e66.b6fd3", - "type": "ui_gauge", - "z": "449c1517.e25f4c", - "name": "Timeserver offset", - "group": "edb7cc8d.a3817", - "order": 2, - "width": 0, - "height": 0, - "gtype": "gage", - "title": "Offset gateway to server", - "label": "milliseconds", - "format": "{{value}}", - "min": 0, - "max": "2000", - "colors": [ - "#00b500", - "#e6e600", - "#ca3838" - ], - "seg1": "", - "seg2": "", - "x": 690, - "y": 387, - "wires": [] - }, { "id": "6aeb3720.a89618", "type": "ui_text", @@ -309,6 +182,147 @@ ] ] }, + { + "id": "cc245719.3c4cd8", + "type": "debug", + "z": "449c1517.e25f4c", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "x": 860, + "y": 140, + "wires": [] + }, + { + "id": "9b4f492d.fbfd18", + "type": "change", + "z": "449c1517.e25f4c", + "name": "Payload", + "rules": [ + { + "t": "change", + "p": "topic", + "pt": "msg", + "from": "up", + "fromt": "str", + "to": "down", + "tot": "str" + }, + { + "t": "set", + "p": "payload.confirmed", + "pt": "msg", + "to": "false", + "tot": "bool" + }, + { + "t": "set", + "p": "payload.schedule", + "pt": "msg", + "to": "replace", + "tot": "str" + }, + { + "t": "move", + "p": "payload", + "pt": "msg", + "to": "payload.payload_raw", + "tot": "msg" + }, + { + "t": "move", + "p": "port", + "pt": "msg", + "to": "payload.port", + "tot": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 220, + "y": 520, + "wires": [ + [ + "53a85e2c.2728d" + ] + ] + }, + { + "id": "1c9a7438.6e38ec", + "type": "mqtt out", + "z": "449c1517.e25f4c", + "name": "send", + "topic": "", + "qos": "", + "retain": "", + "broker": "2a15ab6f.ab2244", + "x": 710, + "y": 520, + "wires": [] + }, + { + "id": "90e76b02.6298f8", + "type": "json", + "z": "449c1517.e25f4c", + "name": "Convert", + "property": "payload", + "action": "", + "pretty": false, + "x": 560, + "y": 520, + "wires": [ + [ + "1c9a7438.6e38ec" + ] + ] + }, + { + "id": "53a85e2c.2728d", + "type": "base64", + "z": "449c1517.e25f4c", + "name": "Encode", + "action": "", + "property": "payload.payload_raw", + "x": 400, + "y": 520, + "wires": [ + [ + "90e76b02.6298f8" + ] + ] + }, + { + "id": "de908e66.b6fd3", + "type": "ui_gauge", + "z": "449c1517.e25f4c", + "name": "Timeserver offset", + "group": "edb7cc8d.a3817", + "order": 2, + "width": 0, + "height": 0, + "gtype": "gage", + "title": "Offset gateway to server", + "label": "milliseconds", + "format": "{{value}}", + "min": 0, + "max": "2000", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "", + "seg2": "", + "x": 690, + "y": 387, + "wires": [] + }, { "id": "d5a35bab.44cb18", "type": "ui_text", @@ -341,20 +355,6 @@ "y": 467, "wires": [] }, - { - "id": "cc245719.3c4cd8", - "type": "debug", - "z": "449c1517.e25f4c", - "name": "", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "x": 860, - "y": 140, - "wires": [] - }, { "id": "2a15ab6f.ab2244", "type": "mqtt-broker", diff --git a/src/Timeserver/timeserver.java b/src/Timeserver/timeserver.java index 0896b23f..66e4f94a 100644 --- a/src/Timeserver/timeserver.java +++ b/src/Timeserver/timeserver.java @@ -1,12 +1,13 @@ /* LoRaWAN Timeserver -construct 7 byte timesync_answer from gateway timestamp and node's time_sync_req +VERSION: 1.3 + +construct 6 byte timesync_answer from gateway timestamp and node's time_sync_req byte meaning 1 sequence number (taken from node's time_sync_req) -2 timezone in 15 minutes steps -3..6 current second (from epoch time 1970) -7 1/250ths fractions of current second +2..5 current second (from GPS epoch starting 1980) +6 1/250ths fractions of current second */ @@ -24,7 +25,7 @@ function timecompare(a, b) { return comparison; } -let confidence = 2000; // max millisecond diff gateway time to server time +let confidence = 1000; // max millisecond diff gateway time to server time // guess if we have received a valid time_sync_req command if (msg.payload.payload_raw.length != 1) @@ -72,11 +73,11 @@ var offset = server_time - timestamp; var seconds = Math.floor(timestamp/1000); var fractions = (timestamp % 1000) / 4; -let buf = new ArrayBuffer(7); +let buf = new ArrayBuffer(6); new DataView(buf).setUint8(0, seqNo); -// Timezone (in 15min steps) -var timezone = 8; // CET = UTC+2h -new DataView(buf).setUint8(1, timezone); +// Timezone (in 15min steps) -> deprecated +//var timezone = 8; // CET = UTC+2h +//new DataView(buf).setUint8(1, timezone); new DataView(buf).setUint32(2, seconds); new DataView(buf).setUint8(6, fractions);