signalk-to-noforeignland 0.1.26 → 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 +14 -0
- package/doc/beta_install.md +62 -0
- package/index.js +131 -11
- package/package.json +8 -4
- package/track/nfl-track-sent.jsonl +2 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for OIDC authentication
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Run tests if available
|
|
27
|
+
run: npm test --if-present
|
|
28
|
+
|
|
29
|
+
- name: Publish to NPM
|
|
30
|
+
run: npm publish
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
0.1.27
|
|
2
|
+
* Final version after successful testing SV MOIN and SV KIAPA NUI
|
|
3
|
+
|
|
4
|
+
0.1.27-beta.1
|
|
5
|
+
* NEW: NPMJS requires new method for publishing. The old tokens will expire Nov 19th, 2025, so moving to OIDC authentication.
|
|
6
|
+
* NEW: Check the GPS status in navigation.position, else retry and throw PluginError on Dashboard
|
|
7
|
+
* CHANGE: Keep track data on disk rewritten and migrate old files to new structure, so nfl-track-sent.jsonl becomes a continuous archive of all sent track data over time, when enabled. New Logic:
|
|
8
|
+
* * New points accumulate in nfl-track-pending.jsonl
|
|
9
|
+
* * Send succeeds → API confirms receipt
|
|
10
|
+
* * If keepFiles=true: The content of pending file is appended to nfl-track-sent.jsonl (line 588)
|
|
11
|
+
* * Pending file is deleted
|
|
12
|
+
* * Next GPS points → create a new pending file
|
|
13
|
+
* * Next successful send → appends again to the same nfl-track-sent.jsonl
|
|
14
|
+
|
|
1
15
|
0.1.26
|
|
2
16
|
* Same as 0.1.26-beta.1
|
|
3
17
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
How-to install the latest beta on your device?
|
|
2
|
+
|
|
3
|
+
This guide assumes you have a default install with default folders.
|
|
4
|
+
This is written for a Cerbo GX or a RPI, jump to the section you need want to go to.
|
|
5
|
+
It is recommended to enable the Debug Log for this plugin in Server -> Plugin Config before updating.
|
|
6
|
+
|
|
7
|
+
**For Raspberry PI:**
|
|
8
|
+
|
|
9
|
+
1. Backup old data
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
cd ~
|
|
13
|
+
mkdir nfl-backup
|
|
14
|
+
cp -a .signalk/node_modules/signalk-to-noforeignland/* nfl-backup/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Get new files from repo (main for latest)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
cd ~/.signalk/node_modules/signalk-to-noforeignland/
|
|
21
|
+
rm *
|
|
22
|
+
#NOTE: Trackdir will not be deleted, this is what we want.
|
|
23
|
+
wget https://github.com/noforeignland/nfl-signalk/archive/refs/heads/main.zip
|
|
24
|
+
unzip main.zip
|
|
25
|
+
cp -r nfl-signalk-main/* .
|
|
26
|
+
rm main.zip
|
|
27
|
+
rm -rf nfl-signalk-main/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Restart Server & Check logs
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
sudo systemctl restart signalk.service && sudo journalctl -u signalk.service -f
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
**For Cerbo GX with Image Large:**
|
|
38
|
+
1. Backup old data
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
cd ~
|
|
42
|
+
mkdir nfl-backup
|
|
43
|
+
cp -a /data/conf/signalk/node_modules/signalk-to-noforeignland/* nfl-backup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Get new files from repo (main for latest)
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
cd /data/conf/signalk/node_modules/signalk-to-noforeignland/
|
|
50
|
+
rm *
|
|
51
|
+
# NOTE: Trackdir will not be deleted, this is what we want.
|
|
52
|
+
wget https://github.com/noforeignland/nfl-signalk/archive/refs/heads/main.zip
|
|
53
|
+
unzip main.zip
|
|
54
|
+
cp -r nfl-signalk-main/* .
|
|
55
|
+
rm main.zip
|
|
56
|
+
rm -rf nfl-signalk-main/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. Restart Server & Check logs
|
|
60
|
+
```
|
|
61
|
+
Restart Server vom Webgui and check logs in Webgui
|
|
62
|
+
```
|
package/index.js
CHANGED
|
@@ -11,7 +11,8 @@ const isReachable = require('is-reachable');
|
|
|
11
11
|
const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
|
|
12
12
|
const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
|
|
13
13
|
const defaultTracksDir = 'track';
|
|
14
|
-
const routeSaveName = 'nfl-track.jsonl';
|
|
14
|
+
const routeSaveName = 'nfl-track-pending.jsonl'; // Changed: separate pending file
|
|
15
|
+
const routeSentName = 'nfl-track-sent.jsonl'; // New: archive for sent data
|
|
15
16
|
|
|
16
17
|
class SignalkToNoforeignland {
|
|
17
18
|
constructor(app) {
|
|
@@ -148,6 +149,10 @@ getSchema() {
|
|
|
148
149
|
|
|
149
150
|
async start(options = {}, restartPlugin) {
|
|
150
151
|
|
|
152
|
+
// Position data health check
|
|
153
|
+
this.positionCheckInterval = null;
|
|
154
|
+
this.lastPositionReceived = null;
|
|
155
|
+
|
|
151
156
|
// Backward compatibility: migrate old flat structure to new nested structure
|
|
152
157
|
let needsSave = false;
|
|
153
158
|
if (options.boatApiKey && !options.mandatory) {
|
|
@@ -227,6 +232,9 @@ getSchema() {
|
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
// NEW: Migrate old track file to new naming scheme on startup
|
|
236
|
+
await this.migrateOldTrackFile();
|
|
237
|
+
|
|
230
238
|
this.app.debug('track logger started, now logging to', this.options.trackDir);
|
|
231
239
|
this.app.setPluginStatus(`Started${needsSave ? ' (config migrated)' : ''}`);
|
|
232
240
|
this.upSince = new Date().getTime();
|
|
@@ -247,10 +255,38 @@ getSchema() {
|
|
|
247
255
|
// start cron job
|
|
248
256
|
this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
|
|
249
257
|
this.cron.start();
|
|
258
|
+
|
|
259
|
+
// Start position health check (every 5 minutes)
|
|
260
|
+
this.startPositionHealthCheck();
|
|
250
261
|
}
|
|
251
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
|
+
|
|
252
281
|
stop() {
|
|
253
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
|
+
|
|
254
290
|
if (this.cron) {
|
|
255
291
|
this.cron.stop();
|
|
256
292
|
this.cron = undefined;
|
|
@@ -360,6 +396,7 @@ getSchema() {
|
|
|
360
396
|
|
|
361
397
|
// Punkt speichern
|
|
362
398
|
this.lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
|
|
399
|
+
this.lastPositionReceived = new Date().getTime(); // Track for health check
|
|
363
400
|
await this.savePoint(this.lastPosition);
|
|
364
401
|
|
|
365
402
|
// shouldDoLog zurücksetzen wenn minSpeed aktiv ist
|
|
@@ -378,6 +415,7 @@ getSchema() {
|
|
|
378
415
|
t: point.timestamp
|
|
379
416
|
};
|
|
380
417
|
this.app.debug(`save data point:`, obj);
|
|
418
|
+
// CHANGED: Save to pending file
|
|
381
419
|
await fs.appendFile(path.join(this.options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
|
|
382
420
|
|
|
383
421
|
const lastSaveTime = new Date().toISOString();
|
|
@@ -443,6 +481,57 @@ getSchema() {
|
|
|
443
481
|
return res;
|
|
444
482
|
}
|
|
445
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
|
+
|
|
446
535
|
// periodic interval called by cron
|
|
447
536
|
async interval() {
|
|
448
537
|
if ((this.checkBoatMoving()) && await this.checkTrack() && await this.testInternet()) {
|
|
@@ -503,6 +592,7 @@ getSchema() {
|
|
|
503
592
|
}
|
|
504
593
|
|
|
505
594
|
async checkTrack() {
|
|
595
|
+
// CHANGED: Check pending file instead
|
|
506
596
|
const trackFile = path.join(this.options.trackDir, routeSaveName);
|
|
507
597
|
this.app.debug('checking the track', trackFile, 'if should send');
|
|
508
598
|
const exists = await fs.pathExists(trackFile);
|
|
@@ -522,7 +612,9 @@ getSchema() {
|
|
|
522
612
|
|
|
523
613
|
async sendApiData() {
|
|
524
614
|
this.app.debug('sending the data');
|
|
525
|
-
|
|
615
|
+
// CHANGED: Read from pending file
|
|
616
|
+
const pendingFile = path.join(this.options.trackDir, routeSaveName);
|
|
617
|
+
const trackData = await this.createTrack(pendingFile);
|
|
526
618
|
if (!trackData) {
|
|
527
619
|
this.app.debug('Recorded track did not contain any valid track points, aborting sending.');
|
|
528
620
|
this.app.setPluginError(`Failed to send track - Recorded track did not contain any valid track points, aborting sending.`);
|
|
@@ -564,14 +656,9 @@ getSchema() {
|
|
|
564
656
|
this.lastSuccessfulTransfer = new Date();
|
|
565
657
|
this.app.debug('Track successfully sent to API');
|
|
566
658
|
this.app.setPluginStatus(`Started - last Track sent successfully at ${new Date().toISOString()}`);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
await fs.move(path.join(this.options.trackDir, routeSaveName), path.join(this.options.trackDir, filename));
|
|
571
|
-
} else {
|
|
572
|
-
this.app.debug('Deleting track file');
|
|
573
|
-
await fs.remove(path.join(this.options.trackDir, routeSaveName));
|
|
574
|
-
}
|
|
659
|
+
|
|
660
|
+
// CHANGED: New file handling logic
|
|
661
|
+
await this.handleSuccessfulSend(pendingFile);
|
|
575
662
|
return; // Erfolg - beende Funktion
|
|
576
663
|
} else {
|
|
577
664
|
this.app.debug('Could not send track to API, returned response json:', responseBody);
|
|
@@ -605,6 +692,39 @@ getSchema() {
|
|
|
605
692
|
}
|
|
606
693
|
}
|
|
607
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
|
+
|
|
608
728
|
async createTrack(inputPath) {
|
|
609
729
|
const fileStream = fs.createReadStream(inputPath);
|
|
610
730
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
@@ -635,4 +755,4 @@ getSchema() {
|
|
|
635
755
|
module.exports = function (app) {
|
|
636
756
|
const instance = new SignalkToNoforeignland(app);
|
|
637
757
|
return instance.getPluginObject();
|
|
638
|
-
};
|
|
758
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-to-noforeignland",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "SignalK track logger to noforeignland.com
|
|
3
|
+
"version": "0.1.27",
|
|
4
|
+
"description": "SignalK track logger to noforeignland.com",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"signalk-node-server-plugin",
|
|
8
8
|
"signalk-category-utility"
|
|
9
9
|
],
|
|
10
10
|
"author": "Dirk Wahrheit",
|
|
11
|
+
"license": "MIT",
|
|
11
12
|
"homepage": "https://github.com/noforeignland/nfl-signalk",
|
|
12
13
|
"bugs": {
|
|
13
14
|
"url": "https://github.com/noforeignland/nfl-signalk/issues"
|
|
@@ -16,8 +17,11 @@
|
|
|
16
17
|
"type": "git",
|
|
17
18
|
"url": "https://github.com/noforeignland/nfl-signalk.git"
|
|
18
19
|
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
19
23
|
"scripts": {
|
|
20
|
-
"test": "echo \"
|
|
24
|
+
"test": "echo \"No tests specified\" && exit 0"
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"cron": "^2.1.0",
|
|
@@ -25,4 +29,4 @@
|
|
|
25
29
|
"is-reachable": "^5.2.1",
|
|
26
30
|
"node-fetch": "^2.6.7"
|
|
27
31
|
}
|
|
28
|
-
}
|
|
32
|
+
}
|