signalk-vessels-to-ais 1.6.1 → 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 +11 -11
- package/.mocharc.json +5 -0
- package/README.md +1 -0
- package/index.js +163 -382
- package/lib/helpers.js +357 -0
- package/package.json +8 -7
- package/test/helpers.test.js +609 -0
- package/test/plugin.test.js +479 -0
package/.github/dependabot.yml
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
package/.mocharc.json
ADDED
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ User can configure:
|
|
|
9
9
|
- Own data can be added to AIS sending
|
|
10
10
|
|
|
11
11
|
New:
|
|
12
|
+
- v2.0.0, refactor: use direct data access (app.getPath) instead of REST API, removed node-fetch and moment dependencies, added unit tests
|
|
12
13
|
- v1.6.1, fix: ggencoder ^1.0.9 is use
|
|
13
14
|
- v1.6.0, fix: enhance error handling for AIS timestamp retrieval
|
|
14
15
|
- v1.5.1, fix: fix: improve shipName type checking
|
package/index.js
CHANGED
|
@@ -23,399 +23,189 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
23
23
|
SOFTWARE.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
|
|
28
33
|
const AisEncode = require('ggencoder').AisEncode;
|
|
29
|
-
const moment = require('moment');
|
|
30
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');
|
|
31
47
|
|
|
32
48
|
module.exports = function createPlugin(app) {
|
|
33
49
|
const plugin = {};
|
|
34
|
-
plugin.id = 'signalk-vessels-to-ais';
|
|
35
|
-
plugin.name = 'Other vessels data to AIS NMEA0183';
|
|
36
|
-
plugin.description = 'SignalK server plugin to convert other vessel data to NMEA0183 AIS format
|
|
37
|
-
|
|
38
|
-
let positionUpdate = null;
|
|
39
|
-
let distance;
|
|
40
|
-
let sendOwn;
|
|
41
|
-
let url;
|
|
42
|
-
let intervalRun;
|
|
43
|
-
const setStatus = app.setPluginStatus || app.setProviderStatus;
|
|
44
|
-
|
|
45
|
-
let useTag;
|
|
46
|
-
let eventName
|
|
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)';
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
let intervalId = null;
|
|
55
|
+
let positionUpdate = 60;
|
|
56
|
+
let distance = 100;
|
|
57
|
+
let sendOwn = true;
|
|
58
|
+
let useTag = false;
|
|
59
|
+
let eventName = 'nmea0183out';
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
plugin.start = function (options) {
|
|
55
|
-
useTag = options.useTag;
|
|
56
|
-
eventName = options.eventName;
|
|
57
|
-
|
|
58
|
-
positionUpdate = options.position_update * 60;
|
|
59
|
-
distance = options.distance;
|
|
60
|
-
sendOwn = options.sendOwn;
|
|
61
|
-
|
|
62
|
-
const port = options.port || 3000;
|
|
63
|
-
const portSec = options.portSec || 3443;
|
|
64
|
-
|
|
65
|
-
url = `https://localhost:${portSec}/signalk/v1/api/vessels`;
|
|
66
|
-
getParam = { method: 'GET', agent: httpsAgent };
|
|
67
|
-
fetchNew(url, getParam)
|
|
68
|
-
.then((res) => {
|
|
69
|
-
console.log(`${plugin.id}: SSL enabled, using https`);
|
|
70
|
-
if (!res.ok) {
|
|
71
|
-
console.error(`${plugin.id}: SSL enabled, but error accessing server. Check 'Allow Readonly Access' and enable it.`);
|
|
72
|
-
setStatus("Error accessing server. Check 'Allow Readonly Access' and enable it");
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
.catch(() => {
|
|
76
|
-
url = `http://localhost:${port}/signalk/v1/api/vessels`;
|
|
77
|
-
getParam = { method: 'GET' };
|
|
78
|
-
fetchNew(url, getParam)
|
|
79
|
-
.then((res) => {
|
|
80
|
-
console.log(`${plugin.id}: SSL disabled, using http`);
|
|
81
|
-
if (!res.ok) {
|
|
82
|
-
console.error(`${plugin.id}: SSL disabled, but error accessing server. Check 'Allow Readonly Access' and enable it.`);
|
|
83
|
-
setStatus("Error accessing server. Check 'Allow Readonly Access' and enable it");
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
})
|
|
87
|
-
.finally(() => {
|
|
88
|
-
// eslint-disable-next-line no-use-before-define
|
|
89
|
-
intervalRun = setInterval(readData, (positionUpdate * 1000), getParam);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
app.debug('Plugin started');
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
//----------------------------------------------------------------------------
|
|
96
|
-
// State Mapping
|
|
97
|
-
|
|
98
|
-
const stateMapping = {
|
|
99
|
-
motoring: 0,
|
|
100
|
-
'UnderWayUsingEngine': 0,
|
|
101
|
-
'under way using engine': 0,
|
|
102
|
-
'underway using engine': 0,
|
|
103
|
-
anchored: 1,
|
|
104
|
-
'AtAnchor': 1,
|
|
105
|
-
'at anchor': 1,
|
|
106
|
-
'not under command': 2,
|
|
107
|
-
'restricted manouverability': 3,
|
|
108
|
-
'constrained by draft': 4,
|
|
109
|
-
'constrained by her draught': 4,
|
|
110
|
-
moored: 5,
|
|
111
|
-
'Moored': 5,
|
|
112
|
-
aground: 6,
|
|
113
|
-
fishing: 7,
|
|
114
|
-
'engaged in fishing': 7,
|
|
115
|
-
sailing: 8,
|
|
116
|
-
'UnderWaySailing': 8,
|
|
117
|
-
'under way sailing': 8,
|
|
118
|
-
'underway sailing': 8,
|
|
119
|
-
'hazardous material high speed': 9,
|
|
120
|
-
'hazardous material wing in ground': 10,
|
|
121
|
-
'reserved for future use': 13,
|
|
122
|
-
'ais-sart': 14,
|
|
123
|
-
default: 15,
|
|
124
|
-
'UnDefined': 15,
|
|
125
|
-
'undefined': 15,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
//----------------------------------------------------------------------------
|
|
129
|
-
// Rad to Deg
|
|
130
|
-
function radToDegrees(radians) {
|
|
131
|
-
const pi = Math.PI;
|
|
132
|
-
return ((radians * 180) / pi);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
//----------------------------------------------------------------------------
|
|
136
|
-
// m/s to knots
|
|
137
|
-
function msToKnots(speed) {
|
|
138
|
-
return ((speed * 3.6) / 1.852);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
//----------------------------------------------------------------------------
|
|
142
|
-
// nmea out
|
|
61
|
+
const setStatus = app.setPluginStatus || app.setProviderStatus;
|
|
143
62
|
|
|
144
|
-
|
|
63
|
+
// NMEA output
|
|
64
|
+
function aisOut(encMsg) {
|
|
145
65
|
const enc = new AisEncode(encMsg);
|
|
146
66
|
const sentence = enc.nmea;
|
|
147
|
-
let
|
|
67
|
+
let tagString = '';
|
|
148
68
|
if (useTag) {
|
|
149
|
-
|
|
150
|
-
taggString = createTagBlock(aisTime);
|
|
69
|
+
tagString = createTagBlock();
|
|
151
70
|
}
|
|
152
71
|
if (sentence && sentence.length > 0) {
|
|
153
|
-
app.debug(
|
|
154
|
-
app.emit(eventName,
|
|
72
|
+
app.debug(tagString + sentence);
|
|
73
|
+
app.emit(eventName, tagString + sentence);
|
|
155
74
|
}
|
|
156
75
|
}
|
|
157
76
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
'
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
'A',
|
|
170
|
-
'B',
|
|
171
|
-
'C',
|
|
172
|
-
'D',
|
|
173
|
-
'E',
|
|
174
|
-
'F',
|
|
175
|
-
];
|
|
176
|
-
|
|
177
|
-
function toHexString(v) {
|
|
178
|
-
const msn = (v >> 4) & 0x0f;
|
|
179
|
-
const lsn = (v >> 0) & 0x0f;
|
|
180
|
-
return mHex[msn] + mHex[lsn];
|
|
181
|
-
}
|
|
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
|
+
}
|
|
182
88
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
tagBlockChecksum ^= tagBlock.charCodeAt(i);
|
|
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;
|
|
192
97
|
}
|
|
193
|
-
return `\\${tagBlock}*${toHexString(tagBlockChecksum)}\\`;
|
|
194
|
-
}
|
|
195
98
|
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
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));
|
|
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);
|
|
411
180
|
}
|
|
412
181
|
}
|
|
413
182
|
|
|
414
|
-
|
|
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`);
|
|
415
192
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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');
|
|
419
209
|
};
|
|
420
210
|
|
|
421
211
|
plugin.schema = {
|
|
@@ -424,21 +214,12 @@ module.exports = function createPlugin(app) {
|
|
|
424
214
|
position_update: {
|
|
425
215
|
type: 'number',
|
|
426
216
|
default: 1,
|
|
427
|
-
title: 'How often AIS data is sent to NMEA0183 out (in minutes)
|
|
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,
|
|
217
|
+
title: 'How often AIS data is sent to NMEA0183 out (in minutes)',
|
|
218
|
+
description: 'E.g. 0.5 = 30s, 1 = 1min',
|
|
438
219
|
},
|
|
439
220
|
sendOwn: {
|
|
440
221
|
type: 'boolean',
|
|
441
|
-
title: 'Send own AIS data
|
|
222
|
+
title: 'Send own AIS data (VDO)',
|
|
442
223
|
default: true,
|
|
443
224
|
},
|
|
444
225
|
useTag: {
|