@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/solvers/aws.js ADDED
@@ -0,0 +1,102 @@
1
+
2
+
3
+ let iotwireless;
4
+ const solvePosition = async (args, data) => {
5
+
6
+ const isWifi = data.semtechEncoded && (data.semtechEncoded.msgtype === "wifi");
7
+
8
+ let body = isWifi ? data.wifi : (data.semtechEncoded ? data.semtechEncoded : data.semtechGpsEncoded);
9
+ body = JSON.parse(JSON.stringify(body)); // Make a copy of this object since it is manipulated below
10
+ if (isWifi) {
11
+ if ((!body.wifiAccessPoints) || body.wifiAccessPoints.length < 2) {
12
+ console.log(data);
13
+ throw new Error("Not enough access points to resolve wifi position")
14
+ }
15
+ body.wifiAccessPoints.forEach((accessPoint) => {
16
+ accessPoint.Rss = accessPoint.signalStrength;
17
+ delete accessPoint.signalStrength;
18
+ accessPoint.MacAddress = accessPoint.macAddress
19
+ delete accessPoint.macAddress;
20
+ });
21
+ const params = {
22
+ WiFiAccessPoints: body.wifiAccessPoints,
23
+ }
24
+ console.log ('Sending request to AWS to resolve position');
25
+ console.log('Params: ', params);
26
+ return await iotwireless.getPositionEstimate(params, function(err, response) {
27
+ if (err) {
28
+ console.log('Something went wrong when calling "getPositionEstimate" for the AWS WiFi solver', err, err.stack);
29
+ } else {
30
+ const buf = Buffer.from(response.GeoJsonPayload);
31
+ const decodedString = buf.toString();
32
+ const decodedResponse = JSON.parse(decodedString);
33
+ args.v && console.log("AWS WiFi solver response:", decodedResponse);
34
+ return {
35
+ latitude: decodedResponse?.coordinates[1],
36
+ longitude: decodedResponse?.coordinates[0],
37
+ verticalAccuracy: decodedResponse?.properties?.verticalAccuracy,
38
+ verticalConfidenceLevel: decodedResponse?.properties?.verticalConfidenceLevel,
39
+ accuracy: decodedResponse?.properties?.horizontalAccuracy,
40
+ horizontalConfidenceLevel: decodedResponse?.properties?.horizontalConfidenceLevel,
41
+ positionTimestamp: decodedResponse?.properties?.timestamp,
42
+ }
43
+ }
44
+ });
45
+ } else {
46
+ if (!body.msgtype === 'gnss' || !body?.gnss_capture_time || !body.payload) {
47
+ console.log('Error, data:', body);
48
+ throw new Error("Not enough information to resolve Gnss position");
49
+ } else {
50
+ const params = {
51
+ "Gnss": {
52
+ "CaptureTime": body?.gnss_capture_time,
53
+ "Payload": body?.payload,
54
+ }
55
+ };
56
+ return await iotwireless.getPositionEstimate(params, function(err, response) {
57
+ if (err) {
58
+ console.log('Something went wrong when calling "getPositionEstimate" for the AWS Gnss solver', err, err.stack);
59
+ } else {
60
+ const buf = Buffer.from(response.GeoJsonPayload);
61
+ const decodedString = buf.toString();
62
+ const decodedResponse = JSON.parse(decodedString);
63
+ args.v && console.log("AWS Gnss solver response:", decodedResponse);
64
+ return {
65
+ latitude: decodedResponse?.coordinates[0],
66
+ longitude: decodedResponse?.coordinates[1],
67
+ altitude: decodedResponse?.coordinates[2],
68
+ verticalAccuracy: decodedResponse?.properties?.verticalAccuracy,
69
+ verticalConfidenceLevel: decodedResponse?.properties?.verticalConfidenceLevel,
70
+ accuracy: decodedResponse?.properties?.horizontalAccuracy,
71
+ horizontalConfidenceLevel: decodedResponse?.properties?.horizontalConfidenceLevel,
72
+ positionTimestamp: decodedResponse?.properties?.timestamp,
73
+ }
74
+ }
75
+ });
76
+ }
77
+ }
78
+ }
79
+
80
+ module.exports.api = {
81
+ solvePosition,
82
+ loadAlmanac : async (args) => { return undefined; },
83
+ checkArgumentsOrExit: (args)=>{if (args.z !== 'aws') throw new Error("Flag -z <aws> is required for AWS solver."); },
84
+ getVersionString: ()=>"AWS Solver",
85
+ initialize: (args) => {
86
+ const CONSTANTS = require('../constants');
87
+ const AWS = require('aws-sdk');
88
+ AWS.config.apiVersions = {
89
+ iotwireless: CONSTANTS.AWS.VERSION,
90
+ // other service API versions
91
+ };
92
+ // Set the region and user credentials
93
+ const config = {
94
+ accessKeyId: CONSTANTS.AWS.ACCESS_KEY_ID,
95
+ secretAccessKey: CONSTANTS.AWS.SECRET_ACCESS_KEY,
96
+ region: CONSTANTS.AWS.REGION,
97
+ // other service API versions
98
+ }
99
+ // Create the service object (IotWireless- Service)
100
+ iotwireless = new AWS.IoTWireless(config);
101
+ }
102
+ };
@@ -0,0 +1,263 @@
1
+ const combainLoraCloud= "https://lw.traxmate.io";
2
+ const endpointWifi = combainLoraCloud +"/api/v1/solve/loraWifi";
3
+ const endpointGnss = combainLoraCloud +"/api/v1/solve/gnss_lora_edge_singleframe";
4
+ const endpointAlmanac = combainLoraCloud + "/api/v1/almanac/full";
5
+
6
+ // import fetch from 'node-fetch'
7
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
8
+
9
+ const solvePosition = async (args, data) => {
10
+ if (!args.k)
11
+ return;
12
+
13
+ const isWifi = data.semtechEncoded && (data.semtechEncoded.msgtype === "wifi");
14
+ const endpoint = isWifi ? endpointWifi : endpointGnss;
15
+ let body = isWifi ? data.wifi : (data.semtechEncoded ? data.semtechEncoded : data.semtechGpsEncoded);
16
+ body = JSON.parse(JSON.stringify(body)); // Make a copy of this object since it is manipulated below
17
+ if (isWifi) {
18
+ delete body.timestamp;
19
+ if ((!body.wifiAccessPoints) || body.wifiAccessPoints.length < 2) {
20
+ console.log(data);
21
+ return {errors:["Too few access points to solve position ("+ (body.wifiAccessPoints ? body.wifiAccessPoints.length:"none" )+")"]};
22
+ }
23
+
24
+ // The below is a workaround since this data is likely not available in good enough precision, in particular with mobile gateways
25
+ body.lorawan = [
26
+ {
27
+ "gatewayId": "00-00-E4-77-6B-00-1A-5D",
28
+ "antennaId": 0,
29
+ "rssi": -86.0,
30
+ "snr": 15.0,
31
+ "toa": 10000,
32
+ "antennaLocation": {
33
+ "latitude":46.98886,
34
+ "longitude":6.91287,
35
+ "altitude":513
36
+ }
37
+ },
38
+ {
39
+ "gatewayId": "00-00-E4-77-6B-00-1A-5D",
40
+ "antennaId": 1,
41
+ "rssi": -87.0,
42
+ "snr": 15.0,
43
+ "toa": 5000,
44
+ "antennaLocation": {
45
+ "latitude":46.98886,
46
+ "longitude":6.91287,
47
+ "altitude":513
48
+ }
49
+ },
50
+ {
51
+ "gatewayId": "00-00-E4-77-6B-00-1A-97",
52
+ "antennaId": 0,
53
+ "rssi": -89.0,
54
+ "snr": 15.0,
55
+ "toa": 8000,
56
+ "antennaLocation": {
57
+ "latitude":46.983753,
58
+ "longitude":6.906008,
59
+ "altitude":479
60
+ }
61
+ },
62
+ {
63
+ "gatewayId": "00-00-E4-77-6B-00-1A-97",
64
+ "antennaId": 1,
65
+ "rssi": -89.0,
66
+ "snr": 10.0,
67
+ "toa": 20000,
68
+ "antennaLocation": {
69
+ "latitude":46.983753,
70
+ "longitude":6.906008,
71
+ "altitude":479
72
+ }
73
+ }];
74
+ }
75
+ // console.log(endpoint);
76
+ delete body.msgtype;
77
+ delete body.timestamp;
78
+ // console.log(body);
79
+ const response = await fetch(endpoint,
80
+ {
81
+ method:"POST",
82
+ headers: {
83
+ "Authorization": args.k,
84
+ "Content-type" : "application/json",
85
+ },
86
+ body:JSON.stringify(body),
87
+ })
88
+ .then(response => {
89
+ if(!response.ok) {
90
+ console.log(response.statusText);
91
+ throw new Error('Could not fetch ' + (isWifi ? "wifi":"gnss")+ ' api, response: ' + response.status);
92
+ }
93
+ return response.json();
94
+ })
95
+ .then(data => data)
96
+ .catch(err => {
97
+ console.log("API failed: " + endpoint + " " + err.message);
98
+ return null;
99
+ });
100
+
101
+ args.v && console.log("Combain loraCloud solver response:", response);
102
+ if (isWifi) {
103
+ if (response && response.result && response.result.algorithmType !== "Wifi")
104
+ return {errors:["Got wrong type of response: " + response.result.algorithmType]};
105
+ }
106
+
107
+ // A small cludge or two - but positions are not particularily interresting unless they
108
+ // are timestamped. The resolver can give timestamps, but this is added as an insurance
109
+ if (response && response.result && response.result.latitude)
110
+ response.result.positionTimestamp = new Date();
111
+ return response;
112
+ }
113
+
114
+ // Reverse run-length encode, encodes into a buffer which ends with a byte
115
+ // of format either of
116
+ // (0xxx xxxx) is replaced with x bytes of zeros (x>=1)
117
+ // (1xxx xxxx) meaning it should copy xx bytes. (x>=1)
118
+ // Next previous byte is of same format.
119
+ // Use this function with a following RRLEDecode for checking that there is no buffer underrun
120
+ // before sending the data.
121
+ // TODO: Optimize to avoid excessive copying of data.
122
+ const RRLEEncode = (buf) => {
123
+ let output = Buffer.from('', 'hex');
124
+ let pos = 0;
125
+ let len = buf.length;
126
+ let nonzeros = 0;
127
+ while (pos < len) {
128
+ // Count how many zeros are at the current position
129
+ let zeros;
130
+ for (zeros = 0; zeros < len-pos && zeros < 128; ++zeros) {
131
+ if (buf[pos+zeros] !== 0)
132
+ break;
133
+ }
134
+ if (zeros >= 2) {
135
+ // Output how many non-zeros we just cooked
136
+ if (nonzeros > 0) {
137
+ // add an overhead byte
138
+ let buffer2 = Buffer.from([0x80 + nonzeros]);
139
+ output = Buffer.concat([output, buffer2]);
140
+ }
141
+ // output code 0xxx xxxx
142
+ let buffer2 = Buffer.from([zeros]);
143
+ output = Buffer.concat([output, buffer2]);
144
+ pos += (zeros-1);
145
+ nonzeros = 0;
146
+ }
147
+ else { // Less than two zeroes here
148
+ // Copy this positions value to output
149
+ nonzeros++;
150
+ let buffer2 = Buffer.from([buf[pos]]);
151
+ output = Buffer.concat([output, buffer2]);
152
+ }
153
+
154
+ if (nonzeros == 127 || pos === len-1) {
155
+ if (nonzeros > 0) {
156
+ // Need to dump one extra byte stating how many non-zeros
157
+ let buffer2 = Buffer.from([0x80 + nonzeros]);
158
+ output = Buffer.concat([output, buffer2]);
159
+ nonzeros = 0;
160
+ }
161
+ }
162
+
163
+ pos++;
164
+ }
165
+
166
+ console.log("RRLE Result: " + output.length + " bytes, original: " + buf.length + " bytes");
167
+ return output;
168
+ }
169
+
170
+ const RRLEDecode = (buf, expectedSize) => {
171
+ let inpos = buf.length;
172
+ let result = Buffer.alloc(expectedSize, 0);
173
+ let outpos = expectedSize;
174
+ while (inpos > 0) {
175
+ if (outpos <= 0)
176
+ throw new Error(`Negative output position`);
177
+ inpos--;
178
+ if (outpos < inpos)
179
+ throw new Error(`Output position before input position: outpos: ${outpos} inpos: ${inpos}`);
180
+ let len = buf[inpos] & 0x7f;
181
+ let zero = (buf[inpos] & 0x80) === 0;
182
+
183
+ if (len > outpos)
184
+ throw new Error(`Invalid length found at inpos ${inpos}, would cause output underrun`);
185
+
186
+ if (!zero) {
187
+ while (len--)
188
+ result[--outpos] = buf[--inpos];
189
+ } else {
190
+ // Skip zeros
191
+ outpos-=len;
192
+ }
193
+ }
194
+ if (outpos != 0 || inpos != 0)
195
+ throw new Error("Output and input did not end at zero");
196
+ return result;
197
+ }
198
+
199
+ let almanacCache;
200
+ let almanacTimestamp_ms;
201
+
202
+ const ALMANAC_CACHE_MAX_AGE_S = 60*60*24;
203
+
204
+ const loadAlmanac = async (args) => {
205
+ const nowMs = new Date().getTime();
206
+ if (almanacCache && almanacTimestamp_ms && nowMs-almanacTimestamp_ms < ALMANAC_CACHE_MAX_AGE_S*1000)
207
+ return almanacCache;
208
+
209
+ if (!args.k)
210
+ return;
211
+ console.log(endpointAlmanac);
212
+ const response = await fetch(endpointAlmanac,
213
+ {
214
+ method:"GET",
215
+ headers: {
216
+ // "Ocp-Apim-Subscription-Key": loraoldcloudapikey,
217
+ "Authorization" : args.k,
218
+ "Content-type" : "application/json"
219
+ },
220
+ })
221
+ .then(response => { if(!response.ok) throw new Error('Login failed - check username and password'); return response.json()})
222
+ .then(data => data)
223
+ .catch(err => {
224
+ console.log("Login failed - connection problem?");
225
+ return null;
226
+ });
227
+
228
+ if (response && response.result && response.result.almanac_image) {
229
+ const buf = Buffer.from(response.result.almanac_image, "base64");
230
+ response.result.almanac_image = buf.toString("hex");
231
+
232
+ // Optionally add a compressed and tested version of the same image to the result
233
+ try {
234
+ const compressed = RRLEEncode(buf);
235
+ if (compressed.length >= buf.length)
236
+ throw new Error("The compressed length exceeds the uncompressed length");
237
+ const decompressed = RRLEDecode(compressed, buf.length);
238
+ if (decompressed.length != buf.length)
239
+ throw new Error("The uncompress yields wrong length");
240
+ const compressed_hex = decompressed.toString('hex');
241
+ if (compressed_hex !== response.result.almanac_image)
242
+ throw new Error("Decompression generated wrong result");
243
+ response.result.almanac_compressed = compressed.toString('hex');
244
+ } catch (err) {
245
+ console.log(err);
246
+ }
247
+ }
248
+
249
+ if (response && response.result && response.result.almanac_image) {
250
+ console.log("ALMANAC ADDED TO CACHE:", response);
251
+ almanacTimestamp_ms = nowMs;
252
+ almanacCache = response;
253
+ }
254
+ return response;
255
+ }
256
+
257
+ module.exports.api = {
258
+ solvePosition,
259
+ loadAlmanac,
260
+ checkArgumentsOrExit: (args)=>{if (!args.k) throw new Error("-k <API key> is required for combainLoraCloud solver"); },
261
+ getVersionString: ()=>"combainLoraCloud Solver",
262
+ initialize: (args) => {}
263
+ };
@@ -0,0 +1,287 @@
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 loracloud="https://mgs.loracloud.com";
26
+ const endpointWifi = loracloud+"/api/v1/solve/loraWifi";
27
+ const endpointGnss = loracloud+"/api/v1/solve/gnss_lr1110_singleframe";
28
+ const endpointAlmanac = loracloud + "/api/v1/almanac/full";
29
+
30
+ // import fetch from 'node-fetch'
31
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
32
+
33
+ const solvePosition = async (args, data) => {
34
+ if (!args.k)
35
+ return;
36
+
37
+ const isWifi = data.semtechEncoded && (data.semtechEncoded.msgtype === "wifi");
38
+ const endpoint = isWifi ? endpointWifi : endpointGnss;
39
+ let body = isWifi ? data.wifi : (data.semtechEncoded ? data.semtechEncoded : data.semtechGpsEncoded);
40
+ body = JSON.parse(JSON.stringify(body)); // Make a copy of this object since it is manipulated below
41
+ if (isWifi) {
42
+ delete body.timestamp;
43
+ if ((!body.wifiAccessPoints) || body.wifiAccessPoints.length < 2) {
44
+ console.log(data);
45
+ return {errors:["Too few access points to solve position ("+ (body.wifiAccessPoints ? body.wifiAccessPoints.length:"none" )+")"]};
46
+ }
47
+
48
+ // The below is a workaround since this data is likely not available in good enough precision, in particular with mobile gateways
49
+ body.lorawan = [
50
+ {
51
+ "gatewayId": "00-00-E4-77-6B-00-1A-5D",
52
+ "antennaId": 0,
53
+ "rssi": -86.0,
54
+ "snr": 15.0,
55
+ "toa": 10000,
56
+ "antennaLocation": {
57
+ "latitude":46.98886,
58
+ "longitude":6.91287,
59
+ "altitude":513
60
+ }
61
+ },
62
+ {
63
+ "gatewayId": "00-00-E4-77-6B-00-1A-5D",
64
+ "antennaId": 1,
65
+ "rssi": -87.0,
66
+ "snr": 15.0,
67
+ "toa": 5000,
68
+ "antennaLocation": {
69
+ "latitude":46.98886,
70
+ "longitude":6.91287,
71
+ "altitude":513
72
+ }
73
+ },
74
+ {
75
+ "gatewayId": "00-00-E4-77-6B-00-1A-97",
76
+ "antennaId": 0,
77
+ "rssi": -89.0,
78
+ "snr": 15.0,
79
+ "toa": 8000,
80
+ "antennaLocation": {
81
+ "latitude":46.983753,
82
+ "longitude":6.906008,
83
+ "altitude":479
84
+ }
85
+ },
86
+ {
87
+ "gatewayId": "00-00-E4-77-6B-00-1A-97",
88
+ "antennaId": 1,
89
+ "rssi": -89.0,
90
+ "snr": 10.0,
91
+ "toa": 20000,
92
+ "antennaLocation": {
93
+ "latitude":46.983753,
94
+ "longitude":6.906008,
95
+ "altitude":479
96
+ }
97
+ }];
98
+ }
99
+ // console.log(endpoint);
100
+ delete body.msgtype;
101
+ delete body.timestamp;
102
+ // console.log(body);
103
+ const response = await fetch(endpoint,
104
+ {
105
+ method:"POST",
106
+ headers: {
107
+ "Authorization": args.k,
108
+ "Content-type" : "application/json",
109
+ },
110
+ body:JSON.stringify(body),
111
+ })
112
+ .then(response => {
113
+ if(!response.ok) {
114
+ console.log(response.statusText);
115
+ throw new Error('Could not fetch ' + (isWifi ? "wifi":"gnss")+ ' api, response: ' + response.status);
116
+ }
117
+ return response.json();
118
+ })
119
+ .then(data => data)
120
+ .catch(err => {
121
+ console.log("API failed: " + endpoint + " " + err.message);
122
+ return null;
123
+ });
124
+
125
+ args.v && console.log("Semtech solver response:", response);
126
+ if (isWifi) {
127
+ if (response && response.result && response.result.algorithmType !== "Wifi")
128
+ return {errors:["Got wrong type of response: " + response.result.algorithmType]};
129
+ }
130
+
131
+ // A small cludge or two - but positions are not particularily interresting unless they
132
+ // are timestamped. The resolver can give timestamps, but this is added as an insurance
133
+ if (response && response.result && response.result.latitude)
134
+ response.result.positionTimestamp = new Date();
135
+ return response;
136
+ }
137
+
138
+ // Reverse run-length encode, encodes into a buffer which ends with a byte
139
+ // of format either of
140
+ // (0xxx xxxx) is replaced with x bytes of zeros (x>=1)
141
+ // (1xxx xxxx) meaning it should copy xx bytes. (x>=1)
142
+ // Next previous byte is of same format.
143
+ // Use this function with a following RRLEDecode for checking that there is no buffer underrun
144
+ // before sending the data.
145
+ // TODO: Optimize to avoid excessive copying of data.
146
+ const RRLEEncode = (buf) => {
147
+ let output = Buffer.from('', 'hex');
148
+ let pos = 0;
149
+ let len = buf.length;
150
+ let nonzeros = 0;
151
+ while (pos < len) {
152
+ // Count how many zeros are at the current position
153
+ let zeros;
154
+ for (zeros = 0; zeros < len-pos && zeros < 128; ++zeros) {
155
+ if (buf[pos+zeros] !== 0)
156
+ break;
157
+ }
158
+ if (zeros >= 2) {
159
+ // Output how many non-zeros we just cooked
160
+ if (nonzeros > 0) {
161
+ // add an overhead byte
162
+ let buffer2 = Buffer.from([0x80 + nonzeros]);
163
+ output = Buffer.concat([output, buffer2]);
164
+ }
165
+ // output code 0xxx xxxx
166
+ let buffer2 = Buffer.from([zeros]);
167
+ output = Buffer.concat([output, buffer2]);
168
+ pos += (zeros-1);
169
+ nonzeros = 0;
170
+ }
171
+ else { // Less than two zeroes here
172
+ // Copy this positions value to output
173
+ nonzeros++;
174
+ let buffer2 = Buffer.from([buf[pos]]);
175
+ output = Buffer.concat([output, buffer2]);
176
+ }
177
+
178
+ if (nonzeros == 127 || pos === len-1) {
179
+ if (nonzeros > 0) {
180
+ // Need to dump one extra byte stating how many non-zeros
181
+ let buffer2 = Buffer.from([0x80 + nonzeros]);
182
+ output = Buffer.concat([output, buffer2]);
183
+ nonzeros = 0;
184
+ }
185
+ }
186
+
187
+ pos++;
188
+ }
189
+
190
+ console.log("RRLE Result: " + output.length + " bytes, original: " + buf.length + " bytes");
191
+ return output;
192
+ }
193
+
194
+ const RRLEDecode = (buf, expectedSize) => {
195
+ let inpos = buf.length;
196
+ let result = Buffer.alloc(expectedSize, 0);
197
+ let outpos = expectedSize;
198
+ while (inpos > 0) {
199
+ if (outpos <= 0)
200
+ throw new Error(`Negative output position`);
201
+ inpos--;
202
+ if (outpos < inpos)
203
+ throw new Error(`Output position before input position: outpos: ${outpos} inpos: ${inpos}`);
204
+ let len = buf[inpos] & 0x7f;
205
+ let zero = (buf[inpos] & 0x80) === 0;
206
+
207
+ if (len > outpos)
208
+ throw new Error(`Invalid length found at inpos ${inpos}, would cause output underrun`);
209
+
210
+ if (!zero) {
211
+ while (len--)
212
+ result[--outpos] = buf[--inpos];
213
+ } else {
214
+ // Skip zeros
215
+ outpos-=len;
216
+ }
217
+ }
218
+ if (outpos != 0 || inpos != 0)
219
+ throw new Error("Output and input did not end at zero");
220
+ return result;
221
+ }
222
+
223
+ let almanacCache;
224
+ let almanacTimestamp_ms;
225
+
226
+ const ALMANAC_CACHE_MAX_AGE_S = 60*60*24;
227
+
228
+ const loadAlmanac = async (args) => {
229
+ const nowMs = new Date().getTime();
230
+ if (almanacCache && almanacTimestamp_ms && nowMs-almanacTimestamp_ms < ALMANAC_CACHE_MAX_AGE_S*1000)
231
+ return almanacCache;
232
+
233
+ if (!args.k)
234
+ return;
235
+ console.log(endpointAlmanac);
236
+ const response = await fetch(endpointAlmanac,
237
+ {
238
+ method:"GET",
239
+ headers: {
240
+ // "Ocp-Apim-Subscription-Key": loraoldcloudapikey,
241
+ "Authorization" : args.k,
242
+ "Content-type" : "application/json"
243
+ },
244
+ })
245
+ .then(response => { if(!response.ok) throw new Error('Login failed - check username and password'); return response.json()})
246
+ .then(data => data)
247
+ .catch(err => {
248
+ console.log("Login failed - connection problem?");
249
+ return null;
250
+ });
251
+
252
+ if (response && response.result && response.result.almanac_image) {
253
+ const buf = Buffer.from(response.result.almanac_image, "base64");
254
+ response.result.almanac_image = buf.toString("hex");
255
+
256
+ // Optionally add a compressed and tested version of the same image to the result
257
+ try {
258
+ const compressed = RRLEEncode(buf);
259
+ if (compressed.length >= buf.length)
260
+ throw new Error("The compressed length exceeds the uncompressed length");
261
+ const decompressed = RRLEDecode(compressed, buf.length);
262
+ if (decompressed.length != buf.length)
263
+ throw new Error("The uncompress yields wrong length");
264
+ const compressed_hex = decompressed.toString('hex');
265
+ if (compressed_hex !== response.result.almanac_image)
266
+ throw new Error("Decompression generated wrong result");
267
+ response.result.almanac_compressed = compressed.toString('hex');
268
+ } catch (err) {
269
+ console.log(err);
270
+ }
271
+ }
272
+
273
+ if (response && response.result && response.result.almanac_image) {
274
+ console.log("ALMANAC ADDED TO CACHE:", response);
275
+ almanacTimestamp_ms = nowMs;
276
+ almanacCache = response;
277
+ }
278
+ return response;
279
+ }
280
+
281
+ module.exports.api = {
282
+ solvePosition,
283
+ loadAlmanac,
284
+ checkArgumentsOrExit: (args)=>{if (!args.k) throw new Error("-k <API key> is required for LoraCloud solver"); },
285
+ getVersionString: ()=>"LoraCloud Solver",
286
+ initialize: (args) => {}
287
+ };
@@ -0,0 +1,40 @@
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
+ This is a solver which has no implementation, e.g. you do not get positioning
25
+ */
26
+
27
+ const loraCloudLoadAlmanac = require('./combain-loracloud').loadAlmanac;
28
+
29
+ module.exports.api = {
30
+ initialize : async (args) => { return undefined; },
31
+ solvePosition : async (args) => {return undefined; },
32
+ loadAlmanac : async (args) => {
33
+ if (!args.k)
34
+ return undefined;
35
+ // The almanac loading procedure may be valid also for non-loracloud solvers
36
+ return loraCloudLoadAlmanac.loadAlmanac(args);
37
+ },
38
+ checkArgumentsOrExit: (args)=>{if (args.k) console.log("No position solver, using loracloud key (-k) for almanac downloads") },
39
+ getVersionString: ()=>"No Position Solver",
40
+ };