signalk-vessels-to-ais 1.6.0 → 2.0.0-beta.1
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/dependabot.yml +0 -0
- package/.mocharc.json +5 -0
- package/README.md +34 -32
- package/index.js +244 -463
- package/lib/helpers.js +357 -0
- package/package.json +27 -26
- package/test/helpers.test.js +609 -0
- package/test/plugin.test.js +479 -0
package/index.js
CHANGED
|
@@ -1,463 +1,244 @@
|
|
|
1
|
-
/* eslint-disable no-bitwise */
|
|
2
|
-
/*
|
|
3
|
-
MIT License
|
|
4
|
-
|
|
5
|
-
Copyright (c) 2020 Karl-Erik Gustafsson
|
|
6
|
-
|
|
7
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
-
in the Software without restriction, including without limitation the rights
|
|
10
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
-
furnished to do so, subject to the following conditions:
|
|
13
|
-
|
|
14
|
-
The above copyright notice and this permission notice shall be included in all
|
|
15
|
-
copies or substantial portions of the Software.
|
|
16
|
-
|
|
17
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
-
SOFTWARE.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
lat = jsonContent[jsonKey].navigation.position.value.latitude;
|
|
246
|
-
} catch (error) { lat = null; }
|
|
247
|
-
try {
|
|
248
|
-
lon = jsonContent[jsonKey].navigation.position.value.longitude;
|
|
249
|
-
} catch (error) { lon = null; }
|
|
250
|
-
try {
|
|
251
|
-
sog = msToKnots(jsonContent[jsonKey].navigation.speedOverGround.value);
|
|
252
|
-
} catch (error) { sog = null; }
|
|
253
|
-
try {
|
|
254
|
-
cog = radToDegrees(jsonContent[jsonKey].navigation.courseOverGroundTrue.value);
|
|
255
|
-
} catch (error) { cog = null; }
|
|
256
|
-
try {
|
|
257
|
-
rot = radToDegrees(jsonContent[jsonKey].navigation.rateOfTurn.value);
|
|
258
|
-
} catch (error) { rot = null; }
|
|
259
|
-
try {
|
|
260
|
-
navStat = stateMapping[jsonContent[jsonKey].navigation.state.value];
|
|
261
|
-
} catch (error) { navStat = ''; }
|
|
262
|
-
try {
|
|
263
|
-
hdg = radToDegrees(jsonContent[jsonKey].navigation.headingTrue.value);
|
|
264
|
-
} catch (error) { hdg = null; }
|
|
265
|
-
try {
|
|
266
|
-
dst = jsonContent[jsonKey].navigation.destination.commonName.value;
|
|
267
|
-
} catch (error) { dst = ''; }
|
|
268
|
-
try {
|
|
269
|
-
callSign = jsonContent[jsonKey].communication.callsignVhf.value || jsonContent[jsonKey].communication.callsignVhf;
|
|
270
|
-
} catch (error) { callSign = ''; }
|
|
271
|
-
try {
|
|
272
|
-
imo = (jsonContent[jsonKey].registrations.value.imo).substring(4, 20);
|
|
273
|
-
} catch (error) { imo = null; }
|
|
274
|
-
try {
|
|
275
|
-
id = jsonContent[jsonKey].design.aisShipType.value.id;
|
|
276
|
-
} catch (error) { id = null; }
|
|
277
|
-
try {
|
|
278
|
-
type = jsonContent[jsonKey].design.aisShipType.value.name;
|
|
279
|
-
} catch (error) { type = ''; }
|
|
280
|
-
try {
|
|
281
|
-
draftCur = (jsonContent[jsonKey].design.draft.value.current) / 10;
|
|
282
|
-
} catch (error) { draftCur = null; }
|
|
283
|
-
try {
|
|
284
|
-
length = jsonContent[jsonKey].design.length.value.overall;
|
|
285
|
-
} catch (error) { length = null; }
|
|
286
|
-
try {
|
|
287
|
-
beam = (jsonContent[jsonKey].design.beam.value) / 2;
|
|
288
|
-
} catch (error) { beam = null; }
|
|
289
|
-
try {
|
|
290
|
-
ais = jsonContent[jsonKey].sensors.ais.class.value;
|
|
291
|
-
} catch (error) { ais = null; }
|
|
292
|
-
|
|
293
|
-
if (shipName % 1 === 0) {
|
|
294
|
-
shipName = '';
|
|
295
|
-
}
|
|
296
|
-
if (dst % 1 === 0) {
|
|
297
|
-
dst = '';
|
|
298
|
-
}
|
|
299
|
-
if (callSign % 1 === 0) {
|
|
300
|
-
callSign = '';
|
|
301
|
-
}
|
|
302
|
-
if (type % 1 === 0) {
|
|
303
|
-
type = '';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (i === 0) {
|
|
307
|
-
own = true;
|
|
308
|
-
} else {
|
|
309
|
-
own = false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const a = { lat: ownLat, lon: ownLon };
|
|
313
|
-
const b = { lat, lon };
|
|
314
|
-
const dist = (haversine(a, b) / 1000).toFixed(2);
|
|
315
|
-
|
|
316
|
-
if (dist <= distance) {
|
|
317
|
-
encMsg3 = {
|
|
318
|
-
own,
|
|
319
|
-
aistype: 3, // class A position report
|
|
320
|
-
repeat: 0,
|
|
321
|
-
mmsi,
|
|
322
|
-
navstatus: navStat,
|
|
323
|
-
sog,
|
|
324
|
-
lon,
|
|
325
|
-
lat,
|
|
326
|
-
cog,
|
|
327
|
-
hdg,
|
|
328
|
-
rot,
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
encMsg5 = {
|
|
332
|
-
own,
|
|
333
|
-
aistype: 5, // class A static
|
|
334
|
-
repeat: 0,
|
|
335
|
-
mmsi,
|
|
336
|
-
imo,
|
|
337
|
-
cargo: id,
|
|
338
|
-
callsign: callSign,
|
|
339
|
-
shipname: shipName,
|
|
340
|
-
draught: draftCur,
|
|
341
|
-
destination: dst,
|
|
342
|
-
dimA: 0,
|
|
343
|
-
dimB: length,
|
|
344
|
-
dimC: beam,
|
|
345
|
-
dimD: beam,
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
encMsg18 = {
|
|
349
|
-
own,
|
|
350
|
-
aistype: 18, // class B position report
|
|
351
|
-
repeat: 0,
|
|
352
|
-
mmsi,
|
|
353
|
-
sog,
|
|
354
|
-
accuracy: 0,
|
|
355
|
-
lon,
|
|
356
|
-
lat,
|
|
357
|
-
cog,
|
|
358
|
-
hdg,
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
encMsg240 = {
|
|
362
|
-
own,
|
|
363
|
-
aistype: 24, // class B static
|
|
364
|
-
repeat: 0,
|
|
365
|
-
part: 0,
|
|
366
|
-
mmsi,
|
|
367
|
-
shipname: shipName,
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
encMsg241 = {
|
|
371
|
-
own,
|
|
372
|
-
aistype: 24, // class B static
|
|
373
|
-
repeat: 0,
|
|
374
|
-
part: 1,
|
|
375
|
-
mmsi,
|
|
376
|
-
cargo: id,
|
|
377
|
-
callsign: callSign,
|
|
378
|
-
dimA: 0,
|
|
379
|
-
dimB: length,
|
|
380
|
-
dimC: beam,
|
|
381
|
-
dimD: beam,
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
if (aisDelay && (ais === 'A' || ais === 'B' || ais === 'BASE')) {
|
|
385
|
-
// eslint-disable-next-line no-useless-concat
|
|
386
|
-
app.debug(`Distance range: ${distance}km, AIS target distance: ${dist}km` + `, Class ${ais} Vessel` + `, MMSI:${mmsi}`);
|
|
387
|
-
if (ais === 'A') {
|
|
388
|
-
app.debug(`class A, ${i}, time: ${aisTime}`);
|
|
389
|
-
aisOut(encMsg3, aisTime);
|
|
390
|
-
aisOut(encMsg5, aisTime);
|
|
391
|
-
}
|
|
392
|
-
if (ais === 'B') {
|
|
393
|
-
app.debug(`class ${ais}, ${i}, time: ${aisTime}`);
|
|
394
|
-
aisOut(encMsg18, aisTime);
|
|
395
|
-
aisOut(encMsg240, aisTime);
|
|
396
|
-
aisOut(encMsg241, aisTime);
|
|
397
|
-
}
|
|
398
|
-
if (ais === 'BASE') {
|
|
399
|
-
app.debug(`class ${ais}, ${i}, time: ${aisTime}`);
|
|
400
|
-
aisOut(encMsg3, aisTime);
|
|
401
|
-
}
|
|
402
|
-
app.debug('--------------------------------------------------------');
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const dateobj = new Date(Date.now());
|
|
407
|
-
const date = dateobj.toISOString();
|
|
408
|
-
setStatus(`AIS NMEA message sent: ${date}`);
|
|
409
|
-
})
|
|
410
|
-
.catch((err) => console.error(err));
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
//----------------------------------------------------------------------------
|
|
415
|
-
|
|
416
|
-
plugin.stop = function stop() {
|
|
417
|
-
clearInterval(intervalRun);
|
|
418
|
-
app.debug('Stopped');
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
plugin.schema = {
|
|
422
|
-
type: 'object',
|
|
423
|
-
properties: {
|
|
424
|
-
position_update: {
|
|
425
|
-
type: 'number',
|
|
426
|
-
default: 1,
|
|
427
|
-
title: 'How often AIS data is sent to NMEA0183 out (in minutes). E.g. 0.5 = 30s, 1 = 1min',
|
|
428
|
-
},
|
|
429
|
-
port: {
|
|
430
|
-
type: 'number',
|
|
431
|
-
title: 'HTTP port',
|
|
432
|
-
default: 3000,
|
|
433
|
-
},
|
|
434
|
-
portSec: {
|
|
435
|
-
type: 'number',
|
|
436
|
-
title: 'HTTPS port',
|
|
437
|
-
default: 3443,
|
|
438
|
-
},
|
|
439
|
-
sendOwn: {
|
|
440
|
-
type: 'boolean',
|
|
441
|
-
title: 'Send own AIS data, VDO',
|
|
442
|
-
default: true,
|
|
443
|
-
},
|
|
444
|
-
useTag: {
|
|
445
|
-
type: 'boolean',
|
|
446
|
-
title: 'Add Tag-block',
|
|
447
|
-
default: false,
|
|
448
|
-
},
|
|
449
|
-
distance: {
|
|
450
|
-
type: 'integer',
|
|
451
|
-
default: 100,
|
|
452
|
-
title: 'AIS target within range [km]',
|
|
453
|
-
},
|
|
454
|
-
eventName: {
|
|
455
|
-
type: 'string',
|
|
456
|
-
default: 'nmea0183out',
|
|
457
|
-
title: 'Output event name',
|
|
458
|
-
},
|
|
459
|
-
},
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
return plugin;
|
|
463
|
-
};
|
|
1
|
+
/* eslint-disable no-bitwise */
|
|
2
|
+
/*
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2020 Karl-Erik Gustafsson
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Modified version using app.getPath() instead of REST API
|
|
28
|
+
* - No HTTP/HTTPS configuration needed
|
|
29
|
+
* - No node-fetch dependency
|
|
30
|
+
* - Direct access to SignalK data model
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const AisEncode = require('ggencoder').AisEncode;
|
|
34
|
+
const haversine = require('haversine-distance');
|
|
35
|
+
const {
|
|
36
|
+
getValue,
|
|
37
|
+
getTimestamp,
|
|
38
|
+
createTagBlock,
|
|
39
|
+
extractVesselData,
|
|
40
|
+
buildAisMessage3,
|
|
41
|
+
buildAisMessage5,
|
|
42
|
+
buildAisMessage18,
|
|
43
|
+
buildAisMessage24A,
|
|
44
|
+
buildAisMessage24B,
|
|
45
|
+
isDataFresh,
|
|
46
|
+
} = require('./lib/helpers');
|
|
47
|
+
|
|
48
|
+
module.exports = function createPlugin(app) {
|
|
49
|
+
const plugin = {};
|
|
50
|
+
plugin.id = 'signalk-vessels-to-ais-ws';
|
|
51
|
+
plugin.name = 'Other vessels data to AIS NMEA0183 (WebSocket)';
|
|
52
|
+
plugin.description = 'SignalK server plugin to convert other vessel data to NMEA0183 AIS format using direct data access (no REST API)';
|
|
53
|
+
|
|
54
|
+
let intervalId = null;
|
|
55
|
+
let positionUpdate = 60;
|
|
56
|
+
let distance = 100;
|
|
57
|
+
let sendOwn = true;
|
|
58
|
+
let useTag = false;
|
|
59
|
+
let eventName = 'nmea0183out';
|
|
60
|
+
|
|
61
|
+
const setStatus = app.setPluginStatus || app.setProviderStatus;
|
|
62
|
+
|
|
63
|
+
// NMEA output
|
|
64
|
+
function aisOut(encMsg) {
|
|
65
|
+
const enc = new AisEncode(encMsg);
|
|
66
|
+
const sentence = enc.nmea;
|
|
67
|
+
let tagString = '';
|
|
68
|
+
if (useTag) {
|
|
69
|
+
tagString = createTagBlock();
|
|
70
|
+
}
|
|
71
|
+
if (sentence && sentence.length > 0) {
|
|
72
|
+
app.debug(tagString + sentence);
|
|
73
|
+
app.emit(eventName, tagString + sentence);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Main data processing function
|
|
79
|
+
* Uses app.getPath() instead of REST API
|
|
80
|
+
*/
|
|
81
|
+
function processVessels() {
|
|
82
|
+
// Get own position for distance calculation
|
|
83
|
+
const ownPosition = app.getSelfPath('navigation.position.value');
|
|
84
|
+
if (!ownPosition || ownPosition.latitude === undefined || ownPosition.longitude === undefined) {
|
|
85
|
+
app.debug('Own position not available, skipping');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ownLat = ownPosition.latitude;
|
|
90
|
+
const ownLon = ownPosition.longitude;
|
|
91
|
+
|
|
92
|
+
// Get all vessels using direct data access - NO REST API!
|
|
93
|
+
const vessels = app.getPath('vessels');
|
|
94
|
+
if (!vessels) {
|
|
95
|
+
app.debug('No vessels data available');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const vesselIds = Object.keys(vessels);
|
|
100
|
+
let processedCount = 0;
|
|
101
|
+
|
|
102
|
+
// Get own vessel identifier for comparison
|
|
103
|
+
const selfId = app.selfId || 'self';
|
|
104
|
+
|
|
105
|
+
for (const vesselId of vesselIds) {
|
|
106
|
+
const vessel = vessels[vesselId];
|
|
107
|
+
|
|
108
|
+
// Determine if this is own vessel by checking 'self' key or matching selfId
|
|
109
|
+
const isOwn = vesselId === 'self' || vesselId === selfId;
|
|
110
|
+
|
|
111
|
+
// Skip own vessel if not configured to send
|
|
112
|
+
if (isOwn && !sendOwn) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract AIS timestamp for freshness check
|
|
117
|
+
let aisTime = getValue(vessel, 'sensors.ais.class.timestamp');
|
|
118
|
+
if (!aisTime) {
|
|
119
|
+
aisTime = getTimestamp(vessel, 'navigation.position');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check data freshness
|
|
123
|
+
const aisDelay = isDataFresh(aisTime, positionUpdate);
|
|
124
|
+
|
|
125
|
+
// Extract vessel data using helper
|
|
126
|
+
const data = extractVesselData(vessel);
|
|
127
|
+
|
|
128
|
+
// Skip if no position
|
|
129
|
+
if (data.lat === null || data.lon === null) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Calculate distance from own vessel
|
|
134
|
+
const a = { lat: ownLat, lon: ownLon };
|
|
135
|
+
const b = { lat: data.lat, lon: data.lon };
|
|
136
|
+
const dist = (haversine(a, b) / 1000).toFixed(2);
|
|
137
|
+
|
|
138
|
+
// Check if within distance range
|
|
139
|
+
if (parseFloat(dist) > distance) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Send AIS messages based on class
|
|
144
|
+
if (aisDelay && (data.aisClass === 'A' || data.aisClass === 'B' || data.aisClass === 'BASE')) {
|
|
145
|
+
app.debug(
|
|
146
|
+
`Distance range: ${distance}km, AIS target distance: ${dist}km, Class ${data.aisClass} Vessel, MMSI:${data.mmsi}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (data.aisClass === 'A') {
|
|
150
|
+
app.debug(`Class A, MMSI: ${data.mmsi}, Name: ${data.shipName || 'Unknown'}`);
|
|
151
|
+
aisOut(buildAisMessage3(data, isOwn));
|
|
152
|
+
aisOut(buildAisMessage5(data, isOwn));
|
|
153
|
+
processedCount++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (data.aisClass === 'B') {
|
|
157
|
+
app.debug(`Class B, MMSI: ${data.mmsi}, Name: ${data.shipName || 'Unknown'}`);
|
|
158
|
+
aisOut(buildAisMessage18(data, isOwn));
|
|
159
|
+
aisOut(buildAisMessage24A(data, isOwn));
|
|
160
|
+
aisOut(buildAisMessage24B(data, isOwn));
|
|
161
|
+
processedCount++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (data.aisClass === 'BASE') {
|
|
165
|
+
app.debug(`Base Station, MMSI: ${data.mmsi}`);
|
|
166
|
+
aisOut(buildAisMessage3(data, isOwn));
|
|
167
|
+
processedCount++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
app.debug('--------------------------------------------------------');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const dateObj = new Date(Date.now());
|
|
175
|
+
const date = dateObj.toISOString();
|
|
176
|
+
setStatus(`${processedCount} AIS targets sent: ${date}`);
|
|
177
|
+
|
|
178
|
+
if (processedCount > 0) {
|
|
179
|
+
app.reportOutputMessages(processedCount);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
plugin.start = function (options) {
|
|
184
|
+
positionUpdate = (options.position_update || 1) * 60;
|
|
185
|
+
distance = options.distance || 100;
|
|
186
|
+
sendOwn = options.sendOwn !== false;
|
|
187
|
+
useTag = options.useTag || false;
|
|
188
|
+
eventName = options.eventName || 'nmea0183out';
|
|
189
|
+
|
|
190
|
+
app.debug('Plugin starting with direct data access (no REST API)');
|
|
191
|
+
app.debug(`Update interval: ${positionUpdate}s, Distance: ${distance}km`);
|
|
192
|
+
|
|
193
|
+
// Initial run
|
|
194
|
+
processVessels();
|
|
195
|
+
|
|
196
|
+
// Set up periodic processing
|
|
197
|
+
intervalId = setInterval(processVessels, positionUpdate * 1000);
|
|
198
|
+
|
|
199
|
+
setStatus('Running');
|
|
200
|
+
app.debug('Plugin started');
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
plugin.stop = function () {
|
|
204
|
+
if (intervalId) {
|
|
205
|
+
clearInterval(intervalId);
|
|
206
|
+
intervalId = null;
|
|
207
|
+
}
|
|
208
|
+
app.debug('Plugin stopped');
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
plugin.schema = {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
position_update: {
|
|
215
|
+
type: 'number',
|
|
216
|
+
default: 1,
|
|
217
|
+
title: 'How often AIS data is sent to NMEA0183 out (in minutes)',
|
|
218
|
+
description: 'E.g. 0.5 = 30s, 1 = 1min',
|
|
219
|
+
},
|
|
220
|
+
sendOwn: {
|
|
221
|
+
type: 'boolean',
|
|
222
|
+
title: 'Send own AIS data (VDO)',
|
|
223
|
+
default: true,
|
|
224
|
+
},
|
|
225
|
+
useTag: {
|
|
226
|
+
type: 'boolean',
|
|
227
|
+
title: 'Add Tag-block',
|
|
228
|
+
default: false,
|
|
229
|
+
},
|
|
230
|
+
distance: {
|
|
231
|
+
type: 'integer',
|
|
232
|
+
default: 100,
|
|
233
|
+
title: 'AIS target within range [km]',
|
|
234
|
+
},
|
|
235
|
+
eventName: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
default: 'nmea0183out',
|
|
238
|
+
title: 'Output event name',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return plugin;
|
|
244
|
+
};
|