signalk-to-noforeignland 0.1.20 → 0.1.22
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/index.js +466 -564
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,576 +10,478 @@ const fetch = require('node-fetch');
|
|
|
10
10
|
const isReachable = require('is-reachable');
|
|
11
11
|
const sendEmail = require('./sendEmail');
|
|
12
12
|
const createGPX = require('./createGPX');
|
|
13
|
+
const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
|
|
13
14
|
const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
|
|
14
15
|
// const msToKn = 1.944;
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
module.exports = function(app) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function doOnValue(delta) {
|
|
189
|
-
|
|
190
|
-
for (update of delta.updates) {
|
|
191
|
-
// app.debug(`update:`, update);
|
|
192
|
-
if (options.filterSource && update.$source !== options.filterSource) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
let timestamp = update.timestamp;
|
|
196
|
-
for (value of update.values) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
Math.abs(value.value.latitude) <= 0.01 &&
|
|
201
|
-
Math.abs(value.value.longitude) <= 0.01
|
|
202
|
-
) {
|
|
203
|
-
// Coordinates are within ±0.1 of (0,0)
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// app.debug(`value:`, value);
|
|
208
|
-
|
|
209
|
-
if (!shouldDoLog) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (!isValidLatitude(value.value.latitude) || !isValidLongitude(value.value.longitude)) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (lastPosition) {
|
|
218
|
-
if (new Date(lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
|
|
219
|
-
app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', lastPosition.timestamp);
|
|
220
|
-
// SK sometimes messes up timestamps, when that happens we throw the update
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
|
|
224
|
-
app.debug('Updated lastPosition:', lastPosition);
|
|
225
|
-
|
|
226
|
-
const distance = equirectangularDistance(lastPosition.pos, value.value)
|
|
227
|
-
if (options.minMove && distance < options.minMove) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// if (calculatedSpeed(distance, (timestamp - lastPosition.timestamp) / 1000) > maxAllowedSpeed) {
|
|
231
|
-
// app.debug('got error position', value.value, 'ignoring...');
|
|
232
|
-
// return;
|
|
233
|
-
// }
|
|
234
|
-
} else{
|
|
235
|
-
lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
await savePoint(lastPosition);
|
|
239
|
-
if (options.minSpeed) {
|
|
240
|
-
app.debug('setting shouldDoLog to false');
|
|
241
|
-
shouldDoLog = false;
|
|
242
|
-
}
|
|
243
|
-
if (!initialSent) {
|
|
244
|
-
initialSent = true;
|
|
245
|
-
app.debug('sending initial fix');
|
|
246
|
-
if (await testInternet()) {
|
|
247
|
-
await sendLatestPoint();
|
|
248
|
-
lastSentTime = Date.now();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function savePoint(point) {
|
|
261
|
-
//{pos: {latitude, longitude}, timestamp}
|
|
262
|
-
// Date.parse(timestamp)
|
|
263
|
-
const obj = {
|
|
264
|
-
lat: point.pos.latitude,
|
|
265
|
-
lon: point.pos.longitude,
|
|
266
|
-
t: point.timestamp,
|
|
267
|
-
}
|
|
268
|
-
app.debug(`save data point:`, obj);
|
|
269
|
-
await fs.appendFile(path.join(options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function isValidLatitude(obj) {
|
|
273
|
-
return isDefinedNumber(obj) && obj > -90 && obj < 90
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function isValidLongitude(obj) {
|
|
277
|
-
return isDefinedNumber(obj) && obj > -180 && obj < 180
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isDefinedNumber(obj) {
|
|
281
|
-
return (obj !== undefined && obj !== null && typeof obj === 'number');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// function calculatedSpeed(distance, timeSecs) {
|
|
285
|
-
// // m/s to knots ~= speedinms * 1.944
|
|
286
|
-
// return (distance / timeSecs) * msToKn
|
|
287
|
-
// }
|
|
288
|
-
|
|
289
|
-
function equirectangularDistance(from, to) {
|
|
290
|
-
// https://www.movable-type.co.uk/scripts/latlong.html
|
|
291
|
-
// from,to: {longitude: xx, latitude: xx}
|
|
292
|
-
const rad = Math.PI / 180;
|
|
293
|
-
const φ1 = from.latitude * rad;
|
|
294
|
-
const φ2 = to.latitude * rad;
|
|
295
|
-
const Δλ = (to.longitude - from.longitude) * rad;
|
|
296
|
-
const R = 6371e3;
|
|
297
|
-
const x = Δλ * Math.cos((φ1 + φ2) / 2);
|
|
298
|
-
const y = (φ2 - φ1);
|
|
299
|
-
const d = Math.sqrt(x * x + y * y) * R;
|
|
300
|
-
return d;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function createDir(dir) {
|
|
304
|
-
let res = true;
|
|
305
|
-
if (fs.existsSync(dir)) {
|
|
306
|
-
try {
|
|
307
|
-
fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
app.debug('[createDir]', error.message);
|
|
311
|
-
app.setPluginError(`No rights to directory ${dir}`);
|
|
312
|
-
res = false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
try {
|
|
317
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
switch (error.code) {
|
|
321
|
-
case 'EACCES': // Permission denied
|
|
322
|
-
case 'EPERM': // Operation not permitted
|
|
323
|
-
app.debug(`False to create ${dir} by Permission denied`);
|
|
324
|
-
app.setPluginError(`False to create ${dir} by Permission denied`);
|
|
325
|
-
res = false;
|
|
326
|
-
break;
|
|
327
|
-
case 'ETIMEDOUT': // Operation timed out
|
|
328
|
-
app.debug(`False to create ${dir} by Operation timed out`);
|
|
329
|
-
app.setPluginError(`False to create ${dir} by Operation timed out`);
|
|
330
|
-
res = false;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return res;
|
|
336
|
-
} // end function createDir
|
|
337
|
-
|
|
338
|
-
async function interval() {
|
|
339
|
-
const now = Date.now();
|
|
340
|
-
const twentyFourHrs = 24 * 3600 * 1000;
|
|
341
|
-
//const twentyFourHrs = 60 * 1000;
|
|
342
|
-
if (options.ping && (!lastSentTime || now - lastSentTime >= twentyFourHrs)) {
|
|
343
|
-
app.debug('24 hrs elapsed since last send, pushing periodic fix');
|
|
344
|
-
if (await testInternet()) {
|
|
345
|
-
await sendLatestPoint();
|
|
346
|
-
}
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (checkBoatMoving() && await checkTrack() && await testInternet()) {
|
|
351
|
-
await sendData();
|
|
352
|
-
lastSentTime = now;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
function checkBoatMoving() {
|
|
361
|
-
if (options.sendWhileMoving || !options.trackFrequency) {
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
const time = lastPosition ? lastPosition.currentTime : upSince;
|
|
365
|
-
|
|
366
|
-
const secsSinceLastPoint = (new Date().getTime() - time) / 1000
|
|
367
|
-
if (secsSinceLastPoint > (options.trackFrequency * 2)) {
|
|
368
|
-
app.debug('Boat stopped moving, last move at least', secsSinceLastPoint, 'seconds ago');
|
|
369
|
-
return true;
|
|
370
|
-
} else {
|
|
371
|
-
app.debug('Boat is still moving, last move', secsSinceLastPoint, 'seconds ago');
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function testInternet() {
|
|
377
|
-
app.debug('testing internet connection');
|
|
378
|
-
const check = await isReachable(internetTestAddress, { timeout: options.internetTestTimeout || internetTestTimeout });
|
|
379
|
-
app.debug('internet connection = ', check);
|
|
380
|
-
return check;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async function checkTrack() {
|
|
384
|
-
const trackFile = path.join(options.trackDir, routeSaveName);
|
|
385
|
-
app.debug('checking the track', trackFile, 'if should send');
|
|
386
|
-
const exists = await fs.pathExists(trackFile);
|
|
387
|
-
const size = exists ? (await fs.lstat(trackFile)).size : 0;
|
|
388
|
-
app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
|
|
389
|
-
return size > 0;
|
|
390
|
-
}
|
|
18
|
+
module.exports = function (app) {
|
|
19
|
+
var plugin = {};
|
|
20
|
+
plugin.id = 'signalk-to-noforeignland';
|
|
21
|
+
plugin.name = 'SignalK to Noforeignland';
|
|
22
|
+
plugin.description = 'SignalK track logger to noforeignland.com';
|
|
23
|
+
|
|
24
|
+
plugin.schema = {
|
|
25
|
+
"title": plugin.name,
|
|
26
|
+
"description": "Some parameters need for use",
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["emailCron", "boatApiKey"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"trackFrequency": {
|
|
31
|
+
"type": "integer",
|
|
32
|
+
"title": "Position tracking frequency in seconds.",
|
|
33
|
+
"description": "To keep file sizes small we only log positions once in a while (unless you set this value to 0)",
|
|
34
|
+
"default": 60
|
|
35
|
+
},
|
|
36
|
+
"minMove": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"title": "Minimum boat move to log in meters",
|
|
39
|
+
"description": "To keep file sizes small we only log positions if a move larger than this size is noted (if set to 0 will log every move)",
|
|
40
|
+
"default": 50
|
|
41
|
+
},
|
|
42
|
+
"minSpeed": {
|
|
43
|
+
"type": "number",
|
|
44
|
+
"title": "Minimum boat speed to log in knots",
|
|
45
|
+
"description": "To keep file sizes small we only log positions if boat speed goes above this value to minimize recording position on anchor or mooring (if set to 0 will log every move)",
|
|
46
|
+
"default": 1.5
|
|
47
|
+
},
|
|
48
|
+
"emailCron": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"title": "Send attempt CRON",
|
|
51
|
+
"description": "We send the tracking data to NFL once in a while, you can set the schedule with this setting. CRON format: https://crontab.guru/",
|
|
52
|
+
"default": '*/10 * * * *',
|
|
53
|
+
},
|
|
54
|
+
'boatApiKey': {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"title": "Boat API key",
|
|
57
|
+
"description": "Boat API key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key. *required only in API method is set*",
|
|
58
|
+
},
|
|
59
|
+
"internetTestTimeout": {
|
|
60
|
+
"type": "number",
|
|
61
|
+
"title": "Timeout for testing internet connection in ms",
|
|
62
|
+
"description": "Set this number higher for slower computers and internet connections",
|
|
63
|
+
"default": 2000,
|
|
64
|
+
},
|
|
65
|
+
"sendWhileMoving": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"title": "Attempt sending location while moving",
|
|
68
|
+
"description": "Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?",
|
|
69
|
+
"default": false
|
|
70
|
+
},
|
|
71
|
+
"filterSource": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"title": "Position source device",
|
|
74
|
+
"description": "Set this value to the name of a source if you want to only use the position given by that source.",
|
|
75
|
+
},
|
|
76
|
+
"trackDir": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"title": "Directory to cache tracks.",
|
|
79
|
+
"description": "Path in server filesystem, absolute or from plugin directory. optional param (only used to keep file cache).",
|
|
80
|
+
},
|
|
81
|
+
"keepFiles": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"title": "Should keep track files on disk?",
|
|
84
|
+
"description": "If you have a lot of hard drive space you can keep the track files for logging purposes.",
|
|
85
|
+
"default": false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
var unsubscribes = [];
|
|
91
|
+
var unsubscribesControl = [];
|
|
92
|
+
var routeSaveName = 'nfl-track.jsonl';
|
|
93
|
+
let lastPosition;
|
|
94
|
+
let upSince;
|
|
95
|
+
let cron;
|
|
96
|
+
const creator = 'signalk-track-logger';
|
|
97
|
+
const defaultTracksDir = 'track';
|
|
98
|
+
// const maxAllowedSpeed = 100;
|
|
99
|
+
|
|
100
|
+
plugin.start = function (options, restartPlugin) {
|
|
101
|
+
if (!options.trackDir) options.trackDir = defaultTracksDir;
|
|
102
|
+
if (!path.isAbsolute(options.trackDir)) options.trackDir = path.join(__dirname, options.trackDir);
|
|
103
|
+
//app.debug('options.trackDir=',options.trackDir);
|
|
104
|
+
if (!createDir(options.trackDir)) {
|
|
105
|
+
plugin.stop();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
app.debug('track logger started, now logging to', options.trackDir);
|
|
110
|
+
app.setPluginStatus(`Started`);
|
|
111
|
+
|
|
112
|
+
doLogging();
|
|
113
|
+
|
|
114
|
+
function doLogging() {
|
|
115
|
+
let shouldDoLog = true
|
|
116
|
+
//subscribe for position
|
|
117
|
+
app.subscriptionmanager.subscribe({
|
|
118
|
+
"context": "vessels.self",
|
|
119
|
+
"subscribe": [
|
|
120
|
+
{
|
|
121
|
+
"path": "navigation.position",
|
|
122
|
+
"format": "delta",
|
|
123
|
+
"policy": "instant",
|
|
124
|
+
"minPeriod": options.trackFrequency ? options.trackFrequency * 1000 : 0,
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
unsubscribes,
|
|
129
|
+
subscriptionError => {
|
|
130
|
+
app.debug('Error subscription to data:' + subscriptionError);
|
|
131
|
+
app.setPluginError('Error subscription to data:' + subscriptionError.message);
|
|
132
|
+
},
|
|
133
|
+
doOnValue
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
//subscribe for speed
|
|
137
|
+
if (options.minSpeed) {
|
|
138
|
+
app.subscriptionmanager.subscribe({
|
|
139
|
+
"context": "vessels.self",
|
|
140
|
+
"subscribe": [
|
|
141
|
+
{
|
|
142
|
+
"path": "navigation.speedOverGround",
|
|
143
|
+
"format": "delta",
|
|
144
|
+
"policy": "instant",
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
unsubscribes,
|
|
149
|
+
subscriptionError => {
|
|
150
|
+
app.debug('Error subscription to data:' + subscriptionError);
|
|
151
|
+
app.setPluginError('Error subscription to data:' + subscriptionError.message);
|
|
152
|
+
},
|
|
153
|
+
delta => {
|
|
154
|
+
// app.debug('got speed delta', delta);
|
|
155
|
+
delta.updates.forEach(update => {
|
|
156
|
+
// app.debug(`update:`, update);
|
|
157
|
+
if (options.filterSource && update.$source !== options.filterSource) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
update.values.forEach(value => {
|
|
161
|
+
// value.value is sog in m/s so 'sog*2' is in knots
|
|
162
|
+
if (!shouldDoLog && options.minSpeed < value.value * 2) {
|
|
163
|
+
app.debug('setting shouldDoLog to true');
|
|
164
|
+
shouldDoLog = true;
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function doOnValue(delta) {
|
|
173
|
+
|
|
174
|
+
for (update of delta.updates) {
|
|
175
|
+
// app.debug(`update:`, update);
|
|
176
|
+
if (options.filterSource && update.$source !== options.filterSource) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let timestamp = update.timestamp;
|
|
180
|
+
for (value of update.values) {
|
|
181
|
+
|
|
391
182
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const url = options.apiUrl || "https://www.noforeignland.com/home/api/v1/boat/tracking/track";
|
|
398
|
-
const timestamp = Date.now();//we want to trigger a refresh on NFL even if we didn't get a delta
|
|
399
|
-
const lat = lastPosition.pos.latitude;
|
|
400
|
-
const lon = lastPosition.pos.longitude;
|
|
401
|
-
|
|
402
|
-
if (Math.abs(lat) <= 0.01 &&
|
|
403
|
-
Math.abs(lon) <= 0.01
|
|
183
|
+
if (
|
|
184
|
+
Math.abs(value.value.latitude) <= 0.01 &&
|
|
185
|
+
Math.abs(value.value.longitude) <= 0.01
|
|
404
186
|
) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (!isValidLatitude(lat) || !isValidLongitude(lon)) {
|
|
408
|
-
app.debug('Invalid lastPosition for 24hr ping, skipping');
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
const singlePointTrack = [[timestamp, lat, lon]];
|
|
412
|
-
const params = new URLSearchParams();
|
|
413
|
-
params.append('timestamp', timestamp);
|
|
414
|
-
params.append('track', JSON.stringify(singlePointTrack));
|
|
415
|
-
params.append('boatApiKey', options.boatApiKey);
|
|
416
|
-
|
|
417
|
-
const headers = {
|
|
418
|
-
'X-NFL-API-Key': pluginApiKey
|
|
419
|
-
};
|
|
420
|
-
app.debug('Sending latest position to API as single-point track', singlePointTrack);
|
|
421
|
-
try {
|
|
422
|
-
const response = await fetch(url, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
|
|
423
|
-
if (response.ok) {
|
|
424
|
-
const responseBody = await response.json();
|
|
425
|
-
if (responseBody.status === 'ok') {
|
|
426
|
-
app.debug('Latest position successfully sent to API');
|
|
427
|
-
} else {
|
|
428
|
-
app.debug('API responded with error:', responseBody);
|
|
429
|
-
}
|
|
430
|
-
} else {
|
|
431
|
-
app.debug('API responded with HTTP error:', response.status, response.statusText);
|
|
432
|
-
}
|
|
433
|
-
} catch (err) {
|
|
434
|
-
app.debug('Failed to send latest position to API:', err);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
async function sendData() {
|
|
441
|
-
if (options.boatApiKey) {
|
|
442
|
-
sendApiData();
|
|
443
|
-
} else {
|
|
444
|
-
sendEmailData();
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async function sendApiData() {
|
|
449
|
-
const url = options.apiUrl;
|
|
450
|
-
app.debug('sending to ' + url)
|
|
451
|
-
app.debug('sending the data');
|
|
452
|
-
const trackData = await createTrack(path.join(options.trackDir, routeSaveName));
|
|
453
|
-
if (!trackData) {
|
|
454
|
-
app.debug('Recorded track did not contain any valid track points, aborting sending.');
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
app.debug('created track data with timestamp:', new Date(trackData.timestamp));
|
|
458
|
-
|
|
459
|
-
const params = new URLSearchParams();
|
|
460
|
-
params.append('timestamp', trackData.timestamp);
|
|
461
|
-
params.append('track', JSON.stringify(trackData.track));
|
|
462
|
-
params.append('boatApiKey', options.boatApiKey);
|
|
463
|
-
|
|
464
|
-
const headers = {
|
|
465
|
-
'X-NFL-API-Key': pluginApiKey
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
app.debug('sending track to API');
|
|
469
|
-
try {
|
|
470
|
-
const response = await fetch(url, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
|
|
471
|
-
if (response.ok) {
|
|
472
|
-
const responseBody = await response.json();
|
|
473
|
-
if (responseBody.status === 'ok') {
|
|
474
|
-
app.debug('Track successfully sent to API');
|
|
475
|
-
if (options.keepFiles) {
|
|
476
|
-
const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
|
|
477
|
-
app.debug('moving and keeping track file: ', filename);
|
|
478
|
-
await fs.move(path.join(options.trackDir, routeSaveName), path.join(options.trackDir, filename));
|
|
479
|
-
} else {
|
|
480
|
-
app.debug('Deleting track file');
|
|
481
|
-
await fs.remove(path.join(options.trackDir, routeSaveName));
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
app.debug('Could not send track to API, returned response json:', responseBody);
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
|
|
488
|
-
}
|
|
489
|
-
} catch (err) {
|
|
490
|
-
app.debug('Could not send track to API due to error:', err);
|
|
187
|
+
// Coordinates are within ±0.1 of (0,0)
|
|
188
|
+
return;
|
|
491
189
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
190
|
+
|
|
191
|
+
// app.debug(`value:`, value);
|
|
192
|
+
|
|
193
|
+
if (!shouldDoLog) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!isValidLatitude(value.value.latitude) || !isValidLongitude(value.value.longitude)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if (lastPosition) {
|
|
202
|
+
if (new Date(lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
|
|
203
|
+
app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', lastPosition.timestamp);
|
|
204
|
+
// SK sometimes messes up timestamps, when that happens we throw the update
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const distance = equirectangularDistance(lastPosition.pos, value.value)
|
|
208
|
+
if (options.minMove && distance < options.minMove) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// if (calculatedSpeed(distance, (timestamp - lastPosition.timestamp) / 1000) > maxAllowedSpeed) {
|
|
212
|
+
// app.debug('got error position', value.value, 'ignoring...');
|
|
213
|
+
// return;
|
|
214
|
+
// }
|
|
215
|
+
}
|
|
216
|
+
lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
|
|
217
|
+
|
|
218
|
+
await savePoint(lastPosition);
|
|
219
|
+
if (options.minSpeed) {
|
|
220
|
+
app.debug('setting shouldDoLog to false');
|
|
221
|
+
shouldDoLog = false;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function savePoint(point) {
|
|
229
|
+
//{pos: {latitude, longitude}, timestamp}
|
|
230
|
+
// Date.parse(timestamp)
|
|
231
|
+
const obj = {
|
|
232
|
+
lat: point.pos.latitude,
|
|
233
|
+
lon: point.pos.longitude,
|
|
234
|
+
t: point.timestamp,
|
|
235
|
+
}
|
|
236
|
+
app.debug(`save data point:`, obj);
|
|
237
|
+
await fs.appendFile(path.join(options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isValidLatitude(obj) {
|
|
241
|
+
return isDefinedNumber(obj) && obj > -90 && obj < 90
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isValidLongitude(obj) {
|
|
245
|
+
return isDefinedNumber(obj) && obj > -180 && obj < 180
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isDefinedNumber(obj) {
|
|
249
|
+
return (obj !== undefined && obj !== null && typeof obj === 'number');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// function calculatedSpeed(distance, timeSecs) {
|
|
253
|
+
// // m/s to knots ~= speedinms * 1.944
|
|
254
|
+
// return (distance / timeSecs) * msToKn
|
|
255
|
+
// }
|
|
256
|
+
|
|
257
|
+
function equirectangularDistance(from, to) {
|
|
258
|
+
// https://www.movable-type.co.uk/scripts/latlong.html
|
|
259
|
+
// from,to: {longitude: xx, latitude: xx}
|
|
260
|
+
const rad = Math.PI / 180;
|
|
261
|
+
const φ1 = from.latitude * rad;
|
|
262
|
+
const φ2 = to.latitude * rad;
|
|
263
|
+
const Δλ = (to.longitude - from.longitude) * rad;
|
|
264
|
+
const R = 6371e3;
|
|
265
|
+
const x = Δλ * Math.cos((φ1 + φ2) / 2);
|
|
266
|
+
const y = (φ2 - φ1);
|
|
267
|
+
const d = Math.sqrt(x * x + y * y) * R;
|
|
268
|
+
return d;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createDir(dir) {
|
|
272
|
+
let res = true;
|
|
273
|
+
if (fs.existsSync(dir)) {
|
|
274
|
+
try {
|
|
275
|
+
fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
app.debug('[createDir]', error.message);
|
|
279
|
+
app.setPluginError(`No rights to directory ${dir}`);
|
|
280
|
+
res = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
switch (error.code) {
|
|
289
|
+
case 'EACCES': // Permission denied
|
|
290
|
+
case 'EPERM': // Operation not permitted
|
|
291
|
+
app.debug(`False to create ${dir} by Permission denied`);
|
|
292
|
+
app.setPluginError(`False to create ${dir} by Permission denied`);
|
|
293
|
+
res = false;
|
|
294
|
+
break;
|
|
295
|
+
case 'ETIMEDOUT': // Operation timed out
|
|
296
|
+
app.debug(`False to create ${dir} by Operation timed out`);
|
|
297
|
+
app.setPluginError(`False to create ${dir} by Operation timed out`);
|
|
298
|
+
res = false;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return res;
|
|
304
|
+
} // end function createDir
|
|
305
|
+
|
|
306
|
+
async function interval() {
|
|
307
|
+
if ((checkBoatMoving()) && await checkTrack() && await testInternet()) {
|
|
308
|
+
await sendData();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function checkBoatMoving() {
|
|
313
|
+
if (options.sendWhileMoving || !options.trackFrequency) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const time = lastPosition ? lastPosition.currentTime : upSince;
|
|
317
|
+
|
|
318
|
+
const secsSinceLastPoint = (new Date().getTime() - time)/1000
|
|
319
|
+
if (secsSinceLastPoint > (options.trackFrequency * 2)) {
|
|
320
|
+
app.debug('Boat stopped moving, last move at least', secsSinceLastPoint,'seconds ago');
|
|
321
|
+
return true;
|
|
322
|
+
} else {
|
|
323
|
+
app.debug('Boat is still moving, last move', secsSinceLastPoint,'seconds ago');
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function testInternet() {
|
|
329
|
+
app.debug('testing internet connection');
|
|
330
|
+
const check = await isReachable(internetTestAddress, { timeout: options.internetTestTimeout || internetTestTimeout });
|
|
331
|
+
app.debug('internet connection = ', check);
|
|
332
|
+
return check;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function checkTrack() {
|
|
336
|
+
const trackFile = path.join(options.trackDir, routeSaveName);
|
|
337
|
+
app.debug('checking the track', trackFile, 'if should send');
|
|
338
|
+
const exists = await fs.pathExists(trackFile);
|
|
339
|
+
const size = exists ? (await fs.lstat(trackFile)).size : 0;
|
|
340
|
+
app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
|
|
341
|
+
return size > 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function sendData() {
|
|
345
|
+
if (options.boatApiKey) {
|
|
346
|
+
sendApiData();
|
|
347
|
+
} else {
|
|
348
|
+
sendEmailData();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function sendApiData() {
|
|
353
|
+
app.debug('sending the data');
|
|
354
|
+
const trackData = await createTrack(path.join(options.trackDir, routeSaveName));
|
|
355
|
+
if (!trackData) {
|
|
356
|
+
app.debug('Recorded track did not contain any valid track points, aborting sending.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
app.debug('created track data with timestamp:', new Date(trackData.timestamp));
|
|
360
|
+
|
|
361
|
+
const params = new URLSearchParams();
|
|
362
|
+
params.append('timestamp', trackData.timestamp);
|
|
363
|
+
params.append('track', JSON.stringify(trackData.track));
|
|
364
|
+
params.append('boatApiKey', options.boatApiKey);
|
|
365
|
+
|
|
366
|
+
const headers = {
|
|
367
|
+
'X-NFL-API-Key': pluginApiKey
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
app.debug('sending track to API');
|
|
371
|
+
try {
|
|
372
|
+
const response = await fetch(apiUrl, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
|
|
373
|
+
if (response.ok) {
|
|
374
|
+
const responseBody = await response.json();
|
|
375
|
+
if (responseBody.status === 'ok') {
|
|
376
|
+
app.debug('Track successfully sent to API');
|
|
377
|
+
if (options.keepFiles) {
|
|
378
|
+
const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
|
|
379
|
+
app.debug('moving and keeping track file: ', filename);
|
|
380
|
+
await fs.move(path.join(options.trackDir, routeSaveName), path.join(options.trackDir, filename));
|
|
381
|
+
} else {
|
|
382
|
+
app.debug('Deleting track file');
|
|
383
|
+
await fs.remove(path.join(options.trackDir, routeSaveName));
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
app.debug('Could not send track to API, returned response json:', responseBody);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
app.debug('Could not send track to API due to error:', err);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function createTrack(inputPath) {
|
|
397
|
+
const fileStream = fs.createReadStream(inputPath);
|
|
398
|
+
|
|
399
|
+
const rl = readline.createInterface({
|
|
400
|
+
input: fileStream,
|
|
401
|
+
crlfDelay: Infinity
|
|
402
|
+
});
|
|
403
|
+
const track = []
|
|
404
|
+
let lastTimestamp;
|
|
405
|
+
for await (const line of rl) {
|
|
406
|
+
if (line) {
|
|
407
|
+
try {
|
|
408
|
+
const point = JSON.parse(line);
|
|
409
|
+
|
|
410
|
+
const timestamp = new Date(point.t).getTime();
|
|
411
|
+
if (!isNaN(timestamp) && isValidLatitude(point.lat) && isValidLongitude(point.lon)) {
|
|
412
|
+
track.push([timestamp, point.lat, point.lon]);
|
|
413
|
+
lastTimestamp = timestamp;
|
|
550
414
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
app.debug('could not parse line from track file:', line);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (track.length > 0) {
|
|
422
|
+
return { timestamp: new Date(lastTimestamp).getTime(), track };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function sendEmailData() {
|
|
427
|
+
app.debug('sending the data');
|
|
428
|
+
const gpxFiles = await createGPX({ input: path.join(options.trackDir, routeSaveName), outputDir: options.trackDir, creator });
|
|
429
|
+
app.debug('created GPX files', gpxFiles);
|
|
430
|
+
try {
|
|
431
|
+
for (let file of gpxFiles) {
|
|
432
|
+
app.debug('sending', file);
|
|
433
|
+
try {
|
|
434
|
+
!await sendEmail({
|
|
435
|
+
emailService: options.emailService,
|
|
436
|
+
user: options.emailUser,
|
|
437
|
+
password: options.emailPassword,
|
|
438
|
+
from: options.emailFrom,
|
|
439
|
+
to: options.emailTo,
|
|
440
|
+
trackFile: file
|
|
441
|
+
})
|
|
442
|
+
} catch (err) {
|
|
443
|
+
app.debug('Sending email failed:', err);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} finally {
|
|
448
|
+
for (let file of gpxFiles) {
|
|
449
|
+
app.debug('deleting', file);
|
|
450
|
+
await fs.rm(file);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
await fs.rm(path.join(options.trackDir, routeSaveName));
|
|
454
|
+
}
|
|
455
|
+
//every 10 minute but staggered to the second so we don't all send at once.
|
|
456
|
+
if (!options.emailCron || options.emailCron === '*/10 * * * *') {
|
|
457
|
+
const startMinute = Math.floor(Math.random() * 10); // Random minute within the 10-minute range
|
|
458
|
+
const startSecond = Math.floor(Math.random() * 60); // Random second within the minute
|
|
459
|
+
options.emailCron = `${startSecond} ${startMinute}/10 * * * *`; // Every 10 minutes, starting at a random minute and second within each 10-minute block
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
upSince = new Date().getTime();
|
|
463
|
+
|
|
464
|
+
app.debug('Setting CRON to ', options.emailCron);
|
|
465
|
+
cron = new CronJob(
|
|
466
|
+
options.emailCron,
|
|
467
|
+
interval
|
|
468
|
+
);
|
|
469
|
+
cron.start();
|
|
470
|
+
}; // end plugin.start
|
|
471
|
+
|
|
472
|
+
plugin.stop = function () {
|
|
473
|
+
app.debug('plugin stopped');
|
|
474
|
+
if (cron) {
|
|
475
|
+
cron.stop();
|
|
476
|
+
cron = undefined;
|
|
477
|
+
}
|
|
478
|
+
unsubscribesControl.forEach(f => f());
|
|
479
|
+
unsubscribesControl = [];
|
|
480
|
+
unsubscribes.forEach(f => f());
|
|
481
|
+
unsubscribes = [];
|
|
482
|
+
app.setPluginStatus('Plugin stopped');
|
|
483
|
+
}; // end plugin.stop
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
return plugin;
|
|
585
487
|
};
|