signalk-to-noforeignland 0.1.22 → 0.1.25

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 ADDED
@@ -0,0 +1,49 @@
1
+ 0.1.25
2
+ * CHANGE: Minimum boat move default increased from 50m to 80m
3
+ * CHANGE: Updated the README.md
4
+ * CHANGE: Use public ipv4 DNS instead of local with cache for testInternet()
5
+ * Final version after successful testing SV MOIN and SV KIAPA NUI
6
+
7
+ 0.1.25-beta.3
8
+ * NEW: Check if boat key is set on startup, else report error to dashboard
9
+ * CHANGE: Changing the order and label of the Plugin Settings to make it more clear for unexpierienced users and grouped to Mandatory, Advanced and Expert.
10
+ * CHANGE: Migration of < 0.1.25 Plugin settings to new structure.
11
+ * CHANGE: PluginStatus last track sent "Never" changed to "Not transfered since plugin start" to avoid confusions.
12
+
13
+
14
+ 0.1.25-beta.2
15
+ * CHANGE: Typo in pluginName fixed
16
+ * CHANGE: Dates for SetPlugin now ISO8601 formated (https://github.com/noforeignland/nfl-signalk/issues/9)
17
+
18
+ 0.1.25-beta.1
19
+ * CHANGE: User mattzilla470 reported timout Issues on VE Cerbo with a small CPU and using 4G (https://github.com/noforeignland/nfl-signalk/issues/7). So added a timout option in the plugin config and a tripple retry while increasing the timeout for the API call.
20
+
21
+ 0.1.24
22
+ * CHANGE: testInternet() only uses ipv4 now, some users don't have ipv6 configured properly and where unable to reach the API, when testInternet returned false
23
+ * NEW: PluginStatus on SK dashboard now shows last savePoint and last API transfer, so a user has more feedback what the app is doing without enabling the debug log and crawling though it.
24
+ * CHANGE: Renamed CHANGELOG to CHANGELOG.md
25
+ * CHANGE: CHANGELOG ORDER - newest on top.
26
+ * Final version after successful testing SV MOIN and SV KIAPA NUI
27
+
28
+ 0.1.23
29
+ * Final version after successful testing SV MOIN and SV KIAPA NUI
30
+
31
+ 0.1.23-beta.1
32
+ * Renamed branch to follow the release versions.
33
+ * CLEANUP - More debug info for the SK dashboard using this.app.setPluginError
34
+ * CLEANUP - Removed CreateGPX, was only used for removed Email transmission of the track
35
+
36
+ 0.1.22-beta.2
37
+
38
+ * CLEANUP and move to Object Oriented Javascript
39
+
40
+ 0.1.22-beta.1
41
+
42
+ * CONFIG: Attempt sending location while moving - Default changed from false to true
43
+ * CONFIG: Ping added for 24h ping if boat is not moved. - Default: true
44
+ * Package.json - Nodemailer dependency removed
45
+ * Marked for removal - Depricated "sendEmailData" function.
46
+ * REMOVED - sendEmail.js
47
+ * CLEANUP - Renamed emaiCron to apiCron
48
+ * NEW: 24h api ping, when enabled, even if boat didn't move.
49
+
package/README.md CHANGED
@@ -5,13 +5,22 @@ Effortlessly log your boat's movement to **noforeignland.com**
5
5
  * Automatically log your position to NFL
6
6
  * Send detailed tracks to log your entire trip and not just your final position
7
7
  * Can be used in near real time or cache and upload when stopped and data-connection is available.
8
+ * Can sent a 24h keepalive, when off the boat for a while.
8
9
 
9
- An internet connection is required in order to update NFL.
10
+ ## Issues
11
+ * Report issues on GitHub (https://github.com/noforeignland/nfl-signalk/issues)
10
12
 
11
13
  ## Requirements
14
+ * An internet connection is required in order to update NFL.
15
+ * A navigation.position data path inside Signal K for self, which is your current GPS position
12
16
  * A **noforeignland.com** account
13
17
  * Your Boat API Key from the **noforeignland.com** website:
14
18
  * Account > Settings > Boat tracking > API Key
15
19
 
16
20
  > Note your Boat API Key is not available in the app.
17
- > You must sign in to the **noforeignland.com** website (using the same authentication method you use for the app: Google. Facebook, Email).
21
+ > You must sign in to the **noforeignland.com** website (using the same authentication method you use for the app: Google. Facebook, Email).
22
+
23
+ ## Configuration
24
+ 1. Add your boat's API Key into the Server > Plugin Config > Signal K to Noforeignland > Boat API Key
25
+ 2. Hit "Submit"
26
+ 3. Restart the Signal K server
package/index.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  const { EOL } = require('os');
3
2
  const internetTestAddress = 'google.com';
4
3
  const internetTestTimeout = 1000;
@@ -8,480 +7,632 @@ const CronJob = require('cron').CronJob;
8
7
  const readline = require('readline');
9
8
  const fetch = require('node-fetch');
10
9
  const isReachable = require('is-reachable');
11
- const sendEmail = require('./sendEmail');
12
- const createGPX = require('./createGPX');
10
+
13
11
  const apiUrl = 'https://www.noforeignland.com/home/api/v1/boat/tracking/track';
14
12
  const pluginApiKey = '0ede6cb6-5213-45f5-8ab4-b4836b236f97';
15
- // const msToKn = 1.944;
16
-
17
-
18
- module.exports = function (app) {
19
- var plugin = {};
20
- plugin.id = 'signalk-to-noforeignland';
21
- plugin.name = 'SignalK to Noforeignland';
22
- plugin.description = 'SignalK track logger to noforeignland.com';
23
-
24
- plugin.schema = {
25
- "title": plugin.name,
26
- "description": "Some parameters need for use",
27
- "type": "object",
28
- "required": ["emailCron", "boatApiKey"],
29
- "properties": {
30
- "trackFrequency": {
31
- "type": "integer",
32
- "title": "Position tracking frequency in seconds.",
33
- "description": "To keep file sizes small we only log positions once in a while (unless you set this value to 0)",
34
- "default": 60
35
- },
36
- "minMove": {
37
- "type": "number",
38
- "title": "Minimum boat move to log in meters",
39
- "description": "To keep file sizes small we only log positions if a move larger than this size is noted (if set to 0 will log every move)",
40
- "default": 50
41
- },
42
- "minSpeed": {
43
- "type": "number",
44
- "title": "Minimum boat speed to log in knots",
45
- "description": "To keep file sizes small we only log positions if boat speed goes above this value to minimize recording position on anchor or mooring (if set to 0 will log every move)",
46
- "default": 1.5
47
- },
48
- "emailCron": {
49
- "type": "string",
50
- "title": "Send attempt CRON",
51
- "description": "We send the tracking data to NFL once in a while, you can set the schedule with this setting. CRON format: https://crontab.guru/",
52
- "default": '*/10 * * * *',
53
- },
54
- 'boatApiKey': {
55
- "type": "string",
56
- "title": "Boat API key",
57
- "description": "Boat API key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key. *required only in API method is set*",
58
- },
59
- "internetTestTimeout": {
60
- "type": "number",
61
- "title": "Timeout for testing internet connection in ms",
62
- "description": "Set this number higher for slower computers and internet connections",
63
- "default": 2000,
13
+ const defaultTracksDir = 'track';
14
+ const routeSaveName = 'nfl-track.jsonl';
15
+
16
+ class SignalkToNoforeignland {
17
+ constructor(app) {
18
+ this.app = app;
19
+ this.pluginId = 'signalk-to-noforeignland';
20
+ this.pluginName = 'Signal K to Noforeignland';
21
+ this.creator = 'signalk-track-logger';
22
+
23
+ // runtime state
24
+ this.unsubscribes = [];
25
+ this.unsubscribesControl = [];
26
+ this.lastPosition = null;
27
+ this.upSince = null;
28
+ this.cron = null;
29
+ this.options = {};
30
+ this.lastSuccessfulTransfer = null;
31
+ }
32
+
33
+ getSchema() {
34
+ return {
35
+ title: this.pluginName,
36
+ description: 'Some parameters need for use',
37
+ type: 'object',
38
+ required: ['boatApiKey', 'apiCron'],
39
+ properties: {
40
+ // Mandatory Settings Group
41
+ mandatory: {
42
+ type: 'object',
43
+ title: 'Mandatory Settings',
44
+ properties: {
45
+ boatApiKey: {
46
+ type: 'string',
47
+ title: 'Boat API Key',
48
+ description: 'Boat API Key from noforeignland.com. Can be found in Account > Settings > Boat tracking > API Key.'
49
+ }
50
+ }
64
51
  },
65
- "sendWhileMoving": {
66
- "type": "boolean",
67
- "title": "Attempt sending location while moving",
68
- "description": "Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?",
69
- "default": false
52
+
53
+ // Advanced Settings Group
54
+ advanced: {
55
+ type: 'object',
56
+ title: 'Advanced Settings',
57
+ properties: {
58
+ minMove: {
59
+ type: 'number',
60
+ title: 'Minimum boat move to log in meters',
61
+ 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)',
62
+ default: 80
63
+ },
64
+ minSpeed: {
65
+ type: 'number',
66
+ title: 'Minimum boat speed to log in knots',
67
+ 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)',
68
+ default: 1.5
69
+ },
70
+ sendWhileMoving: {
71
+ type: 'boolean',
72
+ title: 'Attempt sending location while moving',
73
+ description: 'Should the plugin attempt to send tracking data to NFL while detecting the vessel is moving or only when stopped?',
74
+ default: true
75
+ },
76
+ ping_api_every_24h: {
77
+ type: 'boolean',
78
+ title: 'Force a send every 24 hours',
79
+ description: 'Keeps your boat active on NFL in your current location even if you do not move',
80
+ default: true
81
+ }
82
+ }
70
83
  },
71
- "filterSource": {
72
- "type": "string",
73
- "title": "Position source device",
74
- "description": "Set this value to the name of a source if you want to only use the position given by that source.",
84
+
85
+ // Expert Settings Group
86
+ expert: {
87
+ type: 'object',
88
+ title: 'Expert Settings',
89
+ properties: {
90
+ filterSource: {
91
+ type: 'string',
92
+ title: 'Position source device',
93
+ 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.'
94
+ },
95
+ trackDir: {
96
+ type: 'string',
97
+ title: 'Directory to cache tracks',
98
+ description: 'EMPTY DEFAULT IS FINE - Path in server filesystem, absolute or from plugin directory.\noptional param (only used to keep file cache).'
99
+ },
100
+ keepFiles: {
101
+ type: 'boolean',
102
+ title: 'Keep track files on disk',
103
+ description: 'If you have a lot of hard drive space you can keep the track files for logging purposes.',
104
+ default: false
105
+ },
106
+ trackFrequency: {
107
+ type: 'integer',
108
+ title: 'Position tracking frequency in seconds',
109
+ description: 'To keep file sizes small we only log positions once in a while (unless you set this value to 0)',
110
+ default: 60
111
+ },
112
+ apiCron: {
113
+ type: 'string',
114
+ title: 'Send attempt CRON',
115
+ 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/',
116
+ default: '*/10 * * * *'
117
+ },
118
+ internetTestTimeout: {
119
+ type: 'number',
120
+ title: 'Timeout for testing internet connection in ms',
121
+ description: 'Set this number higher for slower computers and internet connections',
122
+ default: 2000
123
+ },
124
+ apiTimeout: {
125
+ type: 'integer',
126
+ title: 'API request timeout in seconds',
127
+ description: 'Timeout for sending data to NFL API. Increase for slow connections.',
128
+ default: 30,
129
+ minimum: 10,
130
+ maximum: 180
131
+ }
132
+ }
133
+ }
134
+ }
135
+ };
136
+ }
137
+
138
+ getPluginObject() {
139
+ return {
140
+ id: this.pluginId,
141
+ name: this.pluginName,
142
+ description: 'SignalK track logger to noforeignland.com',
143
+ schema: this.getSchema(),
144
+ start: this.start.bind(this),
145
+ stop: this.stop.bind(this)
146
+ };
147
+ }
148
+
149
+ async start(options = {}, restartPlugin) {
150
+
151
+ // Backward compatibility: migrate old flat structure to new nested structure
152
+ let needsSave = false;
153
+ if (options.boatApiKey && !options.mandatory) {
154
+ // Old config detected, migrate to new structure
155
+ this.app.debug('Migrating old configuration to new grouped structure');
156
+ needsSave = true;
157
+
158
+ options = {
159
+ mandatory: {
160
+ boatApiKey: options.boatApiKey
75
161
  },
76
- "trackDir": {
77
- "type": "string",
78
- "title": "Directory to cache tracks.",
79
- "description": "Path in server filesystem, absolute or from plugin directory. optional param (only used to keep file cache).",
162
+ advanced: {
163
+ minMove: options.minMove !== undefined ? options.minMove : 50,
164
+ minSpeed: options.minSpeed !== undefined ? options.minSpeed : 1.5,
165
+ sendWhileMoving: options.sendWhileMoving !== undefined ? options.sendWhileMoving : true,
166
+ ping_api_every_24h: options.ping_api_every_24h !== undefined ? options.ping_api_every_24h : true
80
167
  },
81
- "keepFiles": {
82
- "type": "boolean",
83
- "title": "Should keep track files on disk?",
84
- "description": "If you have a lot of hard drive space you can keep the track files for logging purposes.",
85
- "default": false
168
+ expert: {
169
+ filterSource: options.filterSource,
170
+ trackDir: options.trackDir,
171
+ keepFiles: options.keepFiles !== undefined ? options.keepFiles : false,
172
+ trackFrequency: options.trackFrequency !== undefined ? options.trackFrequency : 60,
173
+ internetTestTimeout: options.internetTestTimeout !== undefined ? options.internetTestTimeout : 2000,
174
+ apiCron: options.apiCron || '*/10 * * * *',
175
+ apiTimeout: options.apiTimeout !== undefined ? options.apiTimeout : 30
86
176
  }
177
+ };
178
+
179
+ // Save the migrated configuration
180
+ try {
181
+ this.app.debug('Saving migrated configuration...');
182
+ await this.app.savePluginOptions(options, () => {
183
+ this.app.debug('Configuration successfully migrated and saved');
184
+ });
185
+ } catch (err) {
186
+ this.app.debug('Failed to save migrated configuration:', err.message);
187
+ // Continue anyway - the migration will work in memory
87
188
  }
189
+ }
190
+
191
+ // Flatten the nested structure for easier access and apply defaults
192
+ this.options = {
193
+ // Mandatory defaults
194
+ boatApiKey: options.mandatory?.boatApiKey,
195
+
196
+ // Advanced defaults
197
+ minMove: options.advanced?.minMove !== undefined ? options.advanced.minMove : 80,
198
+ minSpeed: options.advanced?.minSpeed !== undefined ? options.advanced.minSpeed : 1.5,
199
+ sendWhileMoving: options.advanced?.sendWhileMoving !== undefined ? options.advanced.sendWhileMoving : true,
200
+ ping_api_every_24h: options.advanced?.ping_api_every_24h !== undefined ? options.advanced.ping_api_every_24h : true,
201
+
202
+ // Expert defaults
203
+ filterSource: options.expert?.filterSource,
204
+ trackDir: options.expert?.trackDir || defaultTracksDir,
205
+ keepFiles: options.expert?.keepFiles !== undefined ? options.expert.keepFiles : false,
206
+ trackFrequency: options.expert?.trackFrequency !== undefined ? options.expert.trackFrequency : 60,
207
+ internetTestTimeout: options.expert?.internetTestTimeout !== undefined ? options.expert.internetTestTimeout : 2000,
208
+ apiCron: options.expert?.apiCron || '*/10 * * * *',
209
+ apiTimeout: options.expert?.apiTimeout !== undefined ? options.expert.apiTimeout : 30
88
210
  };
89
-
90
- var unsubscribes = [];
91
- var unsubscribesControl = [];
92
- var routeSaveName = 'nfl-track.jsonl';
93
- let lastPosition;
94
- let upSince;
95
- let cron;
96
- const creator = 'signalk-track-logger';
97
- const defaultTracksDir = 'track';
98
- // const maxAllowedSpeed = 100;
99
-
100
- plugin.start = function (options, restartPlugin) {
101
- if (!options.trackDir) options.trackDir = defaultTracksDir;
102
- if (!path.isAbsolute(options.trackDir)) options.trackDir = path.join(__dirname, options.trackDir);
103
- //app.debug('options.trackDir=',options.trackDir);
104
- if (!createDir(options.trackDir)) {
105
- plugin.stop();
106
- return;
211
+
212
+ // Validate that boatApiKey is set
213
+ if (!this.options.boatApiKey || this.options.boatApiKey.trim() === '') {
214
+ 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.';
215
+ this.app.debug(errorMsg);
216
+ this.app.setPluginError(errorMsg);
217
+ this.stop();
218
+ return;
219
+ }
220
+
221
+ if (!path.isAbsolute(this.options.trackDir)) {
222
+ this.options.trackDir = path.join(__dirname, this.options.trackDir);
223
+ }
224
+
225
+ if (!this.createDir(this.options.trackDir)) {
226
+ this.stop();
227
+ return;
228
+ }
229
+
230
+ this.app.debug('track logger started, now logging to', this.options.trackDir);
231
+ this.app.setPluginStatus(`Started${needsSave ? ' (config migrated)' : ''}`);
232
+ this.upSince = new Date().getTime();
233
+
234
+ // adjust default CRON if unchanged
235
+ if (!this.options.apiCron || this.options.apiCron === '*/10 * * * *') {
236
+ const startMinute = Math.floor(Math.random() * 10);
237
+ const startSecond = Math.floor(Math.random() * 60);
238
+ this.options.apiCron = `${startSecond} ${startMinute}/10 * * * *`;
239
+ }
240
+
241
+ this.app.debug('Setting CRON to ', this.options.apiCron);
242
+ this.app.debug('trackFrequency is set to', this.options.trackFrequency, 'seconds');
243
+
244
+ // subscribe and logging
245
+ this.doLogging();
246
+
247
+ // start cron job
248
+ this.cron = new CronJob(this.options.apiCron, this.interval.bind(this));
249
+ this.cron.start();
250
+ }
251
+
252
+ stop() {
253
+ this.app.debug('plugin stopped');
254
+ if (this.cron) {
255
+ this.cron.stop();
256
+ this.cron = undefined;
107
257
  }
108
-
109
- app.debug('track logger started, now logging to', options.trackDir);
110
- app.setPluginStatus(`Started`);
111
-
112
- doLogging();
113
-
114
- function doLogging() {
115
- let shouldDoLog = true
116
- //subscribe for position
117
- app.subscriptionmanager.subscribe({
118
- "context": "vessels.self",
119
- "subscribe": [
120
- {
121
- "path": "navigation.position",
122
- "format": "delta",
123
- "policy": "instant",
124
- "minPeriod": options.trackFrequency ? options.trackFrequency * 1000 : 0,
125
- }
126
- ]
127
- },
128
- unsubscribes,
129
- subscriptionError => {
130
- app.debug('Error subscription to data:' + subscriptionError);
131
- app.setPluginError('Error subscription to data:' + subscriptionError.message);
132
- },
133
- doOnValue
134
- );
135
-
136
- //subscribe for speed
137
- if (options.minSpeed) {
138
- app.subscriptionmanager.subscribe({
139
- "context": "vessels.self",
140
- "subscribe": [
141
- {
142
- "path": "navigation.speedOverGround",
143
- "format": "delta",
144
- "policy": "instant",
145
- }
146
- ]
147
- },
148
- unsubscribes,
149
- subscriptionError => {
150
- app.debug('Error subscription to data:' + subscriptionError);
151
- app.setPluginError('Error subscription to data:' + subscriptionError.message);
152
- },
153
- delta => {
154
- // app.debug('got speed delta', delta);
155
- delta.updates.forEach(update => {
156
- // app.debug(`update:`, update);
157
- if (options.filterSource && update.$source !== options.filterSource) {
158
- return;
159
- }
160
- update.values.forEach(value => {
161
- // value.value is sog in m/s so 'sog*2' is in knots
162
- if (!shouldDoLog && options.minSpeed < value.value * 2) {
163
- app.debug('setting shouldDoLog to true');
164
- shouldDoLog = true;
165
- }
166
- })
167
- })
168
- }
169
- );
170
- }
171
-
172
- async function doOnValue(delta) {
173
-
174
- for (update of delta.updates) {
175
- // app.debug(`update:`, update);
176
- if (options.filterSource && update.$source !== options.filterSource) {
258
+ this.unsubscribesControl.forEach(f => f());
259
+ this.unsubscribesControl = [];
260
+ this.unsubscribes.forEach(f => f());
261
+ this.unsubscribes = [];
262
+ this.app.setPluginStatus('Plugin stopped');
263
+ }
264
+
265
+ doLogging() {
266
+ // subscribe for position
267
+ let shouldDoLog = true;
268
+
269
+ this.app.subscriptionmanager.subscribe({
270
+ context: 'vessels.self',
271
+ subscribe: [{
272
+ path: 'navigation.position',
273
+ format: 'delta',
274
+ policy: 'instant',
275
+ minPeriod: this.options.trackFrequency ? this.options.trackFrequency * 1000 : 0
276
+ }]
277
+ }, this.unsubscribes, (subscriptionError) => {
278
+ this.app.debug('Error subscription to data:' + subscriptionError);
279
+ this.app.setPluginError('Error subscription to data:' + subscriptionError.message);
280
+ }, this.doOnValue.bind(this, () => shouldDoLog, newShould => { shouldDoLog = newShould; }));
281
+
282
+ // subscribe for speed
283
+ if (this.options.minSpeed) {
284
+ this.app.subscriptionmanager.subscribe({
285
+ context: 'vessels.self',
286
+ subscribe: [{
287
+ path: 'navigation.speedOverGround',
288
+ format: 'delta',
289
+ policy: 'instant'
290
+ }]
291
+ }, this.unsubscribes, (subscriptionError) => {
292
+ this.app.debug('Error subscription to data:' + subscriptionError);
293
+ this.app.setPluginError('Error subscription to data:' + subscriptionError.message);
294
+ }, (delta) => {
295
+ delta.updates.forEach(update => {
296
+ if (this.options.filterSource && update.$source !== this.options.filterSource) {
177
297
  return;
178
298
  }
179
- let timestamp = update.timestamp;
180
- for (value of update.values) {
181
-
182
-
183
- if (
184
- Math.abs(value.value.latitude) <= 0.01 &&
185
- Math.abs(value.value.longitude) <= 0.01
186
- ) {
187
- // Coordinates are within ±0.1 of (0,0)
188
- return;
189
- }
190
-
191
- // app.debug(`value:`, value);
192
-
193
- if (!shouldDoLog) {
194
- return;
299
+ update.values.forEach(value => {
300
+ const speedInKnots = value.value * 1.94384;
301
+ if (!shouldDoLog && this.options.minSpeed < speedInKnots) {
302
+ this.app.debug('setting shouldDoLog to true, speed:', speedInKnots.toFixed(2), 'knots');
303
+ shouldDoLog = true;
195
304
  }
196
- if (!isValidLatitude(value.value.latitude) || !isValidLongitude(value.value.longitude)) {
197
- return;
198
- }
199
-
200
-
201
- if (lastPosition) {
202
- if (new Date(lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
203
- app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', lastPosition.timestamp);
204
- // SK sometimes messes up timestamps, when that happens we throw the update
205
- return;
206
- }
207
- const distance = equirectangularDistance(lastPosition.pos, value.value)
208
- if (options.minMove && distance < options.minMove) {
209
- return;
210
- }
211
- // if (calculatedSpeed(distance, (timestamp - lastPosition.timestamp) / 1000) > maxAllowedSpeed) {
212
- // app.debug('got error position', value.value, 'ignoring...');
213
- // return;
214
- // }
215
- }
216
- lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
217
-
218
- await savePoint(lastPosition);
219
- if (options.minSpeed) {
220
- app.debug('setting shouldDoLog to false');
221
- shouldDoLog = false;
222
- }
223
- };
224
- };
225
- }
226
- }
227
-
228
- async function savePoint(point) {
229
- //{pos: {latitude, longitude}, timestamp}
230
- // Date.parse(timestamp)
231
- const obj = {
232
- lat: point.pos.latitude,
233
- lon: point.pos.longitude,
234
- t: point.timestamp,
235
- }
236
- app.debug(`save data point:`, obj);
237
- await fs.appendFile(path.join(options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
238
- }
239
-
240
- function isValidLatitude(obj) {
241
- return isDefinedNumber(obj) && obj > -90 && obj < 90
242
- }
243
-
244
- function isValidLongitude(obj) {
245
- return isDefinedNumber(obj) && obj > -180 && obj < 180
246
- }
247
-
248
- function isDefinedNumber(obj) {
249
- return (obj !== undefined && obj !== null && typeof obj === 'number');
305
+ });
306
+ });
307
+ });
250
308
  }
309
+ }
251
310
 
252
- // function calculatedSpeed(distance, timeSecs) {
253
- // // m/s to knots ~= speedinms * 1.944
254
- // return (distance / timeSecs) * msToKn
255
- // }
256
-
257
- function equirectangularDistance(from, to) {
258
- // https://www.movable-type.co.uk/scripts/latlong.html
259
- // from,to: {longitude: xx, latitude: xx}
260
- const rad = Math.PI / 180;
261
- const φ1 = from.latitude * rad;
262
- const φ2 = to.latitude * rad;
263
- const Δλ = (to.longitude - from.longitude) * rad;
264
- const R = 6371e3;
265
- const x = Δλ * Math.cos((φ1 + φ2) / 2);
266
- const y = (φ2 - φ1);
267
- const d = Math.sqrt(x * x + y * y) * R;
268
- return d;
311
+ async doOnValue(getShouldDoLog, setShouldDoLog, delta) {
312
+ for (const update of delta.updates) {
313
+ if (this.options.filterSource && update.$source !== this.options.filterSource) {
314
+ return;
269
315
  }
316
+ const timestamp = update.timestamp;
317
+ for (const value of update.values) {
318
+ // Validierung: GPS nahe (0,0)
319
+ if (Math.abs(value.value.latitude) <= 0.01 && Math.abs(value.value.longitude) <= 0.01) {
320
+ this.app.debug('GPS coordinates near (0,0), ignoring point to avoid invalid data logging.');
321
+ return;
322
+ }
323
+
324
+ // Validate for valid lat/lon
325
+ if (!this.isValidLatitude(value.value.latitude) || !this.isValidLongitude(value.value.longitude)) {
326
+ this.app.debug('got invalid position, ignoring...', value.value);
327
+ return;
328
+ }
270
329
 
271
- function createDir(dir) {
272
- let res = true;
273
- if (fs.existsSync(dir)) {
274
- try {
275
- fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
276
- }
277
- catch (error) {
278
- app.debug('[createDir]', error.message);
279
- app.setPluginError(`No rights to directory ${dir}`);
280
- res = false;
330
+ // 24h-Ping Check: Setze Flag, aber breche NICHT ab
331
+ let force24hSave = false;
332
+ if (this.options.ping_api_every_24h && this.lastPosition) {
333
+ const timeSinceLastPoint = (new Date().getTime() - this.lastPosition.currentTime);
334
+ if (timeSinceLastPoint >= 24 * 60 * 60 * 1000) {
335
+ this.app.debug('24h since last point, forcing save of point to keep boat active on NFL');
336
+ force24hSave = true;
281
337
  }
282
338
  }
283
- else {
284
- try {
285
- fs.mkdirSync(dir, { recursive: true });
339
+
340
+ // Wenn wir nicht loggen sollen UND es kein 24h-Force ist, dann raus
341
+ if (!force24hSave && !getShouldDoLog()) {
342
+ return;
343
+ }
344
+
345
+ // Wenn wir eine letzte Position haben, prüfe Timestamp und Distanz
346
+ if (this.lastPosition && !force24hSave) {
347
+ // Timestamp-Validierung
348
+ if (new Date(this.lastPosition.timestamp).getTime() > new Date(timestamp).getTime()) {
349
+ this.app.debug('got error in timestamp:', timestamp, 'is earlier than previous:', this.lastPosition.timestamp);
350
+ return;
286
351
  }
287
- catch (error) {
288
- switch (error.code) {
289
- case 'EACCES': // Permission denied
290
- case 'EPERM': // Operation not permitted
291
- app.debug(`False to create ${dir} by Permission denied`);
292
- app.setPluginError(`False to create ${dir} by Permission denied`);
293
- res = false;
294
- break;
295
- case 'ETIMEDOUT': // Operation timed out
296
- app.debug(`False to create ${dir} by Operation timed out`);
297
- app.setPluginError(`False to create ${dir} by Operation timed out`);
298
- res = false;
299
- break;
300
- }
352
+
353
+ // Distance-Check (nur wenn NICHT 24h-Force)
354
+ const distance = this.equirectangularDistance(this.lastPosition.pos, value.value);
355
+ if (this.options.minMove && distance < this.options.minMove) {
356
+ this.app.debug('Distance', distance.toFixed(2), 'm is less than minMove', this.options.minMove, 'm - skipping');
357
+ return;
301
358
  }
302
359
  }
303
- return res;
304
- } // end function createDir
305
360
 
306
- async function interval() {
307
- if ((checkBoatMoving()) && await checkTrack() && await testInternet()) {
308
- await sendData();
361
+ // Punkt speichern
362
+ this.lastPosition = { pos: value.value, timestamp, currentTime: new Date().getTime() };
363
+ await this.savePoint(this.lastPosition);
364
+
365
+ // shouldDoLog zurücksetzen wenn minSpeed aktiv ist
366
+ if (this.options.minSpeed) {
367
+ this.app.debug('options.minSpeed - setting shouldDoLog to false');
368
+ setShouldDoLog(false);
309
369
  }
310
370
  }
371
+ }}
311
372
 
312
- function checkBoatMoving() {
313
- if (options.sendWhileMoving || !options.trackFrequency) {
314
- return true;
315
- }
316
- const time = lastPosition ? lastPosition.currentTime : upSince;
317
-
318
- const secsSinceLastPoint = (new Date().getTime() - time)/1000
319
- if (secsSinceLastPoint > (options.trackFrequency * 2)) {
320
- app.debug('Boat stopped moving, last move at least', secsSinceLastPoint,'seconds ago');
321
- return true;
322
- } else {
323
- app.debug('Boat is still moving, last move', secsSinceLastPoint,'seconds ago');
324
- return false;
373
+
374
+ async savePoint(point) {
375
+ const obj = {
376
+ lat: point.pos.latitude,
377
+ lon: point.pos.longitude,
378
+ t: point.timestamp
379
+ };
380
+ this.app.debug(`save data point:`, obj);
381
+ await fs.appendFile(path.join(this.options.trackDir, routeSaveName), JSON.stringify(obj) + EOL);
382
+
383
+ const lastSaveTime = new Date().toISOString();
384
+ const lastTransferTime = this.lastSuccessfulTransfer ? this.lastSuccessfulTransfer.toISOString() : 'Not transfered since plugin start';
385
+ this.app.setPluginStatus(`Last save: ${lastSaveTime} | Last transfer: ${lastTransferTime}`);
386
+ }
387
+
388
+ isValidLatitude(obj) {
389
+ return this.isDefinedNumber(obj) && obj > -90 && obj < 90;
390
+ }
391
+ isValidLongitude(obj) {
392
+ return this.isDefinedNumber(obj) && obj > -180 && obj < 180;
393
+ }
394
+ isDefinedNumber(obj) {
395
+ return (obj !== undefined && obj !== null && typeof obj === 'number');
396
+ }
397
+
398
+ equirectangularDistance(from, to) {
399
+ const rad = Math.PI / 180;
400
+ const φ1 = from.latitude * rad;
401
+ const φ2 = to.latitude * rad;
402
+ const Δλ = (to.longitude - from.longitude) * rad;
403
+ const R = 6371e3;
404
+ const x = Δλ * Math.cos((φ1 + φ2) / 2);
405
+ const y = (φ2 - φ1);
406
+ const d = Math.sqrt(x * x + y * y) * R;
407
+ return d;
408
+ }
409
+
410
+ createDir(dir) {
411
+ let res = true;
412
+ if (fs.existsSync(dir)) {
413
+ try {
414
+ fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
415
+ } catch (error) {
416
+ this.app.debug('[createDir]', error.message);
417
+ this.app.setPluginError(`No rights to directory ${dir}`);
418
+ res = false;
419
+ }
420
+ } else {
421
+ try {
422
+ fs.mkdirSync(dir, { recursive: true });
423
+ } catch (error) {
424
+ switch (error.code) {
425
+ case 'EACCES':
426
+ case 'EPERM':
427
+ this.app.debug(`False to create ${dir} by Permission denied`);
428
+ this.app.setPluginError(`False to create ${dir} by Permission denied`);
429
+ res = false;
430
+ break;
431
+ case 'ETIMEDOUT':
432
+ this.app.debug(`False to create ${dir} by Operation timed out`);
433
+ this.app.setPluginError(`False to create ${dir} by Operation timed out`);
434
+ res = false;
435
+ break;
436
+ default:
437
+ this.app.debug(`Error creating directory ${dir}: ${error.message}`);
438
+ this.app.setPluginError(`Error creating directory ${dir}: ${error.message}`);
439
+ res = false;
440
+ }
325
441
  }
326
442
  }
443
+ return res;
444
+ }
327
445
 
328
- async function testInternet() {
329
- app.debug('testing internet connection');
330
- const check = await isReachable(internetTestAddress, { timeout: options.internetTestTimeout || internetTestTimeout });
331
- app.debug('internet connection = ', check);
332
- return check;
446
+ // periodic interval called by cron
447
+ async interval() {
448
+ if ((this.checkBoatMoving()) && await this.checkTrack() && await this.testInternet()) {
449
+ await this.sendData();
333
450
  }
334
-
335
- async function checkTrack() {
336
- const trackFile = path.join(options.trackDir, routeSaveName);
337
- app.debug('checking the track', trackFile, 'if should send');
338
- const exists = await fs.pathExists(trackFile);
339
- const size = exists ? (await fs.lstat(trackFile)).size : 0;
340
- app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
341
- return size > 0;
451
+ }
452
+
453
+ checkBoatMoving() {
454
+ if (!this.options.trackFrequency) {
455
+ return true; // Kein Tracking → immer senden
456
+ }
457
+ const time = this.lastPosition ? this.lastPosition.currentTime : this.upSince;
458
+ const secsSinceLastPoint = (new Date().getTime() - time) / 1000;
459
+ const isMoving = secsSinceLastPoint <= (this.options.trackFrequency * 2);
460
+ if (isMoving) {
461
+ this.app.debug('Boat is still moving, last move', secsSinceLastPoint, 'seconds ago');
462
+ return this.options.sendWhileMoving; // Nur senden wenn gewünscht
463
+ } else {
464
+ this.app.debug('Boat stopped moving, last move at least', secsSinceLastPoint, 'seconds ago');
465
+ return true; // Immer senden wenn gestoppt
466
+ }
467
+ }
468
+
469
+ async testInternet() {
470
+ const dns = require('dns').promises;
471
+
472
+ this.app.debug('testing internet connection');
473
+
474
+ const timeoutMs = this.options.internetTestTimeout || 2000;
475
+
476
+ // Prüfe mehrere öffentliche DNS-Server
477
+ const dnsServers = [
478
+ { name: 'Google DNS', ip: '8.8.8.8' },
479
+ { name: 'Cloudflare DNS', ip: '1.1.1.1' }
480
+ ];
481
+
482
+ for (const server of dnsServers) {
483
+ try {
484
+ // Versuche, den DNS-Server direkt zu erreichen
485
+ // Wir machen einen reverse lookup auf die IP selbst
486
+ const result = await Promise.race([
487
+ dns.reverse(server.ip),
488
+ new Promise((_, reject) =>
489
+ setTimeout(() => reject(new Error('DNS timeout')), timeoutMs)
490
+ )
491
+ ]);
492
+
493
+ this.app.debug(`internet connection = true, ${server.name} (${server.ip}) is reachable`);
494
+ return true;
495
+ } catch (err) {
496
+ this.app.debug(`${server.name} (${server.ip}) not reachable:`, err.message);
497
+ // Weiter zum nächsten Server
342
498
  }
343
-
344
- async function sendData() {
345
- if (options.boatApiKey) {
346
- sendApiData();
347
- } else {
348
- sendEmailData();
349
- }
499
+ }
500
+
501
+ this.app.debug('internet connection = false, no public DNS servers reachable');
502
+ return false;
503
+ }
504
+
505
+ async checkTrack() {
506
+ const trackFile = path.join(this.options.trackDir, routeSaveName);
507
+ this.app.debug('checking the track', trackFile, 'if should send');
508
+ const exists = await fs.pathExists(trackFile);
509
+ const size = exists ? (await fs.lstat(trackFile)).size : 0;
510
+ this.app.debug(`'${trackFile}'.size=${size} ${trackFile}'.exists=${exists}`);
511
+ return size > 0;
512
+ }
513
+
514
+ async sendData() {
515
+ if (this.options.boatApiKey) {
516
+ await this.sendApiData();
517
+ } else {
518
+ this.app.debug('Failed to send track - no boat API key set in plugin settings.');
519
+ this.app.setPluginError(`Failed to send track - no boat API key set in plugin settings.`);
350
520
  }
351
-
352
- async function sendApiData() {
353
- app.debug('sending the data');
354
- const trackData = await createTrack(path.join(options.trackDir, routeSaveName));
355
- if (!trackData) {
356
- app.debug('Recorded track did not contain any valid track points, aborting sending.');
357
- return;
358
- }
359
- app.debug('created track data with timestamp:', new Date(trackData.timestamp));
360
-
361
- const params = new URLSearchParams();
362
- params.append('timestamp', trackData.timestamp);
363
- params.append('track', JSON.stringify(trackData.track));
364
- params.append('boatApiKey', options.boatApiKey);
365
-
366
- const headers = {
367
- 'X-NFL-API-Key': pluginApiKey
368
- }
369
-
370
- app.debug('sending track to API');
521
+ }
522
+
523
+ async sendApiData() {
524
+ this.app.debug('sending the data');
525
+ const trackData = await this.createTrack(path.join(this.options.trackDir, routeSaveName));
526
+ if (!trackData) {
527
+ this.app.debug('Recorded track did not contain any valid track points, aborting sending.');
528
+ this.app.setPluginError(`Failed to send track - Recorded track did not contain any valid track points, aborting sending.`);
529
+ return;
530
+ }
531
+ this.app.debug('created track data with timestamp:', new Date(trackData.timestamp));
532
+ const params = new URLSearchParams();
533
+ params.append('timestamp', trackData.timestamp);
534
+ params.append('track', JSON.stringify(trackData.track));
535
+ params.append('boatApiKey', this.options.boatApiKey);
536
+ const headers = { 'X-NFL-API-Key': pluginApiKey };
537
+ this.app.debug('sending track to API');
538
+
539
+ // Retry-Logik mit exponentiell steigendem Timeout
540
+ const maxRetries = 3;
541
+ const baseTimeout = (this.options.apiTimeout || 30) * 1000; // Konfigurierbarer Basis-Timeout in ms
542
+
543
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
371
544
  try {
372
- const response = await fetch(apiUrl, { method: 'POST', body: params, headers: new fetch.Headers(headers) });
545
+ const currentTimeout = baseTimeout * attempt; // 30s, 60s, 90s
546
+ this.app.debug(`Attempt ${attempt}/${maxRetries} with ${currentTimeout}ms timeout`);
547
+
548
+ // AbortController für Timeout
549
+ const controller = new AbortController();
550
+ const timeoutId = setTimeout(() => controller.abort(), currentTimeout);
551
+
552
+ const response = await fetch(apiUrl, {
553
+ method: 'POST',
554
+ body: params,
555
+ headers: new fetch.Headers(headers),
556
+ signal: controller.signal
557
+ });
558
+
559
+ clearTimeout(timeoutId);
560
+
373
561
  if (response.ok) {
374
562
  const responseBody = await response.json();
375
563
  if (responseBody.status === 'ok') {
376
- app.debug('Track successfully sent to API');
377
- if (options.keepFiles) {
564
+ this.lastSuccessfulTransfer = new Date();
565
+ this.app.debug('Track successfully sent to API');
566
+ this.app.setPluginStatus(`Started - last Track sent successfully at ${new Date().toISOString()}`);
567
+ if (this.options.keepFiles) {
378
568
  const filename = new Date().toJSON().slice(0, 19).replace(/:/g, '') + '-nfl-track.jsonl';
379
- app.debug('moving and keeping track file: ', filename);
380
- await fs.move(path.join(options.trackDir, routeSaveName), path.join(options.trackDir, filename));
569
+ this.app.debug('moving and keeping track file: ', filename);
570
+ await fs.move(path.join(this.options.trackDir, routeSaveName), path.join(this.options.trackDir, filename));
381
571
  } else {
382
- app.debug('Deleting track file');
383
- await fs.remove(path.join(options.trackDir, routeSaveName));
572
+ this.app.debug('Deleting track file');
573
+ await fs.remove(path.join(this.options.trackDir, routeSaveName));
384
574
  }
575
+ return; // Erfolg - beende Funktion
385
576
  } else {
386
- app.debug('Could not send track to API, returned response json:', responseBody);
577
+ this.app.debug('Could not send track to API, returned response json:', responseBody);
578
+ // Bei API-Fehler nicht erneut versuchen
579
+ this.app.setPluginError(`Failed to send track - API returned error.`);
580
+ return;
387
581
  }
388
582
  } else {
389
- app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
583
+ this.app.debug('Could not send track to API, returned response code:', response.status, response.statusText);
584
+ // Bei 4xx Fehler nicht erneut versuchen
585
+ if (response.status >= 400 && response.status < 500) {
586
+ this.app.setPluginError(`Failed to send track - HTTP ${response.status}.`);
587
+ return;
588
+ }
589
+ // Bei 5xx Fehler retry
590
+ throw new Error(`HTTP ${response.status}`);
390
591
  }
391
592
  } catch (err) {
392
- app.debug('Could not send track to API due to error:', err);
393
- }
394
- }
395
-
396
- async function createTrack(inputPath) {
397
- const fileStream = fs.createReadStream(inputPath);
398
-
399
- const rl = readline.createInterface({
400
- input: fileStream,
401
- crlfDelay: Infinity
402
- });
403
- const track = []
404
- let lastTimestamp;
405
- for await (const line of rl) {
406
- if (line) {
407
- try {
408
- const point = JSON.parse(line);
409
-
410
- const timestamp = new Date(point.t).getTime();
411
- if (!isNaN(timestamp) && isValidLatitude(point.lat) && isValidLongitude(point.lon)) {
412
- track.push([timestamp, point.lat, point.lon]);
413
- lastTimestamp = timestamp;
414
- }
415
-
416
- } catch (error) {
417
- app.debug('could not parse line from track file:', line);
418
- }
593
+ this.app.debug(`Attempt ${attempt} failed:`, err.message);
594
+
595
+ // Bei letztem Versuch Fehler setzen
596
+ if (attempt === maxRetries) {
597
+ this.app.debug('Could not send track to API after', maxRetries, 'attempts:', err);
598
+ this.app.setPluginError(`Failed to send track after ${maxRetries} attempts - check logs for details.`);
599
+ } else {
600
+ // Kurze Pause vor nächstem Versuch
601
+ const waitTime = 2000 * attempt; // 2s, 4s
602
+ this.app.debug(`Waiting ${waitTime}ms before retry...`);
603
+ await new Promise(resolve => setTimeout(resolve, waitTime));
419
604
  }
420
605
  }
421
- if (track.length > 0) {
422
- return { timestamp: new Date(lastTimestamp).getTime(), track };
423
- }
424
606
  }
425
-
426
- async function sendEmailData() {
427
- app.debug('sending the data');
428
- const gpxFiles = await createGPX({ input: path.join(options.trackDir, routeSaveName), outputDir: options.trackDir, creator });
429
- app.debug('created GPX files', gpxFiles);
430
- try {
431
- for (let file of gpxFiles) {
432
- app.debug('sending', file);
433
- try {
434
- !await sendEmail({
435
- emailService: options.emailService,
436
- user: options.emailUser,
437
- password: options.emailPassword,
438
- from: options.emailFrom,
439
- to: options.emailTo,
440
- trackFile: file
441
- })
442
- } catch (err) {
443
- app.debug('Sending email failed:', err);
444
- return;
607
+ }
608
+ async createTrack(inputPath) {
609
+ const fileStream = fs.createReadStream(inputPath);
610
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
611
+ const track = [];
612
+ let lastTimestamp;
613
+ for await (const line of rl) {
614
+ if (line) {
615
+ try {
616
+ const point = JSON.parse(line);
617
+ const timestamp = new Date(point.t).getTime();
618
+ if (!isNaN(timestamp) && this.isValidLatitude(point.lat) && this.isValidLongitude(point.lon)) {
619
+ track.push([timestamp, point.lat, point.lon]);
620
+ lastTimestamp = timestamp;
445
621
  }
446
- }
447
- } finally {
448
- for (let file of gpxFiles) {
449
- app.debug('deleting', file);
450
- await fs.rm(file);
622
+ } catch (error) {
623
+ this.app.debug('could not parse line from track file:', line);
624
+ this.app.setPluginError(`Failed could not parse line from track file - check logs for details.`);
451
625
  }
452
626
  }
453
- await fs.rm(path.join(options.trackDir, routeSaveName));
454
627
  }
455
- //every 10 minute but staggered to the second so we don't all send at once.
456
- if (!options.emailCron || options.emailCron === '*/10 * * * *') {
457
- const startMinute = Math.floor(Math.random() * 10); // Random minute within the 10-minute range
458
- const startSecond = Math.floor(Math.random() * 60); // Random second within the minute
459
- options.emailCron = `${startSecond} ${startMinute}/10 * * * *`; // Every 10 minutes, starting at a random minute and second within each 10-minute block
460
- }
461
-
462
- upSince = new Date().getTime();
463
-
464
- app.debug('Setting CRON to ', options.emailCron);
465
- cron = new CronJob(
466
- options.emailCron,
467
- interval
468
- );
469
- cron.start();
470
- }; // end plugin.start
471
-
472
- plugin.stop = function () {
473
- app.debug('plugin stopped');
474
- if (cron) {
475
- cron.stop();
476
- cron = undefined;
628
+ if (track.length > 0) {
629
+ return { timestamp: new Date(lastTimestamp).getTime(), track };
477
630
  }
478
- unsubscribesControl.forEach(f => f());
479
- unsubscribesControl = [];
480
- unsubscribes.forEach(f => f());
481
- unsubscribes = [];
482
- app.setPluginStatus('Plugin stopped');
483
- }; // end plugin.stop
484
-
631
+ return null;
632
+ }
633
+ }
485
634
 
486
- return plugin;
635
+ module.exports = function (app) {
636
+ const instance = new SignalkToNoforeignland(app);
637
+ return instance.getPluginObject();
487
638
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "signalk-to-noforeignland",
3
- "version": "0.1.22",
3
+ "version": "0.1.25",
4
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
- "author": "Ian Miller",
10
+ "author": "Dirk Wahrheit",
11
11
  "homepage": "https://github.com/noforeignland/nfl-signalk",
12
12
  "bugs": {
13
13
  "url": "https://github.com/noforeignland/nfl-signalk/issues"
@@ -23,7 +23,6 @@
23
23
  "cron": "^2.1.0",
24
24
  "fs-extra": "^10.1.0",
25
25
  "is-reachable": "^5.2.1",
26
- "node-fetch": "^2.6.7",
27
- "nodemailer": "^6.8.0"
26
+ "node-fetch": "^2.6.7"
28
27
  }
29
28
  }
package/createGPX.js DELETED
@@ -1,48 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const readline = require('readline');
4
-
5
- async function createGPX(options) {
6
- await writeHeader(options);
7
- const fileStream = fs.createReadStream(options.input);
8
-
9
- const rl = readline.createInterface({
10
- input: fileStream,
11
- crlfDelay: Infinity
12
- });
13
-
14
- for await (const line of rl) {
15
- if (line){
16
- const point = JSON.parse(line);
17
- let trkpt = ' <trkpt ';
18
- trkpt += `lat="${point.lat}" lon="${point.lon}">\n`;
19
- trkpt += ` <time>${point.t}</time>\n`;
20
- trkpt += ' </trkpt>\n';
21
- fs.appendFileSync(path.join(options.outputDir, 'track.gpx'), trkpt);
22
- }
23
- }
24
- await writeFooter(options);
25
- return [path.join(options.outputDir, 'track.gpx')];
26
- }
27
-
28
- async function writeHeader(options) {
29
- const header = `<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
30
- <gpx version="1.1" creator="${options.creator}"
31
- xmlns="http://www.topografix.com/GPX/1/1"
32
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
- xmlns:gpxx="http://www8.garmin.com/xmlschemas/GpxExtensions/v3"
34
- xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd https://www8.garmin.com/xmlschemas/GpxExtensions/v3 https://www8.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd"
35
- >
36
- <metadata/>
37
- <trk>
38
- <trkseg>
39
- `;
40
- fs.writeFileSync(path.join(options.outputDir, 'track.gpx'), header);
41
- }
42
-
43
- async function writeFooter(options) {
44
- const footer = ' </trkseg>\n </trk>\n</gpx>';
45
- fs.appendFileSync(path.join(options.outputDir, 'track.gpx'), footer);
46
- }
47
-
48
- module.exports = createGPX;
package/sendEmail.js DELETED
@@ -1,24 +0,0 @@
1
- const nodemailer = require('nodemailer');
2
-
3
- async function sendEmail(options) {
4
- const mail = nodemailer.createTransport({
5
- service: options.emailService,
6
- auth: {
7
- user: options.user,
8
- pass: options.password,
9
- }
10
- });
11
- var mailOptions = {
12
- from: options.from,
13
- to: options.to,
14
- subject: `GPX ${(new Date()).toISOString()}`,
15
- text: 'Generated by SignalK Tracker',
16
- attachments: [{ // file on disk as an attachment
17
- filename: 'track.gpx',
18
- path: options.trackFile // stream this file
19
- }],
20
- };
21
- await mail.sendMail(mailOptions);
22
- }
23
-
24
- module.exports = sendEmail;