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 +21 -0
- package/README.md +318 -0
- package/ais-encoder.js +222 -0
- package/img/OpenCpn1.png +0 -0
- package/img/OpenCpn2.png +0 -0
- package/index.js +1005 -0
- package/package.json +21 -0
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
|
+

|
|
247
|
+

|
|
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;
|
package/img/OpenCpn1.png
ADDED
|
Binary file
|
package/img/OpenCpn2.png
ADDED
|
Binary file
|