signalk-ais-navionics-converter 1.0.1 → 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/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 newClients = [];
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 all vessel details',
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: 'MMSI for detailed debug output in server log - only visible if plugin is in debug mode',
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
- newClients.push(socket);
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
- newClients = newClients.filter(c => c !== socket);
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
- newClients = newClients.filter(c => c !== socket);
264
+ newTcpClients = newTcpClients.filter(c => c !== socket);
246
265
  });
247
266
  });
248
267
 
249
268
  tcpServer.listen(options.tcpPort, () => {
250
- app.debug(`NMEA TCP Server listening on port ${options.tcpPort}`);
251
- app.setPluginStatus(`TCP Server running on port ${options.tcpPort}`);
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, 'Startup');
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); // ← Wichtig: resolve(null) statt reject()
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); // ← Wichtig: resolve(null) statt reject()
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 || vessel1.callSign;
575
- const vessel2Callsign = vessel2.communication?.callsignVhf || vessel2.callsign || 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 Vessels
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 logMMSI = options.logMMSI || '';
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
- // Vessel existiert in beiden Quellen - merge mit Timestamp-Vergleich
734
+ // Schiff existiert in beiden Quellen - merge mit Timestamp-Vergleich
665
735
  merged[vesselId] = mergeVesselData(merged[vesselId], cloudVessel);
666
-
667
736
  if (shouldLog) {
668
- // app.debug(`Merged vessel ${vesselId} (${mmsi}):`);
669
- if ((options.logDebugJSON && options.logDebugDetails) || (logMMSI && logMMSI !== '' && mmsi === logMMSI)){
670
- app.debug(JSON.stringify(merged[vesselId], null, 2));
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
- // Vessel nur in Cloud - direkt hinzufügen
742
+ // Schiff nur in Cloud - direkt hinzufügen
675
743
  merged[vesselId] = cloudVessel;
676
-
677
744
  if (shouldLog) {
678
- // app.debug(`Cloud-only vessel ${vesselId} (${mmsi}):`);
679
- if ((options.logDebugJSON && options.logDebugDetails) || (logMMSI && logMMSI !== '' && mmsi === logMMSI)){
680
- app.debug(JSON.stringify(cloudVessel, null, 2));
681
- }
745
+ app.debug(`Cloud-only vessel ${vesselId} (${mmsi}):`);
746
+ app.debug(JSON.stringify(cloudVessel, null, 2));
747
+ loggedVessels.add(vesselId);
682
748
  }
683
- if (merged[vesselId].name && merged[vesselId].navigation.position.timestamp && options.staleDataShipnameAddTime > 0) {
684
- const timestamp = new Date(merged[vesselId].navigation.position.timestamp); // UTC-Zeit
685
- const nowUTC = new Date(); // aktuelle Zeit (intern ebenfalls UTC-basiert)
686
- const diffMs = nowUTC - timestamp; // Differenz in Millisekunden
687
- const diffMinutes = Math.floor(diffMs / (1000 * 60)); // Umrechnung in Minuten
688
- if (diffMinutes >= options.staleDataShipnameAddTime) {
689
- // Füge Zeitstempel zum Schiffsnamen hinzu und kürze auf 20 Zeichen
690
- let suffix = "";
691
- if (diffMinutes > 1439) { // mehr als 23:59 Minuten → Tage
692
- const days = Math.ceil(diffMinutes / (60 * 24));
693
- suffix = ` DAY${days}`;
694
- } else if (diffMinutes > 59) { // mehr als 59 Minuten → Stunden
695
- const hours = Math.ceil(diffMinutes / 60);
696
- suffix = ` HOUR${hours}`;
697
- } else { // sonst → Minuten
698
- suffix = ` MIN${diffMinutes}`;
699
- }
700
- // Füge Zeitstempel zum Schiffsnamen hinzu und kürze auf 20 Zeichen
701
- merged[vesselId].name = `${merged[vesselId].name}${suffix}`.substring(0, 20);
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 = mergeVesselSources(signalkVessels, cloudVessels, options);
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: vessel.imo || vessel.registrations?.imo || '0'
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
- let callSign = vessel.callsign || '';
774
- const hasRealCallsign = callSign && callSign.length > 0;
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
- callSign = 'UNKNOWN';
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 ((options.logDebugDetails && options.logDebugStale) || (options.logMMSI && vessel.mmsi === options.logMMSI)) {
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 ((options.logDebugDetails && options.logDebugSOG) || (options.logMMSI && vessel.mmsi === options.logMMSI)) {
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 || (!options.logMMSI || vessel.mmsi === options.logMMSI)) {
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
- getVessels(options).then(vessels => {
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 = newClients.length > 0;
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
- callSign: vessel.callSign
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 = (options.logDebugDetails && options.logDebugAIS) || (options.logMMSI && vessel.mmsi === options.logMMSI);
922
-
923
- // Type 1
924
- const payload1 = AISEncoder.createPositionReport(vessel, options);
925
- if (payload1) {
926
- const sentence1 = AISEncoder.createNMEASentence(payload1, 1, 1, messageIdCounter, 'B');
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(`[${vessel.mmsi}] Type 1: ${sentence1}`);
1078
+ app.debug(`MMSI ${vessel.mmsi} - Type ${isClassA?'1':'19'}: ${sentence}`);
930
1079
  }
931
1080
  if (sendToTCP) {
932
- broadcastTCP(sentence1);
933
- sentCount++;
934
- lastTCPBroadcast.set(vessel.mmsi, nowTimestamp);
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(sentence1, options);
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 shouldSendType5 = vessel.callSign && vessel.callSign.length > 0 &&
960
- (vessel.callSign !== 'UNKNOWN' || !options.skipWithoutCallsign);
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 (shouldSendType5 && sendToTCP) {
963
- const payload5 = AISEncoder.createStaticVoyage(vessel);
964
- if (payload5) {
965
- if (payload5.length <= 62) {
966
- const sentence5 = AISEncoder.createNMEASentence(payload5, 1, 1, messageIdCounter, 'B');
967
-
968
- if (shouldLogDebug) {
969
- app.debug(`[${vessel.mmsi}] Type 5: ${sentence5}`);
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
- const fragment1 = payload5.substring(0, 62);
975
- const fragment2 = payload5.substring(62);
976
- const sentence5_1 = AISEncoder.createNMEASentence(fragment1, 2, 1, messageIdCounter, 'B');
977
- const sentence5_2 = AISEncoder.createNMEASentence(fragment2, 2, 2, messageIdCounter, 'B');
978
-
979
- if (shouldLogDebug) {
980
- app.debug(`[${vessel.mmsi}] Type 5 (1/2): ${sentence5_1}`);
981
- app.debug(`[${vessel.mmsi}] Type 5 (2/2): ${sentence5_2}`);
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
- newClients = [];
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) {