diff --git a/include/button.h b/include/button.h index da168520..b6951ceb 100644 --- a/include/button.h +++ b/include/button.h @@ -1,9 +1,6 @@ #ifndef _BUTTON_H #define _BUTTON_H -#include "display.h" -#include "senddata.h" - void readButton(); #endif \ No newline at end of file diff --git a/include/irqhandler.h b/include/irqhandler.h index 3b0921c1..f7e31b92 100644 --- a/include/irqhandler.h +++ b/include/irqhandler.h @@ -20,12 +20,10 @@ int mask_user_IRQ(); int unmask_user_IRQ(); #ifdef HAS_DISPLAY -#include "display.h" void IRAM_ATTR DisplayIRQ(); #endif #ifdef HAS_BUTTON -#include "button.h" void IRAM_ATTR ButtonIRQ(); #endif diff --git a/src/Timeserver/Nodered-Timeserver.json b/src/Timeserver/Nodered-Timeserver.json index 3d08a95b..ee46c577 100644 --- a/src/Timeserver/Nodered-Timeserver.json +++ b/src/Timeserver/Nodered-Timeserver.json @@ -1 +1,387 @@ -[{"id":"49e3c067.e782e","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":240,"y":513,"wires":[["84f1cda2.069e7"]]},{"id":"cc140589.dea168","type":"mqtt in","z":"449c1517.e25f4c","name":"listen","topic":"+/devices/+/up","qos":"2","broker":"2a15ab6f.ab2244","x":110,"y":120,"wires":[["4f97d75.6c87528"]]},{"id":"72d5e7ee.d1eba8","type":"mqtt out","z":"449c1517.e25f4c","name":"send","topic":"","qos":"","retain":"","broker":"2a15ab6f.ab2244","x":730,"y":513,"wires":[]},{"id":"4f97d75.6c87528","type":"json","z":"449c1517.e25f4c","name":"Convert","property":"payload","action":"","pretty":false,"x":260,"y":120,"wires":[["9f4b8dd3.2f0d2"]]},{"id":"9f4b8dd3.2f0d2","type":"switch","z":"449c1517.e25f4c","name":"Timeport","property":"payload.port","propertyType":"msg","rules":[{"t":"eq","v":"9","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":420,"y":120,"wires":[["8ed813a9.a9319"]]},{"id":"dac8aafa.389298","type":"json","z":"449c1517.e25f4c","name":"Convert","property":"payload","action":"","pretty":false,"x":580,"y":513,"wires":[["72d5e7ee.d1eba8"]]},{"id":"8ed813a9.a9319","type":"base64","z":"449c1517.e25f4c","name":"Decode","action":"","property":"payload.payload_raw","x":420,"y":200,"wires":[["831ab883.d6a238"]]},{"id":"84f1cda2.069e7","type":"base64","z":"449c1517.e25f4c","name":"Encode","action":"","property":"payload.payload_raw","x":420,"y":513,"wires":[["dac8aafa.389298"]]},{"id":"6190967b.01f758","type":"comment","z":"449c1517.e25f4c","name":"LoRaWAN Timeserver v1.1","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":170,"y":40,"wires":[]},{"id":"831ab883.d6a238","type":"function","z":"449c1517.e25f4c","name":"Generate Time Answer","func":"/* LoRaWAN Timeserver\n\nconstruct 5 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1..4 current second (from epoch time 1970)\n5 1/250ths fractions of current second\n\nFPort = sequence number (taken from node's time_sync_req)\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(5);\nnew DataView(buf).setUint32(0, seconds);\nnew DataView(buf).setUint8(4, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = seqNo;\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];","outputs":5,"noerr":0,"x":360,"y":340,"wires":[["37722d4b.08e3c2","a8a04c7a.c5fbd","a15454a9.fa0948"],["46ce842a.614d5c"],["a5dbb4ef.019168"],["1cb58e7f.221362"],["49e3c067.e782e"]],"outputLabels":["gw_eui","offset_ms","device","seq_no","time_sync_ans"]},{"id":"37722d4b.08e3c2","type":"debug","z":"449c1517.e25f4c","name":"Timeserver Gw","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","x":700,"y":240,"wires":[],"icon":"node-red/bridge.png"},{"id":"8712a5ac.ed18e8","type":"ui_text","z":"449c1517.e25f4c","group":"edb7cc8d.a3817","order":3,"width":0,"height":0,"name":"Recent time","label":"Last answer at:","format":"{{msg.payload}}","layout":"col-center","x":810,"y":300,"wires":[]},{"id":"46ce842a.614d5c","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":710,"y":380,"wires":[]},{"id":"a8a04c7a.c5fbd","type":"ui_text","z":"449c1517.e25f4c","group":"edb7cc8d.a3817","order":1,"width":0,"height":0,"name":"Recent server","label":"Gateway","format":"{{msg.payload}}","layout":"col-center","x":700,"y":340,"wires":[]},{"id":"a15454a9.fa0948","type":"function","z":"449c1517.e25f4c","name":"Time","func":"msg.payload = new Date().toLocaleString('en-GB', {timeZone: 'Europe/Berlin'});\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":300,"wires":[["8712a5ac.ed18e8"]]},{"id":"a5dbb4ef.019168","type":"ui_text","z":"449c1517.e25f4c","group":"edb7cc8d.a3817","order":1,"width":0,"height":0,"name":"Recent Device","label":"Device","format":"{{msg.payload}}","layout":"col-center","x":700,"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":[]},{"id":"2a15ab6f.ab2244","type":"mqtt-broker","z":"","name":"eu.thethings.network:1883","broker":"eu.thethings.network","port":"1883","tls":"","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"edb7cc8d.a3817","type":"ui_group","z":"","name":"Timeserver","tab":"d525a5d.0832858","order":4,"disp":true,"width":"6","collapse":false},{"id":"d525a5d.0832858","type":"ui_tab","z":"","name":"Timeserver","icon":"sync","order":3,"disabled":false,"hidden":false}] \ No newline at end of file +[ + { + "id": "49e3c067.e782e", + "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": 240, + "y": 513, + "wires": [ + [ + "84f1cda2.069e7" + ] + ] + }, + { + "id": "cc140589.dea168", + "type": "mqtt in", + "z": "449c1517.e25f4c", + "name": "listen", + "topic": "+/devices/+/up", + "qos": "2", + "broker": "2a15ab6f.ab2244", + "x": 110, + "y": 120, + "wires": [ + [ + "4f97d75.6c87528" + ] + ] + }, + { + "id": "72d5e7ee.d1eba8", + "type": "mqtt out", + "z": "449c1517.e25f4c", + "name": "send", + "topic": "", + "qos": "", + "retain": "", + "broker": "2a15ab6f.ab2244", + "x": 730, + "y": 513, + "wires": [] + }, + { + "id": "4f97d75.6c87528", + "type": "json", + "z": "449c1517.e25f4c", + "name": "Convert", + "property": "payload", + "action": "", + "pretty": false, + "x": 260, + "y": 120, + "wires": [ + [ + "9f4b8dd3.2f0d2" + ] + ] + }, + { + "id": "9f4b8dd3.2f0d2", + "type": "switch", + "z": "449c1517.e25f4c", + "name": "Timeport", + "property": "payload.port", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "9", + "vt": "num" + } + ], + "checkall": "true", + "repair": false, + "outputs": 1, + "x": 420, + "y": 120, + "wires": [ + [ + "8ed813a9.a9319" + ] + ] + }, + { + "id": "dac8aafa.389298", + "type": "json", + "z": "449c1517.e25f4c", + "name": "Convert", + "property": "payload", + "action": "", + "pretty": false, + "x": 580, + "y": 513, + "wires": [ + [ + "72d5e7ee.d1eba8" + ] + ] + }, + { + "id": "8ed813a9.a9319", + "type": "base64", + "z": "449c1517.e25f4c", + "name": "Decode", + "action": "", + "property": "payload.payload_raw", + "x": 420, + "y": 200, + "wires": [ + [ + "831ab883.d6a238" + ] + ] + }, + { + "id": "84f1cda2.069e7", + "type": "base64", + "z": "449c1517.e25f4c", + "name": "Encode", + "action": "", + "property": "payload.payload_raw", + "x": 420, + "y": 513, + "wires": [ + [ + "dac8aafa.389298" + ] + ] + }, + { + "id": "6190967b.01f758", + "type": "comment", + "z": "449c1517.e25f4c", + "name": "LoRaWAN Timeserver v1.1", + "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": 170, + "y": 40, + "wires": [] + }, + { + "id": "831ab883.d6a238", + "type": "function", + "z": "449c1517.e25f4c", + "name": "Generate Time Answer", + "func": "/* LoRaWAN Timeserver\n\nconstruct 5 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1..4 current second (from epoch time 1970)\n5 1/250ths fractions of current second\n\nFPort = sequence number (taken from node's time_sync_req)\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(5);\nnew DataView(buf).setUint32(0, seconds);\nnew DataView(buf).setUint8(4, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = seqNo;\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];", + "outputs": 5, + "noerr": 0, + "x": 360, + "y": 340, + "wires": [ + [ + "37722d4b.08e3c2", + "a8a04c7a.c5fbd", + "a15454a9.fa0948" + ], + [ + "46ce842a.614d5c" + ], + [ + "a5dbb4ef.019168" + ], + [ + "1cb58e7f.221362" + ], + [ + "49e3c067.e782e" + ] + ], + "outputLabels": [ + "gw_eui", + "offset_ms", + "device", + "seq_no", + "time_sync_ans" + ] + }, + { + "id": "37722d4b.08e3c2", + "type": "debug", + "z": "449c1517.e25f4c", + "name": "Timeserver Gw", + "active": true, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "x": 700, + "y": 240, + "wires": [], + "icon": "node-red/bridge.png" + }, + { + "id": "8712a5ac.ed18e8", + "type": "ui_text", + "z": "449c1517.e25f4c", + "group": "edb7cc8d.a3817", + "order": 3, + "width": 0, + "height": 0, + "name": "Recent time", + "label": "Last answer at:", + "format": "{{msg.payload}}", + "layout": "col-center", + "x": 810, + "y": 300, + "wires": [] + }, + { + "id": "46ce842a.614d5c", + "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": 710, + "y": 380, + "wires": [] + }, + { + "id": "a8a04c7a.c5fbd", + "type": "ui_text", + "z": "449c1517.e25f4c", + "group": "edb7cc8d.a3817", + "order": 1, + "width": 0, + "height": 0, + "name": "Recent server", + "label": "Gateway", + "format": "{{msg.payload}}", + "layout": "col-center", + "x": 700, + "y": 340, + "wires": [] + }, + { + "id": "a15454a9.fa0948", + "type": "function", + "z": "449c1517.e25f4c", + "name": "Time", + "func": "msg.payload = new Date().toLocaleString('en-GB', {timeZone: 'Europe/Berlin'});\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 670, + "y": 300, + "wires": [ + [ + "8712a5ac.ed18e8" + ] + ] + }, + { + "id": "a5dbb4ef.019168", + "type": "ui_text", + "z": "449c1517.e25f4c", + "group": "edb7cc8d.a3817", + "order": 1, + "width": 0, + "height": 0, + "name": "Recent Device", + "label": "Device", + "format": "{{msg.payload}}", + "layout": "col-center", + "x": 700, + "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": [] + }, + { + "id": "2a15ab6f.ab2244", + "type": "mqtt-broker", + "z": "", + "name": "eu.thethings.network:1883", + "broker": "eu.thethings.network", + "port": "1883", + "tls": "", + "clientid": "", + "usetls": false, + "compatmode": true, + "keepalive": "60", + "cleansession": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "closeTopic": "", + "closeQos": "0", + "closePayload": "", + "willTopic": "", + "willQos": "0", + "willPayload": "" + }, + { + "id": "edb7cc8d.a3817", + "type": "ui_group", + "z": "", + "name": "Timeserver", + "tab": "d525a5d.0832858", + "order": 4, + "disp": true, + "width": "6", + "collapse": false + }, + { + "id": "d525a5d.0832858", + "type": "ui_tab", + "z": "", + "name": "Timeserver", + "icon": "sync", + "order": 3, + "disabled": false, + "hidden": false + } +] \ No newline at end of file