@noforeignland/signalk-to-noforeignland 1.1.0-beta.2 → 1.1.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  1.1.0
2
+ * CHANGE: Removed postinstall script - SignalK server will use --ignore-scripts (issue #2181). Runtime PluginCleanup handles all migration.
2
3
  * CHANGE: Refactored Project structure, see PROJECT_STRUCTURE.md
4
+ * CHANGE: Use phrase GNSS instead of GPS - Thanks Piotr
5
+ * BUGFIX: Removed unreliable DNS-based internet connectivity test (beta.11) - API retry logic is sufficient
6
+ * BUGFIX: Reverted to beta.3 working configuration (beta.10) - undoing all failed schema experiments from beta.4-9
7
+ * CHANGE: Improved cleanup error handling - only shows error after all retry attempts fail
8
+ * CHANGE: Added installed plugins logging for better debugging
9
+ * CHANGE: Platform-specific cleanup instructions (Cerbo GX vs standard)
3
10
 
4
11
  1.0.1
5
12
  * CHANGE: Cleanup previous installs and migrate plugin config, removes depricated old plugins.
@@ -27,17 +34,17 @@ The "new" one has to be installed once manually, after that everything is back t
27
34
  0.1.29-beta.1
28
35
  * CHANGE: After talking to Treppo from Sugnal K core team we should use the data path "noforeignland.*", code and docs changed.
29
36
  * CHANGE: README.md info added and format optimized
30
- * CHANGE: GPS source at the end for setPluginStatus - Cerbo has a long source name and truncates it.
37
+ * CHANGE: GNSS source at the end for setPluginStatus - Cerbo has a long source name and truncates it.
31
38
  * CHANGE: Use "npm install instead" of "npm ci" to push from Github to npmjs using OIDC authentication
32
39
  * CHANGE: Moved navigation.position source from path noforeignland.status to noforeignland.source
33
40
 
34
41
  0.1.28
35
- * BUGFIX: No GPS data found - introduced in 0.1.27 - Thanks Piotr
42
+ * BUGFIX: No GNSS data found - introduced in 0.1.27 - Thanks Piotr
36
43
 
37
44
  0.1.28-beta.2
38
45
  * EXPERIMENTAL: Signal K data path to visualize the plugin behaviour or error in Node Red, KIP, etc. Using plugin.signalk-to-noforeignland.* for now. See README.md for details.
39
46
  * CHANGE: Using app.getDataDirPath() to store transient data, thanks Jeremy, they are now in "plugin-config-data/signalk-to-noforeignland/nfl-track" and renamed to pending.json1 and sent.json1
40
- * CHANGE: Optimize GPS detection if multiple navigation.position from different sources exist.
47
+ * CHANGE: Optimize GNSS detection if multiple navigation.position from different sources exist.
41
48
  * CHANGE: PluginStatus optimized for limited space available.
42
49
  * CHANGE: doc/beta_install.md changed for new file structure.
43
50
 
@@ -49,13 +56,13 @@ The "new" one has to be installed once manually, after that everything is back t
49
56
 
50
57
  0.1.27-beta.1
51
58
  * NEW: NPMJS requires new method for publishing. The old tokens will expire Nov 19th, 2025, so moving to OIDC authentication.
52
- * NEW: Check the GPS status in navigation.position, else retry and throw PluginError on Dashboard
59
+ * NEW: Check the GNSS status in navigation.position, else retry and throw PluginError on Dashboard
53
60
  * 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:
54
61
  * New points accumulate in nfl-track-pending.jsonl
55
62
  * Send succeeds → API confirms receipt
56
63
  * If keepFiles=true: The content of pending file is appended to nfl-track-sent.jsonl (line 588)
57
64
  * Pending file is deleted
58
- * Next GPS points → create a new pending file
65
+ * Next GNSS points → create a new pending file
59
66
  * Next successful send → appends again to the same nfl-track-sent.jsonl
60
67
 
61
68
  0.1.26
@@ -0,0 +1,357 @@
1
+ # Project Structure
2
+
3
+ This document describes the architecture and organization of the SignalK to Noforeignland plugin.
4
+
5
+ ## Overview
6
+
7
+ This plugin follows a modular architecture where each responsibility is encapsulated in a dedicated module. The main orchestrator ([index.js](index.js)) coordinates all modules through a class-based approach.
8
+
9
+ ## Directory Layout
10
+
11
+ ```
12
+ nfl-signalk/
13
+ index.js # Main plugin entry point & orchestrator
14
+ cleanup-old-plugin.js # Postinstall script for removing old versions
15
+ package.json # NPM package configuration
16
+ lib/ # Core modules directory
17
+ ConfigManager.js # Configuration handling & migration
18
+ TrackLogger.js # Position subscription & logging
19
+ TrackSender.js # API communication & track sending
20
+ HealthMonitor.js # Position data health checks
21
+ PluginCleanup.js # Runtime cleanup of old plugins
22
+ TrackMigration.js # Track file migration utilities
23
+ DataPathEmitter.js # SignalK delta emission
24
+ DirectoryUtils.js # File system utilities
25
+ README.md # User documentation
26
+ CHANGELOG.md # Version history
27
+ PROJECT_STRUCTURE.md # This file
28
+ ```
29
+
30
+ ## Module Responsibilities
31
+
32
+ ### Core Orchestrator
33
+
34
+ #### [index.js](index.js)
35
+ **Purpose**: Main plugin class that orchestrates all modules and implements SignalK plugin lifecycle.
36
+
37
+ **Key Responsibilities**:
38
+ - Exports SignalK plugin object with `start()` and `stop()` methods
39
+ - Initializes all module instances in correct order
40
+ - Manages plugin state (options, upSince, cron, lastSuccessfulTransfer)
41
+ - Coordinates between modules during startup sequence
42
+ - Handles plugin status and error reporting to SignalK UI
43
+ - Implements CRON-based interval for data sending
44
+
45
+ **Key Methods**:
46
+ - `start(options, restartPlugin)`: 13-step startup sequence
47
+ - `stop()`: Cleanup and shutdown
48
+ - `interval()`: CRON job handler for sending tracks
49
+ - `handleSavePoint(lastPosition)`: Callback when track point is saved
50
+ - `handleHealthy()`: Callback when health check passes
51
+ - `setPluginStatus(status)`: Update UI status
52
+ - `setPluginError(error)`: Report error to UI
53
+
54
+ ### Configuration & Migration
55
+
56
+ #### [lib/ConfigManager.js](lib/ConfigManager.js)
57
+ **Purpose**: Handles plugin configuration, schema definition, and config migration.
58
+
59
+ **Key Responsibilities**:
60
+ - Defines plugin schema (mandatory/advanced/expert settings groups)
61
+ - Migrates old flat config structure to new nested structure
62
+ - Flattens nested config back to internal format with defaults
63
+ - Validates boat API key
64
+ - Resolves track directory paths (absolute vs relative)
65
+ - Randomizes CRON schedule to avoid server load spikes
66
+
67
+ **Config Structure**:
68
+ ```javascript
69
+ {
70
+ mandatory: { boatApiKey: string },
71
+ advanced: { minMove, minSpeed, sendWhileMoving, ping_api_every_24h },
72
+ expert: { filterSource, trackDir, keepFiles, trackFrequency, apiCron,
73
+ internetTestTimeout, apiTimeout }
74
+ }
75
+ ```
76
+
77
+ ### Position Tracking
78
+
79
+ #### [lib/TrackLogger.js](lib/TrackLogger.js)
80
+ **Purpose**: Subscribes to position updates and logs them to disk.
81
+
82
+ **Key Responsibilities**:
83
+ - Subscribes to `navigation.position` and optionally `navigation.speedOverGround`
84
+ - Implements GNSS source auto-selection (stick to one source unless stale)
85
+ - Supports manual source filtering via `filterSource` config
86
+ - Validates positions (rejects coordinates near 0,0)
87
+ - Applies movement filters (minSpeed, minMove, trackFrequency)
88
+ - Implements 24-hour keepalive ping
89
+ - Saves track points to JSONL file (`pending.jsonl`)
90
+ - Calculates distances using equirectangular approximation
91
+
92
+ **GNSS Source Logic**:
93
+ - If `filterSource` is set: only accept that specific source
94
+ - If not set: auto-select first source, switch only if stale (>5 min)
95
+ - Prevents position jumps when multiple GNSS devices are present
96
+
97
+ **Track Point Format**:
98
+ ```json
99
+ {"lat": 37.8136, "lon": -122.4784, "t": "2025-01-15T10:30:00.000Z"}
100
+ ```
101
+
102
+ #### [lib/TrackSender.js](lib/TrackSender.js)
103
+ **Purpose**: Sends accumulated track data to the Noforeignland API.
104
+
105
+ **Key Responsibilities**:
106
+ - Reads track points from `pending.jsonl`
107
+ - Tests internet connectivity via DNS lookups (8.8.8.8, 1.1.1.1)
108
+ - Checks if boat is moving (based on last position timestamp)
109
+ - Sends track data to NFL API with retry logic (3 attempts)
110
+ - Handles successful sends (archive to `sent.jsonl` or delete)
111
+ - Validates position coordinates before sending
112
+
113
+ **API Request Format**:
114
+ ```javascript
115
+ POST https://www.noforeignland.com/home/api/v1/boat/tracking/track
116
+ Headers: { 'X-NFL-API-Key': pluginApiKey }
117
+ Body: {
118
+ timestamp: ms,
119
+ track: [[ms, lat, lon], [ms, lat, lon], ...],
120
+ boatApiKey: userBoatApiKey
121
+ }
122
+ ```
123
+
124
+ **Retry Logic**:
125
+ - Attempt 1: base timeout (default 30s)
126
+ - Attempt 2: 2x timeout + 2s delay
127
+ - Attempt 3: 3x timeout + 4s delay
128
+
129
+ ### Health & Monitoring
130
+
131
+ #### [lib/HealthMonitor.js](lib/HealthMonitor.js)
132
+ **Purpose**: Monitors GNSS position data freshness and reports issues.
133
+
134
+ **Key Responsibilities**:
135
+ - Initial check: 2 minutes after startup
136
+ - Periodic checks: every 5 minutes
137
+ - Detects stale position data (>5 minutes old)
138
+ - Detects missing position data
139
+ - Provides context-aware error messages (filtered vs auto-selected source)
140
+ - Calls error/healthy callbacks to update plugin status
141
+
142
+ **Health States**:
143
+ - Healthy: Position received within last 5 minutes
144
+ - Warning: No position for >5 minutes
145
+ - Error: No position ever received (or 2+ min after startup)
146
+
147
+ ### Cleanup & Migration
148
+
149
+ #### [lib/PluginCleanup.js](lib/PluginCleanup.js)
150
+ **Purpose**: Runtime cleanup of old plugin versions.
151
+
152
+ **Key Responsibilities**:
153
+ - Detects Victron Cerbo vs standard SignalK paths
154
+ - Migrates config from old `signalk-to-noforeignland` to new scoped name
155
+ - Removes old plugin directories (`signalk-to-noforeignland`, `signalk-to-nfl`)
156
+ - Multiple retry attempts (immediate, 5s, 15s, 30s delays)
157
+ - Falls back to manual instructions if removal fails
158
+ - Returns promise resolving to 'all_removed' or 'partial_removal'
159
+
160
+ **Old Plugins Removed**:
161
+ - `signalk-to-noforeignland` (unscoped predecessor)
162
+ - `signalk-to-nfl` (deprecated alias)
163
+
164
+ #### [cleanup-old-plugin.js](cleanup-old-plugin.js)
165
+ **Purpose**: Postinstall script for immediate cleanup during npm install.
166
+
167
+ **Key Responsibilities**:
168
+ - Runs via `npm postinstall` hook
169
+ - Uses direct `fs.rmSync()` instead of npm uninstall (safer during install)
170
+ - 2-second delay to allow npm to complete current operation
171
+ - Detects Victron Cerbo vs standard SignalK paths
172
+ - Migrates config files
173
+ - Provides fallback manual instructions
174
+
175
+ **Why Two Cleanup Mechanisms?**
176
+ 1. **Postinstall**: Runs during installation, removes old plugins immediately
177
+ 2. **Runtime**: Handles cases where postinstall failed (permissions, timing)
178
+
179
+ #### [lib/TrackMigration.js](lib/TrackMigration.js)
180
+ **Purpose**: Migrates track files from old naming/location schemes.
181
+
182
+ **Key Responsibilities**:
183
+ - Migrates old file names to new naming scheme:
184
+ - `nfl-track.jsonl` -> `pending.jsonl`
185
+ - `nfl-track-pending.jsonl` -> `pending.jsonl`
186
+ - `nfl-track-sent.jsonl` -> `sent.jsonl`
187
+ - Migrates track files from old plugin directory to new data directory
188
+ - Removes old track directory if empty
189
+ - Non-destructive: only migrates if target doesn't exist
190
+
191
+ ### SignalK Integration
192
+
193
+ #### [lib/DataPathEmitter.js](lib/DataPathEmitter.js)
194
+ **Purpose**: Creates and updates SignalK data paths for external integrations.
195
+
196
+ **Key Responsibilities**:
197
+ - Emits SignalK deltas to create data paths
198
+ - Updates status paths on state changes
199
+ - Manages error state independently
200
+ - Provides both ISO8601 and locale-formatted timestamps
201
+
202
+ **Data Paths Created**:
203
+ ```
204
+ noforeignland.savepoint # ISO8601 timestamp of last save
205
+ noforeignland.savepoint_local # Locale string of last save
206
+ noforeignland.sent_to_api # ISO8601 timestamp of last API transfer
207
+ noforeignland.sent_to_api_local # Locale string of last API transfer
208
+ noforeignland.status # Status string (save/transfer/source info)
209
+ noforeignland.status_boolean # 0 = OK, 1 = error
210
+ noforeignland.source # Active GNSS source name
211
+ ```
212
+
213
+ **Usage**: These paths can be consumed by Node-RED, KIP dashboards, or other SignalK consumers.
214
+
215
+ #### [lib/DirectoryUtils.js](lib/DirectoryUtils.js)
216
+ **Purpose**: File system utilities with proper error handling.
217
+
218
+ **Key Responsibilities**:
219
+ - Creates directories recursively
220
+ - Checks read/write permissions
221
+ - Provides context-specific error messages
222
+ - Handles common error codes (EACCES, EPERM, ETIMEDOUT)
223
+
224
+ ## Data Flow
225
+
226
+ ### Startup Sequence
227
+ ```
228
+ 1. ConfigManager: Migrate old config -> flatten -> validate
229
+ 2. DirectoryUtils: Create track directory
230
+ 3. PluginCleanup: Remove old plugins (async, non-blocking)
231
+ 4. TrackMigration: Migrate old track files
232
+ 5. Initialize: TrackLogger, TrackSender, HealthMonitor
233
+ 6. ConfigManager: Randomize CRON schedule
234
+ 7. TrackLogger: Start position subscription
235
+ 8. CRON: Start interval job
236
+ 9. HealthMonitor: Start health checks
237
+ ```
238
+
239
+ ### Position Logging Flow
240
+ ```
241
+ navigation.position delta
242
+ -> TrackLogger.doOnValue()
243
+ -> handleSourceSelection() [filter/auto-select]
244
+ -> isValidPosition() [validate lat/lon]
245
+ -> shouldLogPosition() [check distance/frequency]
246
+ -> savePoint() [append to pending.jsonl]
247
+ -> callback to index.handleSavePoint()
248
+ -> DataPathEmitter.emitSavepoint()
249
+ -> setPluginStatus()
250
+ ```
251
+
252
+ ### Track Sending Flow (CRON Interval)
253
+ ```
254
+ CRON trigger
255
+ -> index.interval()
256
+ -> TrackSender.isBoatMoving() [check last position age]
257
+ -> TrackSender.hasTrackData() [check pending.jsonl exists]
258
+ -> TrackSender.testInternet() [DNS lookup test]
259
+ -> TrackSender.sendTrack()
260
+ -> createTrackFromFile() [read pending.jsonl]
261
+ -> sendWithRetry() [POST to API, 3 attempts]
262
+ -> handleSuccessfulSend() [archive or delete]
263
+ -> DataPathEmitter.emitApiTransfer()
264
+ -> setPluginStatus()
265
+ ```
266
+
267
+ ### Health Monitoring Flow
268
+ ```
269
+ HealthMonitor timer (every 5 min)
270
+ -> performHealthCheck()
271
+ -> check lastPositionReceived timestamp
272
+ -> if stale/missing: call onError callback
273
+ -> index.setPluginError()
274
+ -> DataPathEmitter.setError()
275
+ -> app.setPluginError()
276
+ -> if healthy: call onHealthy callback
277
+ -> index.handleHealthy()
278
+ -> setPluginStatus() [clear error if needed]
279
+ ```
280
+
281
+ ## File Locations
282
+
283
+ ### Victron Cerbo GX
284
+ - SignalK directory: `/data/conf/signalk`
285
+ - Plugin installed at: `/data/conf/signalk/node_modules/@noforeignland/signalk-to-noforeignland`
286
+ - Config: `/data/conf/signalk/plugin-config-data/@noforeignland-signalk-to-noforeignland.json`
287
+ - Track files: `/data/conf/signalk/plugin-config-data/@noforeignland-signalk-to-noforeignland/nfl-track/`
288
+
289
+ ### Standard SignalK
290
+ - SignalK directory: `~/.signalk`
291
+ - Plugin installed at: `~/.signalk/node_modules/@noforeignland/signalk-to-noforeignland`
292
+ - Config: `~/.signalk/plugin-config-data/@noforeignland-signalk-to-noforeignland.json`
293
+ - Track files: `~/.signalk/plugin-config-data/@noforeignland-signalk-to-noforeignland/nfl-track/`
294
+
295
+ ## Development Notes
296
+
297
+ ### Adding New Features
298
+ 1. Create module in `lib/` if it's a distinct responsibility
299
+ 2. Instantiate in [index.js](index.js) constructor
300
+ 3. Initialize in `start()` method at appropriate step
301
+ 4. Clean up in `stop()` method
302
+ 5. Update this document
303
+
304
+ ### Configuration Changes
305
+ 1. Update schema in [ConfigManager.js](lib/ConfigManager.js) `getSchema()`
306
+ 2. Add migration logic in `migrateOldConfig()` if breaking change
307
+ 3. Update `flattenConfig()` to include new fields with defaults
308
+ 4. Test both old and new config formats
309
+
310
+ ### Track File Format Changes
311
+ 1. Update `TrackLogger.savePoint()` for writing
312
+ 2. Update `TrackSender.createTrackFromFile()` for reading
313
+ 3. Add migration logic in [TrackMigration.js](lib/TrackMigration.js) if needed
314
+
315
+ ### Testing Checklist
316
+ - [ ] Test on Victron Cerbo GX (path detection, limited storage)
317
+ - [ ] Test on standard SignalK installation
318
+ - [ ] Test config migration from old versions
319
+ - [ ] Test with multiple GNSS sources (auto-selection)
320
+ - [ ] Test with filtered source
321
+ - [ ] Test with no internet connection
322
+ - [ ] Test with slow internet (timeout scenarios)
323
+ - [ ] Test cleanup of old plugins
324
+ - [ ] Test track file migration
325
+
326
+ ## Dependencies
327
+
328
+ ```json
329
+ {
330
+ "cron": "^2.1.0", // CRON job scheduling
331
+ "fs-extra": "^10.1.0", // Enhanced file system operations
332
+ "node-fetch": "^2.6.7" // HTTP requests to API
333
+ }
334
+ ```
335
+
336
+ ## Version History Notes
337
+
338
+ - **1.1.0**: Refactored to modular structure
339
+ - **1.0.x**: Scoped package name, cleanup mechanism
340
+ - **0.1.x**: Original flat structure (deprecated)
341
+
342
+ ## Known Technical Debt
343
+
344
+ 1. **Empty process.exit()**: [cleanup-old-plugin.js](cleanup-old-plugin.js) setTimeout may not complete
345
+ 2. **Error timing**: [PluginCleanup.js](lib/PluginCleanup.js) shows error before retries complete
346
+ 3. **No test suite**: Manual testing only, no automated tests
347
+
348
+ ## SignalK Plugin Conventions
349
+
350
+ This plugin follows SignalK plugin conventions:
351
+ - Exports function returning `{id, name, schema, start, stop}`
352
+ - Uses `app.debug()` for logging
353
+ - Uses `app.setPluginStatus()` and `app.setPluginError()` for UI feedback
354
+ - Subscribes via `app.subscriptionmanager.subscribe()`
355
+ - Emits deltas via `app.handleMessage(pluginId, delta)`
356
+ - Config stored in `plugin-config-data/` directory
357
+ - Data stored in plugin's data directory
package/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # Signal K To Noforeignland
2
2
  Effortlessly log your boat's movement to **noforeignland.com**
3
3
 
4
- ## Important for 0.1.x users
5
- Upgrade to >1.0.1 by installing this plugin from the Appstore ageain. Your old config will be migrated.
6
-
7
4
  ## Features
8
5
  * Automatically log your position to noforeignland.com
9
6
  * Send detailed tracks to log your entire trip and not just your final position
@@ -14,12 +11,13 @@ Upgrade to >1.0.1 by installing this plugin from the Appstore ageain. Your old c
14
11
  * SK data paths about the plugin status for your own dashboard or Node Red coding
15
12
 
16
13
  ## Issues
17
- * Server -> Plugin Config -> Signal K to Noforeignland -> Enable debug log (top right)
18
- * Report issues on GitHub (https://github.com/noforeignland/nfl-signalk/issues)
14
+ * Enable debug logging: Server -> Plugin Config -> Signal K to Noforeignland -> Enable debug log (top right)
15
+ * Check logs: Server -> Server Log (in SignalK web UI)
16
+ * Report issues on [GitHub](https://github.com/noforeignland/nfl-signalk/issues)
19
17
 
20
18
  ## Requirements
21
19
  * An internet connection is required in order to update noforeignland.com
22
- * A navigation.position data path inside Signal K for self, which is your current GPS position
20
+ * A navigation.position data path inside Signal K for self, which is your current GNSS position
23
21
  * A **noforeignland.com** account
24
22
  * Your Boat API Key from the **noforeignland.com** website:
25
23
  * Account > Settings > Boat tracking > API Key
@@ -44,10 +42,11 @@ noforeignland.source - string - string - data source of
44
42
  notifications.noforeignland.status_boolean - json object - auto created
45
43
  ```
46
44
 
47
- https://github.com/noforeignland/nfl-signalk/issues
48
-
49
45
  # Virctron Cerbo GX Users
50
46
 
51
47
  ## Limited storage
52
- Signal K can quickly exhaust the small onboard storage of the device, especially when a lot of logging is enabled. Vicron Energy addressed this here:
48
+ Signal K can quickly exhaust the small onboard storage of the device, especially when a lot of logging is enabled and not properly configured. Even concider to not enable "Keep track files on disk" if you are moving a lot.
49
+
50
+ Vicron Energy addressed this here:
53
51
  https://www.victronenergy.com/live/venus-os:large#disk_space_issues_data_partition_full
52
+
@@ -84,9 +84,6 @@ chown -R signalk:signalk /data/conf/signalk/*
84
84
  ```
85
85
 
86
86
 
87
- ----------------------------------------
88
-
89
-
90
87
  # dev tree (unstable) install - NOT RECOMMENDED
91
88
 
92
89
  1. Backup as above
@@ -111,6 +108,20 @@ chown -R signalk:signalk /data/conf/signalk/*
111
108
  ```
112
109
 
113
110
  3. Restart Server & Check logs
111
+ ```
112
+ svc -t /service/signalk-server
113
+ ```
114
+
115
+ # Manual Fixes
116
+
117
+ ## WARNING: found multiple copies of plugin with id signalk-to-noforeignland at /data/conf/signalk/node_modules/ and /data/conf/signalk/node_modules/
118
+
119
+ ```
120
+ cd /data/conf/signalk && npm uninstall signalk-to-noforeignland signalk-to-nfl
121
+ ```
122
+
123
+ and than
124
+
114
125
  ```
115
126
  svc -t /service/signalk-server
116
127
  ```
package/index.js CHANGED
@@ -202,17 +202,8 @@ class SignalkToNoforeignland {
202
202
  if (!hasTrack) {
203
203
  return;
204
204
  }
205
-
206
- // Test internet connection
207
- const hasInternet = await this.trackSender.testInternet();
208
- if (!hasInternet) {
209
- const errorMsg = 'No internet connection detected. Unable to send tracking data to NFL. DNS lookups failed - check your internet connection.';
210
- this.app.debug(errorMsg);
211
- this.setPluginError(errorMsg);
212
- return;
213
- }
214
-
215
- // Send track data
205
+
206
+ // Send track data (has built-in retry logic)
216
207
  const success = await this.trackSender.sendTrack();
217
208
 
218
209
  if (success) {
@@ -43,15 +43,15 @@ class HealthMonitor {
43
43
 
44
44
  if (!lastPositionReceived) {
45
45
  const errorMsg = this.options.filterSource
46
- ? `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.`
47
- : 'No GPS position data received. Check that your GPS is connected and SignalK is receiving navigation.position data.';
46
+ ? `No GNSS position data received from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device, or leave empty to use any GNSS source.`
47
+ : 'No GNSS position data received. Check that your GNSS is connected and SignalK is receiving navigation.position data.';
48
48
 
49
49
  onError(errorMsg);
50
50
  this.app.debug('Position health check: No position data ever received' + filterMsg);
51
51
  } else if (timeSinceLastPosition > 300) {
52
52
  const errorMsg = this.options.filterSource
53
- ? `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.`
54
- : `No GPS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GPS connection.`;
53
+ ? `No GNSS 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.`
54
+ : `No GNSS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GNSS connection.`;
55
55
 
56
56
  onError(errorMsg);
57
57
  this.app.debug(`Position health check: No position for ${timeSinceLastPosition.toFixed(0)} seconds` + filterMsg);
@@ -68,8 +68,8 @@ class HealthMonitor {
68
68
  if (!lastPositionReceived) {
69
69
  const activeSource = this.options.filterSource || autoSelectedSource || 'any';
70
70
  const errorMsg = this.options.filterSource
71
- ? `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.`
72
- : 'No GPS position data received after 2 minutes. Check that your GPS is connected and SignalK is receiving navigation.position data.';
71
+ ? `No GNSS 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 GNSS source.`
72
+ : 'No GNSS position data received after 2 minutes. Check that your GNSS is connected and SignalK is receiving navigation.position data.';
73
73
 
74
74
  onError(errorMsg);
75
75
  this.app.debug('Initial position check: No position data received' +
@@ -13,37 +13,130 @@ class PluginCleanup {
13
13
  try {
14
14
  // Detect SignalK directory (standard or Victron Cerbo)
15
15
  const victronPath = '/data/conf/signalk';
16
- const standardPath = process.env.SIGNALK_NODE_CONFIG_DIR ||
16
+ const standardPath = process.env.SIGNALK_NODE_CONFIG_DIR ||
17
17
  path.join(process.env.HOME || process.env.USERPROFILE, '.signalk');
18
-
18
+
19
19
  const configDir = fs.existsSync(victronPath) ? victronPath : standardPath;
20
20
  this.app.debug(`Using SignalK directory: ${configDir}`);
21
-
21
+
22
22
  const configPath = path.join(configDir, 'plugin-config-data');
23
-
23
+
24
24
  // 1. Config Migration - only from signalk-to-noforeignland
25
25
  await this.migrateConfig(configPath);
26
-
27
- // 2. Check and remove old plugins
28
- await this.removeOldPlugins(configDir);
29
-
26
+
27
+ // 2. Verify what plugins are actually present
28
+ this.logInstalledPlugins(configDir);
29
+
30
+ // 3. Check and remove old plugins
31
+ return await this.removeOldPlugins(configDir);
32
+
30
33
  } catch (err) {
31
34
  this.app.debug('Error during old plugin cleanup:', err.message);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Log which SignalK NFL plugins are currently installed (for debugging)
41
+ */
42
+ logInstalledPlugins(configDir) {
43
+ const nodeModulesDir = path.join(configDir, 'node_modules');
44
+ const pluginsToCheck = [
45
+ '@noforeignland/signalk-to-noforeignland',
46
+ 'signalk-to-noforeignland',
47
+ 'signalk-to-nfl'
48
+ ];
49
+
50
+ const found = [];
51
+ for (const pluginName of pluginsToCheck) {
52
+ const pluginPath = path.join(nodeModulesDir, pluginName);
53
+ if (fs.existsSync(pluginPath)) {
54
+ const packageJsonPath = path.join(pluginPath, 'package.json');
55
+ let version = 'unknown';
56
+ try {
57
+ if (fs.existsSync(packageJsonPath)) {
58
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
59
+ version = packageJson.version;
60
+ }
61
+ } catch (e) {
62
+ // Ignore version read errors
63
+ }
64
+ found.push(`${pluginName}@${version}`);
65
+ }
66
+ }
67
+
68
+ if (found.length > 0) {
69
+ this.app.debug(`Installed NFL plugins: ${found.join(', ')}`);
32
70
  }
33
71
  }
34
72
 
35
73
  /**
36
- * Migrate config from old plugin to new scoped version
74
+ * Migrate config from old plugin ID to new plugin ID
75
+ *
76
+ * Handles config migration from:
77
+ * 1. Old unscoped plugin "signalk-to-noforeignland" (v0.1.x)
78
+ * 2. Beta versions with wrong plugin ID (v1.1.0-beta.1/2/3)
79
+ *
80
+ * Both used config filename: "signalk-to-noforeignland.json"
81
+ * New version uses: "@noforeignland-signalk-to-noforeignland.json"
82
+ *
83
+ * Since both sources use the same filename, we simply copy if it exists.
37
84
  */
38
85
  async migrateConfig(configPath) {
39
86
  const oldConfigFile = path.join(configPath, 'signalk-to-noforeignland.json');
40
87
  const newConfigFile = path.join(configPath, '@noforeignland-signalk-to-noforeignland.json');
41
-
88
+
89
+ // Only migrate if old config exists and new config doesn't
42
90
  if (fs.existsSync(oldConfigFile) && !fs.existsSync(newConfigFile)) {
43
- this.app.debug('Migrating configuration from old plugin "signalk-to-noforeignland"...');
44
- fs.copyFileSync(oldConfigFile, newConfigFile);
45
- fs.copyFileSync(oldConfigFile, `${oldConfigFile}.backup`);
46
- this.app.debug('✓ Configuration migrated successfully');
91
+ this.app.debug('Migrating configuration from old plugin to new scoped plugin...');
92
+ this.app.debug(` Source: ${oldConfigFile}`);
93
+ this.app.debug(` Target: ${newConfigFile}`);
94
+
95
+ try {
96
+ // Copy to new location
97
+ fs.copyFileSync(oldConfigFile, newConfigFile);
98
+
99
+ // Create backup of old config
100
+ const backupFile = `${oldConfigFile}.backup-${Date.now()}`;
101
+ fs.copyFileSync(oldConfigFile, backupFile);
102
+
103
+ this.app.debug('✓ Configuration successfully migrated');
104
+ this.app.debug(` Backup saved: ${backupFile}`);
105
+ } catch (err) {
106
+ this.app.debug(`⨯ Config migration failed: ${err.message}`);
107
+ this.app.debug(' You may need to reconfigure the plugin manually');
108
+ }
109
+ } else if (fs.existsSync(newConfigFile)) {
110
+ this.app.debug('Configuration already in new location');
111
+
112
+ // Check if config has incorrect "configuration" wrapper and fix it
113
+ try {
114
+ const configData = JSON.parse(fs.readFileSync(newConfigFile, 'utf8'));
115
+ if (configData.configuration && typeof configData.configuration === 'object') {
116
+ this.app.debug('Detected nested "configuration" wrapper, unwrapping...');
117
+
118
+ // Unwrap: move properties from configuration object to root
119
+ const unwrapped = {
120
+ ...configData.configuration,
121
+ enabled: configData.enabled,
122
+ enableLogging: configData.enableLogging,
123
+ enableDebug: configData.enableDebug
124
+ };
125
+
126
+ // Backup before fixing
127
+ const fixBackupFile = `${newConfigFile}.backup-unwrap-${Date.now()}`;
128
+ fs.copyFileSync(newConfigFile, fixBackupFile);
129
+
130
+ // Write fixed config
131
+ fs.writeFileSync(newConfigFile, JSON.stringify(unwrapped, null, 2));
132
+ this.app.debug('✓ Configuration unwrapped successfully');
133
+ this.app.debug(` Backup: ${fixBackupFile}`);
134
+ }
135
+ } catch (err) {
136
+ this.app.debug(`⨯ Could not check/fix config structure: ${err.message}`);
137
+ }
138
+ } else {
139
+ this.app.debug('No old configuration found, first-time setup');
47
140
  }
48
141
  }
49
142
 
@@ -87,16 +180,11 @@ class PluginCleanup {
87
180
  this.app.debug('All old plugins removed successfully');
88
181
  return 'all_removed';
89
182
  }
90
-
91
- // Show warning for plugins that couldn't be removed immediately
183
+
184
+ // Don't show error immediately - log that we're retrying
92
185
  const stillPresentNames = stillPresent.map(p => `"${p.name}"`).join(' and ');
93
- const stillPresentCmd = stillPresent.map(p => p.name).join(' ');
94
-
95
- this.app.setPluginError(
96
- `Old plugin(s) ${stillPresentNames} still installed. ` +
97
- `Will retry removal, or uninstall manually: cd ${configDir} && npm uninstall ${stillPresentCmd}`
98
- );
99
-
186
+ this.app.debug(`Old plugin(s) ${stillPresentNames} still present, will retry removal...`);
187
+
100
188
  // Delayed removal attempts (multiple tries with increasing delays)
101
189
  return new Promise((resolve) => {
102
190
  const delays = [5000, 15000, 30000]; // 5s, 15s, 30s
@@ -104,12 +192,22 @@ class PluginCleanup {
104
192
 
105
193
  const attemptRemoval = async () => {
106
194
  if (attemptIndex >= delays.length) {
107
- // Final attempt failed
195
+ // Final attempt failed - NOW show error only if plugins still exist
108
196
  const remaining = stillPresent.filter(p => fs.existsSync(p.dir));
109
197
  if (remaining.length > 0) {
110
- const remainingNames = remaining.map(p => p.name).join(' ');
111
- this.app.debug(`Could not remove old plugins after multiple attempts: ${remainingNames}`);
112
- this.app.debug(`Please manually run: cd ${configDir} && npm uninstall ${remainingNames}`);
198
+ const remainingNames = remaining.map(p => `"${p.name}"`).join(' and ');
199
+ const remainingCmd = remaining.map(p => p.name).join(' ');
200
+
201
+ this.app.debug(`Could not remove old plugins after ${delays.length} attempts: ${remainingNames}`);
202
+
203
+ // Show error with platform-specific commands
204
+ const isVictronCerbo = configDir === '/data/conf/signalk';
205
+ const cmdPrefix = isVictronCerbo ? 'On Victron Cerbo GX, ' : '';
206
+
207
+ this.app.setPluginError(
208
+ `${cmdPrefix}Old plugin(s) ${remainingNames} detected. ` +
209
+ `Manual removal required: cd ${configDir} && npm uninstall ${remainingCmd}`
210
+ );
113
211
  resolve('partial_removal');
114
212
  } else {
115
213
  this.app.debug('All old plugins eventually removed');
@@ -130,7 +130,7 @@ class TrackLogger {
130
130
  }
131
131
 
132
132
  /**
133
- * Handle GPS source selection (auto-select or filtered)
133
+ * Handle GNSS source selection (auto-select or filtered)
134
134
  */
135
135
  handleSourceSelection(update) {
136
136
  if (!this.options.filterSource) {
@@ -142,7 +142,7 @@ class TrackLogger {
142
142
  if (!this.autoSelectedSource) {
143
143
  this.autoSelectedSource = update.$source;
144
144
  this.lastPositionReceived = new Date().getTime();
145
- this.app.debug(`Auto-selected GPS source: '${this.autoSelectedSource}'`);
145
+ this.app.debug(`Auto-selected GNSS source: '${this.autoSelectedSource}'`);
146
146
  } else if (update.$source !== this.autoSelectedSource) {
147
147
  if (timeSinceLastPosition && timeSinceLastPosition > 300) {
148
148
  this.app.debug(`Switching from stale source '${this.autoSelectedSource}' to '${update.$source}' (no data for ${timeSinceLastPosition.toFixed(0)}s)`);
@@ -172,7 +172,7 @@ class TrackLogger {
172
172
  isValidPosition(position) {
173
173
  // Check if near (0,0) - likely invalid
174
174
  if (Math.abs(position.latitude) <= 0.01 && Math.abs(position.longitude) <= 0.01) {
175
- this.app.debug('GPS coordinates near (0,0), ignoring point to avoid invalid data logging.');
175
+ this.app.debug('GNSS coordinates near (0,0), ignoring point to avoid invalid data logging.');
176
176
  return false;
177
177
  }
178
178
 
@@ -36,44 +36,6 @@ class TrackSender {
36
36
  }
37
37
  }
38
38
 
39
- /**
40
- * Test internet connectivity
41
- */
42
- async testInternet() {
43
- const dns = require('dns').promises;
44
-
45
- this.app.debug('testing internet connection');
46
-
47
- const timeoutMs = this.options.internetTestTimeout || 2000;
48
- this.app.debug(`Using internet test timeout: ${timeoutMs}ms`);
49
-
50
- const dnsServers = [
51
- { name: 'Google DNS', ip: '8.8.8.8' },
52
- { name: 'Cloudflare DNS', ip: '1.1.1.1' }
53
- ];
54
-
55
- for (const server of dnsServers) {
56
- try {
57
- const startTime = Date.now();
58
- const result = await Promise.race([
59
- dns.reverse(server.ip),
60
- new Promise((_, reject) =>
61
- setTimeout(() => reject(new Error('DNS timeout')), timeoutMs)
62
- )
63
- ]);
64
- const elapsed = Date.now() - startTime;
65
-
66
- this.app.debug(`internet connection = true, ${server.name} (${server.ip}) is reachable (took ${elapsed}ms)`);
67
- return true;
68
- } catch (err) {
69
- this.app.debug(`${server.name} (${server.ip}) not reachable:`, err.message);
70
- }
71
- }
72
-
73
- this.app.debug(`internet connection = false, no public DNS servers reachable (timeout was ${timeoutMs}ms)`);
74
- return false;
75
- }
76
-
77
39
  /**
78
40
  * Check if track file exists and has content
79
41
  */
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "signalk": {
4
4
  "id": "@noforeignland/signalk-to-noforeignland"
5
5
  },
6
- "version": "1.1.0-beta.2",
6
+ "version": "1.1.0",
7
7
  "description": "SignalK track logger to noforeignland.com",
8
8
  "main": "index.js",
9
9
  "keywords": [
@@ -24,8 +24,7 @@
24
24
  "node": ">=18.0.0"
25
25
  },
26
26
  "scripts": {
27
- "test": "echo \"No tests specified\" && exit 0",
28
- "postinstall": "node cleanup-old-plugin.js"
27
+ "test": "echo \"No tests specified\" && exit 0"
29
28
  },
30
29
  "dependencies": {
31
30
  "cron": "^2.1.0",
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Cleanup script for old plugin versions
5
- * This runs as a standalone script during npm postinstall
6
- * NOT a class - just a simple executable script
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- // Detect SignalK directory (standard ~/.signalk or Victron Cerbo /data/conf/signalk)
13
- function getSignalKDir() {
14
- const victronPath = '/data/conf/signalk';
15
- const homePath = path.join(process.env.HOME || '', '.signalk');
16
-
17
- // Check Victron path first
18
- if (fs.existsSync(victronPath)) {
19
- return victronPath;
20
- }
21
-
22
- return homePath;
23
- }
24
-
25
- const signalkDir = getSignalKDir();
26
- console.log(`Using SignalK directory: ${signalkDir}`);
27
-
28
- // 1. Migration (only for signalk-to-noforeignland config)
29
- try {
30
- const configPath = path.join(signalkDir, 'plugin-config-data');
31
- const oldConfig = path.join(configPath, 'signalk-to-noforeignland.json');
32
- const newConfig = path.join(configPath, '@noforeignland-signalk-to-noforeignland.json');
33
-
34
- if (fs.existsSync(oldConfig) && !fs.existsSync(newConfig)) {
35
- fs.copyFileSync(oldConfig, newConfig);
36
- fs.copyFileSync(oldConfig, `${oldConfig}.backup`);
37
- console.log('✓ Configuration migrated');
38
- }
39
- } catch (e) {
40
- console.warn('Could not migrate config:', e.message);
41
- }
42
-
43
- // 2. Uninstall old plugins (with delay so npm can finish)
44
- setTimeout(() => {
45
- try {
46
- const oldPlugins = [
47
- { dir: path.join(signalkDir, 'node_modules', 'signalk-to-noforeignland'), name: 'signalk-to-noforeignland' },
48
- { dir: path.join(signalkDir, 'node_modules', 'signalk-to-nfl'), name: 'signalk-to-nfl' }
49
- ];
50
-
51
- let removedAny = false;
52
- const failedPlugins = [];
53
-
54
- for (const plugin of oldPlugins) {
55
- if (fs.existsSync(plugin.dir)) {
56
- try {
57
- console.log(`Removing old plugin "${plugin.name}"...`);
58
- // Direct deletion is safer than npm uninstall during installation
59
- fs.rmSync(plugin.dir, { recursive: true, force: true });
60
- console.log(`✓ Old plugin "${plugin.name}" removed`);
61
- removedAny = true;
62
- } catch (e) {
63
- console.warn(`Could not remove "${plugin.name}":`, e.message);
64
- failedPlugins.push(plugin.name);
65
- }
66
- }
67
- }
68
-
69
- if (failedPlugins.length > 0) {
70
- const uninstallCmd = failedPlugins.join(' ');
71
- console.warn(`Please run manually: npm uninstall ${uninstallCmd}`);
72
- }
73
-
74
- if (!removedAny && failedPlugins.length === 0) {
75
- console.log('No old plugins found - already clean!');
76
- }
77
- } catch (e) {
78
- console.warn('Error during cleanup:', e.message);
79
- }
80
- }, 2000); // Wait 2 seconds for npm to finish