@lifefinder/vsm-mqtt-client-open-source 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ const mqtt = require('mqtt')
2
+
3
+ const printUsageAndExit = (info) => {
4
+ console.log(info);
5
+ process.exit(1);
6
+ }
7
+
8
+ // Try to determine the maximum downlink frame size
9
+ /* TODO: Inventory all these for max size and use in below function
10
+ const sfToSizeTable = {
11
+ "SF12BW125" : 51,
12
+ "SF11BW125" : 51,
13
+ "SF10BW125" : 51,
14
+ "SF9BW125" : 115,
15
+ "SF8BW125" : 222,
16
+ "SF7BW125" : 222,
17
+ "SF12BW250" :
18
+ "SF11BW250" :
19
+ "SF10BW250" :
20
+ "SF9BW250" :
21
+ "SF8BW250" :
22
+ "SF7BW250" :
23
+ "SF12BW500" :
24
+ "SF11BW500" :
25
+ "SF10BW500" :
26
+ "SF9BW500" :
27
+ "SF8BW500" :
28
+ "SF7BW500" :
29
+ "LRFHSS1BW137" : 7),
30
+ "LRFHSS2BW137" : 7),
31
+ "LRFHSS1BW336" : 6),
32
+ "LRFHSS2BW336" : 6),
33
+ "LRFHSS1BW1523": 523),
34
+ "LRFHSS2BW1523": 523),
35
+ "FSK50" :
36
+ */
37
+
38
+ const getMaxSize = (sfbw) => {
39
+ if (sfbw == "SF12BW125" ||
40
+ sfbw == "SF11BW125" ||
41
+ sfbw == "SF10BW125")
42
+ return 51;
43
+ if (sfbw == "SF9BW125")
44
+ return 115;
45
+ if (sfbw == "SF8BW125" ||
46
+ sfbw == "SF7BW125" ||
47
+ sfbw == "SF8BW250")
48
+ return 222;
49
+ console.log("Warning: Unhandled helium spreading: " + sfbw);
50
+ }
51
+
52
+ module.exports.api = {
53
+ getVersionString: () => { return "Helium MQTT Integration"; },
54
+ checkArgumentsOrExit: (args) => {
55
+ if (!args.u)
56
+ printUsageAndExit("Helium: -u <mqtt broker user name> is required")
57
+ if (!args.p)
58
+ printUsageAndExit("Helium: -u <mqtt broker password> is required")
59
+ if (!args.s)
60
+ printUsageAndExit("Helium: -s <mqtt broker url> is required");
61
+ },
62
+ connectAndSubscribe: async (args, devices, onUplinkDevicePortBufferDateLatLng) => {
63
+ args.v && console.log("Trying to connect to " + args.s);
64
+ try {
65
+ // Create an MQTT client instance
66
+ /* const options = {
67
+ // Clean session
68
+ clean: true,
69
+ connectTimeout: 4000,
70
+ // Authentication
71
+ clientId: args.u,
72
+ username: args.u,
73
+ password: args.p,
74
+ } */
75
+
76
+ const client = mqtt.connect(args.s /*, options*/);
77
+
78
+ client.on('connect', () => {
79
+ args.v && console.log("Helium: Connected to mqtt broker");
80
+
81
+ // Do we have a device list?
82
+ if (Array.isArray(devices) && devices.length > 0) {
83
+ for (let i = 0; i < devices.length; ++i) {
84
+ const topic = `helium/vsm/rx/${devices[i].toUpperCase()}`;
85
+ client.subscribe(topic, (err) => {
86
+ if (err)
87
+ console.log(`Helium subscribe: ${topic} failed:` + err.message );
88
+ else
89
+ args.v && console.log(`Helium subscribed ok to ${topic}`);
90
+ });
91
+ }
92
+ } else {
93
+ // Do a wildcard subscription to any device starting with the assigned range of
94
+ // Sensative DevEUIs
95
+ const topic = `helium/vsm/rx/#`;
96
+ client.subscribe(topic, (err) => {
97
+ if (err)
98
+ console.log(`Helium subscribe: ${topic} failed:` + err.message );
99
+ else
100
+ args.v && console.log(`Helium subscribed ok to ${topic}`);
101
+ });
102
+ }
103
+ });
104
+ client.on('message', async (topic, message) => {
105
+ // message is Buffer
106
+ args.v && console.log(topic, message.toString());
107
+
108
+ const obj = JSON.parse(message.toString('utf-8'));
109
+ console.log(obj);
110
+ const data = Buffer.from(obj.payload, "base64");
111
+ const port = obj.port;
112
+ const id = obj.dev_eui;
113
+
114
+ let lat, lng;
115
+ let date;
116
+ let maxSize = 40; // Some default
117
+ // Take first gateways lat & lng values, any gateway likely to hear this is likely within 150km
118
+ if (obj.hotspots && obj.hotspots.length > 0) {
119
+ let gwinfo = obj.hotspots[0];
120
+ lat = gwinfo.lat;
121
+ lng = gwinfo.long;
122
+ maxSize = getMaxSize(gwinfo.spreading);
123
+ }
124
+ date = new Date(obj.reported_at);
125
+ if (!date)
126
+ date = new Date()
127
+
128
+ await onUplinkDevicePortBufferDateLatLng(client, id, port, data, date, lat, lng, maxSize);
129
+ });
130
+ return client;
131
+ } catch (e) {
132
+ console.log("Chirpstack: Got exception: " + e.message);
133
+ throw e;
134
+ }
135
+ },
136
+ sendDownlink: async (client, args, deviceId, port, data, confirmed) => {
137
+ if (!Buffer.isBuffer(data))
138
+ throw new Error("Helium sendDownlink: data must be a buffer object");
139
+ const devEUI = deviceId.toUpperCase();
140
+ const topic = `helium/vsm/tx/${devEUI}`;
141
+ const obj = {
142
+ confirmed,
143
+ port,
144
+ payload_raw: data.toString('base64'),
145
+ };
146
+ client.publish(topic, JSON.stringify(obj));
147
+ args.v && console.log("Publish downlink on port " + port + " data: " + data.toString("hex"));
148
+ },
149
+ }
150
+
@@ -0,0 +1,136 @@
1
+ const mqtt = require("mqtt");
2
+ const { isDate } = require("util/types");
3
+
4
+ const printUsageAndExit = (info) => {
5
+ console.log(info);
6
+ process.exit(1);
7
+ }
8
+
9
+ const getMaxSize = (obj) => {
10
+ if (Number.isInteger(obj.dr)) {
11
+ if (obj.dr <= 2)
12
+ return 51;
13
+ if (obj.dr == 3)
14
+ return 115;
15
+ return 222;
16
+ }
17
+
18
+ return 51;
19
+ }
20
+
21
+ module.exports.api = {
22
+ getVersionString: () => { return "Yggio Integration"; },
23
+ checkArgumentsOrExit: (args) => {
24
+ const CONSTANTS = require('../constants');
25
+ if (!args.a)
26
+ printUsageAndExit("Chirpstack: -a <application-id> is required");
27
+ if (!args.s)
28
+ printUsageAndExit("Chirpstack: -s <server url> is required");
29
+ if (!CONSTANTS.MONGODB.URI)
30
+ printUsageAndExit("Yggio: MONGODB.URI must be set in constants.json");
31
+ if (args.c)
32
+ console.log("Chirpstack: Using custom client id: " + args.c);
33
+ },
34
+ connectAndSubscribe: async (args, devices, onUplinkDevicePortBufferDateLatLng) => {
35
+ const TIMEOUT = 600000; // 10 minutes
36
+ let interval;
37
+ let client;
38
+ const runYggioIntegration = async () => {
39
+ const { MongoClient } = require("mongodb");
40
+ const CONSTANTS = require("../constants");
41
+ try {
42
+ args.v && console.log("Connecting to Yggio MongoDB");
43
+ const client = new MongoClient(CONSTANTS.MONGODB.URI, { useUnifiedTopology: true });
44
+ args.v && console.log("Connected to Yggio MongoDB");
45
+
46
+ const database = client.db("fafnir");
47
+ const entities = database.collection("entities");
48
+ const devicesResult = await entities
49
+ .find({"deviceModelName": "sensative-vsm-lora", "devEui": {$exists: true}})
50
+ .toArray();
51
+ devices = devicesResult.map(device => device.devEui);
52
+ console.log("Device count: " + devices.length);
53
+ } catch (e) {
54
+ console.log("Yggio: Got exception: " + e.message);
55
+ clearInterval(interval);
56
+ throw e;
57
+ }
58
+
59
+ args.v && console.log("Trying to connect to " + args.s + " with application " + args.a);
60
+ try {
61
+ if (!client?.connected) {
62
+ args.v && console.log("Connecting to chirpstack server");
63
+ client = mqtt.connect(args.s, {username: args.u, password: args.p, clientId: args.c || CONSTANTS.MQTT.CLIENT_ID});
64
+ } else {
65
+ args.v && console.log("Already connected to chirpstack server");
66
+ }
67
+
68
+ client.on("connect", () => {
69
+ args.v && console.log("Connected to chirpstack server");
70
+
71
+ if (Array.isArray(devices) && devices.length > 0) {
72
+ for (let i = 0; i < devices.length; ++i) {
73
+ const topic = `application/${args.a}/device/${devices[i].toLowerCase()}/event/up`;
74
+ client.subscribe(topic, (err) => {
75
+ if (err)
76
+ console.log(`Chirpstack subscribe: ${topic} failed:` + err.message );
77
+ else
78
+ args.v && console.log(`Chirpstack subscribed ok to ${topic}`);
79
+ });
80
+ }
81
+ }
82
+ });
83
+ client.on("message", async (topic, message) => {
84
+ // message is Buffer
85
+ args.v && console.log(topic, message.toString());
86
+
87
+ const obj = JSON.parse(message.toString("utf-8"));
88
+ if (!obj.data)
89
+ return;
90
+ const data = Buffer.from(obj.data, "base64");
91
+ const port = obj.fPort;
92
+ const id = obj.deviceInfo?.devEui || obj.devEUI;
93
+ const maxSize = getMaxSize(obj);
94
+
95
+ let lat, lng;
96
+ let date;
97
+ // Take first gateways lat & lng values, any gateway likely to hear this is likely within 150km
98
+ if (obj.rxInfo && obj.rxInfo.length > 0) {
99
+ let gwinfo = obj.rxInfo[0];
100
+ if (gwinfo.location) {
101
+ lat = gwinfo.location.latitude;
102
+ lng = gwinfo.location.longitude;
103
+ }
104
+ date = new Date(gwinfo.time);
105
+ }
106
+ if (! (date && isDate(date)))
107
+ date = new Date()
108
+
109
+ await onUplinkDevicePortBufferDateLatLng(client, id, port, data, date, lat, lng, maxSize);
110
+ });
111
+ return client;
112
+ } catch (e) {
113
+ console.log("Chirpstack: Got exception: " + e.message);
114
+ clearInterval(interval);
115
+ throw e;
116
+ }
117
+ };
118
+ runYggioIntegration();
119
+ interval = setInterval(runYggioIntegration, TIMEOUT);
120
+ },
121
+ sendDownlink: async (client, args, deviceId, port, data, confirmed) => {
122
+ if (!Buffer.isBuffer(data))
123
+ throw new Error("Chirpstack sendDownlink: data must be a buffer object");
124
+ const devEUI = deviceId.toLowerCase();
125
+ const topic = `application/${args.a}/device/${devEUI}/command/down`;
126
+ const obj = {
127
+ devEui: devEUI,
128
+ confirmed,
129
+ fPort: port,
130
+ payload: data.toString("base64"),
131
+ };
132
+ client.publish(topic, JSON.stringify(obj));
133
+ args.v && console.log("Publish downlink on port " + port + " data: " + data.toString("hex"));
134
+ },
135
+ }
136
+
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@lifefinder/vsm-mqtt-client-open-source",
3
+ "version": "0.0.46",
4
+ "description": "MQTT client for vsm sensors",
5
+ "main": "vsm-mqtt-client.js",
6
+ "author": "Lars Mats",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "dependencies": {
13
+ "minimist": "1.2.8",
14
+ "mongodb": "6.3.0",
15
+ "mqtt": "4.3.7",
16
+ "node-fetch": "3.3.1",
17
+ "vsm-translator-open-source": "^0.2.157"
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ // Publisher which only displays output on standardout
2
+
3
+ module.exports.api = {
4
+ checkArgumentsOrExit: (args) => { },
5
+ initialize: (args) => { },
6
+ publish: (args, deviceid, obj) => {
7
+ console.log(deviceid, obj);
8
+ },
9
+ getVersionString: () => {
10
+ return "Console Publisher";
11
+ }
12
+ }
13
+
@@ -0,0 +1,38 @@
1
+ // Publisher which publishes on https
2
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
3
+
4
+ const printErrorAndExit = (info) => {
5
+ console.log(info);
6
+ process.exit(1);
7
+ }
8
+
9
+ module.exports.api = {
10
+ checkArgumentsOrExit: (args) => {
11
+ if (!args.S)
12
+ printErrorAndExit("HTPPS Publisher: -S <url> is required");
13
+ },
14
+ initialize: (args) => {
15
+ },
16
+ publish: async (args, deviceid, obj) => {
17
+ const url = args.S;
18
+ console.log("HTTPS Publish to " + url, deviceid, JSON.stringify(obj));
19
+ try {
20
+ await fetch(url, {
21
+ method:"POST",
22
+ body: JSON.stringify(obj),
23
+ headers: {
24
+ "Accept": "application/json",
25
+ "Content-type" : "application/json",
26
+ "cache-control": "no-cache"
27
+ },}).then(response => response.json())
28
+ .then(data => {console.log(" Response: ", data); return data; })
29
+ .catch(err => {console.log(" HTTPS Publish Failed: " + err.message);});
30
+ } catch (e) {
31
+ console.log(" HTTPS Publisher: Failed to publish: ", e.message);
32
+ }
33
+ },
34
+ getVersionString: () => {
35
+ return "HTTPS Publisher";
36
+ }
37
+ }
38
+
@@ -0,0 +1,53 @@
1
+ // Publisher which publishes on mqtt
2
+
3
+ const mqtt = require('mqtt')
4
+
5
+ const REPLACE_STRING = "deveui";
6
+
7
+ const printErrorAndExit = (info) => {
8
+ console.log(info);
9
+ process.exit(1);
10
+ }
11
+
12
+ let client;
13
+
14
+ module.exports.api = {
15
+ checkArgumentsOrExit: (args) => {
16
+ if (!args.S)
17
+ printErrorAndExit("MQTT Publisher: -S <mqtt server> is required");
18
+ if (!args.T)
19
+ printErrorAndExit("MQTT Publisher: -T <topic format> is required");
20
+ if (!args.T.includes(REPLACE_STRING))
21
+ printErrorAndExit("MQTT Publisher: -T <topic format> must contain the substutution string " + REPLACE_STRING)
22
+ },
23
+ initialize: (args) => {
24
+ /* const options = {
25
+ // Clean session
26
+ clean: true,
27
+ connectTimeout: 4000,
28
+ // Authentication
29
+ clientId: args.u,
30
+ username: args.u,
31
+ password: args.p,
32
+ } */
33
+
34
+ client = mqtt.connect(args.S /*, options*/);
35
+
36
+ client.on('connect', () => {
37
+ args.v && console.log("MQTT Publisher: Connected to mqtt broker");
38
+ });
39
+ },
40
+ publish: (args, deviceid, obj) => {
41
+ console.log("MQTT Publishing", deviceid, obj);
42
+ const topic = args.T.replace(REPLACE_STRING, deviceid);
43
+ try {
44
+ client.publish(topic, JSON.stringify(obj));
45
+ } catch (e) {
46
+ console.log("MQTT Publisher: Failed to publish: ", e.message);
47
+ }
48
+ },
49
+ getVersionString: () => {
50
+ return "MQTT Publisher";
51
+ }
52
+ }
53
+
package/rules.js ADDED
@@ -0,0 +1,246 @@
1
+ /*
2
+ The MIT License (MIT)
3
+
4
+ Copyright Sensative AB 2023. All rights reserved.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ */
24
+
25
+ const { mergeDeep, delay } = require('./util');
26
+ let translatorVersion = "";
27
+ try {
28
+ // Running at top level
29
+ translatorVersion = require('./node_modules/vsm-translator-open-source/package.json').version;
30
+ } catch (e) {
31
+ // Running as npm package
32
+ translatorVersion = require('../vsm-translator-open-source/package.json').version;
33
+ }
34
+ console.log("Translator Version: " + translatorVersion);
35
+
36
+ const ASSISTANCE_INTERVAL_S = 60*30; // max 300km/h
37
+ const MAX_ALMANAC_AGE_S = 60*60*24*30; // This is a monthly process
38
+ const ALMANAC_DOWNLOAD_INTERVAL_S = 60*60*12; // No more frequent tries than this
39
+
40
+ const byteToHex2 = (b) => {
41
+ const table = "0123456789abcdef";
42
+ return "" + table[(b>>4)&0xf] + table[b&0xf];
43
+ }
44
+
45
+ const int32ToHex8 = (n) => {
46
+ return "" + byteToHex2(n>>24) + byteToHex2(n>>16) + byteToHex2(n>>8) + byteToHex2(n);
47
+ }
48
+
49
+
50
+ const downlinkAssistancePositionIfMissing = async (args, integration, client, solver, deviceid, next, lat, lng) => {
51
+ if (lat && lng && next && next.gnss) {
52
+ let updateRequired = false;
53
+ if (next.gnss.lastAssistanceUpdateAttempt) {
54
+ lastTime = new Date(next.gnss.lastAssistanceUpdateAttempt);
55
+ now = new Date();
56
+ if (now.getTime() - lastTime.getTime() < ASSISTANCE_INTERVAL_S*1000) {
57
+ return next; // Do nothing
58
+ }
59
+ }
60
+
61
+ if (!next.gnss.assistanceLatitude || Math.abs(lat - next.gnss.assistanceLatitude) > 0.1)
62
+ updateRequired = true;
63
+ if (!next.gnss.assistanceLongitude || Math.abs(lng - next.gnss.assistanceLongitude) > 0.1)
64
+ updateRequired = true;
65
+ if (updateRequired) {
66
+ next.gnss.lastAssistanceUpdateAttempt = new Date();
67
+
68
+ const lat16 = Math.round(2048*lat / 90) & 0xffff;
69
+ const lon16 = Math.round(2048*lng / 180) & 0xffff;
70
+ let downlink = "01"; // Begin with 01 which indicates that this is a assisted position
71
+ let str = lat16.toString(16);
72
+ while (str.length < 4)
73
+ str = "0"+str;
74
+ downlink += str;
75
+ str = lon16.toString(16);
76
+ while (str.length < 4)
77
+ str = "0"+str;
78
+ downlink += str;
79
+
80
+ integration.api.sendDownlink(client, args, deviceid, 21, Buffer.from(downlink, "hex"), false /* confirmed */ );
81
+ }
82
+ }
83
+ return next;
84
+ }
85
+
86
+ const downlinkCrcRequest = (args, integration, client, deviceid) => {
87
+ integration.api.sendDownlink(client, args, deviceid, 15, Buffer.from("00", "hex"), false /* confirmed */ );
88
+ }
89
+
90
+ const downlinkDeviceTimeDelta = (args, integration, client, deviceid, deltaS) => {
91
+ let buffer = "08" + int32ToHex8(deltaS);
92
+ integration.api.sendDownlink(client, args, deviceid, 21, Buffer.from(buffer, "hex"), false /* confirmed */ );
93
+ }
94
+
95
+ const downlinkAlmanac = async (args, integration, client, solver, deviceid, maxSize) => {
96
+ const f = async () => {
97
+ const almanac = await solver.api.loadAlmanac(args);
98
+ if (!(almanac && almanac.result && almanac.result.almanac_image)) {
99
+ console.log("Bad alamanac data");
100
+ return;
101
+ }
102
+
103
+ const compressedAlmanac = almanac.result.almanac_compressed;
104
+ const image = compressedAlmanac ? compressedAlmanac : almanac.result.almanac_image;
105
+
106
+ let maxDownlinkSize = maxSize - 6; // Give space for some mac commands
107
+ if (maxDownlinkSize < 30) {
108
+ // Does not make sense with this small downlink size for almanac download
109
+ console.log("Too many almanac downlinks, cancelling until better connection acheived");
110
+ return;
111
+ }
112
+ const almanacTypeStr = (compressedAlmanac ? "Compressed" : "Full");
113
+ console.log("Almanac image type: " + almanacTypeStr);
114
+ console.log("Selected payload size: " + maxDownlinkSize);
115
+ console.log("Almanac image size: " + image.length / 2 );
116
+ console.log("Downlink count: " + image.length / 2 / maxDownlinkSize);
117
+
118
+ let chunks = image.match(new RegExp('.{1,' + (maxDownlinkSize*2 /* 40 is randomly selected */ ) + '}', 'g'));
119
+ console.log("Chunks: " + chunks.length);
120
+
121
+ for (let i = 0; i < chunks.length; ++i) {
122
+ var data;
123
+ if (i === 0) // Begin new almanac
124
+ data = "02";
125
+ else if (i === chunks.length-1) {
126
+ if (compressedAlmanac)
127
+ data = "05"; // End compressed almanac
128
+ else
129
+ data = "04"; // End uncompressed almanac
130
+ }
131
+ else
132
+ data = "03"; // Plain almanac segment
133
+ data += chunks[i];
134
+
135
+ try {
136
+ await integration.api.sendDownlink(client, args, deviceid, 21, Buffer.from(data, "hex"), true);
137
+ console.log(deviceid, almanacTypeStr + " Almanac downlink " + (i+1) + " of " + chunks.length + " - enqueueing");
138
+ await delay(1000); // Increase chance of correct order in chirpstack
139
+ } catch (e) { return; }
140
+ }
141
+ }
142
+ // Do not await the results here
143
+ f();
144
+ }
145
+
146
+
147
+ const rules = [
148
+ // Detect if we do not know which application the device is running (meaning it cannot be translated fully)
149
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
150
+ // Check if the rules CRC is registerred
151
+ if (next.vsm && next.vsm.rulesCrc32)
152
+ return next;
153
+ // This needs to be resolved ASAP
154
+ downlinkCrcRequest(args, integration, client, deviceid);
155
+ return next;
156
+ },
157
+
158
+ // Detect if device time is off, and if so downlink a time correction
159
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
160
+ // Check if this update was a gnss message containing deviceTime
161
+ if (!updates.gnss)
162
+ return next;
163
+ if (!(updates.gnss.deviceTime && updates.gnss.deviceTimeTimestamp))
164
+ return next;
165
+ let deviceDateS = new Date(updates.gnss.deviceTime).getTime()/1000;
166
+ let receivedDateS = new Date(updates.gnss.deviceTimeTimestamp).getTime()/1000;
167
+ let deltaS = receivedDateS - deviceDateS;
168
+ args.v && console.log("Device time offset: " + deltaS);
169
+ if (deltaS >= 5 || deltaS <= 5) {
170
+ // Send downlink
171
+ console.log("Updating device time for " + deviceid + " by " + deltaS + "s");
172
+ downlinkDeviceTimeDelta(args, integration, client, deviceid, Math.round(deltaS));
173
+ }
174
+ return next;
175
+ },
176
+
177
+ // Solve positions and add the solution to the data
178
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
179
+ if (updates.semtechEncoded && !args.hasOwnProperty("N")) {
180
+ // Call semtech to resolve the location
181
+ console.log("New positioning data");
182
+ let solved = await solver.api.solvePosition(args, updates);
183
+ if (solved && solved.result && solved.result.latitude && solved.result.longitude) {
184
+ // Extra check: If we have a result here but no assistance data in the device, use this to generate an assistance position
185
+ // and downlink it to the device
186
+ downlinkAssistancePositionIfMissing(args, integration, client, solver, deviceid, next, lat, lng);
187
+ return solved.result;
188
+ } else {
189
+ return null;
190
+ }
191
+ }
192
+ },
193
+
194
+ // Detect absense of device assistance position OR the too large difference of lat & long vs assistance position,
195
+ // try to solve that by downloading new assistance position
196
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
197
+ // try download from gateway position only if there is no assistance position, else use solutions
198
+ if (next.gnss && !next.gnss.assistanceLatitude)
199
+ downlinkAssistancePositionIfMissing(args, integration, client, solver, deviceid, next, lat, lng);
200
+ },
201
+
202
+ // Detect if almanac download is called for
203
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
204
+ // Do we know if there is an almanac timestamp?
205
+ if (!(next.gnss && next.gnss.almanacTimestamp))
206
+ return next;
207
+
208
+ const almanacDate = new Date(next.gnss.almanacTimestamp);
209
+ if (date.getTime() - almanacDate.getTime() < MAX_ALMANAC_AGE_S*1000)
210
+ return next; // Unmodified
211
+
212
+ const lastAttemptMs = next.gnss.lastAlmanacDownloadAttempt ? new Date(next.gnss.lastAlmanacDownloadAttempt).getTime() : 0;
213
+ const lastAttemptPeriodS = (date.getTime() - lastAttemptMs)/1000;
214
+ if (lastAttemptPeriodS < ALMANAC_DOWNLOAD_INTERVAL_S)
215
+ return next; // Do not attempt a download now
216
+ next.gnss.lastAlmanacDownloadAttempt = date;
217
+
218
+ // Run this asynchronously rather than wait
219
+ if (!solver.api.downlinkAlmanac) {
220
+ return next;
221
+ }
222
+ downlinkAlmanac(args, integration, client, solver, deviceid, next.encodedData.maxSize);
223
+
224
+ return next;
225
+ },
226
+
227
+ // Update translator version
228
+ async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
229
+ if (next.vsm) {
230
+ next.vsm.translatorVersion = translatorVersion;
231
+ } else
232
+ next.vsm = { translatorVersion };
233
+ return next;
234
+ },
235
+ ];
236
+
237
+ module.exports.processRules = async (args, integration, client, solver, deviceid, next, updates, date, lat, lng) => {
238
+ // console.log("processRules - updates:", deviceid, updates);
239
+ for (let i = 0; i < rules.length; ++i) {
240
+ synthesized = await rules[i](args, integration, client, solver, deviceid, next, updates, date, lat, lng);
241
+ if (synthesized) {
242
+ next = mergeDeep(next, synthesized);
243
+ }
244
+ }
245
+ return next;
246
+ }