@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.
- package/LICENSE.txt +21 -0
- package/README.md +210 -0
- package/chirpstack-devices.list +4 -0
- package/constants.json +14 -0
- package/decorators/default.js +10 -0
- package/decorators/minimal.js +19 -0
- package/decorators/yggio-push.js +11 -0
- package/helium-devices.list +4 -0
- package/integrations/chirpstack3.js +135 -0
- package/integrations/chirpstack4.js +120 -0
- package/integrations/helium.js +150 -0
- package/integrations/yggio.js +136 -0
- package/package.json +19 -0
- package/publishers/console.js +13 -0
- package/publishers/https.js +38 -0
- package/publishers/mqtt.js +53 -0
- package/rules.js +246 -0
- package/solvers/aws.js +102 -0
- package/solvers/combain-loracloud.js +263 -0
- package/solvers/loracloud.js +287 -0
- package/solvers/none.js +40 -0
- package/store.js +82 -0
- package/util.js +35 -0
- package/vsm-mqtt-client.js +299 -0
|
@@ -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
|
+
}
|