signalk-ais-navionics-converter 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 formifan2002
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # SignalK AIS to NMEA 0183 Converter
2
+
3
+ A SignalK plugin that converts AIS vessel data to NMEA 0183 sentences and broadcasts them via TCP to clients like 'Navionics Boating App' or 'OpenCpn'. Optionally forwards AIS data to VesselFinder.com and integrates cloud vessel data from AISFleet.com.
4
+
5
+ It is intended for vessels that do not have their own AIS receiver on board.
6
+
7
+ IMPORTANT: The following are required for the plugin to function:
8
+ - a valid GPS position (visible in the SignalK Data Browser) in navigation.position
9
+ - MMSI of your own vessel maintained in SignalK server settings (Server → Settings)
10
+
11
+ ## Content
12
+
13
+ - [Features](#features)
14
+ - [Installation](#installation)
15
+ - [Configuration Parameters](#configuration-parameters)
16
+ - [Basic Settings](#basic-settings)
17
+ - [TCP Port](#tcp-port)
18
+ - [Update Interval for Changed Vessels](#update-interval-for-changed-vessels)
19
+ - [Update Interval for Unchanged Vessels](#update-interval-for-unchanged-vessels)
20
+ - [Data Filtering](#data-filtering)
21
+ - [Skip Vessels Without Callsign](#skip-vessels-without-callsign)
22
+ - [Skip Vessels With Stale Data](#skip-vessels-with-stale-data)
23
+ - [Stale Data Threshold](#stale-data-threshold)
24
+ - [Timestamp Added to Ship Name](#timestamp-added-to-ship-name)
25
+ - [Speed Over Ground (SOG) Correction](#speed-over-ground-sog-correction)
26
+ - [Minimum SOG for Alarm](#minimum-sog-for-alarm)
27
+ - [Maximum Minutes Before SOG Set to Zero](#maximum-minutes-before-sog-set-to-zero)
28
+ - [VesselFinder Integration](#vesselfinder-integration)
29
+ - [Enable VesselFinder Forwarding](#enable-vesselfinder-forwarding)
30
+ - [VesselFinder Host](#vesselfinder-host)
31
+ - [VesselFinder UDP Port](#vesselfinder-udp-port)
32
+ - [VesselFinder Update Rate](#vesselfinder-update-rate)
33
+ - [AISFleet Cloud Integration](#aisfleet-cloud-integration)
34
+ - [Include Vessels from AISFleetcom](#include-vessels-from-aisfleetcom)
35
+ - [Radius for Cloud Vessels](#radius-for-cloud-vessels)
36
+ - [Debug Options](#debug-options)
37
+ - [Debug All Vessel Details](#debug-all-vessel-details)
38
+ - [Debug MMSI](#debug-mmsi)
39
+ - [Debug Stale](#Debug-stale)
40
+ - [Debug JSON](#debug-json)
41
+ - [Debug AIS](#debug-ais-data)
42
+ - [Debug corrected SOG](#debug-corrected-sog)
43
+ - [How It Works](#how-it-works)
44
+ - [Data Flow](#data-flow)
45
+ - [Merge Logic](#merge-logic)
46
+ - [Client Connection Handling](#client-connection-handling)
47
+ - [Usage with Navionics Boating App](#usage-with-navionics-boating-app)
48
+ - [Usage with OpenCpn](#usage-with-openCpn)
49
+ - [Troubleshooting](#troubleshooting)
50
+ - [No vessels appear in navigation app](#no-vessels-appear-in-navigation-app)
51
+ - [Vessels disappear after a while](#vessels-disappear-after-a-while)
52
+ - [False collision warnings](#false-collision-warnings)
53
+ - [Too manyfew vessels](#too-manyfew-vessels)
54
+ - [Debug Logging](#debug-logging)
55
+ - [Requirements](#requirements)
56
+ - [License](#license)
57
+ - [Author](#author)
58
+ - [Contributing](#contributing)
59
+ - [Changelog](#changelog)
60
+ - [Version 1.0.0](#version-100)
61
+
62
+ ## Features
63
+
64
+ - **TCP Server**: Broadcasts NMEA 0183 AIS messages ([Type 1 - Position report](https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a) and [Type 5 - static and voyage related data](https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_5_static_and_voyage_related_data)) to connected clients
65
+ - **Smart Update Logic**:
66
+ - Sends updates immediately when vessel data changes
67
+ - Resends unchanged vessels periodically to prevent timeout in navigation apps
68
+ - Sends complete dataset to newly connected clients
69
+ - **Cloud Integration**: Fetches nearby vessels from AISFleet.com API and merges with local AIS data
70
+ - **Data Quality**:
71
+ - Filters stale data based on configurable age threshold
72
+ - Corrects invalid COG/Heading values (360° → 0°)
73
+ - Optionally sets SOG to 0 for vessels with outdated position data
74
+ - Adds timestamp suffix to vessel names with old position data
75
+ - **VesselFinder.com Support**: Optional UDP forwarding of AIS Type 1 messages
76
+ - **Flexible Filtering**: Skip vessels without callsign or valid identification
77
+ - **Debug Options**: Detailed logging for specific MMSI or all vessels
78
+
79
+ Warning: AIS data provided by this plugin can assist navigation. However, it never replaces the skipper's responsibility. I exclude any liability for damages resulting from the use of this plugin.
80
+
81
+ ## Installation
82
+
83
+ 1. Install via SignalK App Store
84
+ 2. Restart SignalK server
85
+ 3. Configure the plugin in SignalK admin interface → Server → Plugin Config → AIS to NMEA 0183 converter for TPC clients (e.g. Navionics)
86
+
87
+ ## Configuration Parameters
88
+
89
+ ### Basic Settings
90
+
91
+ #### TCP Port
92
+ - **Default**: 10113
93
+ - **Description**: Port for the NMEA 0183 TCP server. Configure your navigation app (e.g., Navionics, OpenCpn) to connect to this port.
94
+
95
+ #### Update Interval for Changed Vessels
96
+ - **Default**: 15 (seconds)
97
+ - **Description**: How often to check for and send updates when vessel data changes. Lower values = more real-time updates but higher CPU usage.
98
+
99
+ #### Update Interval for Unchanged Vessels
100
+ - **Default**: 60 (seconds)
101
+ - **Description**: How often to resend data for vessels that haven't changed. Important for apps like Navionics to prevent vessels from disappearing. Set to 0 to disable (not recommended).
102
+
103
+ ### Data Filtering
104
+
105
+ #### Skip Vessels Without Callsign
106
+ - **Default**: false
107
+ - **Description**: When enabled, vessels without a callsign will not be transmitted.
108
+
109
+ #### Skip Vessels With Stale Data
110
+ - **Default**: true
111
+ - **Description**: When enabled, vessels with outdated position data will not be transmitted.
112
+
113
+ #### Stale Data Threshold
114
+ - **Default**: 60 (minutes)
115
+ - **Description**: Position data older than this threshold will be considered stale and filtered out (if "Skip Vessels With Stale Data" is enabled).
116
+
117
+ #### Timestamp Added to Ship Name
118
+ - **Default**: 5 (minutes, 0=disabled)
119
+ - **Description**: Adds a timestamp suffix to the vessel name if position data is older than specified minutes. Format: `SHIPNAME MIN15`, `SHIPNAME HOUR2`, or `SHIPNAME DAY3`. Vessel names are truncated to 20 characters.
120
+
121
+ ### Speed Over Ground (SOG) Correction
122
+
123
+ #### Minimum SOG for Alarm
124
+ - **Default**: 0.2 (m/s)
125
+ - **Description**: SOG values below this threshold will be set to 0. Used by the AIS encoder.
126
+
127
+ #### Maximum Minutes Before SOG Set to Zero
128
+ - **Default**: 0 (minutes, 0=disabled)
129
+ - **Description**: Automatically sets SOG to 0 for vessels whose position timestamp is older than specified minutes. Prevents false collision warnings in navigation apps for vessels with outdated data. Set to 0 to disable this feature.
130
+
131
+ ### VesselFinder Integration
132
+
133
+ #### Enable VesselFinder Forwarding
134
+ - **Default**: false
135
+ - **Description**: When enabled, AIS Type 1 messages (position reports) are forwarded to VesselFinder.com via UDP.
136
+
137
+ #### VesselFinder Host
138
+ - **Default**: ais.vesselfinder.com
139
+ - **Description**: Hostname for VesselFinder UDP server.
140
+
141
+ #### VesselFinder UDP Port
142
+ - **Default**: 5500
143
+ - **Description**: UDP port for VesselFinder server.
144
+
145
+ #### VesselFinder Update Rate
146
+ - **Default**: 60 (seconds)
147
+ - **Description**: How often to send position updates to VesselFinder.
148
+
149
+ ### AISFleet Cloud Integration
150
+
151
+ #### Include Vessels from AISFleet.com
152
+ - **Default**: true
153
+ - **Description**: Fetches nearby vessels from AISFleet.com cloud API and merges them with local SignalK vessel data. Requires internet connection and own position.
154
+
155
+ #### Radius for Cloud Vessels
156
+ - **Default**: 10 (nm / nautical miles)
157
+ - **Description**: Radius around own vessel position to fetch cloud vessels from AISFleet.com (1-100 nm).
158
+
159
+ ### Debug Options
160
+
161
+ #### Debug All Vessel Details
162
+ - **Default**: false
163
+ - **Description**: Enables detailed debug logging for all vessels in the server log. Only visible when plugin is in debug mode. Useful for troubleshooting or understanding of send data.
164
+
165
+ #### Debug MMSI
166
+ - **Default**: empty
167
+ - **Description**: MMSI number for detailed debug output of a specific vessel. Only visible when plugin is in debug mode. Leave empty to disable.
168
+
169
+ #### Debug Stale
170
+ - **Default**: false
171
+ - **Description**: Stale vessels will be shown in debug log - only visible if plugin is in debug mode and debug all vessel details is enabled
172
+
173
+ #### Debug JSON
174
+ - **Default**: false
175
+ - **Description**: JSON data of vessels will be shown in debug log - only visible if plugin is in debug mode and debug all vessel details is enabled
176
+
177
+ #### Debug AIS data
178
+ - **Default**: false
179
+ - **Description**: Detailed debug AIS data output in server log for all vessels - only visible if plugin is in debug mode and debug all vessel details is enabled
180
+
181
+ #### Debug corrected SOG
182
+ - **Default**: false
183
+ - **Description**: Detailed debug output in server log for all vessels with corrected SOG - only visible if plugin is in debug mode and debug all vessel details is enabled
184
+
185
+ ## How It Works
186
+
187
+ ### Data Flow
188
+
189
+ 1. **Data Collection**:
190
+ - Fetches vessel data from SignalK API (`http://<IP_OF_SIGNALK_SERVER>:<PORT_SIGNALK_SERVER>/signalk/v1/api/vessels`)
191
+ - use other SignalK plugins (like AisHub WS) to receive more vessels for SignalK with AIS information
192
+ - Optionally fetches nearby vessels from AISFleet.com API
193
+ - Merges both sources, preferring newer timestamps
194
+
195
+ 2. **Data Processing**:
196
+ - Filters out stale data based on age threshold
197
+ - Corrects invalid navigation values (COG 360° → 0°)
198
+ - Applies SOG correction for outdated positions
199
+ - Filters vessels without valid name AND callsign
200
+
201
+ 3. **NMEA Generation**:
202
+ - Creates AIS Type 1 messages (position reports)
203
+ - Creates AIS Type 5 messages (static vessel data)
204
+ - Encodes as NMEA 0183 sentences
205
+
206
+ 4. **Broadcasting**:
207
+ - Sends to all connected TCP clients
208
+ - Optionally forwards to VesselFinder.com via UDP
209
+ - Resends periodically to prevent client timeouts
210
+
211
+ ### Merge Logic
212
+
213
+ When the same vessel exists in both SignalK and AISFleet cloud data:
214
+ - **Position/Navigation**: Uses data with newer timestamp
215
+ - **Name**: Prefers any non-"Unknown" value
216
+ - **Callsign**: Prefers any non-empty value
217
+
218
+ ### Client Connection Handling
219
+
220
+ When a new TCP client connects:
221
+ - Complete dataset of all vessels is sent immediately
222
+ - Client appears in "clients" count in logs
223
+ - Ensures navigation app shows all vessels right away
224
+
225
+ ## Usage with Navionics Boating App
226
+
227
+ 1. Configure plugin with desired settings
228
+ 2. In Navionics app:
229
+ - Go to Menu → Connected devices
230
+ - Add new connection (via + sign in upper right corner)
231
+ - Enter name for the connection (e.g. SignalkAIS)
232
+ - Enter SignalK server IP address
233
+ - Enter configured TCP port (default: 10113 - same setting like in SignalK plugin)
234
+ - Select TCP (not UDP)
235
+ - Save connection
236
+ - Go to Menu → Chart options → AIS settings
237
+ - turn 'Display AIS targets' on
238
+ 3. Vessels should appear on chart immediately and in 'Connected devices' you should see as status for the connection: 'connected' (in green) and 'AIS data reception'
239
+
240
+ ## Usage with OpenCpn
241
+
242
+ 1. Configure plugin with desired settings
243
+ 2. In OpenCpn:
244
+ - Go to Menu → Tools → Options → Connections
245
+ - Add new connection as follows:
246
+ ![OpenCPN Screenshot1](img/OpenCpn1.png)
247
+ ![OpenCPN Screenshot2](img/OpenCpn2.png)
248
+ - Save connection
249
+ - Go to Menu → AIS
250
+ - enable AIS targets
251
+ 3. Vessels should appear on chart immediately
252
+
253
+ ## Troubleshooting
254
+
255
+ ### No vessels appear in navigation app
256
+ - Check that plugin is enabled and TCP server is running
257
+ - Verify TCP port in app matches plugin configuration
258
+ - Check firewall settings on SignalK server
259
+ - Enable debug logging to see if vessels are being sent
260
+
261
+ ### Vessels disappear after a while
262
+ - Increase "Update Interval for Unchanged Vessels" (recommended: 60 seconds)
263
+ - Check if "Skip Vessels With Stale Data" is filtering out vessels
264
+ - Increase "Stale Data Threshold" if needed
265
+
266
+ ### False collision warnings
267
+ - Enable "Maximum Minutes Before SOG Set to Zero"
268
+ - Set appropriate threshold (e.g., 5-10 minutes)
269
+ - This prevents CPA calculations for vessels with outdated position
270
+
271
+ ### Too many/few vessels
272
+ - Adjust "Stale Data Threshold" to filter outdated vessels
273
+ - Enable/disable "Include Vessels from AISFleet.com"
274
+ - Adjust "Radius for Cloud Vessels" to control area coverage
275
+
276
+ ## Debug Logging
277
+
278
+ To enable debug output:
279
+ 1. In SignalK admin interface: Server → Settings → Logging
280
+ 2. Change setting for plugin `AIS to NMEA 0183 converter for TPC clients (e.g. Navionics)` to debug logging
281
+ 3. Restart SignalK
282
+ 4. Check logs in Server → Logs
283
+
284
+ Debug information includes:
285
+ - Number of vessels sent/unchanged
286
+ - Connected clients count
287
+ - Vessel filtering reasons
288
+ - AIS sentence output (when Debug MMSI is set)
289
+
290
+ ## Requirements
291
+
292
+ - SignalK server (Node.js version)
293
+ - Own vessel position (for AISFleet cloud integration)
294
+ - Own vessel MMSI
295
+ - Internet connection (for AISFleet cloud features)
296
+ - Navigation app with NMEA 0183 TCP support (e.g. Navionics boating app or OpenCpn)
297
+
298
+ ## License
299
+
300
+ MIT
301
+
302
+ ## Author
303
+
304
+ Dirk Behrendt
305
+
306
+ ## Contributing
307
+
308
+ Issues and pull requests are welcome!
309
+
310
+ ## Changelog
311
+
312
+ ### Version 1.0.0
313
+ - Initial release
314
+ - TCP server for NMEA 0183 AIS broadcasting
315
+ - SignalK and AISFleet cloud data integration
316
+ - VesselFinder.com UDP forwarding
317
+ - Smart filtering and data correction
318
+ - Configurable update intervals
package/ais-encoder.js ADDED
@@ -0,0 +1,222 @@
1
+ class AISEncoder {
2
+ static encode6bit(val) {
3
+ if (val < 0 || val > 63) throw new Error("6-bit out of range: " + val);
4
+ return val <= 39 ? String.fromCharCode(val + 48) : String.fromCharCode(val + 56);
5
+ }
6
+
7
+ static toTwosComplement(value, bits) {
8
+ if (value < 0) value = (1 << bits) + value;
9
+ return value;
10
+ }
11
+
12
+ static textToSixBit(str, length) {
13
+ const table = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !"#$%&\'()*+,-./0123456789:;<=>?';
14
+ let bits = '';
15
+ str = str || '';
16
+ for (let i = 0; i < length; i++) {
17
+ let c = i < str.length ? str[i].toUpperCase() : '@';
18
+ let idx = table.indexOf(c);
19
+ if (idx < 0) idx = 0;
20
+ bits += idx.toString(2).padStart(6, '0');
21
+ }
22
+ return bits;
23
+ }
24
+
25
+ static callsignToSixBit(callsign) {
26
+ callsign = (callsign || '').trim().toUpperCase();
27
+ const padded = callsign.padEnd(7, '@').substring(0, 7);
28
+ return this.textToSixBit(padded, 7);
29
+ }
30
+
31
+ static bitsToPayload(bits) {
32
+ let payload = '';
33
+ while (bits.length % 6 !== 0) {
34
+ bits += '0';
35
+ }
36
+
37
+ for (let i = 0; i < bits.length; i += 6) {
38
+ let chunk = bits.substring(i, i + 6);
39
+ let val = parseInt(chunk, 2);
40
+ payload += this.encode6bit(val);
41
+ }
42
+ return payload;
43
+ }
44
+
45
+ static calculateChecksum(nmea) {
46
+ let cs = 0;
47
+ for (let i = 1; i < nmea.length; i++) cs ^= nmea.charCodeAt(i);
48
+ return cs.toString(16).toUpperCase().padStart(2, '0');
49
+ }
50
+
51
+ static createPositionReport(vessel, config) {
52
+ try {
53
+ const mmsi = parseInt(vessel.mmsi);
54
+ if (!mmsi || mmsi === 0) return null;
55
+
56
+ const nav = vessel.navigation || {};
57
+ const pos = nav.position?.value || nav.position || {};
58
+ const latitude = pos.latitude;
59
+ const longitude = pos.longitude;
60
+ if (latitude === undefined || longitude === undefined) return null;
61
+
62
+ const state = nav.state?.value || '';
63
+ let navStatus = 15;
64
+ const stateMap = {
65
+ 'motoring': 0, 'anchored': 1, 'not under command': 2, 'restricted maneuverability': 3,
66
+ 'constrained by draft': 4, 'moored': 5, 'aground': 6, 'fishing': 7,
67
+ 'sailing': 8, 'hazardous material high speed': 9, 'hazardous material wing in ground': 10,
68
+ 'power-driven vessel towing astern': 11, 'power-driven vessel pushing ahead': 12,
69
+ 'reserved': 13, 'ais-sart': 14, 'undefined': 15
70
+ };
71
+ if (state && stateMap[state] !== undefined) navStatus = stateMap[state];
72
+
73
+ const timestamp = 60;
74
+ const raim = 0;
75
+ const maneuver = 0;
76
+
77
+ const rateOfTurn = nav.rateOfTurn?.value || 0;
78
+ let rot = -128;
79
+ if (rateOfTurn !== 0) {
80
+ rot = Math.round(rateOfTurn * 4.733 * Math.sqrt(Math.abs(rateOfTurn)));
81
+ rot = Math.max(-126, Math.min(126, rot));
82
+ }
83
+
84
+ const sog = nav.speedOverGround?.value || nav.speedOverGround || 0;
85
+ const cog = nav.courseOverGroundTrue?.value || nav.courseOverGroundTrue || 0;
86
+ const heading = nav.headingTrue?.value || nav.headingTrue || 0;
87
+
88
+ let sogValue = typeof sog === 'number' ? sog : 0;
89
+ if (sogValue < config.minAlarmSOG) sogValue = 0;
90
+
91
+ const cogValue = typeof cog === 'object' ? 0 : (typeof cog === 'number' ? cog : 0);
92
+ const headingValue = typeof heading === 'object' ? 0 : (typeof heading === 'number' ? heading : 0);
93
+
94
+ const lon = Math.round(longitude * 600000);
95
+ const lat = Math.round(latitude * 600000);
96
+
97
+ const sogKnots = sogValue * 1.94384;
98
+ const sog10 = Math.round(sogKnots * 10);
99
+
100
+ const cogDegrees = cogValue * 180 / Math.PI;
101
+ let cog10;
102
+ if (sogKnots < config.minAlarmSOG) {
103
+ cog10 = 0; // or 3600 to indicate not available
104
+ } else {
105
+ cog10 = Math.round(cogDegrees * 10);
106
+ }
107
+
108
+ const headingDegrees = headingValue * 180 / Math.PI;
109
+ const headingInt = Math.round(headingDegrees);
110
+
111
+ let bits = '';
112
+ bits += (1).toString(2).padStart(6, '0');
113
+ bits += (0).toString(2).padStart(2, '0');
114
+ bits += mmsi.toString(2).padStart(30, '0');
115
+ bits += navStatus.toString(2).padStart(4, '0');
116
+ bits += this.toTwosComplement(rot, 8).toString(2).padStart(8, '0');
117
+ bits += sog10.toString(2).padStart(10, '0');
118
+ bits += '0';
119
+ bits += this.toTwosComplement(lon, 28).toString(2).padStart(28, '0');
120
+ bits += this.toTwosComplement(lat, 27).toString(2).padStart(27, '0');
121
+ bits += cog10.toString(2).padStart(12, '0');
122
+ bits += headingInt.toString(2).padStart(9, '0');
123
+ bits += timestamp.toString(2).padStart(6, '0');
124
+ bits += maneuver.toString(2).padStart(2, '0');
125
+ bits += '000';
126
+ bits += raim.toString();
127
+ bits += '0000000000000000000';
128
+
129
+ return this.bitsToPayload(bits);
130
+ } catch (error) {
131
+ console.error('Error creating position report:', error);
132
+ return null;
133
+ }
134
+ }
135
+
136
+ static createStaticVoyage(vessel) {
137
+ try {
138
+ const mmsi = parseInt(vessel.mmsi);
139
+ if (!mmsi || mmsi === 0) return null;
140
+
141
+ const design = vessel.design || {};
142
+ const length = design.length?.value?.overall || 0;
143
+ const beam = design.beam?.value || 0;
144
+ const draft = design.draft?.value?.maximum || 0;
145
+ const shipType = design.aisShipType?.value?.id || 0;
146
+
147
+ const imo = parseInt(vessel.imo) || 0;
148
+ const aisVersion = 0;
149
+
150
+ const ais = vessel.sensors?.ais || {};
151
+ const fromBow = ais.fromBow?.value || 0;
152
+ const fromCenter = ais.fromCenter?.value || 0;
153
+
154
+ const toBow = Math.round(fromBow);
155
+ const toStern = Math.round(Math.max(0, length - fromBow));
156
+ const toPort = Math.round(Math.max(0, beam / 2 - fromCenter));
157
+ const toStarboard = Math.round(Math.max(0, beam / 2 + fromCenter));
158
+
159
+ let epfd = 1;
160
+ const positionSource = vessel.navigation?.position?.$source || '';
161
+ if (positionSource.includes('gps')) epfd = 1;
162
+ else if (positionSource.includes('gnss')) epfd = 1;
163
+ else if (positionSource.includes('glonass')) epfd = 2;
164
+ else if (positionSource.includes('galileo')) epfd = 3;
165
+
166
+ const destination = vessel.navigation?.destination?.commonName?.value || '';
167
+ const etaString = vessel.navigation?.courseGreatCircle?.activeRoute?.estimatedTimeOfArrival?.value || '';
168
+
169
+ let etaMonth = 0, etaDay = 0, etaHour = 24, etaMinute = 60;
170
+ if (etaString && etaString !== '00-00T00:00Z' && etaString !== '00-00T24:60Z') {
171
+ const etaMatch = etaString.match(/(\d+)-(\d+)T(\d+):(\d+)/);
172
+ if (etaMatch) {
173
+ etaMonth = parseInt(etaMatch[1]) || 0;
174
+ etaDay = parseInt(etaMatch[2]) || 0;
175
+ etaHour = parseInt(etaMatch[3]) || 24;
176
+ etaMinute = parseInt(etaMatch[4]) || 60;
177
+ }
178
+ }
179
+
180
+ const draughtDecimeters = Math.round(draft * 10);
181
+ const dte = 0;
182
+
183
+ let bits = '';
184
+ bits += (5).toString(2).padStart(6,'0');
185
+ bits += (0).toString(2).padStart(2,'0');
186
+ bits += mmsi.toString(2).padStart(30,'0');
187
+ bits += aisVersion.toString(2).padStart(2,'0');
188
+ bits += imo.toString(2).padStart(30,'0');
189
+ bits += this.callsignToSixBit(vessel.callSign ?? '');
190
+ bits += this.textToSixBit(vessel.name ?? '', 20);
191
+ bits += shipType.toString(2).padStart(8,'0');
192
+ bits += toBow.toString(2).padStart(9,'0');
193
+ bits += toStern.toString(2).padStart(9,'0');
194
+ bits += toPort.toString(2).padStart(6,'0');
195
+ bits += toStarboard.toString(2).padStart(6,'0');
196
+ bits += epfd.toString(2).padStart(4,'0');
197
+ bits += etaMonth.toString(2).padStart(4,'0');
198
+ bits += etaDay.toString(2).padStart(5,'0');
199
+ bits += etaHour.toString(2).padStart(5,'0');
200
+ bits += etaMinute.toString(2).padStart(6,'0');
201
+ bits += draughtDecimeters.toString(2).padStart(8,'0');
202
+ bits += this.textToSixBit(destination, 20);
203
+ bits += dte.toString(2);
204
+ bits += '0';
205
+
206
+ return this.bitsToPayload(bits);
207
+ } catch(err) {
208
+ console.error('Error creating type5:', err);
209
+ return null;
210
+ }
211
+ }
212
+
213
+ static createNMEASentence(payload, fragmentCount=1, fragmentNum=1, messageId=null, channel='B') {
214
+ const msgId = messageId !== null ? messageId.toString() : '';
215
+ const fillBits = (6 - (payload.length*6)%6)%6;
216
+ const sentence = `AIVDM,${fragmentCount},${fragmentNum},${msgId},${channel},${payload},${fillBits}`;
217
+ const checksum = this.calculateChecksum('!' + sentence);
218
+ return `!${sentence}*${checksum}`;
219
+ }
220
+ }
221
+
222
+ module.exports = AISEncoder;
Binary file
Binary file