signalk-ais-navionics-converter 1.0.2 → 1.0.3
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 +26 -18
- package/ais-encoder.js +543 -66
- package/img/OpenCpn1.png +0 -0
- package/img/OpenCpn2.png +0 -0
- package/index.js +337 -155
- package/package.json +3 -13
- package/public/src_components_PluginConfigurationPanel_jsx.js +1 -1
- package/src/components/PluginConfigurationPanel.jsx +158 -80
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const net = require('net');
|
|
|
2
2
|
const dgram = require('dgram');
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const AISEncoder = require('./ais-encoder');
|
|
5
|
+
const WebSocket = require('ws');
|
|
5
6
|
|
|
6
7
|
module.exports = function(app) {
|
|
7
8
|
let plugin = {
|
|
@@ -9,20 +10,24 @@ module.exports = function(app) {
|
|
|
9
10
|
name: 'AIS to NMEA 0183 converter for TPC clients (e.g. Navionics, OpenCpn)',
|
|
10
11
|
description: 'SignalK plugin to convert AIS data to NMEA 0183 sentences to TCP clients (e.g. Navionics boating app, OpenCpn) and optional to vesselfinder.com'
|
|
11
12
|
};
|
|
12
|
-
|
|
13
|
+
const encoder = new AISEncoder(app);
|
|
13
14
|
let tcpServer = null;
|
|
14
15
|
let udpClient = null;
|
|
16
|
+
let wsServer = null;
|
|
15
17
|
let updateInterval = null;
|
|
16
18
|
let tcpClients = [];
|
|
17
|
-
let
|
|
19
|
+
let newTcpClients = [];
|
|
20
|
+
let newWSClients = [];
|
|
18
21
|
let previousVesselsState = new Map();
|
|
19
22
|
let lastTCPBroadcast = new Map();
|
|
20
23
|
let messageIdCounter = 0;
|
|
21
24
|
let ownMMSI = null;
|
|
22
25
|
let vesselFinderLastUpdate = 0;
|
|
23
26
|
let signalkApiUrl = null;
|
|
27
|
+
let signalkAisfleetUrl = null;
|
|
24
28
|
let cloudVesselsCache = null;
|
|
25
29
|
let cloudVesselsLastFetch = 0;
|
|
30
|
+
let aisfleetEnabled= false;
|
|
26
31
|
|
|
27
32
|
plugin.schema = {
|
|
28
33
|
type: 'object',
|
|
@@ -34,6 +39,12 @@ module.exports = function(app) {
|
|
|
34
39
|
description: 'Port for NMEA TCP server',
|
|
35
40
|
default: 10113
|
|
36
41
|
},
|
|
42
|
+
wsPort: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
title: 'WebSocket Port for AIS and last position timestamp data',
|
|
45
|
+
description: 'Port for WebSocket server (web clients can connect here)',
|
|
46
|
+
default: 10114
|
|
47
|
+
},
|
|
37
48
|
updateInterval: {
|
|
38
49
|
type: 'number',
|
|
39
50
|
title: 'Update interval for changed vessels (seconds, default: 15)',
|
|
@@ -84,14 +95,14 @@ module.exports = function(app) {
|
|
|
84
95
|
},
|
|
85
96
|
logDebugDetails: {
|
|
86
97
|
type: 'boolean',
|
|
87
|
-
title: 'Debug
|
|
98
|
+
title: 'Debug vessel details',
|
|
88
99
|
description: 'Detailed debug output in server log for all vessels - only visible if plugin is in debug mode',
|
|
89
100
|
default: false
|
|
90
101
|
},
|
|
91
102
|
logMMSI: {
|
|
92
103
|
type: 'string',
|
|
93
|
-
title: 'Debug MMSI',
|
|
94
|
-
description: '
|
|
104
|
+
title: 'Debug only MMSI',
|
|
105
|
+
description: 'Only data for this MMSI will be shown in the detailed debug output in server log - only visible if plugin is in debug mode. Must be different from own MMSI.',
|
|
95
106
|
default: ''
|
|
96
107
|
},
|
|
97
108
|
logDebugStale: {
|
|
@@ -173,12 +184,15 @@ module.exports = function(app) {
|
|
|
173
184
|
if (hostname === '0.0.0.0' || hostname === '::') {
|
|
174
185
|
hostname = '127.0.0.1';
|
|
175
186
|
}
|
|
176
|
-
|
|
187
|
+
signalkAisfleetUrl= `http://${hostname}:${port}/signalk/plugins/aisfleet/config`;
|
|
177
188
|
signalkApiUrl = `http://${hostname}:${port}/signalk/v1/api`;
|
|
178
189
|
|
|
179
190
|
// Hole eigene MMSI
|
|
180
191
|
getOwnMMSI().then(() => {
|
|
181
192
|
startTCPServer(options);
|
|
193
|
+
if (options.wsPort && options.wsPort > 0){
|
|
194
|
+
startWebSocketServer(options);
|
|
195
|
+
}
|
|
182
196
|
startUpdateLoop(options);
|
|
183
197
|
|
|
184
198
|
if (options.vesselFinderEnabled && options.vesselFinderHost) {
|
|
@@ -199,7 +213,12 @@ module.exports = function(app) {
|
|
|
199
213
|
tcpServer.close();
|
|
200
214
|
tcpServer = null;
|
|
201
215
|
}
|
|
202
|
-
|
|
216
|
+
if (wsServer) {
|
|
217
|
+
wsServer.clients.forEach(client => client.terminate());
|
|
218
|
+
newWSClients = [];
|
|
219
|
+
wsServer.close();
|
|
220
|
+
wsServer = null;
|
|
221
|
+
}
|
|
203
222
|
if (udpClient) {
|
|
204
223
|
udpClient.close();
|
|
205
224
|
udpClient = null;
|
|
@@ -231,27 +250,85 @@ module.exports = function(app) {
|
|
|
231
250
|
tcpServer = net.createServer((socket) => {
|
|
232
251
|
app.debug(`TCP client connected: ${socket.remoteAddress}:${socket.remotePort}`);
|
|
233
252
|
tcpClients.push(socket);
|
|
234
|
-
|
|
253
|
+
newTcpClients.push(socket);
|
|
235
254
|
|
|
236
255
|
socket.on('end', () => {
|
|
237
256
|
app.debug(`TCP client disconnected`);
|
|
238
257
|
tcpClients = tcpClients.filter(c => c !== socket);
|
|
239
|
-
|
|
258
|
+
newTcpClients = newTcpClients.filter(c => c !== socket);
|
|
240
259
|
});
|
|
241
260
|
|
|
242
261
|
socket.on('error', (err) => {
|
|
243
262
|
app.error(`TCP socket error: ${err}`);
|
|
244
263
|
tcpClients = tcpClients.filter(c => c !== socket);
|
|
245
|
-
|
|
264
|
+
newTcpClients = newTcpClients.filter(c => c !== socket);
|
|
246
265
|
});
|
|
247
266
|
});
|
|
248
267
|
|
|
249
268
|
tcpServer.listen(options.tcpPort, () => {
|
|
250
|
-
|
|
251
|
-
app.
|
|
269
|
+
const statusText= `TCP Server running on port ${options.tcpPort}` + (options.wsPort && options.wsPort > 0 ? ` - WS server on port ${options.wsPort}` : '');
|
|
270
|
+
app.debug(statusText);
|
|
271
|
+
app.setPluginStatus(statusText);
|
|
252
272
|
});
|
|
253
273
|
}
|
|
254
274
|
|
|
275
|
+
function startWebSocketServer(options) {
|
|
276
|
+
const wsPort = options.wsPort || 10114;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
wsServer = new WebSocket.Server({ port: wsPort });
|
|
280
|
+
|
|
281
|
+
wsServer.on('listening', () => {
|
|
282
|
+
app.debug(`AIS WebSocket Server listening on port ${wsPort}`);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
wsServer.on('connection', (ws, req) => {
|
|
286
|
+
const clientIP = req.socket.remoteAddress;
|
|
287
|
+
app.debug(`WebSocket client connected from ${clientIP}`);
|
|
288
|
+
newWSClients.push(ws);
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
processVessels(options, 'New WebSocket Client connected');
|
|
291
|
+
}, 100);
|
|
292
|
+
ws.isAlive = true;
|
|
293
|
+
ws.on('pong', () => {
|
|
294
|
+
ws.isAlive = true;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
ws.on('close', () => {
|
|
298
|
+
newWSClients = newWSClients.filter(c => c !== ws);
|
|
299
|
+
app.debug(`WebSocket client disconnected`);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
ws.on('error', (err) => {
|
|
303
|
+
newWSClients = newWSClients.filter(c => c !== ws);
|
|
304
|
+
app.error(`WebSocket client error: ${err.message}`);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Heartbeat interval (prüft alle 30 Sekunden ob Clients noch verbunden sind)
|
|
309
|
+
const heartbeat = setInterval(() => {
|
|
310
|
+
wsServer.clients.forEach((ws) => {
|
|
311
|
+
if (ws.isAlive === false) {
|
|
312
|
+
return ws.terminate();
|
|
313
|
+
}
|
|
314
|
+
ws.isAlive = false;
|
|
315
|
+
ws.ping();
|
|
316
|
+
});
|
|
317
|
+
}, 30000);
|
|
318
|
+
|
|
319
|
+
wsServer.on('close', () => {
|
|
320
|
+
clearInterval(heartbeat);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
wsServer.on('error', (err) => {
|
|
324
|
+
app.error(`WebSocket Server error: ${err.message}`);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
} catch (err) {
|
|
328
|
+
app.error(`Failed to start WebSocket Server: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
255
332
|
function startVesselFinderForwarding(options) {
|
|
256
333
|
udpClient = dgram.createSocket('udp4');
|
|
257
334
|
app.debug(`VesselFinder UDP forwarding enabled: ${options.vesselFinderHost}:${options.vesselFinderPort}`);
|
|
@@ -267,6 +344,20 @@ module.exports = function(app) {
|
|
|
267
344
|
});
|
|
268
345
|
}
|
|
269
346
|
|
|
347
|
+
function broadcastWebSocket(message) {
|
|
348
|
+
if (!wsServer) return;
|
|
349
|
+
|
|
350
|
+
wsServer.clients.forEach((client) => {
|
|
351
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
352
|
+
try {
|
|
353
|
+
client.send(message);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
app.error(`Error broadcasting to WebSocket client: ${err}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
270
361
|
function sendToVesselFinder(message, options) {
|
|
271
362
|
if (!udpClient || !options.vesselFinderEnabled) return;
|
|
272
363
|
|
|
@@ -284,29 +375,13 @@ module.exports = function(app) {
|
|
|
284
375
|
|
|
285
376
|
function startUpdateLoop(options) {
|
|
286
377
|
// Initiales Update
|
|
287
|
-
processVessels(options, '
|
|
378
|
+
processVessels(options, 'Inital startup');
|
|
288
379
|
|
|
289
380
|
// Regelmäßige Updates
|
|
290
381
|
updateInterval = setInterval(() => {
|
|
291
|
-
processVessels(options, 'Scheduled');
|
|
382
|
+
processVessels(options, 'Scheduled resend');
|
|
292
383
|
}, options.updateInterval * 1000);
|
|
293
|
-
|
|
294
|
-
// Memory-Monitoring alle 5 Minuten wenn Debug aktiv
|
|
295
|
-
if (options.logDebugDetails) {
|
|
296
|
-
setInterval(() => {
|
|
297
|
-
logMemoryUsage();
|
|
298
|
-
}, 5 * 60 * 1000);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function logMemoryUsage() {
|
|
303
|
-
const usage = process.memoryUsage();
|
|
304
|
-
app.debug('=== Memory Usage ===');
|
|
305
|
-
app.debug(`RSS: ${(usage.rss / 1024 / 1024).toFixed(2)} MB`);
|
|
306
|
-
app.debug(`Heap Used: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
|
307
|
-
app.debug(`Heap Total: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
|
|
308
|
-
app.debug(`External: ${(usage.external / 1024 / 1024).toFixed(2)} MB`);
|
|
309
|
-
app.debug(`Map sizes - previousVesselsState: ${previousVesselsState.size}, lastTCPBroadcast: ${lastTCPBroadcast.size}`);
|
|
384
|
+
|
|
310
385
|
}
|
|
311
386
|
|
|
312
387
|
function fetchVesselsFromAPI() {
|
|
@@ -323,7 +398,6 @@ module.exports = function(app) {
|
|
|
323
398
|
const http = require('http');
|
|
324
399
|
|
|
325
400
|
http.get(url, (res) => {
|
|
326
|
-
app.debug(`HTTP Response Status: ${res.statusCode}`);
|
|
327
401
|
let data = '';
|
|
328
402
|
|
|
329
403
|
if (res.statusCode !== 200) {
|
|
@@ -337,7 +411,6 @@ module.exports = function(app) {
|
|
|
337
411
|
});
|
|
338
412
|
|
|
339
413
|
res.on('end', () => {
|
|
340
|
-
app.debug(`HTTP Response received, length: ${data.length} bytes`);
|
|
341
414
|
try {
|
|
342
415
|
const result = JSON.parse(data);
|
|
343
416
|
app.debug(`Parsed JSON, ${Object.keys(result).length} vessels from SignalK API`);
|
|
@@ -345,12 +418,12 @@ module.exports = function(app) {
|
|
|
345
418
|
} catch (err) {
|
|
346
419
|
app.error(`Invalid JSON from ${url}: ${err.message}`);
|
|
347
420
|
app.error(`Data preview: ${data.substring(0, 200)}`);
|
|
348
|
-
resolve(null);
|
|
421
|
+
resolve(null);
|
|
349
422
|
}
|
|
350
423
|
});
|
|
351
424
|
}).on('error', (err) => {
|
|
352
425
|
app.error(`HTTP request error for ${url}: ${err.message}`);
|
|
353
|
-
resolve(null);
|
|
426
|
+
resolve(null);
|
|
354
427
|
});
|
|
355
428
|
});
|
|
356
429
|
}
|
|
@@ -378,7 +451,7 @@ module.exports = function(app) {
|
|
|
378
451
|
}
|
|
379
452
|
|
|
380
453
|
const url = `https://aisfleet.com/api/vessels/nearby?lat=${lat}&lng=${lng}&radius=${radius}&mmsi=${ownMMSI}`;
|
|
381
|
-
app.debug(`Fetching cloud vessels from AISFleet API (radius: ${radius}nm)`);
|
|
454
|
+
app.debug(`Fetching cloud vessels from AISFleet API (radius: ${radius}nm) - URL: ${url}`);
|
|
382
455
|
|
|
383
456
|
const requestConfig = {
|
|
384
457
|
method: 'get',
|
|
@@ -393,8 +466,6 @@ module.exports = function(app) {
|
|
|
393
466
|
return axios.request(requestConfig)
|
|
394
467
|
.then(response => {
|
|
395
468
|
const duration = Date.now() - startTime;
|
|
396
|
-
app.debug(`AISFleet API response time: ${duration}ms`);
|
|
397
|
-
|
|
398
469
|
if (duration > 10000) {
|
|
399
470
|
app.error(`AISFleet API slow response: ${duration}ms - consider reducing radius`);
|
|
400
471
|
}
|
|
@@ -571,8 +642,8 @@ module.exports = function(app) {
|
|
|
571
642
|
// Spezialbehandlung für name und callsign: Gefüllte Werte haben immer Vorrang
|
|
572
643
|
const vessel1Name = vessel1.name;
|
|
573
644
|
const vessel2Name = vessel2.name;
|
|
574
|
-
const vessel1Callsign = vessel1.communication?.callsignVhf || vessel1.callsign
|
|
575
|
-
const vessel2Callsign = vessel2.communication?.callsignVhf || vessel2.callsign
|
|
645
|
+
const vessel1Callsign = vessel1.communication?.callsignVhf || vessel1.callsign;
|
|
646
|
+
const vessel2Callsign = vessel2.communication?.callsignVhf || vessel2.callsign;
|
|
576
647
|
|
|
577
648
|
// Name: Bevorzuge gefüllte Werte über "Unknown" oder leere Werte
|
|
578
649
|
if (vessel2Name && vessel2Name !== 'Unknown' && vessel2Name !== '') {
|
|
@@ -588,7 +659,6 @@ module.exports = function(app) {
|
|
|
588
659
|
merged.communication.callsignVhf = vessel2Callsign;
|
|
589
660
|
// Setze auch die anderen Varianten
|
|
590
661
|
merged.callsign = vessel2Callsign;
|
|
591
|
-
merged.callSign = vessel2Callsign;
|
|
592
662
|
}
|
|
593
663
|
}
|
|
594
664
|
|
|
@@ -642,7 +712,9 @@ module.exports = function(app) {
|
|
|
642
712
|
|
|
643
713
|
function mergeVesselSources(signalkVessels, cloudVessels, options) {
|
|
644
714
|
const merged = {};
|
|
645
|
-
|
|
715
|
+
const loggedVessels = new Set();
|
|
716
|
+
const logMMSI = options.logMMSI || '';
|
|
717
|
+
|
|
646
718
|
app.debug(`Merging vessels - SignalK: ${signalkVessels ? Object.keys(signalkVessels).length : 0}, Cloud: ${cloudVessels ? Object.keys(cloudVessels).length : 0}`);
|
|
647
719
|
|
|
648
720
|
// Füge alle SignalK Vessels hinzu
|
|
@@ -652,71 +724,78 @@ module.exports = function(app) {
|
|
|
652
724
|
}
|
|
653
725
|
}
|
|
654
726
|
|
|
655
|
-
// Merge Cloud
|
|
727
|
+
// Merge SignalK Schiffe mit Cloud Schiffen
|
|
656
728
|
if (cloudVessels) {
|
|
657
729
|
for (const [vesselId, cloudVessel] of Object.entries(cloudVessels)) {
|
|
658
730
|
const mmsiMatch = vesselId.match(/mmsi:(\d+)/);
|
|
659
731
|
const mmsi = mmsiMatch ? mmsiMatch[1] : null;
|
|
660
|
-
const
|
|
661
|
-
const shouldLog = options.logDebugDetails || (logMMSI && logMMSI !== '' && mmsi === logMMSI);
|
|
662
|
-
|
|
732
|
+
const shouldLog = options.logDebugJSON && options.logDebugDetails && (mmsi === "" || mmsi === logMMSI)
|
|
663
733
|
if (merged[vesselId]) {
|
|
664
|
-
//
|
|
734
|
+
// Schiff existiert in beiden Quellen - merge mit Timestamp-Vergleich
|
|
665
735
|
merged[vesselId] = mergeVesselData(merged[vesselId], cloudVessel);
|
|
666
|
-
|
|
667
736
|
if (shouldLog) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
737
|
+
app.debug(`Merged vessel ${vesselId} (${mmsi}):`);
|
|
738
|
+
app.debug(JSON.stringify(merged[vesselId], null, 2));
|
|
739
|
+
loggedVessels.add(vesselId);
|
|
672
740
|
}
|
|
673
741
|
} else {
|
|
674
|
-
//
|
|
742
|
+
// Schiff nur in Cloud - direkt hinzufügen
|
|
675
743
|
merged[vesselId] = cloudVessel;
|
|
676
|
-
|
|
677
744
|
if (shouldLog) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
745
|
+
app.debug(`Cloud-only vessel ${vesselId} (${mmsi}):`);
|
|
746
|
+
app.debug(JSON.stringify(cloudVessel, null, 2));
|
|
747
|
+
loggedVessels.add(vesselId);
|
|
682
748
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
for (const [vesselId, vessel] of Object.entries(merged)) {
|
|
754
|
+
|
|
755
|
+
// MMSI extrahieren
|
|
756
|
+
const mmsiMatch = vesselId.match(/mmsi:(\d+)/);
|
|
757
|
+
const mmsi = mmsiMatch ? mmsiMatch[1] : null;
|
|
758
|
+
|
|
759
|
+
const vessel = merged[vesselId];
|
|
760
|
+
const name = vessel?.name;
|
|
761
|
+
const ts = vessel?.navigation?.position?.timestamp;
|
|
762
|
+
const staleLimit = options.staleDataShipnameAddTime;
|
|
763
|
+
|
|
764
|
+
if (name && ts && staleLimit > 0) {
|
|
765
|
+
|
|
766
|
+
const diffMinutes = Math.floor((Date.now() - new Date(ts)) / 60000);
|
|
767
|
+
|
|
768
|
+
if (diffMinutes >= staleLimit) {
|
|
769
|
+
|
|
770
|
+
let suffix;
|
|
771
|
+
if (diffMinutes >= 1440) {
|
|
772
|
+
suffix = ` DAY${Math.ceil(diffMinutes / 1440)}`;
|
|
773
|
+
} else if (diffMinutes >= 60) {
|
|
774
|
+
suffix = ` HOUR${Math.ceil(diffMinutes / 60)}`;
|
|
775
|
+
} else {
|
|
776
|
+
suffix = ` MIN${diffMinutes}`;
|
|
703
777
|
}
|
|
778
|
+
|
|
779
|
+
vessel.nameStale = `${name}${suffix}`.substring(0, 20);
|
|
704
780
|
}
|
|
705
781
|
}
|
|
782
|
+
const shouldLog = options.logDebugJSON && options.logDebugDetails && (mmsi === "" || mmsi === logMMSI) && !loggedVessels.has(vesselId)
|
|
783
|
+
if (shouldLog) {
|
|
784
|
+
app.debug(`SignalK-only vessel ${vesselId} (${mmsi}):`);
|
|
785
|
+
app.debug(JSON.stringify(vessel, null, 2));
|
|
786
|
+
}
|
|
706
787
|
}
|
|
707
|
-
|
|
708
788
|
app.debug(`Total merged vessels: ${Object.keys(merged).length}`);
|
|
709
789
|
return merged;
|
|
710
790
|
}
|
|
711
791
|
|
|
712
|
-
function getVessels(options) {
|
|
792
|
+
function getVessels(options,aisfleetEnabled) {
|
|
713
793
|
const now = Date.now();
|
|
714
794
|
const cloudUpdateInterval = (options.cloudVesselsUpdateInterval || 60) * 1000; // Default 60 Sekunden
|
|
715
795
|
|
|
716
796
|
// Entscheide ob Cloud Vessels neu geholt werden müssen
|
|
717
|
-
const needsCloudUpdate = options.cloudVesselsEnabled &&
|
|
797
|
+
const needsCloudUpdate = options.cloudVesselsEnabled && !aisfleetEnabled &&
|
|
718
798
|
(now - cloudVesselsLastFetch >= cloudUpdateInterval);
|
|
719
|
-
|
|
720
799
|
const cloudPromise = needsCloudUpdate
|
|
721
800
|
? fetchCloudVessels(options).then(result => {
|
|
722
801
|
if (result) {
|
|
@@ -733,8 +812,8 @@ function getVessels(options) {
|
|
|
733
812
|
]).then(([signalkVessels, cloudVessels]) => {
|
|
734
813
|
const vessels = [];
|
|
735
814
|
|
|
736
|
-
// Merge beide Datenquellen
|
|
737
|
-
const allVessels =
|
|
815
|
+
// Merge beide Datenquellen (falls aisfleet plugin nicht aktiviert ist)
|
|
816
|
+
const allVessels = mergeVesselSources(signalkVessels, cloudVessels, options)
|
|
738
817
|
|
|
739
818
|
if (!allVessels) return vessels;
|
|
740
819
|
for (const [vesselId, vessel] of Object.entries(allVessels)) {
|
|
@@ -746,15 +825,41 @@ function getVessels(options) {
|
|
|
746
825
|
const mmsi = mmsiMatch[1];
|
|
747
826
|
if (ownMMSI && mmsi === ownMMSI) continue;
|
|
748
827
|
|
|
828
|
+
// IMO-Extraktion verbessert - prüfe mehrere Quellen und filtere ungültige Werte
|
|
829
|
+
let imo = null;
|
|
830
|
+
|
|
831
|
+
// Prüfe vessel.registrations.imo zuerst (häufigste SignalK-Struktur)
|
|
832
|
+
if (vessel.registrations?.imo) {
|
|
833
|
+
const imoStr = vessel.registrations.imo.toString().replace(/[^\d]/g, '');
|
|
834
|
+
const imoNum = parseInt(imoStr);
|
|
835
|
+
if (imoNum && imoNum > 0) {
|
|
836
|
+
imo = imoNum;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Falls nicht gefunden, prüfe vessel.imo
|
|
841
|
+
if (!imo && vessel.imo) {
|
|
842
|
+
const imoStr = vessel.imo.toString().replace(/[^\d]/g, '');
|
|
843
|
+
const imoNum = parseInt(imoStr);
|
|
844
|
+
if (imoNum && imoNum > 0) {
|
|
845
|
+
imo = imoNum;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Fallback auf 0 wenn nichts gefunden
|
|
850
|
+
if (!imo) {
|
|
851
|
+
imo = 0;
|
|
852
|
+
}
|
|
853
|
+
|
|
749
854
|
vessels.push({
|
|
750
855
|
mmsi: mmsi,
|
|
751
|
-
name: vessel.name || 'Unknown',
|
|
856
|
+
name: vessel.nameStale || vessel.name || 'Unknown',
|
|
752
857
|
callsign: vessel.callsign || vessel.callSign || vessel.communication?.callsignVhf || '',
|
|
753
858
|
navigation: vessel.navigation || {},
|
|
754
859
|
design: vessel.design || {},
|
|
755
860
|
sensors: vessel.sensors || {},
|
|
756
861
|
destination: vessel.navigation?.destination?.commonName?.value || null,
|
|
757
|
-
imo:
|
|
862
|
+
imo: imo
|
|
758
863
|
});
|
|
759
864
|
}
|
|
760
865
|
|
|
@@ -768,13 +873,28 @@ function getVessels(options) {
|
|
|
768
873
|
function filterVessels(vessels, options) {
|
|
769
874
|
const now = new Date();
|
|
770
875
|
const filtered = [];
|
|
771
|
-
|
|
876
|
+
let countStale = 0;
|
|
877
|
+
let countNoCallsign = 0;
|
|
878
|
+
let countInvalidMMSI = 0;
|
|
879
|
+
let countInvalidNameAndMMSI = 0;
|
|
880
|
+
let countBaseStations = 0;
|
|
772
881
|
for (const vessel of vessels) {
|
|
773
|
-
|
|
774
|
-
|
|
882
|
+
if (vessel?.sensors?.ais?.class?.meta?.value === "BASE") {
|
|
883
|
+
countBaseStations++;
|
|
884
|
+
continue; // Überspringe Basisstationen
|
|
885
|
+
}
|
|
886
|
+
if (!vessel.mmsi || vessel.mmsi.length !== 9 || isNaN(parseInt(vessel.mmsi))) {
|
|
887
|
+
if (options.logDebugDetails && (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
888
|
+
app.debug(`Skipped (invalid MMSI): ${vessel.mmsi} ${vessel.name}`);
|
|
889
|
+
}
|
|
890
|
+
countInvalidMMSI++;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
let callsign = vessel.callsign || '';
|
|
894
|
+
const hasRealCallsign = callsign && callsign.length > 0;
|
|
775
895
|
|
|
776
896
|
if (!hasRealCallsign) {
|
|
777
|
-
|
|
897
|
+
callsign = 'UNKNOWN';
|
|
778
898
|
}
|
|
779
899
|
|
|
780
900
|
// Hole Position Timestamp für mehrere Checks
|
|
@@ -802,9 +922,10 @@ function getVessels(options) {
|
|
|
802
922
|
if (hours > 0) ageStr += `${hours}h `;
|
|
803
923
|
if (minutes > 0) ageStr += `${minutes}m`;
|
|
804
924
|
|
|
805
|
-
if (
|
|
925
|
+
if (options.logDebugDetails && options.logDebugStale && (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
806
926
|
app.debug(`Skipped (stale): ${vessel.mmsi} ${vessel.name} - ${ageStr.trim()} ago`);
|
|
807
927
|
}
|
|
928
|
+
countStale++;
|
|
808
929
|
continue;
|
|
809
930
|
}
|
|
810
931
|
}
|
|
@@ -831,32 +952,66 @@ function getVessels(options) {
|
|
|
831
952
|
} else {
|
|
832
953
|
vessel.navigation.speedOverGround = 0;
|
|
833
954
|
}
|
|
834
|
-
|
|
835
|
-
if (
|
|
955
|
+
|
|
956
|
+
if (options.logDebugDetails && options.logDebugSOG && (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
836
957
|
const ageMinutes = Math.floor(ageMs / 60000)
|
|
837
958
|
app.debug(`SOG corrected to 0 for ${vessel.mmsi} ${vessel.name} - position age: ${ageMinutes}min (was: ${originalSOG.toFixed(2)} m/s)`);
|
|
838
959
|
}
|
|
839
960
|
}
|
|
840
961
|
}
|
|
841
962
|
}
|
|
842
|
-
|
|
963
|
+
|
|
964
|
+
// Filtere Schiffe ohne Name und Callsign aus
|
|
965
|
+
const hasValidName = vessel.name && vessel.name.toLowerCase() !== 'unknown';
|
|
966
|
+
if (!hasValidName && !hasRealCallsign){
|
|
967
|
+
if (options.logDebugDetails && (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
968
|
+
app.debug(`Skipped (no valid name and no valid callsign): ${vessel.mmsi} - Name: "${vessel.name}", Callsign: "${vessel.callsign}"`);
|
|
969
|
+
}
|
|
970
|
+
countInvalidNameAndMMSI++;
|
|
971
|
+
continue ;
|
|
972
|
+
}
|
|
843
973
|
// Callsign check
|
|
844
|
-
if (options.skipWithoutCallsign && !hasRealCallsign)
|
|
845
|
-
if (options.logDebugDetails
|
|
974
|
+
if (options.skipWithoutCallsign && !hasRealCallsign ){
|
|
975
|
+
if (options.logDebugDetails && (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
846
976
|
app.debug(`Skipped (no callsign): ${vessel.mmsi} ${vessel.name}`);
|
|
847
977
|
}
|
|
978
|
+
countNoCallsign++;
|
|
848
979
|
continue;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
vessel.callSign = callSign;
|
|
980
|
+
}
|
|
981
|
+
vessel.callsign = callsign;
|
|
852
982
|
filtered.push(vessel);
|
|
853
983
|
}
|
|
854
|
-
|
|
984
|
+
const countFiltered = countStale + countNoCallsign + countInvalidMMSI + countInvalidNameAndMMSI+countBaseStations;
|
|
985
|
+
(countFiltered > 0) &&
|
|
986
|
+
app.debug(
|
|
987
|
+
`Remaining vessels after filtering: ${filtered.length} (Skipped: ${
|
|
988
|
+
[
|
|
989
|
+
`Total: ${countFiltered}`,
|
|
990
|
+
countStale > 0 ? `Stale: ${countStale}` : "",
|
|
991
|
+
countNoCallsign > 0 ? `No Callsign: ${countNoCallsign}` : "",
|
|
992
|
+
countBaseStations > 0 ? `Base Stations: ${countBaseStations}` : "",
|
|
993
|
+
countInvalidMMSI > 0 ? `Invalid MMSI: ${countInvalidMMSI}` : "",
|
|
994
|
+
countInvalidNameAndMMSI > 0 ? `No Name & Callsign: ${countInvalidNameAndMMSI}` : ""
|
|
995
|
+
].filter(Boolean).join(", ")
|
|
996
|
+
})`
|
|
997
|
+
);
|
|
855
998
|
return filtered;
|
|
856
999
|
}
|
|
857
1000
|
|
|
858
|
-
function processVessels(options, reason) {
|
|
859
|
-
|
|
1001
|
+
async function processVessels(options, reason) {
|
|
1002
|
+
try {
|
|
1003
|
+
// Prüfen ob AIS Fleet Plugin installiert/aktiviert ist
|
|
1004
|
+
let aisfleetEnabled = false;
|
|
1005
|
+
try {
|
|
1006
|
+
const aisResponse = await fetch(signalkAisfleetUrl);
|
|
1007
|
+
if (aisResponse.ok) {
|
|
1008
|
+
const aisData = await aisResponse.json();
|
|
1009
|
+
aisfleetEnabled = !!aisData.enabled;
|
|
1010
|
+
}
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
app.debug("AIS Fleet plugin not installed or unreachable:", err);
|
|
1013
|
+
}
|
|
1014
|
+
getVessels(options,aisfleetEnabled).then(vessels => {
|
|
860
1015
|
try {
|
|
861
1016
|
const filtered = filterVessels(vessels, options);
|
|
862
1017
|
|
|
@@ -867,7 +1022,7 @@ function getVessels(options) {
|
|
|
867
1022
|
cleanupMaps(currentMMSIs, options);
|
|
868
1023
|
|
|
869
1024
|
messageIdCounter = (messageIdCounter + 1) % 10;
|
|
870
|
-
const hasNewClients =
|
|
1025
|
+
const hasNewClients = newTcpClients.length > 0 || newWSClients.length > 0;
|
|
871
1026
|
const nowTimestamp = Date.now();
|
|
872
1027
|
const vesselFinderUpdateDue = options.vesselFinderEnabled &&
|
|
873
1028
|
(nowTimestamp - vesselFinderLastUpdate) >= (options.vesselFinderUpdateRate * 1000);
|
|
@@ -884,7 +1039,7 @@ function getVessels(options) {
|
|
|
884
1039
|
headingTrue: vessel.navigation?.headingTrue,
|
|
885
1040
|
state: vessel.navigation?.state,
|
|
886
1041
|
name: vessel.name,
|
|
887
|
-
|
|
1042
|
+
callsign: vessel.callsign
|
|
888
1043
|
});
|
|
889
1044
|
|
|
890
1045
|
const previousState = previousVesselsState.get(vessel.mmsi);
|
|
@@ -900,39 +1055,36 @@ function getVessels(options) {
|
|
|
900
1055
|
return;
|
|
901
1056
|
}
|
|
902
1057
|
|
|
903
|
-
// Filtere Schiffe ohne Name und Callsign aus
|
|
904
|
-
const hasValidName = vessel.name && vessel.name.toLowerCase() !== 'unknown';
|
|
905
|
-
const hasValidCallsign = vessel.callSign && vessel.callSign.toLowerCase() !== 'unknown' && vessel.callSign !== '';
|
|
906
|
-
|
|
907
|
-
if (!hasValidName && !hasValidCallsign) {
|
|
908
|
-
if (options.logDebugDetails || (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
|
|
909
|
-
app.debug(`Skipped (no valid name and no valid callsign): ${vessel.mmsi} - Name: "${vessel.name}", Callsign: "${vessel.callSign}"`);
|
|
910
|
-
}
|
|
911
|
-
unchangedCount++;
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
1058
|
previousVesselsState.set(vessel.mmsi, currentState);
|
|
916
1059
|
|
|
917
1060
|
// Bestimme ob an TCP gesendet werden soll
|
|
918
1061
|
const sendToTCP = hasChanged || needsTCPResend;
|
|
919
1062
|
|
|
920
1063
|
// Debug-Logging für spezifische MMSI
|
|
921
|
-
const shouldLogDebug =
|
|
922
|
-
|
|
923
|
-
//
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
1064
|
+
const shouldLogDebug = options.logDebugDetails && options.logDebugAIS && (!options.logMMSI || vessel.mmsi === options.logMMSI);
|
|
1065
|
+
|
|
1066
|
+
// AIS-Klasse aus Vessel lesen
|
|
1067
|
+
const aisClass = vessel?.sensors?.ais?.class?.value;
|
|
1068
|
+
// Standard: Class A (Type 1), wenn nichts gesetzt ist
|
|
1069
|
+
const isClassA = !aisClass || aisClass.trim() === '' || aisClass.toUpperCase() === 'A';
|
|
1070
|
+
// Payload je nach Klasse erzeugen
|
|
1071
|
+
const payload = isClassA
|
|
1072
|
+
? encoder.createPositionReportType1(vessel, options)
|
|
1073
|
+
: encoder.createPositionReportType19(vessel, options);
|
|
1074
|
+
if (payload) {
|
|
1075
|
+
const sentence = encoder.createNMEASentence(payload, 1, 1, messageIdCounter, 'B');
|
|
927
1076
|
|
|
928
1077
|
if (shouldLogDebug) {
|
|
929
|
-
app.debug(`
|
|
1078
|
+
app.debug(`MMSI ${vessel.mmsi} - Type ${isClassA?'1':'19'}: ${sentence}`);
|
|
930
1079
|
}
|
|
931
1080
|
if (sendToTCP) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1081
|
+
broadcastTCP(sentence);
|
|
1082
|
+
broadcastWebSocket(sentence);
|
|
1083
|
+
// Zusätzliche Übertragung des kompletten Vessel object
|
|
1084
|
+
broadcastWebSocket(JSON.stringify(vessel));
|
|
1085
|
+
sentCount++;
|
|
1086
|
+
lastTCPBroadcast.set(vessel.mmsi, nowTimestamp);
|
|
1087
|
+
}
|
|
936
1088
|
|
|
937
1089
|
// VesselFinder: nur Type 1 Nachrichten und nur wenn Position nicht älter als 5 Minuten
|
|
938
1090
|
if (vesselFinderUpdateDue) {
|
|
@@ -946,7 +1098,7 @@ function getVessels(options) {
|
|
|
946
1098
|
const fiveMinutes = 5 * 60 * 1000;
|
|
947
1099
|
|
|
948
1100
|
if (posAge <= fiveMinutes) {
|
|
949
|
-
sendToVesselFinder(
|
|
1101
|
+
sendToVesselFinder(sentence, options);
|
|
950
1102
|
vesselFinderCount++;
|
|
951
1103
|
} else if (shouldLogDebug) {
|
|
952
1104
|
app.debug(`[${vessel.mmsi}] Skipped VesselFinder (position age: ${Math.floor(posAge/60000)}min)`);
|
|
@@ -955,46 +1107,73 @@ function getVessels(options) {
|
|
|
955
1107
|
}
|
|
956
1108
|
}
|
|
957
1109
|
|
|
958
|
-
// Type 5 - nur an TCP Clients, NICHT an VesselFinder
|
|
959
|
-
const
|
|
960
|
-
(vessel.
|
|
1110
|
+
// Type 5 (Class A) bzw. Type 24 (Class B) - nur an TCP Clients, NICHT an VesselFinder
|
|
1111
|
+
const shouldSendStaticVoyage = vessel.callsign && vessel.callsign.length > 0 &&
|
|
1112
|
+
(vessel.callsign.toLowerCase() !== 'unknown' || !options.skipWithoutCallsign);
|
|
961
1113
|
|
|
962
|
-
if (
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1114
|
+
if (shouldSendStaticVoyage && sendToTCP) {
|
|
1115
|
+
if (isClassA) {
|
|
1116
|
+
// --- Class A: Type 5 ---
|
|
1117
|
+
const payload5 = encoder.createStaticVoyage(vessel);
|
|
1118
|
+
if (payload5) {
|
|
1119
|
+
if (payload5.length <= 62) {
|
|
1120
|
+
const sentence5 = encoder.createNMEASentence(payload5, 1, 1, messageIdCounter, 'B');
|
|
1121
|
+
if (shouldLogDebug) {
|
|
1122
|
+
app.debug(`[${vessel.mmsi}] Type 5: ${sentence5}`);
|
|
1123
|
+
}
|
|
1124
|
+
broadcastTCP(sentence5);
|
|
1125
|
+
broadcastWebSocket(sentence5);
|
|
1126
|
+
} else {
|
|
1127
|
+
const fragment1 = payload5.substring(0, 62);
|
|
1128
|
+
const fragment2 = payload5.substring(62);
|
|
1129
|
+
const sentence5_1 = encoder.createNMEASentence(fragment1, 2, 1, messageIdCounter, 'B');
|
|
1130
|
+
const sentence5_2 = encoder.createNMEASentence(fragment2, 2, 2, messageIdCounter, 'B');
|
|
1131
|
+
if (shouldLogDebug) {
|
|
1132
|
+
app.debug(`[${vessel.mmsi}] Type 5 (1/2): ${sentence5_1}`);
|
|
1133
|
+
app.debug(`[${vessel.mmsi}] Type 5 (2/2): ${sentence5_2}`);
|
|
1134
|
+
}
|
|
1135
|
+
broadcastTCP(sentence5_1);
|
|
1136
|
+
broadcastWebSocket(sentence5_1);
|
|
1137
|
+
broadcastTCP(sentence5_2);
|
|
1138
|
+
broadcastWebSocket(sentence5_2);
|
|
1139
|
+
}
|
|
970
1140
|
}
|
|
971
|
-
|
|
972
|
-
broadcastTCP(sentence5);
|
|
973
1141
|
} else {
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1142
|
+
// --- Class B: Type 24 ---
|
|
1143
|
+
const payload24 = encoder.createStaticVoyageType24(vessel);
|
|
1144
|
+
if (payload24) {
|
|
1145
|
+
// Part A
|
|
1146
|
+
const sentence24A = encoder.createNMEASentence(payload24.partA, 1, 1, messageIdCounter, 'B');
|
|
1147
|
+
if (shouldLogDebug) {
|
|
1148
|
+
app.debug(`[${vessel.mmsi}] Type 24 Part A: ${sentence24A}`);
|
|
1149
|
+
}
|
|
1150
|
+
broadcastTCP(sentence24A);
|
|
1151
|
+
broadcastWebSocket(sentence24A);
|
|
1152
|
+
|
|
1153
|
+
// Part B
|
|
1154
|
+
const sentence24B = encoder.createNMEASentence(payload24.partB, 1, 1, messageIdCounter, 'B');
|
|
1155
|
+
if (shouldLogDebug) {
|
|
1156
|
+
app.debug(`[${vessel.mmsi}] Type 24 Part B: ${sentence24B}`);
|
|
1157
|
+
}
|
|
1158
|
+
broadcastTCP(sentence24B);
|
|
1159
|
+
broadcastWebSocket(sentence24B);
|
|
982
1160
|
}
|
|
983
|
-
|
|
984
|
-
broadcastTCP(sentence5_1);
|
|
985
|
-
broadcastTCP(sentence5_2);
|
|
986
1161
|
}
|
|
987
|
-
}
|
|
988
1162
|
}
|
|
989
1163
|
});
|
|
990
1164
|
|
|
991
1165
|
if (hasNewClients) {
|
|
992
|
-
|
|
1166
|
+
if (newTcpClients.length > 0) {
|
|
1167
|
+
newTcpClients = [];
|
|
1168
|
+
}
|
|
1169
|
+
if (newWSClients.length > 0) {
|
|
1170
|
+
newWSClients = [];
|
|
1171
|
+
}
|
|
993
1172
|
}
|
|
994
1173
|
|
|
995
1174
|
if (vesselFinderUpdateDue) {
|
|
996
1175
|
vesselFinderLastUpdate = nowTimestamp;
|
|
997
|
-
app.debug(`VesselFinder: sent ${vesselFinderCount} vessels
|
|
1176
|
+
app.debug(`VesselFinder: sent ${vesselFinderCount} vessels changed in last ${options.vesselFinderUpdateRate} seconds.`);
|
|
998
1177
|
}
|
|
999
1178
|
|
|
1000
1179
|
app.debug(`${reason}: sent ${sentCount}, unchanged ${unchangedCount}, clients ${tcpClients.length}`);
|
|
@@ -1005,6 +1184,9 @@ function getVessels(options) {
|
|
|
1005
1184
|
}).catch(err => {
|
|
1006
1185
|
app.error(`Error in processVessels: ${err}`);
|
|
1007
1186
|
});
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
app.error(`Error in processVessels outer try: ${err}`);
|
|
1189
|
+
}
|
|
1008
1190
|
}
|
|
1009
1191
|
|
|
1010
1192
|
function cleanupMaps(currentMMSIs, options) {
|