signalk-vessels-to-ais 1.4.0 → 1.5.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/README.md +31 -29
- package/index.js +455 -458
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
# signalk-vessels-to-ais
|
|
2
|
-
[](https://badge.fury.io/js/signalk-vessels-to-ais)
|
|
3
|
-
[](https://snyk.io/test/github/KEGustafsson/signalk-vessels-to-ais)
|
|
4
|
-
|
|
5
|
-
SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications.
|
|
6
|
-
|
|
7
|
-
User can configure:
|
|
8
|
-
- How often data is sent out
|
|
9
|
-
- Own data can be added to AIS sending
|
|
10
|
-
|
|
11
|
-
New:
|
|
12
|
-
- v1.
|
|
13
|
-
- v1.
|
|
14
|
-
- v1.
|
|
15
|
-
- v1.
|
|
16
|
-
- v1.2.
|
|
17
|
-
- v1.1
|
|
18
|
-
- v1.
|
|
19
|
-
- v1.1.
|
|
20
|
-
- v1.1.
|
|
21
|
-
- v1.1.
|
|
22
|
-
- v1.1.
|
|
23
|
-
- v1.
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- v0.0.
|
|
27
|
-
- v0.0.
|
|
28
|
-
- v0.0.
|
|
29
|
-
- v0.0.
|
|
1
|
+
# signalk-vessels-to-ais
|
|
2
|
+
[](https://badge.fury.io/js/signalk-vessels-to-ais)
|
|
3
|
+
[](https://snyk.io/test/github/KEGustafsson/signalk-vessels-to-ais)
|
|
4
|
+
|
|
5
|
+
SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications.
|
|
6
|
+
|
|
7
|
+
User can configure:
|
|
8
|
+
- How often data is sent out
|
|
9
|
+
- Own data can be added to AIS sending
|
|
10
|
+
|
|
11
|
+
New:
|
|
12
|
+
- v1.5.1, fix: fix: improve shipName type checking
|
|
13
|
+
- v1.5.0, fix: callSign reading
|
|
14
|
+
- v1.4.0, add: Event output name to user configurable
|
|
15
|
+
- v1.3.0, add: Navigational Status variations
|
|
16
|
+
- v1.2.2, fix: if own position is not available
|
|
17
|
+
- v1.2.1, fix: own vessel sending
|
|
18
|
+
- v1.2.0, updated fetch method, no need for NODE_TLS_REJECT_UNAUTHORIZED=0 anymore
|
|
19
|
+
- v1.1.5, updated vessels within selected timeframe are sent out, radius filtering around own vessel and tag-block option added
|
|
20
|
+
- v1.1.4, small fix
|
|
21
|
+
- v1.1.3, add: own vessel data and sending interval modified
|
|
22
|
+
- v1.1.2, fix: http/https url selection and better error info
|
|
23
|
+
- v1.1.1, fix: current status of the plugin updated
|
|
24
|
+
- v1.1.0, fix: numeric value test for text strings of AIS
|
|
25
|
+
- v1.0.0, v1 release
|
|
26
|
+
- v0.0.6, fix: node-fetch issue with self signed cert
|
|
27
|
+
- v0.0.5, fix: callSign default value
|
|
28
|
+
- v0.0.4, fix: beam calc
|
|
29
|
+
- v0.0.3, fix: ais path
|
|
30
|
+
- v0.0.2, fix: data parsing
|
|
31
|
+
- v0.0.1, 1st version
|
package/index.js
CHANGED
|
@@ -1,458 +1,455 @@
|
|
|
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
|
-
const fetchNew = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
|
27
|
-
const https = require('https');
|
|
28
|
-
const AisEncode = require('ggencoder').AisEncode;
|
|
29
|
-
const moment = require('moment');
|
|
30
|
-
const haversine = require('haversine-distance');
|
|
31
|
-
|
|
32
|
-
module.exports = function createPlugin(app) {
|
|
33
|
-
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 and forward it out to 3rd party applications';
|
|
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
|
|
47
|
-
|
|
48
|
-
const httpsAgent = new https.Agent({
|
|
49
|
-
rejectUnauthorized: false,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
let getParam;
|
|
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
|
|
143
|
-
|
|
144
|
-
function aisOut(encMsg, aisTime) {
|
|
145
|
-
const enc = new AisEncode(encMsg);
|
|
146
|
-
const sentence = enc.nmea;
|
|
147
|
-
let taggString = '';
|
|
148
|
-
if (useTag) {
|
|
149
|
-
// eslint-disable-next-line no-use-before-define
|
|
150
|
-
taggString = createTagBlock(aisTime);
|
|
151
|
-
}
|
|
152
|
-
if (sentence && sentence.length > 0) {
|
|
153
|
-
app.debug(taggString + sentence);
|
|
154
|
-
app.emit(eventName, taggString + sentence);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const mHex = [
|
|
159
|
-
'0',
|
|
160
|
-
'1',
|
|
161
|
-
'2',
|
|
162
|
-
'3',
|
|
163
|
-
'4',
|
|
164
|
-
'5',
|
|
165
|
-
'6',
|
|
166
|
-
'7',
|
|
167
|
-
'8',
|
|
168
|
-
'9',
|
|
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
|
-
}
|
|
182
|
-
|
|
183
|
-
function createTagBlock(aisTime) {
|
|
184
|
-
let tagBlock = '';
|
|
185
|
-
tagBlock += 's:SK0001,';
|
|
186
|
-
// tagBlock += 'c:' + aisTime + ','
|
|
187
|
-
tagBlock += `c:${Date.now(aisTime)},`;
|
|
188
|
-
tagBlock = tagBlock.slice(0, -1);
|
|
189
|
-
let tagBlockChecksum = 0;
|
|
190
|
-
for (let i = 0; i < tagBlock.length; i++) {
|
|
191
|
-
tagBlockChecksum ^= tagBlock.charCodeAt(i);
|
|
192
|
-
}
|
|
193
|
-
return `\\${tagBlock}*${toHexString(tagBlockChecksum)}\\`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
//----------------------------------------------------------------------------
|
|
197
|
-
// Read and parse AIS data
|
|
198
|
-
|
|
199
|
-
// eslint-disable-next-line no-shadow
|
|
200
|
-
function readData(getParam) {
|
|
201
|
-
let i, mmsi, aisTime, aisDelay, shipName, lat, lon, sog, cog, rot;
|
|
202
|
-
let navStat, hdg, dst, callSign, imo, id, type;
|
|
203
|
-
let draftCur, length, beam, ais, encMsg3, encMsg5, encMsg18, encMsg240, encMsg241, own;
|
|
204
|
-
const ownLat = app.getSelfPath('navigation.position.value.latitude');
|
|
205
|
-
const ownLon = app.getSelfPath('navigation.position.value.longitude');
|
|
206
|
-
if (typeof ownLat !== "undefined" && typeof ownLon !== "undefined") {
|
|
207
|
-
fetchNew(url, getParam)
|
|
208
|
-
.then((res) => res.json())
|
|
209
|
-
.then((json) => {
|
|
210
|
-
const jsonContent = JSON.parse(JSON.stringify(json));
|
|
211
|
-
const numberAIS = Object.keys(jsonContent).length;
|
|
212
|
-
for (i = 0; i < numberAIS; i++) {
|
|
213
|
-
const jsonKey = Object.keys(jsonContent)[i];
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
aisTime = jsonContent[jsonKey].sensors.ais.class.timestamp;
|
|
217
|
-
} catch (error) {
|
|
218
|
-
if (i === 0) {
|
|
219
|
-
aisTime = jsonContent[jsonKey].navigation.position.timestamp;
|
|
220
|
-
} else {
|
|
221
|
-
aisTime = null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
aisDelay = (parseFloat((moment(new Date(Date.now()))
|
|
226
|
-
.diff(aisTime) / 1000).toFixed(3))) < positionUpdate;
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
mmsi = jsonContent[jsonKey].mmsi;
|
|
230
|
-
} catch (error) { mmsi = null; }
|
|
231
|
-
try {
|
|
232
|
-
shipName = jsonContent[jsonKey]
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
own =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
aisOut(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return plugin;
|
|
458
|
-
};
|
|
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
|
+
const fetchNew = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
|
27
|
+
const https = require('https');
|
|
28
|
+
const AisEncode = require('ggencoder').AisEncode;
|
|
29
|
+
const moment = require('moment');
|
|
30
|
+
const haversine = require('haversine-distance');
|
|
31
|
+
|
|
32
|
+
module.exports = function createPlugin(app) {
|
|
33
|
+
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 and forward it out to 3rd party applications';
|
|
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
|
|
47
|
+
|
|
48
|
+
const httpsAgent = new https.Agent({
|
|
49
|
+
rejectUnauthorized: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let getParam;
|
|
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
|
|
143
|
+
|
|
144
|
+
function aisOut(encMsg, aisTime) {
|
|
145
|
+
const enc = new AisEncode(encMsg);
|
|
146
|
+
const sentence = enc.nmea;
|
|
147
|
+
let taggString = '';
|
|
148
|
+
if (useTag) {
|
|
149
|
+
// eslint-disable-next-line no-use-before-define
|
|
150
|
+
taggString = createTagBlock(aisTime);
|
|
151
|
+
}
|
|
152
|
+
if (sentence && sentence.length > 0) {
|
|
153
|
+
app.debug(taggString + sentence);
|
|
154
|
+
app.emit(eventName, taggString + sentence);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mHex = [
|
|
159
|
+
'0',
|
|
160
|
+
'1',
|
|
161
|
+
'2',
|
|
162
|
+
'3',
|
|
163
|
+
'4',
|
|
164
|
+
'5',
|
|
165
|
+
'6',
|
|
166
|
+
'7',
|
|
167
|
+
'8',
|
|
168
|
+
'9',
|
|
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
|
+
}
|
|
182
|
+
|
|
183
|
+
function createTagBlock(aisTime) {
|
|
184
|
+
let tagBlock = '';
|
|
185
|
+
tagBlock += 's:SK0001,';
|
|
186
|
+
// tagBlock += 'c:' + aisTime + ','
|
|
187
|
+
tagBlock += `c:${Date.now(aisTime)},`;
|
|
188
|
+
tagBlock = tagBlock.slice(0, -1);
|
|
189
|
+
let tagBlockChecksum = 0;
|
|
190
|
+
for (let i = 0; i < tagBlock.length; i++) {
|
|
191
|
+
tagBlockChecksum ^= tagBlock.charCodeAt(i);
|
|
192
|
+
}
|
|
193
|
+
return `\\${tagBlock}*${toHexString(tagBlockChecksum)}\\`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//----------------------------------------------------------------------------
|
|
197
|
+
// Read and parse AIS data
|
|
198
|
+
|
|
199
|
+
// eslint-disable-next-line no-shadow
|
|
200
|
+
function readData(getParam) {
|
|
201
|
+
let i, mmsi, aisTime, aisDelay, shipName, lat, lon, sog, cog, rot;
|
|
202
|
+
let navStat, hdg, dst, callSign, imo, id, type;
|
|
203
|
+
let draftCur, length, beam, ais, encMsg3, encMsg5, encMsg18, encMsg240, encMsg241, own;
|
|
204
|
+
const ownLat = app.getSelfPath('navigation.position.value.latitude');
|
|
205
|
+
const ownLon = app.getSelfPath('navigation.position.value.longitude');
|
|
206
|
+
if (typeof ownLat !== "undefined" && typeof ownLon !== "undefined") {
|
|
207
|
+
fetchNew(url, getParam)
|
|
208
|
+
.then((res) => res.json())
|
|
209
|
+
.then((json) => {
|
|
210
|
+
const jsonContent = JSON.parse(JSON.stringify(json));
|
|
211
|
+
const numberAIS = Object.keys(jsonContent).length;
|
|
212
|
+
for (i = 0; i < numberAIS; i++) {
|
|
213
|
+
const jsonKey = Object.keys(jsonContent)[i];
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
aisTime = jsonContent[jsonKey].sensors.ais.class.timestamp;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (i === 0) {
|
|
219
|
+
aisTime = jsonContent[jsonKey].navigation.position.timestamp;
|
|
220
|
+
} else {
|
|
221
|
+
aisTime = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
aisDelay = (parseFloat((moment(new Date(Date.now()))
|
|
226
|
+
.diff(aisTime) / 1000).toFixed(3))) < positionUpdate;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
mmsi = jsonContent[jsonKey].mmsi;
|
|
230
|
+
} catch (error) { mmsi = null; }
|
|
231
|
+
try {
|
|
232
|
+
shipName = typeof jsonContent[jsonKey]?.name === 'string'
|
|
233
|
+
? jsonContent[jsonKey].name
|
|
234
|
+
: jsonContent[jsonKey]?.name?.value || '';
|
|
235
|
+
} catch (error) { shipName = ''; }
|
|
236
|
+
try {
|
|
237
|
+
lat = jsonContent[jsonKey].navigation.position.value.latitude;
|
|
238
|
+
} catch (error) { lat = null; }
|
|
239
|
+
try {
|
|
240
|
+
lon = jsonContent[jsonKey].navigation.position.value.longitude;
|
|
241
|
+
} catch (error) { lon = null; }
|
|
242
|
+
try {
|
|
243
|
+
sog = msToKnots(jsonContent[jsonKey].navigation.speedOverGround.value);
|
|
244
|
+
} catch (error) { sog = null; }
|
|
245
|
+
try {
|
|
246
|
+
cog = radToDegrees(jsonContent[jsonKey].navigation.courseOverGroundTrue.value);
|
|
247
|
+
} catch (error) { cog = null; }
|
|
248
|
+
try {
|
|
249
|
+
rot = radToDegrees(jsonContent[jsonKey].navigation.rateOfTurn.value);
|
|
250
|
+
} catch (error) { rot = null; }
|
|
251
|
+
try {
|
|
252
|
+
navStat = stateMapping[jsonContent[jsonKey].navigation.state.value];
|
|
253
|
+
} catch (error) { navStat = ''; }
|
|
254
|
+
try {
|
|
255
|
+
hdg = radToDegrees(jsonContent[jsonKey].navigation.headingTrue.value);
|
|
256
|
+
} catch (error) { hdg = null; }
|
|
257
|
+
try {
|
|
258
|
+
dst = jsonContent[jsonKey].navigation.destination.commonName.value;
|
|
259
|
+
} catch (error) { dst = ''; }
|
|
260
|
+
try {
|
|
261
|
+
callSign = jsonContent[jsonKey].communication.callsignVhf.value || jsonContent[jsonKey].communication.callsignVhf;
|
|
262
|
+
} catch (error) { callSign = ''; }
|
|
263
|
+
try {
|
|
264
|
+
imo = (jsonContent[jsonKey].registrations.value.imo).substring(4, 20);
|
|
265
|
+
} catch (error) { imo = null; }
|
|
266
|
+
try {
|
|
267
|
+
id = jsonContent[jsonKey].design.aisShipType.value.id;
|
|
268
|
+
} catch (error) { id = null; }
|
|
269
|
+
try {
|
|
270
|
+
type = jsonContent[jsonKey].design.aisShipType.value.name;
|
|
271
|
+
} catch (error) { type = ''; }
|
|
272
|
+
try {
|
|
273
|
+
draftCur = (jsonContent[jsonKey].design.draft.value.current) / 10;
|
|
274
|
+
} catch (error) { draftCur = null; }
|
|
275
|
+
try {
|
|
276
|
+
length = jsonContent[jsonKey].design.length.value.overall;
|
|
277
|
+
} catch (error) { length = null; }
|
|
278
|
+
try {
|
|
279
|
+
beam = (jsonContent[jsonKey].design.beam.value) / 2;
|
|
280
|
+
} catch (error) { beam = null; }
|
|
281
|
+
try {
|
|
282
|
+
ais = jsonContent[jsonKey].sensors.ais.class.value;
|
|
283
|
+
} catch (error) { ais = null; }
|
|
284
|
+
|
|
285
|
+
if (shipName % 1 === 0) {
|
|
286
|
+
shipName = '';
|
|
287
|
+
}
|
|
288
|
+
if (dst % 1 === 0) {
|
|
289
|
+
dst = '';
|
|
290
|
+
}
|
|
291
|
+
if (callSign % 1 === 0) {
|
|
292
|
+
callSign = '';
|
|
293
|
+
}
|
|
294
|
+
if (type % 1 === 0) {
|
|
295
|
+
type = '';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (i === 0) {
|
|
299
|
+
own = true;
|
|
300
|
+
} else {
|
|
301
|
+
own = false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const a = { lat: ownLat, lon: ownLon };
|
|
305
|
+
const b = { lat, lon };
|
|
306
|
+
const dist = (haversine(a, b) / 1000).toFixed(2);
|
|
307
|
+
|
|
308
|
+
if (dist <= distance) {
|
|
309
|
+
encMsg3 = {
|
|
310
|
+
own,
|
|
311
|
+
aistype: 3, // class A position report
|
|
312
|
+
repeat: 0,
|
|
313
|
+
mmsi,
|
|
314
|
+
navstatus: navStat,
|
|
315
|
+
sog,
|
|
316
|
+
lon,
|
|
317
|
+
lat,
|
|
318
|
+
cog,
|
|
319
|
+
hdg,
|
|
320
|
+
rot,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
encMsg5 = {
|
|
324
|
+
own,
|
|
325
|
+
aistype: 5, // class A static
|
|
326
|
+
repeat: 0,
|
|
327
|
+
mmsi,
|
|
328
|
+
imo,
|
|
329
|
+
cargo: id,
|
|
330
|
+
callsign: callSign,
|
|
331
|
+
shipname: shipName,
|
|
332
|
+
draught: draftCur,
|
|
333
|
+
destination: dst,
|
|
334
|
+
dimA: 0,
|
|
335
|
+
dimB: length,
|
|
336
|
+
dimC: beam,
|
|
337
|
+
dimD: beam,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
encMsg18 = {
|
|
341
|
+
own,
|
|
342
|
+
aistype: 18, // class B position report
|
|
343
|
+
repeat: 0,
|
|
344
|
+
mmsi,
|
|
345
|
+
sog,
|
|
346
|
+
accuracy: 0,
|
|
347
|
+
lon,
|
|
348
|
+
lat,
|
|
349
|
+
cog,
|
|
350
|
+
hdg,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
encMsg240 = {
|
|
354
|
+
own,
|
|
355
|
+
aistype: 24, // class B static
|
|
356
|
+
repeat: 0,
|
|
357
|
+
part: 0,
|
|
358
|
+
mmsi,
|
|
359
|
+
shipname: shipName,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
encMsg241 = {
|
|
363
|
+
own,
|
|
364
|
+
aistype: 24, // class B static
|
|
365
|
+
repeat: 0,
|
|
366
|
+
part: 1,
|
|
367
|
+
mmsi,
|
|
368
|
+
cargo: id,
|
|
369
|
+
callsign: callSign,
|
|
370
|
+
dimA: 0,
|
|
371
|
+
dimB: length,
|
|
372
|
+
dimC: beam,
|
|
373
|
+
dimD: beam,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (aisDelay && (ais === 'A' || ais === 'B' || ais === 'BASE')) {
|
|
377
|
+
// eslint-disable-next-line no-useless-concat
|
|
378
|
+
app.debug(`Distance range: ${distance}km, AIS target distance: ${dist}km` + `, Class ${ais} Vessel` + `, MMSI:${mmsi}`);
|
|
379
|
+
if (ais === 'A') {
|
|
380
|
+
app.debug(`class A, ${i}, time: ${aisTime}`);
|
|
381
|
+
aisOut(encMsg3, aisTime);
|
|
382
|
+
aisOut(encMsg5, aisTime);
|
|
383
|
+
}
|
|
384
|
+
if (ais === 'B') {
|
|
385
|
+
app.debug(`class ${ais}, ${i}, time: ${aisTime}`);
|
|
386
|
+
aisOut(encMsg18, aisTime);
|
|
387
|
+
aisOut(encMsg240, aisTime);
|
|
388
|
+
aisOut(encMsg241, aisTime);
|
|
389
|
+
}
|
|
390
|
+
if (ais === 'BASE') {
|
|
391
|
+
app.debug(`class ${ais}, ${i}, time: ${aisTime}`);
|
|
392
|
+
aisOut(encMsg3, aisTime);
|
|
393
|
+
}
|
|
394
|
+
app.debug('--------------------------------------------------------');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const dateobj = new Date(Date.now());
|
|
399
|
+
const date = dateobj.toISOString();
|
|
400
|
+
setStatus(`AIS NMEA message sent: ${date}`);
|
|
401
|
+
})
|
|
402
|
+
.catch((err) => console.error(err));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
//----------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
plugin.stop = function stop() {
|
|
409
|
+
clearInterval(intervalRun);
|
|
410
|
+
app.debug('Stopped');
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
plugin.schema = {
|
|
414
|
+
type: 'object',
|
|
415
|
+
properties: {
|
|
416
|
+
position_update: {
|
|
417
|
+
type: 'number',
|
|
418
|
+
default: 1,
|
|
419
|
+
title: 'How often AIS data is sent to NMEA0183 out (in minutes). E.g. 0.5 = 30s, 1 = 1min',
|
|
420
|
+
},
|
|
421
|
+
port: {
|
|
422
|
+
type: 'number',
|
|
423
|
+
title: 'HTTP port',
|
|
424
|
+
default: 3000,
|
|
425
|
+
},
|
|
426
|
+
portSec: {
|
|
427
|
+
type: 'number',
|
|
428
|
+
title: 'HTTPS port',
|
|
429
|
+
default: 3443,
|
|
430
|
+
},
|
|
431
|
+
sendOwn: {
|
|
432
|
+
type: 'boolean',
|
|
433
|
+
title: 'Send own AIS data, VDO',
|
|
434
|
+
default: true,
|
|
435
|
+
},
|
|
436
|
+
useTag: {
|
|
437
|
+
type: 'boolean',
|
|
438
|
+
title: 'Add Tag-block',
|
|
439
|
+
default: false,
|
|
440
|
+
},
|
|
441
|
+
distance: {
|
|
442
|
+
type: 'integer',
|
|
443
|
+
default: 100,
|
|
444
|
+
title: 'AIS target within range [km]',
|
|
445
|
+
},
|
|
446
|
+
eventName: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
default: 'nmea0183out',
|
|
449
|
+
title: 'Output event name',
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return plugin;
|
|
455
|
+
};
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "signalk-vessels-to-ais",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"signalk-node-server-plugin"
|
|
11
|
-
],
|
|
12
|
-
"repository": "https://github.com/KEGustafsson/signalk-vessels-to-ais",
|
|
13
|
-
"author": "Karl-Erik Gustafsson",
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"dependencies": {
|
|
16
|
-
"haversine-distance": "^1.2.1",
|
|
17
|
-
"moment": "^2.29.1",
|
|
18
|
-
"node-fetch": "^3.1.1",
|
|
19
|
-
"ggencoder": "^1.0.5"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"eslint": "^7.17.0",
|
|
23
|
-
"eslint-config-airbnb-base": "^14.2.1",
|
|
24
|
-
"eslint-plugin-import": "^2.22.1"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "signalk-vessels-to-ais",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"description": "SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"signalk-node-server-plugin"
|
|
11
|
+
],
|
|
12
|
+
"repository": "https://github.com/KEGustafsson/signalk-vessels-to-ais",
|
|
13
|
+
"author": "Karl-Erik Gustafsson",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"haversine-distance": "^1.2.1",
|
|
17
|
+
"moment": "^2.29.1",
|
|
18
|
+
"node-fetch": "^3.1.1",
|
|
19
|
+
"ggencoder": "^1.0.5"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"eslint": "^7.17.0",
|
|
23
|
+
"eslint-config-airbnb-base": "^14.2.1",
|
|
24
|
+
"eslint-plugin-import": "^2.22.1"
|
|
25
|
+
}
|
|
26
|
+
}
|