signalk-vessels-to-ais 1.6.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/helpers.js ADDED
@@ -0,0 +1,357 @@
1
+ /* eslint-disable no-bitwise */
2
+ /**
3
+ * Helper functions for signalk-vessels-to-ais-ws plugin
4
+ * Extracted for testability
5
+ */
6
+
7
+ // State Mapping - AIS navigation status codes
8
+ const stateMapping = {
9
+ motoring: 0,
10
+ UnderWayUsingEngine: 0,
11
+ 'under way using engine': 0,
12
+ 'underway using engine': 0,
13
+ anchored: 1,
14
+ AtAnchor: 1,
15
+ 'at anchor': 1,
16
+ 'not under command': 2,
17
+ 'restricted manouverability': 3,
18
+ 'constrained by draft': 4,
19
+ 'constrained by her draught': 4,
20
+ moored: 5,
21
+ Moored: 5,
22
+ aground: 6,
23
+ fishing: 7,
24
+ 'engaged in fishing': 7,
25
+ sailing: 8,
26
+ UnderWaySailing: 8,
27
+ 'under way sailing': 8,
28
+ 'underway sailing': 8,
29
+ 'hazardous material high speed': 9,
30
+ 'hazardous material wing in ground': 10,
31
+ 'reserved for future use': 13,
32
+ 'ais-sart': 14,
33
+ default: 15,
34
+ UnDefined: 15,
35
+ undefined: 15,
36
+ };
37
+
38
+ /**
39
+ * Extract value from SignalK data structure
40
+ * Handles both { value: X } objects and direct values
41
+ * @param {object} obj - The object to extract from
42
+ * @param {string} path - Dot-separated path (e.g., 'navigation.position.latitude')
43
+ * @returns {*} The extracted value or null
44
+ */
45
+ function getValue(obj, path) {
46
+ if (obj === undefined || obj === null) return null;
47
+
48
+ const parts = path.split('.');
49
+ let current = obj;
50
+
51
+ for (const part of parts) {
52
+ if (current === undefined || current === null) return null;
53
+ current = current[part];
54
+ }
55
+
56
+ if (current === undefined || current === null) return null;
57
+
58
+ // Handle SignalK value wrapper
59
+ if (typeof current === 'object' && 'value' in current) {
60
+ return current.value;
61
+ }
62
+
63
+ return current;
64
+ }
65
+
66
+ /**
67
+ * Get timestamp from SignalK data
68
+ * @param {object} obj - The object to extract from
69
+ * @param {string} path - Dot-separated path
70
+ * @returns {string|null} ISO timestamp or null
71
+ */
72
+ function getTimestamp(obj, path) {
73
+ if (obj === undefined || obj === null) return null;
74
+
75
+ const parts = path.split('.');
76
+ let current = obj;
77
+
78
+ for (const part of parts) {
79
+ if (current === undefined || current === null) return null;
80
+ current = current[part];
81
+ }
82
+
83
+ if (current === undefined || current === null) return null;
84
+
85
+ if (typeof current === 'object' && 'timestamp' in current) {
86
+ return current.timestamp;
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Convert radians to degrees
94
+ * @param {number|null} radians - Value in radians
95
+ * @returns {number|null} Value in degrees or null
96
+ */
97
+ function radToDegrees(radians) {
98
+ if (radians === null || radians === undefined) return null;
99
+ return (radians * 180) / Math.PI;
100
+ }
101
+
102
+ /**
103
+ * Convert meters per second to knots
104
+ * @param {number|null} speed - Speed in m/s
105
+ * @returns {number|null} Speed in knots or null
106
+ */
107
+ function msToKnots(speed) {
108
+ if (speed === null || speed === undefined) return null;
109
+ return (speed * 3.6) / 1.852;
110
+ }
111
+
112
+ // NMEA checksum hex conversion
113
+ const mHex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
114
+
115
+ /**
116
+ * Convert byte to hex string
117
+ * @param {number} v - Byte value
118
+ * @returns {string} Two-character hex string
119
+ */
120
+ function toHexString(v) {
121
+ const msn = (v >> 4) & 0x0f;
122
+ const lsn = (v >> 0) & 0x0f;
123
+ return mHex[msn] + mHex[lsn];
124
+ }
125
+
126
+ /**
127
+ * Create NMEA tag block with checksum
128
+ * @param {number} [timestamp] - Optional timestamp (defaults to Date.now())
129
+ * @returns {string} Tag block string
130
+ */
131
+ function createTagBlock(timestamp) {
132
+ let tagBlock = '';
133
+ tagBlock += 's:SK0001,';
134
+ tagBlock += `c:${timestamp || Date.now()},`;
135
+ tagBlock = tagBlock.slice(0, -1);
136
+ let tagBlockChecksum = 0;
137
+ for (let i = 0; i < tagBlock.length; i++) {
138
+ tagBlockChecksum ^= tagBlock.charCodeAt(i);
139
+ }
140
+ return `\\${tagBlock}*${toHexString(tagBlockChecksum)}\\`;
141
+ }
142
+
143
+ /**
144
+ * Get navigation status code from state string
145
+ * @param {string} state - Navigation state string
146
+ * @returns {number|string} AIS navigation status code or empty string
147
+ */
148
+ function getNavStatus(state) {
149
+ if (state === null || state === undefined) return '';
150
+ return stateMapping[state] !== undefined ? stateMapping[state] : '';
151
+ }
152
+
153
+ /**
154
+ * Extract vessel data from SignalK vessel object
155
+ * @param {object} vessel - SignalK vessel object
156
+ * @returns {object} Extracted vessel data
157
+ */
158
+ function extractVesselData(vessel) {
159
+ const mmsi = getValue(vessel, 'mmsi');
160
+ let shipName = getValue(vessel, 'name');
161
+ if (typeof shipName === 'number') shipName = '';
162
+
163
+ // Handle SignalK position structure: navigation.position.value = {latitude, longitude}
164
+ const position = getValue(vessel, 'navigation.position');
165
+ const lat = position && typeof position === 'object' ? position.latitude : null;
166
+ const lon = position && typeof position === 'object' ? position.longitude : null;
167
+
168
+ const sog = msToKnots(getValue(vessel, 'navigation.speedOverGround'));
169
+ const cog = radToDegrees(getValue(vessel, 'navigation.courseOverGroundTrue'));
170
+ const rot = radToDegrees(getValue(vessel, 'navigation.rateOfTurn'));
171
+ const hdg = radToDegrees(getValue(vessel, 'navigation.headingTrue'));
172
+
173
+ const navStateValue = getValue(vessel, 'navigation.state');
174
+ const navStat = getNavStatus(navStateValue);
175
+
176
+ let dst = getValue(vessel, 'navigation.destination.commonName');
177
+ if (typeof dst === 'number') dst = '';
178
+
179
+ let callSign = getValue(vessel, 'communication.callsignVhf');
180
+ if (typeof callSign === 'number') callSign = '';
181
+
182
+ let imo = getValue(vessel, 'registrations.imo');
183
+ if (imo && typeof imo === 'string' && imo.startsWith('IMO ')) {
184
+ imo = imo.substring(4);
185
+ }
186
+
187
+ const id = getValue(vessel, 'design.aisShipType.id');
188
+ let type = getValue(vessel, 'design.aisShipType.name');
189
+ if (typeof type === 'number') type = '';
190
+
191
+ // Draft in meters - ggencoder handles conversion to 0.1m units internally
192
+ const draftCur = getValue(vessel, 'design.draft.current');
193
+
194
+ const length = getValue(vessel, 'design.length.overall');
195
+ let beam = getValue(vessel, 'design.beam');
196
+ if (beam !== null) beam /= 2;
197
+
198
+ const aisClass = getValue(vessel, 'sensors.ais.class');
199
+
200
+ return {
201
+ mmsi,
202
+ shipName,
203
+ lat,
204
+ lon,
205
+ sog,
206
+ cog,
207
+ rot,
208
+ hdg,
209
+ navStat,
210
+ dst,
211
+ callSign,
212
+ imo,
213
+ id,
214
+ type,
215
+ draftCur,
216
+ length,
217
+ beam,
218
+ aisClass,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Build AIS message type 3 (Class A position report)
224
+ * @param {object} data - Vessel data
225
+ * @param {boolean} isOwn - Whether this is own vessel
226
+ * @returns {object} AIS message object
227
+ */
228
+ function buildAisMessage3(data, isOwn = false) {
229
+ return {
230
+ own: isOwn,
231
+ aistype: 3,
232
+ repeat: 0,
233
+ mmsi: data.mmsi,
234
+ navstatus: data.navStat,
235
+ sog: data.sog,
236
+ lon: data.lon,
237
+ lat: data.lat,
238
+ cog: data.cog,
239
+ hdg: data.hdg,
240
+ rot: data.rot,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Build AIS message type 5 (Class A static data)
246
+ * @param {object} data - Vessel data
247
+ * @param {boolean} isOwn - Whether this is own vessel
248
+ * @returns {object} AIS message object
249
+ */
250
+ function buildAisMessage5(data, isOwn = false) {
251
+ return {
252
+ own: isOwn,
253
+ aistype: 5,
254
+ repeat: 0,
255
+ mmsi: data.mmsi,
256
+ imo: data.imo,
257
+ cargo: data.id,
258
+ callsign: data.callSign,
259
+ shipname: data.shipName,
260
+ draught: data.draftCur,
261
+ destination: data.dst,
262
+ dimA: 0,
263
+ dimB: data.length,
264
+ dimC: data.beam,
265
+ dimD: data.beam,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Build AIS message type 18 (Class B position report)
271
+ * @param {object} data - Vessel data
272
+ * @param {boolean} isOwn - Whether this is own vessel
273
+ * @returns {object} AIS message object
274
+ */
275
+ function buildAisMessage18(data, isOwn = false) {
276
+ return {
277
+ own: isOwn,
278
+ aistype: 18,
279
+ repeat: 0,
280
+ mmsi: data.mmsi,
281
+ sog: data.sog,
282
+ accuracy: 0,
283
+ lon: data.lon,
284
+ lat: data.lat,
285
+ cog: data.cog,
286
+ hdg: data.hdg,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Build AIS message type 24 part A (Class B static - ship name)
292
+ * @param {object} data - Vessel data
293
+ * @param {boolean} isOwn - Whether this is own vessel
294
+ * @returns {object} AIS message object
295
+ */
296
+ function buildAisMessage24A(data, isOwn = false) {
297
+ return {
298
+ own: isOwn,
299
+ aistype: 24,
300
+ repeat: 0,
301
+ part: 0,
302
+ mmsi: data.mmsi,
303
+ shipname: data.shipName,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Build AIS message type 24 part B (Class B static - call sign, dimensions)
309
+ * @param {object} data - Vessel data
310
+ * @param {boolean} isOwn - Whether this is own vessel
311
+ * @returns {object} AIS message object
312
+ */
313
+ function buildAisMessage24B(data, isOwn = false) {
314
+ return {
315
+ own: isOwn,
316
+ aistype: 24,
317
+ repeat: 0,
318
+ part: 1,
319
+ mmsi: data.mmsi,
320
+ cargo: data.id,
321
+ callsign: data.callSign,
322
+ dimA: 0,
323
+ dimB: data.length,
324
+ dimC: data.beam,
325
+ dimD: data.beam,
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Check if AIS data is fresh (within update interval)
331
+ * @param {string} aisTime - ISO timestamp
332
+ * @param {number} maxAgeSeconds - Maximum age in seconds
333
+ * @returns {boolean} True if data is fresh
334
+ */
335
+ function isDataFresh(aisTime, maxAgeSeconds) {
336
+ if (!aisTime) return false;
337
+ const ageSeconds = (Date.now() - new Date(aisTime).getTime()) / 1000;
338
+ return ageSeconds < maxAgeSeconds;
339
+ }
340
+
341
+ module.exports = {
342
+ stateMapping,
343
+ getValue,
344
+ getTimestamp,
345
+ radToDegrees,
346
+ msToKnots,
347
+ toHexString,
348
+ createTagBlock,
349
+ getNavStatus,
350
+ extractVesselData,
351
+ buildAisMessage3,
352
+ buildAisMessage5,
353
+ buildAisMessage18,
354
+ buildAisMessage24A,
355
+ buildAisMessage24B,
356
+ isDataFresh,
357
+ };
package/package.json CHANGED
@@ -1,26 +1,27 @@
1
- {
2
- "name": "signalk-vessels-to-ais",
3
- "version": "1.6.0",
4
- "description": "SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [
10
- "signalk-node-server-plugin"
11
- ],
12
- "repository": "https://github.com/KEGustafsson/signalk-vessels-to-ais",
13
- "author": "Karl-Erik Gustafsson",
14
- "license": "MIT",
15
- "dependencies": {
16
- "haversine-distance": "^1.2.1",
17
- "moment": "^2.29.1",
18
- "node-fetch": "^3.1.1",
19
- "ggencoder": "^1.0.5"
20
- },
21
- "devDependencies": {
22
- "eslint": "^7.17.0",
23
- "eslint-config-airbnb-base": "^14.2.1",
24
- "eslint-plugin-import": "^2.22.1"
25
- }
26
- }
1
+ {
2
+ "name": "signalk-vessels-to-ais",
3
+ "version": "2.0.0-beta.1",
4
+ "description": "SignalK server plugin to convert other vessel data to NMEA0183 AIS format using direct data access (no REST API)",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha 'test/**/*.test.js'",
8
+ "lint": "eslint index.js lib/"
9
+ },
10
+ "keywords": [
11
+ "signalk-node-server-plugin",
12
+ "signalk-category-ais"
13
+ ],
14
+ "repository": "https://github.com/KEGustafsson/signalk-vessels-to-ais",
15
+ "author": "Karl-Erik Gustafsson",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "haversine-distance": "^1.2.1",
19
+ "ggencoder": "^1.0.9"
20
+ },
21
+ "devDependencies": {
22
+ "eslint": "^7.17.0",
23
+ "eslint-config-airbnb-base": "^14.2.1",
24
+ "eslint-plugin-import": "^2.22.1",
25
+ "mocha": "^10.2.0"
26
+ }
27
+ }