signalk-to-noforeignland 0.1.25 → 0.1.27
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/.github/workflows/github.yml +30 -0
- package/CHANGELOG.md +69 -49
- package/README.md +26 -26
- package/doc/beta_install.md +62 -0
- package/index.js +758 -638
- package/package.json +32 -28
- package/track/nfl-track-sent.jsonl +2 -0
package/index.js
CHANGED
|
@@ -1,638 +1,758 @@
|
|
|
1
|
-
const { EOL } = require('os');
|
|
2
|
-
const internetTestAddress = 'google.com';
|
|
3
|
-
const internetTestTimeout = 1000;
|
|
4
|
-
const fs = require('fs-extra');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const CronJob = require('cron').CronJob;
|
|
7
|
-
const readline = require('readline');
|
|
8
|
-
const fetch = require('node-fetch');
|
|
9
|
-
const isReachable = require('is-reachable');
|
|
10
|
-
|
|
11
|
-
const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
|
|
12
|
-
const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
|
|
13
|
-
const defaultTracksDir = 'track';
|
|
14
|
-
const routeSaveName = 'nfl-track.jsonl';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
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
|
-
this.app.debug('
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
|
|
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
|
-
this.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
};
|
|
1
|
+
const { EOL } = require('os');
|
|
2
|
+
const internetTestAddress = 'google.com';
|
|
3
|
+
const internetTestTimeout = 1000;
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const CronJob = require('cron').CronJob;
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const fetch = require('node-fetch');
|
|
9
|
+
const isReachable = require('is-reachable');
|
|
10
|
+
|
|
11
|
+
const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
|
|
12
|
+
const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
|
|
13
|
+
const defaultTracksDir = 'track';
|
|
14
|
+
const routeSaveName = 'nfl-track-pending.jsonl'; // Changed: separate pending file
|
|
15
|
+
const routeSentName = 'nfl-track-sent.jsonl'; // New: archive for sent data
|
|
16
|
+
|
|
17
|
+
class SignalkToNoforeignland {
|
|
18
|
+
constructor(app) {
|
|
19
|
+
this.app = app;
|
|
20
|
+
this.pluginId = 'signalk-to-noforeignland';
|
|
21
|
+
this.pluginName = 'Signal K to Noforeignland';
|
|
22
|
+
this.creator = 'signalk-track-logger';
|
|
23
|
+
|
|
24
|
+
// runtime state
|
|
25
|
+
this.unsubscribes = [];
|
|
26
|
+
this.unsubscribesControl = [];
|
|
27
|
+
this.lastPosition = null;
|
|
28
|
+
this.upSince = null;
|
|
29
|
+
this.cron = null;
|
|
30
|
+
this.options = {};
|
|
31
|
+
this.lastSuccessfulTransfer = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getSchema() {
|
|
35
|
+
return {
|
|
36
|
+
title: this.pluginName,
|
|
37
|
+
description: 'Some parameters need for use',
|
|
38
|
+
type: 'object',
|
|
39
|
+
required: ['boatApiKey', 'apiCron'],
|
|
40
|
+
properties: {
|
|
41
|
+
// Mandatory Settings Group
|
|
42
|
+
mandatory: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
title: 'Mandatory Settings',
|
|
45
|
+
properties: {
|
|
46
|
+
boatApiKey: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
title: 'Boat API Key',
|
|
49
|
+
description: 'Boat API Key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key.'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Advanced Settings Group
|
|
55
|
+
advanced: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
title: 'Advanced Settings',
|
|
58
|
+
properties: {
|
|
59
|
+
minMove: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
title: 'Minimum boat move to log in meters',
|
|
62
|
+
description: 'To keep file sizes small we only log positions if a move larger than this size (if set to 0 will log every move)',
|
|
63
|
+
default: 80
|
|
64
|
+
},
|
|
65
|
+
minSpeed: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
title: 'Minimum boat speed to log in knots',
|
|
68
|
+
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)',
|
|
69
|
+
default: 1.5
|
|
70
|
+
},
|
|
71
|
+
sendWhileMoving: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
title: 'Attempt sending location while moving',
|
|
74
|
+
description: 'Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?',
|
|
75
|
+
default: true
|
|
76
|
+
},
|
|
77
|
+
ping_api_every_24h: {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
title: 'Force a send every 24 hours',
|
|
80
|
+
description: 'Keeps your boat active on NFL in your current location even if you do not move',
|
|
81
|
+
default: true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Expert Settings Group
|
|
87
|
+
expert: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
title: 'Expert Settings',
|
|
90
|
+
properties: {
|
|
91
|
+
filterSource: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
title: 'Position source device',
|
|
94
|
+
description: 'EMPTY DEFAULT IS FINE - Set this value to the name of a source if you want to only use the position given by that source.'
|
|
95
|
+
},
|
|
96
|
+
trackDir: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
title: 'Directory to cache tracks',
|
|
99
|
+
description: 'EMPTY DEFAULT IS FINE - Path in server filesystem, absolute or from plugin directory.\noptional param (only used to keep file cache).'
|
|
100
|
+
},
|
|
101
|
+
keepFiles: {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
title: 'Keep track files on disk',
|
|
104
|
+
description: 'If you have a lot of hard drive space you can keep the track files for logging purposes.',
|
|
105
|
+
default: false
|
|
106
|
+
},
|
|
107
|
+
trackFrequency: {
|
|
108
|
+
type: 'integer',
|
|
109
|
+
title: 'Position tracking frequency in seconds',
|
|
110
|
+
description: 'To keep file sizes small we only log positions once in a while (unless you set this value to 0)',
|
|
111
|
+
default: 60
|
|
112
|
+
},
|
|
113
|
+
apiCron: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
title: 'Send attempt CRON',
|
|
116
|
+
description: 'We send the tracking data to NFL once in a while, you can set the schedule with this setting.\nCRON format: https://crontab.guru/',
|
|
117
|
+
default: '*/10 * * * *'
|
|
118
|
+
},
|
|
119
|
+
internetTestTimeout: {
|
|
120
|
+
type: 'number',
|
|
121
|
+
title: 'Timeout for testing internet connection in ms',
|
|
122
|
+
description: 'Set this number higher for slower computers and internet connections',
|
|
123
|
+
default: 2000
|
|
124
|
+
},
|
|
125
|
+
apiTimeout: {
|
|
126
|
+
type: 'integer',
|
|
127
|
+
title: 'API request timeout in seconds',
|
|
128
|
+
description: 'Timeout for sending data to NFL API. Increase for slow connections.',
|
|
129
|
+
default: 30,
|
|
130
|
+
minimum: 10,
|
|
131
|
+
maximum: 180
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getPluginObject() {
|
|
140
|
+
return {
|
|
141
|
+
id: this.pluginId,
|
|
142
|
+
name: this.pluginName,
|
|
143
|
+
description: 'SignalK track logger to noforeignland.com',
|
|
144
|
+
schema: this.getSchema(),
|
|
145
|
+
start: this.start.bind(this),
|
|
146
|
+
stop: this.stop.bind(this)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async start(options = {}, restartPlugin) {
|
|
151
|
+
|
|
152
|
+
// Position data health check
|
|
153
|
+
this.positionCheckInterval = null;
|
|
154
|
+
this.lastPositionReceived = null;
|
|
155
|
+
|
|
156
|
+
// Backward compatibility: migrate old flat structure to new nested structure
|
|
157
|
+
let needsSave = false;
|
|
158
|
+
if (options.boatApiKey && !options.mandatory) {
|
|
159
|
+
// Old config detected, migrate to new structure
|
|
160
|
+
this.app.debug('Migrating old configuration to new grouped structure');
|
|
161
|
+
needsSave = true;
|
|
162
|
+
|
|
163
|
+
options = {
|
|
164
|
+
mandatory: {
|
|
165
|
+
boatApiKey: options.boatApiKey
|
|
166
|
+
},
|
|
167
|
+
advanced: {
|
|
168
|
+
minMove: options.minMove !== undefined ? options.minMove : 50,
|
|
169
|
+
minSpeed: options.minSpeed !== undefined ? options.minSpeed : 1.5,
|
|
170
|
+
sendWhileMoving: options.sendWhileMoving !== undefined ? options.sendWhileMoving : true,
|
|
171
|
+
ping_api_every_24h: options.ping_api_every_24h !== undefined ? options.ping_api_every_24h : true
|
|
172
|
+
},
|
|
173
|
+
expert: {
|
|
174
|
+
filterSource: options.filterSource,
|
|
175
|
+
trackDir: options.trackDir,
|
|
176
|
+
keepFiles: options.keepFiles !== undefined ? options.keepFiles : false,
|
|
177
|
+
trackFrequency: options.trackFrequency !== undefined ? options.trackFrequency : 60,
|
|
178
|
+
internetTestTimeout: options.internetTestTimeout !== undefined ? options.internetTestTimeout : 2000,
|
|
179
|
+
apiCron: options.apiCron || '*/10 * * * *',
|
|
180
|
+
apiTimeout: options.apiTimeout !== undefined ? options.apiTimeout : 30
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Save the migrated configuration
|
|
185
|
+
try {
|
|
186
|
+
this.app.debug('Saving migrated configuration...');
|
|
187
|
+
await this.app.savePluginOptions(options, () => {
|
|
188
|
+
this.app.debug('Configuration successfully migrated and saved');
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
this.app.debug('Failed to save migrated configuration:', err.message);
|
|
192
|
+
// Continue anyway - the migration will work in memory
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Flatten the nested structure for easier access and apply defaults
|
|
197
|
+
this.options = {
|
|
198
|
+
// Mandatory defaults
|
|
199
|
+
boatApiKey: options.mandatory?.boatApiKey,
|
|
200
|
+
|
|
201
|
+
// Advanced defaults
|
|
202
|
+
minMove: options.advanced?.minMove !== undefined ? options.advanced.minMove : 80,
|
|
203
|
+
minSpeed: options.advanced?.minSpeed !== undefined ? options.advanced.minSpeed : 1.5,
|
|
204
|
+
sendWhileMoving: options.advanced?.sendWhileMoving !== undefined ? options.advanced.sendWhileMoving : true,
|
|
205
|
+
ping_api_every_24h: options.advanced?.ping_api_every_24h !== undefined ? options.advanced.ping_api_every_24h : true,
|
|
206
|
+
|
|
207
|
+
// Expert defaults
|
|
208
|
+
filterSource: options.expert?.filterSource,
|
|
209
|
+
trackDir: options.expert?.trackDir || defaultTracksDir,
|
|
210
|
+
keepFiles: options.expert?.keepFiles !== undefined ? options.expert.keepFiles : false,
|
|
211
|
+
trackFrequency: options.expert?.trackFrequency !== undefined ? options.expert.trackFrequency : 60,
|
|
212
|
+
internetTestTimeout: options.expert?.internetTestTimeout !== undefined ? options.expert.internetTestTimeout : 2000,
|
|
213
|
+
apiCron: options.expert?.apiCron || '*/10 * * * *',
|
|
214
|
+
apiTimeout: options.expert?.apiTimeout !== undefined ? options.expert.apiTimeout : 30
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Validate that boatApiKey is set
|
|
218
|
+
if (!this.options.boatApiKey || this.options.boatApiKey.trim() === '') {
|
|
219
|
+
const errorMsg = 'No boat API key configured. Please set your API key in plugin settings (Mandatory Settings > Boat API key). You can find your API key at noforeignland.com under Account > Settings > Boat tracking > API Key.';
|
|
220
|
+
this.app.debug(errorMsg);
|
|
221
|
+
this.app.setPluginError(errorMsg);
|
|
222
|
+
this.stop();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!path.isAbsolute(this.options.trackDir)) {
|
|
227
|
+
this.options.trackDir = path.join(__dirname, this.options.trackDir);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!this.createDir(this.options.trackDir)) {
|
|
231
|
+
this.stop();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// NEW: Migrate old track file to new naming scheme on startup
|
|
236
|
+
await this.migrateOldTrackFile();
|
|
237
|
+
|
|
238
|
+
this.app.debug('track logger started, now logging to', this.options.trackDir);
|
|
239
|
+
this.app.setPluginStatus(`Started${needsSave ? ' (config migrated)' : ''}`);
|
|
240
|
+
this.upSince = new Date().getTime();
|
|
241
|
+
|
|
242
|
+
// adjust default CRON if unchanged
|
|
243
|
+
if (!this.options.apiCron || this.options.apiCron === '*/10 * * * *') {
|
|
244
|
+
const startMinute = Math.floor(Math.random() * 10);
|
|
245
|
+
const startSecond = Math.floor(Math.random() * 60);
|
|
246
|
+
this.options.apiCron = `${startSecond} ${startMinute}/10 * * * *`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.app.debug('Setting CRON to ', this.options.apiCron);
|
|
250
|
+
this.app.debug('trackFrequency is set to', this.options.trackFrequency, 'seconds');
|
|
251
|
+
|
|
252
|
+
// subscribe and logging
|
|
253
|
+
this.doLogging();
|
|
254
|
+
|
|
255
|
+
// start cron job
|
|
256
|
+
this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
|
|
257
|
+
this.cron.start();
|
|
258
|
+
|
|
259
|
+
// Start position health check (every 5 minutes)
|
|
260
|
+
this.startPositionHealthCheck();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// NEW: Migrate old track file naming to new scheme
|
|
264
|
+
async migrateOldTrackFile() {
|
|
265
|
+
const oldTrackFile = path.join(this.options.trackDir, 'nfl-track.jsonl');
|
|
266
|
+
const newPendingFile = path.join(this.options.trackDir, routeSaveName);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Check if old file exists and new pending file doesn't
|
|
270
|
+
if (await fs.pathExists(oldTrackFile) && !(await fs.pathExists(newPendingFile))) {
|
|
271
|
+
this.app.debug('Migrating old track file to new naming scheme...');
|
|
272
|
+
await fs.move(oldTrackFile, newPendingFile);
|
|
273
|
+
this.app.debug('Successfully migrated old track file to:', routeSaveName);
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.app.debug('Error during track file migration:', err.message);
|
|
277
|
+
// Non-fatal error, continue startup
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
stop() {
|
|
282
|
+
this.app.debug('plugin stopped');
|
|
283
|
+
|
|
284
|
+
// Stop position health check
|
|
285
|
+
if (this.positionCheckInterval) {
|
|
286
|
+
clearInterval(this.positionCheckInterval);
|
|
287
|
+
this.positionCheckInterval = null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.cron) {
|
|
291
|
+
this.cron.stop();
|
|
292
|
+
this.cron = undefined;
|
|
293
|
+
}
|
|
294
|
+
this.unsubscribesControl.forEach(f => f());
|
|
295
|
+
this.unsubscribesControl = [];
|
|
296
|
+
this.unsubscribes.forEach(f => f());
|
|
297
|
+
this.unsubscribes = [];
|
|
298
|
+
this.app.setPluginStatus('Plugin stopped');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
doLogging() {
|
|
302
|
+
// subscribe for position
|
|
303
|
+
let shouldDoLog = true;
|
|
304
|
+
|
|
305
|
+
this.app.subscriptionmanager.subscribe({
|
|
306
|
+
context: 'vessels.self',
|
|
307
|
+
subscribe: [{
|
|
308
|
+
path: 'navigation.position',
|
|
309
|
+
format: 'delta',
|
|
310
|
+
policy: 'instant',
|
|
311
|
+
minPeriod: this.options.trackFrequency ? this.options.trackFrequency * 1000 : 0
|
|
312
|
+
}]
|
|
313
|
+
}, this.unsubscribes, (subscriptionError) => {
|
|
314
|
+
this.app.debug('Error subscription to data:' + subscriptionError);
|
|
315
|
+
this.app.setPluginError('Error subscription to data:' + subscriptionError.message);
|
|
316
|
+
}, this.doOnValue.bind(this, () => shouldDoLog, newShould => { shouldDoLog = newShould; }));
|
|
317
|
+
|
|
318
|
+
// subscribe for speed
|
|
319
|
+
if (this.options.minSpeed) {
|
|
320
|
+
this.app.subscriptionmanager.subscribe({
|
|
321
|
+
context: 'vessels.self',
|
|
322
|
+
subscribe: [{
|
|
323
|
+
path: 'navigation.speedOverGround',
|
|
324
|
+
format: 'delta',
|
|
325
|
+
policy: 'instant'
|
|
326
|
+
}]
|
|
327
|
+
}, this.unsubscribes, (subscriptionError) => {
|
|
328
|
+
this.app.debug('Error subscription to data:' + subscriptionError);
|
|
329
|
+
this.app.setPluginError('Error subscription to data:' + subscriptionError.message);
|
|
330
|
+
}, (delta) => {
|
|
331
|
+
delta.updates.forEach(update => {
|
|
332
|
+
if (this.options.filterSource && update.$source !== this.options.filterSource) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
update.values.forEach(value => {
|
|
336
|
+
const speedInKnots = value.value * 1.94384;
|
|
337
|
+
if (!shouldDoLog && this.options.minSpeed < speedInKnots) {
|
|
338
|
+
this.app.debug('setting shouldDoLog to true, speed:', speedInKnots.toFixed(2), 'knots');
|
|
339
|
+
shouldDoLog = true;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async doOnValue(getShouldDoLog, setShouldDoLog, delta) {
|
|
348
|
+
for (const update of delta.updates) {
|
|
349
|
+
if (this.options.filterSource && update.$source !== this.options.filterSource) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const timestamp = update.timestamp;
|
|
353
|
+
for (const value of update.values) {
|
|
354
|
+
// Validierung: GPS nahe (0,0)
|
|
355
|
+
if (Math.abs(value.value.latitude) <= 0.01 && Math.abs(value.value.longitude) <= 0.01) {
|
|
356
|
+
this.app.debug('GPS coordinates near (0,0), ignoring point to avoid invalid data logging.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Validate for valid lat/lon
|
|
361
|
+
if (!this.isValidLatitude(value.value.latitude) || !this.isValidLongitude(value.value.longitude)) {
|
|
362
|
+
this.app.debug('got invalid position, ignoring...', value.value);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 24h-Ping Check: Setze Flag, aber breche NICHT ab
|
|
367
|
+
let force24hSave = false;
|
|
368
|
+
if (this.options.ping_api_every_24h && this.lastPosition) {
|
|
369
|
+
const timeSinceLastPoint = (new Date().getTime() - this.lastPosition.currentTime);
|
|
370
|
+
if (timeSinceLastPoint >= 24 * 60 * 60 * 1000) {
|
|
371
|
+
this.app.debug('24h since last point, forcing save of point to keep boat active on NFL');
|
|
372
|
+
force24hSave = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Wenn wir nicht loggen sollen UND es kein 24h-Force ist, dann raus
|
|
377
|
+
if (!force24hSave && !getShouldDoLog()) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Wenn wir eine letzte Position haben, prüfe Timestamp und Distanz
|
|
382
|
+
if (this.lastPosition && !force24hSave) {
|
|
383
|
+
// Timestamp-Validierung
|
|
384
|
+
if (new Date(this.lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
|
|
385
|
+
this.app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', this.lastPosition.timestamp);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Distance-Check (nur wenn NICHT 24h-Force)
|
|
390
|
+
const distance = this.equirectangularDistance(this.lastPosition.pos, value.value);
|
|
391
|
+
if (this.options.minMove && distance < this.options.minMove) {
|
|
392
|
+
this.app.debug('Distance', distance.toFixed(2), 'm is less than minMove', this.options.minMove, 'm - skipping');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Punkt speichern
|
|
398
|
+
this.lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
|
|
399
|
+
this.lastPositionReceived = new Date().getTime(); // Track for health check
|
|
400
|
+
await this.savePoint(this.lastPosition);
|
|
401
|
+
|
|
402
|
+
// shouldDoLog zurücksetzen wenn minSpeed aktiv ist
|
|
403
|
+
if (this.options.minSpeed) {
|
|
404
|
+
this.app.debug('options.minSpeed - setting shouldDoLog to false');
|
|
405
|
+
setShouldDoLog(false);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
async savePoint(point) {
|
|
412
|
+
const obj = {
|
|
413
|
+
lat: point.pos.latitude,
|
|
414
|
+
lon: point.pos.longitude,
|
|
415
|
+
t: point.timestamp
|
|
416
|
+
};
|
|
417
|
+
this.app.debug(`save data point:`, obj);
|
|
418
|
+
// CHANGED: Save to pending file
|
|
419
|
+
await fs.appendFile(path.join(this.options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
|
|
420
|
+
|
|
421
|
+
const lastSaveTime = new Date().toISOString();
|
|
422
|
+
const lastTransferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'None since start';
|
|
423
|
+
this.app.setPluginStatus(`Last save: ${lastSaveTime} | Last transfer: ${lastTransferTime}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
isValidLatitude(obj) {
|
|
427
|
+
return this.isDefinedNumber(obj) && obj > -90 && obj < 90;
|
|
428
|
+
}
|
|
429
|
+
isValidLongitude(obj) {
|
|
430
|
+
return this.isDefinedNumber(obj) && obj > -180 && obj < 180;
|
|
431
|
+
}
|
|
432
|
+
isDefinedNumber(obj) {
|
|
433
|
+
return (obj !== undefined && obj !== null && typeof obj === 'number');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
equirectangularDistance(from, to) {
|
|
437
|
+
const rad = Math.PI / 180;
|
|
438
|
+
const φ1 = from.latitude * rad;
|
|
439
|
+
const φ2 = to.latitude * rad;
|
|
440
|
+
const Δλ = (to.longitude - from.longitude) * rad;
|
|
441
|
+
const R = 6371e3;
|
|
442
|
+
const x = Δλ * Math.cos((φ1 + φ2) / 2);
|
|
443
|
+
const y = (φ2 - φ1);
|
|
444
|
+
const d = Math.sqrt(x * x + y * y) * R;
|
|
445
|
+
return d;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
createDir(dir) {
|
|
449
|
+
let res = true;
|
|
450
|
+
if (fs.existsSync(dir)) {
|
|
451
|
+
try {
|
|
452
|
+
fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.app.debug('[createDir]', error.message);
|
|
455
|
+
this.app.setPluginError(`No rights to directory ${dir}`);
|
|
456
|
+
res = false;
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
try {
|
|
460
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
switch (error.code) {
|
|
463
|
+
case 'EACCES':
|
|
464
|
+
case 'EPERM':
|
|
465
|
+
this.app.debug(`False to create ${dir} by Permission denied`);
|
|
466
|
+
this.app.setPluginError(`False to create ${dir} by Permission denied`);
|
|
467
|
+
res = false;
|
|
468
|
+
break;
|
|
469
|
+
case 'ETIMEDOUT':
|
|
470
|
+
this.app.debug(`False to create ${dir} by Operation timed out`);
|
|
471
|
+
this.app.setPluginError(`False to create ${dir} by Operation timed out`);
|
|
472
|
+
res = false;
|
|
473
|
+
break;
|
|
474
|
+
default:
|
|
475
|
+
this.app.debug(`Error creating directory ${dir}: ${error.message}`);
|
|
476
|
+
this.app.setPluginError(`Error creating directory ${dir}: ${error.message}`);
|
|
477
|
+
res = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return res;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// NEW: Position health check
|
|
485
|
+
startPositionHealthCheck() {
|
|
486
|
+
// Check every 5 minutes if we're receiving position data
|
|
487
|
+
this.positionCheckInterval = setInterval(() => {
|
|
488
|
+
const now = new Date().getTime();
|
|
489
|
+
const timeSinceLastPosition = this.lastPositionReceived
|
|
490
|
+
? (now - this.lastPositionReceived) / 1000
|
|
491
|
+
: null;
|
|
492
|
+
|
|
493
|
+
// Build appropriate error message based on filterSource setting
|
|
494
|
+
const filterMsg = this.options.filterSource
|
|
495
|
+
? ` from source '${this.options.filterSource}'`
|
|
496
|
+
: '';
|
|
497
|
+
|
|
498
|
+
if (!this.lastPositionReceived) {
|
|
499
|
+
// Never received any position data
|
|
500
|
+
const errorMsg = this.options.filterSource
|
|
501
|
+
? `No GPS position data received from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device, or leave empty to use any GPS source.`
|
|
502
|
+
: 'No GPS position data received. Check that your GPS is connected and SignalK is receiving navigation.position data.';
|
|
503
|
+
this.app.setPluginError(errorMsg);
|
|
504
|
+
this.app.debug('Position health check: No position data ever received' + filterMsg);
|
|
505
|
+
} else if (timeSinceLastPosition > 300) {
|
|
506
|
+
// No position data for more than 5 minutes
|
|
507
|
+
const errorMsg = this.options.filterSource
|
|
508
|
+
? `No GPS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check that source '${this.options.filterSource}' is active, or change/clear Position source device in Expert Settings.`
|
|
509
|
+
: `No GPS position data for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GPS connection.`;
|
|
510
|
+
this.app.setPluginError(errorMsg);
|
|
511
|
+
this.app.debug(`Position health check: No position for ${timeSinceLastPosition.toFixed(0)} seconds` + filterMsg);
|
|
512
|
+
} else {
|
|
513
|
+
// Position data is flowing normally
|
|
514
|
+
this.app.debug(`Position health check: OK (last position ${timeSinceLastPosition.toFixed(0)} seconds ago${filterMsg})`);
|
|
515
|
+
// Clear any previous error if position is now flowing
|
|
516
|
+
const lastSaveTime = this.lastPosition ? new Date(this.lastPosition.currentTime).toISOString() : 'Never';
|
|
517
|
+
const lastTransferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'None since start';
|
|
518
|
+
const sourceInfo = this.options.filterSource ? ` (source: ${this.options.filterSource})` : '';
|
|
519
|
+
this.app.setPluginStatus(`Active${sourceInfo} - Last save: ${lastSaveTime} | Last transfer: ${lastTransferTime}`);
|
|
520
|
+
}
|
|
521
|
+
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
522
|
+
|
|
523
|
+
// Also do an initial check after 2 minutes
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
if (!this.lastPositionReceived) {
|
|
526
|
+
const errorMsg = this.options.filterSource
|
|
527
|
+
? `No GPS position data received after 2 minutes from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device. You may need to leave it empty to use any available GPS source.`
|
|
528
|
+
: 'No GPS position data received after 2 minutes. Check that your GPS is connected and SignalK is receiving navigation.position data.';
|
|
529
|
+
this.app.setPluginError(errorMsg);
|
|
530
|
+
this.app.debug('Initial position check: No position data received' + (this.options.filterSource ? ` from source '${this.options.filterSource}'` : ''));
|
|
531
|
+
}
|
|
532
|
+
}, 2 * 60 * 1000);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// periodic interval called by cron
|
|
536
|
+
async interval() {
|
|
537
|
+
if ((this.checkBoatMoving()) && await this.checkTrack() && await this.testInternet()) {
|
|
538
|
+
await this.sendData();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
checkBoatMoving() {
|
|
543
|
+
if (!this.options.trackFrequency) {
|
|
544
|
+
return true; // Kein Tracking → immer senden
|
|
545
|
+
}
|
|
546
|
+
const time = this.lastPosition ? this.lastPosition.currentTime : this.upSince;
|
|
547
|
+
const secsSinceLastPoint = (new Date().getTime() - time) / 1000;
|
|
548
|
+
const isMoving = secsSinceLastPoint <= (this.options.trackFrequency * 2);
|
|
549
|
+
if (isMoving) {
|
|
550
|
+
this.app.debug('Boat is still moving, last move', secsSinceLastPoint, 'seconds ago');
|
|
551
|
+
return this.options.sendWhileMoving; // Nur senden wenn gewünscht
|
|
552
|
+
} else {
|
|
553
|
+
this.app.debug('Boat stopped moving, last move at least', secsSinceLastPoint, 'seconds ago');
|
|
554
|
+
return true; // Immer senden wenn gestoppt
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async testInternet() {
|
|
559
|
+
const dns = require('dns').promises;
|
|
560
|
+
|
|
561
|
+
this.app.debug('testing internet connection');
|
|
562
|
+
|
|
563
|
+
const timeoutMs = this.options.internetTestTimeout || 2000;
|
|
564
|
+
|
|
565
|
+
// Prüfe mehrere öffentliche DNS-Server
|
|
566
|
+
const dnsServers = [
|
|
567
|
+
{ name: 'Google DNS', ip: '8.8.8.8' },
|
|
568
|
+
{ name: 'Cloudflare DNS', ip: '1.1.1.1' }
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
for (const server of dnsServers) {
|
|
572
|
+
try {
|
|
573
|
+
// Versuche, den DNS-Server direkt zu erreichen
|
|
574
|
+
// Wir machen einen reverse lookup auf die IP selbst
|
|
575
|
+
const result = await Promise.race([
|
|
576
|
+
dns.reverse(server.ip),
|
|
577
|
+
new Promise((_, reject) =>
|
|
578
|
+
setTimeout(() => reject(new Error('DNS timeout')), timeoutMs)
|
|
579
|
+
)
|
|
580
|
+
]);
|
|
581
|
+
|
|
582
|
+
this.app.debug(`internet connection = true, ${server.name} (${server.ip}) is reachable`);
|
|
583
|
+
return true;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
this.app.debug(`${server.name} (${server.ip}) not reachable:`, err.message);
|
|
586
|
+
// Weiter zum nächsten Server
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.app.debug('internet connection = false, no public DNS servers reachable');
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async checkTrack() {
|
|
595
|
+
// CHANGED: Check pending file instead
|
|
596
|
+
const trackFile = path.join(this.options.trackDir, routeSaveName);
|
|
597
|
+
this.app.debug('checking the track', trackFile, 'if should send');
|
|
598
|
+
const exists = await fs.pathExists(trackFile);
|
|
599
|
+
const size = exists ? (await fs.lstat(trackFile)).size : 0;
|
|
600
|
+
this.app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
|
|
601
|
+
return size > 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async sendData() {
|
|
605
|
+
if (this.options.boatApiKey) {
|
|
606
|
+
await this.sendApiData();
|
|
607
|
+
} else {
|
|
608
|
+
this.app.debug('Failed to send track - no boat API key set in plugin settings.');
|
|
609
|
+
this.app.setPluginError(`Failed to send track - no boat API key set in plugin settings.`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async sendApiData() {
|
|
614
|
+
this.app.debug('sending the data');
|
|
615
|
+
// CHANGED: Read from pending file
|
|
616
|
+
const pendingFile = path.join(this.options.trackDir, routeSaveName);
|
|
617
|
+
const trackData = await this.createTrack(pendingFile);
|
|
618
|
+
if (!trackData) {
|
|
619
|
+
this.app.debug('Recorded track did not contain any valid track points, aborting sending.');
|
|
620
|
+
this.app.setPluginError(`Failed to send track - Recorded track did not contain any valid track points, aborting sending.`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this.app.debug('created track data with timestamp:', new Date(trackData.timestamp));
|
|
624
|
+
const params = new URLSearchParams();
|
|
625
|
+
params.append('timestamp', trackData.timestamp);
|
|
626
|
+
params.append('track', JSON.stringify(trackData.track));
|
|
627
|
+
params.append('boatApiKey', this.options.boatApiKey);
|
|
628
|
+
const headers = { 'X-NFL-API-Key': pluginApiKey };
|
|
629
|
+
this.app.debug('sending track to API');
|
|
630
|
+
|
|
631
|
+
// Retry-Logik mit exponentiell steigendem Timeout
|
|
632
|
+
const maxRetries = 3;
|
|
633
|
+
const baseTimeout = (this.options.apiTimeout || 30) * 1000; // Konfigurierbarer Basis-Timeout in ms
|
|
634
|
+
|
|
635
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
636
|
+
try {
|
|
637
|
+
const currentTimeout = baseTimeout * attempt; // 30s, 60s, 90s
|
|
638
|
+
this.app.debug(`Attempt ${attempt}/${maxRetries} with ${currentTimeout}ms timeout`);
|
|
639
|
+
|
|
640
|
+
// AbortController für Timeout
|
|
641
|
+
const controller = new AbortController();
|
|
642
|
+
const timeoutId = setTimeout(() => controller.abort(), currentTimeout);
|
|
643
|
+
|
|
644
|
+
const response = await fetch(apiUrl, {
|
|
645
|
+
method: 'POST',
|
|
646
|
+
body: params,
|
|
647
|
+
headers: new fetch.Headers(headers),
|
|
648
|
+
signal: controller.signal
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
clearTimeout(timeoutId);
|
|
652
|
+
|
|
653
|
+
if (response.ok) {
|
|
654
|
+
const responseBody = await response.json();
|
|
655
|
+
if (responseBody.status === 'ok') {
|
|
656
|
+
this.lastSuccessfulTransfer = new Date();
|
|
657
|
+
this.app.debug('Track successfully sent to API');
|
|
658
|
+
this.app.setPluginStatus(`Started - last Track sent successfully at ${new Date().toISOString()}`);
|
|
659
|
+
|
|
660
|
+
// CHANGED: New file handling logic
|
|
661
|
+
await this.handleSuccessfulSend(pendingFile);
|
|
662
|
+
return; // Erfolg - beende Funktion
|
|
663
|
+
} else {
|
|
664
|
+
this.app.debug('Could not send track to API, returned response json:', responseBody);
|
|
665
|
+
// Bei API-Fehler nicht erneut versuchen
|
|
666
|
+
this.app.setPluginError(`Failed to send track - API returned error.`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
this.app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
|
|
671
|
+
// Bei 4xx Fehler nicht erneut versuchen
|
|
672
|
+
if (response.status >= 400 && response.status < 500) {
|
|
673
|
+
this.app.setPluginError(`Failed to send track - HTTP ${response.status}.`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// Bei 5xx Fehler retry
|
|
677
|
+
throw new Error(`HTTP ${response.status}`);
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
this.app.debug(`Attempt ${attempt} failed:`, err.message);
|
|
681
|
+
|
|
682
|
+
// Bei letztem Versuch Fehler setzen
|
|
683
|
+
if (attempt === maxRetries) {
|
|
684
|
+
this.app.debug('Could not send track to API after', maxRetries, 'attempts:', err);
|
|
685
|
+
this.app.setPluginError(`Failed to send track after ${maxRetries} attempts - check logs for details.`);
|
|
686
|
+
} else {
|
|
687
|
+
// Kurze Pause vor nächstem Versuch
|
|
688
|
+
const waitTime = 2000 * attempt; // 2s, 4s
|
|
689
|
+
this.app.debug(`Waiting ${waitTime}ms before retry...`);
|
|
690
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// NEW: Handle file operations after successful send
|
|
697
|
+
async handleSuccessfulSend(pendingFile) {
|
|
698
|
+
const sentFile = path.join(this.options.trackDir, routeSentName);
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
if (this.options.keepFiles) {
|
|
702
|
+
// Append pending data to sent archive
|
|
703
|
+
this.app.debug('Appending sent data to archive file:', routeSentName);
|
|
704
|
+
|
|
705
|
+
// Read pending file content
|
|
706
|
+
const pendingContent = await fs.readFile(pendingFile, 'utf8');
|
|
707
|
+
|
|
708
|
+
// Append to sent file (create if doesn't exist)
|
|
709
|
+
await fs.appendFile(sentFile, pendingContent);
|
|
710
|
+
|
|
711
|
+
this.app.debug('Successfully archived sent track data');
|
|
712
|
+
} else {
|
|
713
|
+
this.app.debug('keepFiles disabled, will delete pending file');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Always delete the pending file after successful send
|
|
717
|
+
this.app.debug('Deleting pending track file');
|
|
718
|
+
await fs.remove(pendingFile);
|
|
719
|
+
this.app.debug('Successfully processed track files after send');
|
|
720
|
+
|
|
721
|
+
} catch (err) {
|
|
722
|
+
this.app.debug('Error handling files after successful send:', err.message);
|
|
723
|
+
// Non-fatal: Data was sent successfully, file handling is secondary
|
|
724
|
+
// Next save will create a new pending file anyway
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async createTrack(inputPath) {
|
|
729
|
+
const fileStream = fs.createReadStream(inputPath);
|
|
730
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
731
|
+
const track = [];
|
|
732
|
+
let lastTimestamp;
|
|
733
|
+
for await (const line of rl) {
|
|
734
|
+
if (line) {
|
|
735
|
+
try {
|
|
736
|
+
const point = JSON.parse(line);
|
|
737
|
+
const timestamp = new Date(point.t).getTime();
|
|
738
|
+
if (!isNaN(timestamp) && this.isValidLatitude(point.lat) && this.isValidLongitude(point.lon)) {
|
|
739
|
+
track.push([timestamp, point.lat, point.lon]);
|
|
740
|
+
lastTimestamp = timestamp;
|
|
741
|
+
}
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.app.debug('could not parse line from track file:', line);
|
|
744
|
+
this.app.setPluginError(`Failed could not parse line from track file - check logs for details.`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (track.length > 0) {
|
|
749
|
+
return { timestamp: new Date(lastTimestamp).getTime(), track };
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
module.exports = function (app) {
|
|
756
|
+
const instance = new SignalkToNoforeignland(app);
|
|
757
|
+
return instance.getPluginObject();
|
|
758
|
+
};
|