diff --git a/README.md b/README.md index d61a1764..f19a4a9c 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Output of sensor and peripheral data is internally switched by a bitmask registe # Time sync -Paxcounter can keep it's time-of-day synced with an external time source. Set *#define TIME_SYNC_INTERVAL* in paxcounter.conf to enable time sync. Supported external time sources are GPS, LORAWAN network time and LORAWAN application timeserver time. An on board DS3231 RTC is kept sycned as fallback time source. Time accuracy depends on board's time base which generates the pulse per second. Supported are GPS PPS, SQW output of RTC, and internal ESP32 hardware timer. Time base is selected by #defines in the board's hal file, see example in [**generic.h**](src/hal/generic.h). If your LORAWAN network does not support network time, you can run a Node-Red timeserver application using the [**Timeserver code**](/src/TTN/Nodered-Timeserver.json) in TTN subdirectory. Configure MQTT nodes in Node-Red to the same LORAWAN application as paxocunter device is using. +Paxcounter can keep it's time-of-day synced with an external time source. Set *#define TIME_SYNC_INTERVAL* in paxcounter.conf to enable time sync. Supported external time sources are GPS, LORAWAN network time and LORAWAN application timeserver time. An on board DS3231 RTC is kept sycned as fallback time source. Time accuracy depends on board's time base which generates the pulse per second. Supported are GPS PPS, SQW output of RTC, and internal ESP32 hardware timer. Time base is selected by #defines in the board's hal file, see example in [**generic.h**](src/hal/generic.h). If your LORAWAN network does not support network time, you can run a Node-Red timeserver application using the [**Timeserver code**](/src/Timeserver/Nodered-Timeserver.json) in TTN subdirectory. Configure MQTT nodes in Node-Red to the same LORAWAN application as paxocunter device is using. # Wall clock controller diff --git a/src/TTN/Nodered-Timeserver.json b/src/Timeserver/Nodered-Timeserver.json similarity index 79% rename from src/TTN/Nodered-Timeserver.json rename to src/Timeserver/Nodered-Timeserver.json index 5f608a24..e7bb487c 100644 --- a/src/TTN/Nodered-Timeserver.json +++ b/src/Timeserver/Nodered-Timeserver.json @@ -49,7 +49,7 @@ "to": "", "reg": false, "x": 240, - "y": 500, + "y": 513, "wires": [ [ "84f1cda2.069e7" @@ -82,7 +82,7 @@ "retain": "", "broker": "2a15ab6f.ab2244", "x": 730, - "y": 500, + "y": 513, "wires": [] }, { @@ -135,7 +135,7 @@ "action": "", "pretty": false, "x": 580, - "y": 500, + "y": 513, "wires": [ [ "72d5e7ee.d1eba8" @@ -153,7 +153,7 @@ "y": 200, "wires": [ [ - "f868bce2.dde67" + "831ab883.d6a238" ] ] }, @@ -165,7 +165,7 @@ "action": "", "property": "payload.payload_raw", "x": 420, - "y": 500, + "y": 513, "wires": [ [ "dac8aafa.389298" @@ -182,38 +182,13 @@ "y": 40, "wires": [] }, - { - "id": "f868bce2.dde67", - "type": "switch", - "z": "449c1517.e25f4c", - "name": "Timechecker", - "property": "payload.metadata.gateways[0].time", - "propertyType": "msg", - "rules": [ - { - "t": "lte", - "v": "payload.metadata.time", - "vt": "msg" - } - ], - "checkall": "true", - "repair": false, - "outputs": 1, - "x": 590, - "y": 200, - "wires": [ - [ - "831ab883.d6a238" - ] - ] - }, { "id": "831ab883.d6a238", "type": "function", "z": "449c1517.e25f4c", "name": "Generate Time Answer", - "func": "/* LoRaWAN Timeserver\n\nconstruct 6 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n0 sequence number (taken from node's time_sync_req)\n1..4 current second (from epoch time 1970)\n5 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\nvar deviceMsg = { payload: msg.payload.dev_id };\nvar gateways = msg.payload.metadata.gateways;\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) return [\"n/a\", \"n/a\", deviceMsg, 0xff];\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;\nvar seqno = msg.payload.payload_raw[0];\n\nlet buf = new ArrayBuffer(6);\nnew DataView(buf).setUint8(0, seqno);\nnew DataView(buf).setUint32(1, seconds);\nnew DataView(buf).setUint8(5, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, msg];", - "outputs": 4, + "func": "/* LoRaWAN Timeserver\n\nconstruct 6 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n0 sequence number (taken from node's time_sync_req)\n1..4 current second (from epoch time 1970)\n5 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\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 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);\nnew DataView(buf).setUint32(1, seconds);\nnew DataView(buf).setUint8(5, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqnoMsg, msg];", + "outputs": 5, "noerr": 0, "x": 360, "y": 340, @@ -229,6 +204,9 @@ [ "a5dbb4ef.019168" ], + [ + "1cb58e7f.221362" + ], [ "49e3c067.e782e" ] @@ -237,6 +215,7 @@ "gw_eui", "offset_ms", "device", + "seq_no", "time_sync_ans" ] }, @@ -251,7 +230,7 @@ "tostatus": true, "complete": "payload", "x": 700, - "y": 280, + "y": 240, "wires": [], "icon": "node-red/bridge.png" }, @@ -268,7 +247,7 @@ "format": "{{msg.payload}}", "layout": "col-center", "x": 810, - "y": 336, + "y": 300, "wires": [] }, { @@ -294,7 +273,7 @@ "seg1": "", "seg2": "", "x": 710, - "y": 416, + "y": 380, "wires": [] }, { @@ -310,7 +289,7 @@ "format": "{{msg.payload}}", "layout": "col-center", "x": 700, - "y": 376, + "y": 340, "wires": [] }, { @@ -322,7 +301,7 @@ "outputs": 1, "noerr": 0, "x": 670, - "y": 336, + "y": 300, "wires": [ [ "8712a5ac.ed18e8" @@ -342,7 +321,23 @@ "format": "{{msg.payload}}", "layout": "col-center", "x": 700, - "y": 456, + "y": 420, + "wires": [] + }, + { + "id": "1cb58e7f.221362", + "type": "ui_text", + "z": "449c1517.e25f4c", + "group": "edb7cc8d.a3817", + "order": 1, + "width": 0, + "height": 0, + "name": "Sequence No", + "label": "Sequence", + "format": "{{msg.payload}}", + "layout": "col-center", + "x": 700, + "y": 460, "wires": [] }, {