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/.github/dependabot.yml +0 -0
- package/.mocharc.json +5 -0
- package/README.md +34 -32
- package/index.js +244 -463
- package/lib/helpers.js +357 -0
- package/package.json +27 -26
- package/test/helpers.test.js +609 -0
- package/test/plugin.test.js +479 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
const {
|
|
3
|
+
stateMapping,
|
|
4
|
+
getValue,
|
|
5
|
+
getTimestamp,
|
|
6
|
+
radToDegrees,
|
|
7
|
+
msToKnots,
|
|
8
|
+
toHexString,
|
|
9
|
+
createTagBlock,
|
|
10
|
+
getNavStatus,
|
|
11
|
+
extractVesselData,
|
|
12
|
+
buildAisMessage3,
|
|
13
|
+
buildAisMessage5,
|
|
14
|
+
buildAisMessage18,
|
|
15
|
+
buildAisMessage24A,
|
|
16
|
+
buildAisMessage24B,
|
|
17
|
+
isDataFresh
|
|
18
|
+
} = require('../lib/helpers')
|
|
19
|
+
|
|
20
|
+
describe('signalk-vessels-to-ais-ws helpers', function () {
|
|
21
|
+
// ============================================================
|
|
22
|
+
// getValue tests
|
|
23
|
+
// ============================================================
|
|
24
|
+
describe('getValue', function () {
|
|
25
|
+
it('returns null for null input', function () {
|
|
26
|
+
assert.strictEqual(getValue(null, 'any.path'), null)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null for undefined input', function () {
|
|
30
|
+
assert.strictEqual(getValue(undefined, 'any.path'), null)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns direct value for simple path', function () {
|
|
34
|
+
const obj = { mmsi: '123456789' }
|
|
35
|
+
assert.strictEqual(getValue(obj, 'mmsi'), '123456789')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns nested value for dotted path', function () {
|
|
39
|
+
const obj = {
|
|
40
|
+
navigation: {
|
|
41
|
+
position: {
|
|
42
|
+
latitude: 60.1
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
assert.strictEqual(getValue(obj, 'navigation.position.latitude'), 60.1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('extracts value from SignalK value wrapper', function () {
|
|
50
|
+
const obj = {
|
|
51
|
+
navigation: {
|
|
52
|
+
speedOverGround: {
|
|
53
|
+
value: 5.5,
|
|
54
|
+
timestamp: '2024-01-01T00:00:00Z'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
assert.strictEqual(getValue(obj, 'navigation.speedOverGround'), 5.5)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns null for non-existent path', function () {
|
|
62
|
+
const obj = { navigation: {} }
|
|
63
|
+
assert.strictEqual(getValue(obj, 'navigation.position.latitude'), null)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('handles path with missing intermediate object', function () {
|
|
67
|
+
const obj = { navigation: null }
|
|
68
|
+
assert.strictEqual(getValue(obj, 'navigation.position.latitude'), null)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns object if value property not present', function () {
|
|
72
|
+
const obj = {
|
|
73
|
+
design: {
|
|
74
|
+
length: {
|
|
75
|
+
overall: 15.5
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
assert.deepStrictEqual(getValue(obj, 'design.length'), { overall: 15.5 })
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// ============================================================
|
|
84
|
+
// getTimestamp tests
|
|
85
|
+
// ============================================================
|
|
86
|
+
describe('getTimestamp', function () {
|
|
87
|
+
it('returns null for null input', function () {
|
|
88
|
+
assert.strictEqual(getTimestamp(null, 'any.path'), null)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns null for undefined input', function () {
|
|
92
|
+
assert.strictEqual(getTimestamp(undefined, 'any.path'), null)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('extracts timestamp from SignalK object', function () {
|
|
96
|
+
const timestamp = '2024-01-01T12:00:00Z'
|
|
97
|
+
const obj = {
|
|
98
|
+
navigation: {
|
|
99
|
+
position: {
|
|
100
|
+
value: { latitude: 60.1, longitude: 24.9 },
|
|
101
|
+
timestamp: timestamp
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
assert.strictEqual(getTimestamp(obj, 'navigation.position'), timestamp)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns null if no timestamp property', function () {
|
|
109
|
+
const obj = {
|
|
110
|
+
navigation: {
|
|
111
|
+
position: {
|
|
112
|
+
value: { latitude: 60.1, longitude: 24.9 }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
assert.strictEqual(getTimestamp(obj, 'navigation.position'), null)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns null for non-existent path', function () {
|
|
120
|
+
const obj = { navigation: {} }
|
|
121
|
+
assert.strictEqual(getTimestamp(obj, 'navigation.position'), null)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// radToDegrees tests
|
|
127
|
+
// ============================================================
|
|
128
|
+
describe('radToDegrees', function () {
|
|
129
|
+
it('returns null for null input', function () {
|
|
130
|
+
assert.strictEqual(radToDegrees(null), null)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('returns null for undefined input', function () {
|
|
134
|
+
assert.strictEqual(radToDegrees(undefined), null)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('converts 0 radians to 0 degrees', function () {
|
|
138
|
+
assert.strictEqual(radToDegrees(0), 0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('converts PI radians to 180 degrees', function () {
|
|
142
|
+
assert.strictEqual(radToDegrees(Math.PI), 180)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('converts PI/2 radians to 90 degrees', function () {
|
|
146
|
+
assert.strictEqual(radToDegrees(Math.PI / 2), 90)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('converts 2*PI radians to 360 degrees', function () {
|
|
150
|
+
assert.strictEqual(radToDegrees(2 * Math.PI), 360)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('converts negative radians correctly', function () {
|
|
154
|
+
assert.strictEqual(radToDegrees(-Math.PI), -180)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('handles fractional radians', function () {
|
|
158
|
+
const result = radToDegrees(1)
|
|
159
|
+
assert(Math.abs(result - 57.29577951308232) < 0.0001)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// ============================================================
|
|
164
|
+
// msToKnots tests
|
|
165
|
+
// ============================================================
|
|
166
|
+
describe('msToKnots', function () {
|
|
167
|
+
it('returns null for null input', function () {
|
|
168
|
+
assert.strictEqual(msToKnots(null), null)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns null for undefined input', function () {
|
|
172
|
+
assert.strictEqual(msToKnots(undefined), null)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('converts 0 m/s to 0 knots', function () {
|
|
176
|
+
assert.strictEqual(msToKnots(0), 0)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('converts 1 m/s to approximately 1.94 knots', function () {
|
|
180
|
+
const result = msToKnots(1)
|
|
181
|
+
assert(Math.abs(result - 1.9438) < 0.001)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('converts 10 m/s to approximately 19.44 knots', function () {
|
|
185
|
+
const result = msToKnots(10)
|
|
186
|
+
assert(Math.abs(result - 19.438) < 0.01)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('converts 0.5144 m/s (1 knot) back to approximately 1 knot', function () {
|
|
190
|
+
const result = msToKnots(0.5144)
|
|
191
|
+
assert(Math.abs(result - 1.0) < 0.01)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// ============================================================
|
|
196
|
+
// toHexString tests
|
|
197
|
+
// ============================================================
|
|
198
|
+
describe('toHexString', function () {
|
|
199
|
+
it('converts 0 to 00', function () {
|
|
200
|
+
assert.strictEqual(toHexString(0), '00')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('converts 15 to 0F', function () {
|
|
204
|
+
assert.strictEqual(toHexString(15), '0F')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('converts 16 to 10', function () {
|
|
208
|
+
assert.strictEqual(toHexString(16), '10')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('converts 255 to FF', function () {
|
|
212
|
+
assert.strictEqual(toHexString(255), 'FF')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('converts 170 to AA', function () {
|
|
216
|
+
assert.strictEqual(toHexString(170), 'AA')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('converts 100 to 64', function () {
|
|
220
|
+
assert.strictEqual(toHexString(100), '64')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// createTagBlock tests
|
|
226
|
+
// ============================================================
|
|
227
|
+
describe('createTagBlock', function () {
|
|
228
|
+
it('creates valid tag block format', function () {
|
|
229
|
+
const tag = createTagBlock(1234567890123)
|
|
230
|
+
assert(tag.startsWith('\\'))
|
|
231
|
+
assert(tag.endsWith('\\'))
|
|
232
|
+
assert(tag.includes('s:SK0001'))
|
|
233
|
+
assert(tag.includes('c:1234567890123'))
|
|
234
|
+
assert(tag.includes('*'))
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('includes checksum in hex format', function () {
|
|
238
|
+
const tag = createTagBlock(1234567890123)
|
|
239
|
+
const match = tag.match(/\*([0-9A-F]{2})\\$/)
|
|
240
|
+
assert(match !== null, 'Tag block should end with *XX\\ format')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('generates consistent checksum for same input', function () {
|
|
244
|
+
const tag1 = createTagBlock(1234567890123)
|
|
245
|
+
const tag2 = createTagBlock(1234567890123)
|
|
246
|
+
assert.strictEqual(tag1, tag2)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('generates different checksum for different timestamp', function () {
|
|
250
|
+
const tag1 = createTagBlock(1234567890123)
|
|
251
|
+
const tag2 = createTagBlock(9876543210987)
|
|
252
|
+
assert.notStrictEqual(tag1, tag2)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// ============================================================
|
|
257
|
+
// stateMapping tests
|
|
258
|
+
// ============================================================
|
|
259
|
+
describe('stateMapping', function () {
|
|
260
|
+
it('maps motoring to 0', function () {
|
|
261
|
+
assert.strictEqual(stateMapping['motoring'], 0)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('maps anchored to 1', function () {
|
|
265
|
+
assert.strictEqual(stateMapping['anchored'], 1)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('maps moored to 5', function () {
|
|
269
|
+
assert.strictEqual(stateMapping['moored'], 5)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('maps sailing to 8', function () {
|
|
273
|
+
assert.strictEqual(stateMapping['sailing'], 8)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('maps multiple variations of same state', function () {
|
|
277
|
+
assert.strictEqual(stateMapping['UnderWayUsingEngine'], 0)
|
|
278
|
+
assert.strictEqual(stateMapping['under way using engine'], 0)
|
|
279
|
+
assert.strictEqual(stateMapping['underway using engine'], 0)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('maps undefined to 15', function () {
|
|
283
|
+
assert.strictEqual(stateMapping['undefined'], 15)
|
|
284
|
+
assert.strictEqual(stateMapping['UnDefined'], 15)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// getNavStatus tests
|
|
290
|
+
// ============================================================
|
|
291
|
+
describe('getNavStatus', function () {
|
|
292
|
+
it('returns empty string for null', function () {
|
|
293
|
+
assert.strictEqual(getNavStatus(null), '')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('returns empty string for undefined', function () {
|
|
297
|
+
assert.strictEqual(getNavStatus(undefined), '')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('returns 0 for motoring', function () {
|
|
301
|
+
assert.strictEqual(getNavStatus('motoring'), 0)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('returns 8 for sailing', function () {
|
|
305
|
+
assert.strictEqual(getNavStatus('sailing'), 8)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('returns empty string for unknown state', function () {
|
|
309
|
+
assert.strictEqual(getNavStatus('unknown_state'), '')
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// ============================================================
|
|
314
|
+
// extractVesselData tests
|
|
315
|
+
// ============================================================
|
|
316
|
+
describe('extractVesselData', function () {
|
|
317
|
+
it('extracts basic vessel data', function () {
|
|
318
|
+
// Using real SignalK structure: position.value contains {latitude, longitude}
|
|
319
|
+
const vessel = {
|
|
320
|
+
mmsi: '123456789',
|
|
321
|
+
name: 'Test Vessel',
|
|
322
|
+
navigation: {
|
|
323
|
+
position: {
|
|
324
|
+
value: { latitude: 60.1, longitude: 24.9 }
|
|
325
|
+
},
|
|
326
|
+
speedOverGround: { value: 5.0 },
|
|
327
|
+
courseOverGroundTrue: { value: Math.PI / 2 }
|
|
328
|
+
},
|
|
329
|
+
sensors: {
|
|
330
|
+
ais: {
|
|
331
|
+
class: { value: 'A' }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const data = extractVesselData(vessel)
|
|
337
|
+
|
|
338
|
+
assert.strictEqual(data.mmsi, '123456789')
|
|
339
|
+
assert.strictEqual(data.shipName, 'Test Vessel')
|
|
340
|
+
assert.strictEqual(data.lat, 60.1)
|
|
341
|
+
assert.strictEqual(data.lon, 24.9)
|
|
342
|
+
assert(Math.abs(data.sog - 9.719) < 0.01) // 5 m/s in knots
|
|
343
|
+
assert.strictEqual(data.cog, 90) // PI/2 in degrees
|
|
344
|
+
assert.strictEqual(data.aisClass, 'A')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('handles missing optional fields', function () {
|
|
348
|
+
const vessel = {
|
|
349
|
+
mmsi: '123456789',
|
|
350
|
+
navigation: {
|
|
351
|
+
position: {
|
|
352
|
+
value: { latitude: 60.1, longitude: 24.9 }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const data = extractVesselData(vessel)
|
|
358
|
+
|
|
359
|
+
assert.strictEqual(data.mmsi, '123456789')
|
|
360
|
+
assert.strictEqual(data.lat, 60.1)
|
|
361
|
+
assert.strictEqual(data.lon, 24.9)
|
|
362
|
+
assert.strictEqual(data.sog, null)
|
|
363
|
+
assert.strictEqual(data.cog, null)
|
|
364
|
+
assert.strictEqual(data.shipName, null)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('converts numeric shipName to empty string', function () {
|
|
368
|
+
const vessel = {
|
|
369
|
+
mmsi: '123456789',
|
|
370
|
+
name: 12345
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const data = extractVesselData(vessel)
|
|
374
|
+
assert.strictEqual(data.shipName, '')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('extracts IMO with prefix removed', function () {
|
|
378
|
+
const vessel = {
|
|
379
|
+
mmsi: '123456789',
|
|
380
|
+
registrations: {
|
|
381
|
+
imo: { value: 'IMO 9876543' }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const data = extractVesselData(vessel)
|
|
386
|
+
assert.strictEqual(data.imo, '9876543')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('handles beam division by 2', function () {
|
|
390
|
+
const vessel = {
|
|
391
|
+
mmsi: '123456789',
|
|
392
|
+
design: {
|
|
393
|
+
beam: { value: 10 }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const data = extractVesselData(vessel)
|
|
398
|
+
assert.strictEqual(data.beam, 5)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('handles draft in meters (no division)', function () {
|
|
402
|
+
const vessel = {
|
|
403
|
+
mmsi: '123456789',
|
|
404
|
+
design: {
|
|
405
|
+
draft: {
|
|
406
|
+
current: { value: 3.5 }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const data = extractVesselData(vessel)
|
|
412
|
+
// Draft is passed through as-is in meters - ggencoder handles conversion
|
|
413
|
+
assert.strictEqual(data.draftCur, 3.5)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// ============================================================
|
|
418
|
+
// AIS Message builders tests
|
|
419
|
+
// ============================================================
|
|
420
|
+
describe('buildAisMessage3 (Class A position)', function () {
|
|
421
|
+
const testData = {
|
|
422
|
+
mmsi: '123456789',
|
|
423
|
+
lat: 60.1,
|
|
424
|
+
lon: 24.9,
|
|
425
|
+
sog: 10.5,
|
|
426
|
+
cog: 180,
|
|
427
|
+
hdg: 175,
|
|
428
|
+
rot: 0.5,
|
|
429
|
+
navStat: 0
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
it('builds message with correct aistype', function () {
|
|
433
|
+
const msg = buildAisMessage3(testData, false)
|
|
434
|
+
assert.strictEqual(msg.aistype, 3)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('sets own flag correctly', function () {
|
|
438
|
+
const msgOwn = buildAisMessage3(testData, true)
|
|
439
|
+
const msgOther = buildAisMessage3(testData, false)
|
|
440
|
+
assert.strictEqual(msgOwn.own, true)
|
|
441
|
+
assert.strictEqual(msgOther.own, false)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('includes all required fields', function () {
|
|
445
|
+
const msg = buildAisMessage3(testData, false)
|
|
446
|
+
assert.strictEqual(msg.mmsi, '123456789')
|
|
447
|
+
assert.strictEqual(msg.lat, 60.1)
|
|
448
|
+
assert.strictEqual(msg.lon, 24.9)
|
|
449
|
+
assert.strictEqual(msg.sog, 10.5)
|
|
450
|
+
assert.strictEqual(msg.cog, 180)
|
|
451
|
+
assert.strictEqual(msg.hdg, 175)
|
|
452
|
+
assert.strictEqual(msg.rot, 0.5)
|
|
453
|
+
assert.strictEqual(msg.navstatus, 0)
|
|
454
|
+
assert.strictEqual(msg.repeat, 0)
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe('buildAisMessage5 (Class A static)', function () {
|
|
459
|
+
const testData = {
|
|
460
|
+
mmsi: '123456789',
|
|
461
|
+
imo: '9876543',
|
|
462
|
+
id: 70,
|
|
463
|
+
callSign: 'ABCD',
|
|
464
|
+
shipName: 'Test Ship',
|
|
465
|
+
draftCur: 3.5,
|
|
466
|
+
dst: 'Helsinki',
|
|
467
|
+
length: 50,
|
|
468
|
+
beam: 8
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
it('builds message with correct aistype', function () {
|
|
472
|
+
const msg = buildAisMessage5(testData, false)
|
|
473
|
+
assert.strictEqual(msg.aistype, 5)
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('includes all required fields', function () {
|
|
477
|
+
const msg = buildAisMessage5(testData, false)
|
|
478
|
+
assert.strictEqual(msg.mmsi, '123456789')
|
|
479
|
+
assert.strictEqual(msg.imo, '9876543')
|
|
480
|
+
assert.strictEqual(msg.cargo, 70)
|
|
481
|
+
assert.strictEqual(msg.callsign, 'ABCD')
|
|
482
|
+
assert.strictEqual(msg.shipname, 'Test Ship')
|
|
483
|
+
assert.strictEqual(msg.draught, 3.5)
|
|
484
|
+
assert.strictEqual(msg.destination, 'Helsinki')
|
|
485
|
+
assert.strictEqual(msg.dimA, 0)
|
|
486
|
+
assert.strictEqual(msg.dimB, 50)
|
|
487
|
+
assert.strictEqual(msg.dimC, 8)
|
|
488
|
+
assert.strictEqual(msg.dimD, 8)
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('buildAisMessage18 (Class B position)', function () {
|
|
493
|
+
const testData = {
|
|
494
|
+
mmsi: '123456789',
|
|
495
|
+
lat: 60.1,
|
|
496
|
+
lon: 24.9,
|
|
497
|
+
sog: 8.0,
|
|
498
|
+
cog: 90,
|
|
499
|
+
hdg: 85
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
it('builds message with correct aistype', function () {
|
|
503
|
+
const msg = buildAisMessage18(testData, false)
|
|
504
|
+
assert.strictEqual(msg.aistype, 18)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('includes accuracy field set to 0', function () {
|
|
508
|
+
const msg = buildAisMessage18(testData, false)
|
|
509
|
+
assert.strictEqual(msg.accuracy, 0)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('includes all required fields', function () {
|
|
513
|
+
const msg = buildAisMessage18(testData, false)
|
|
514
|
+
assert.strictEqual(msg.mmsi, '123456789')
|
|
515
|
+
assert.strictEqual(msg.lat, 60.1)
|
|
516
|
+
assert.strictEqual(msg.lon, 24.9)
|
|
517
|
+
assert.strictEqual(msg.sog, 8.0)
|
|
518
|
+
assert.strictEqual(msg.cog, 90)
|
|
519
|
+
assert.strictEqual(msg.hdg, 85)
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe('buildAisMessage24A (Class B static part A)', function () {
|
|
524
|
+
const testData = {
|
|
525
|
+
mmsi: '123456789',
|
|
526
|
+
shipName: 'Test Boat'
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
it('builds message with correct aistype', function () {
|
|
530
|
+
const msg = buildAisMessage24A(testData, false)
|
|
531
|
+
assert.strictEqual(msg.aistype, 24)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('sets part to 0', function () {
|
|
535
|
+
const msg = buildAisMessage24A(testData, false)
|
|
536
|
+
assert.strictEqual(msg.part, 0)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('includes ship name', function () {
|
|
540
|
+
const msg = buildAisMessage24A(testData, false)
|
|
541
|
+
assert.strictEqual(msg.shipname, 'Test Boat')
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('buildAisMessage24B (Class B static part B)', function () {
|
|
546
|
+
const testData = {
|
|
547
|
+
mmsi: '123456789',
|
|
548
|
+
id: 36,
|
|
549
|
+
callSign: 'XYZ',
|
|
550
|
+
length: 12,
|
|
551
|
+
beam: 4
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
it('builds message with correct aistype', function () {
|
|
555
|
+
const msg = buildAisMessage24B(testData, false)
|
|
556
|
+
assert.strictEqual(msg.aistype, 24)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('sets part to 1', function () {
|
|
560
|
+
const msg = buildAisMessage24B(testData, false)
|
|
561
|
+
assert.strictEqual(msg.part, 1)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('includes all required fields', function () {
|
|
565
|
+
const msg = buildAisMessage24B(testData, false)
|
|
566
|
+
assert.strictEqual(msg.mmsi, '123456789')
|
|
567
|
+
assert.strictEqual(msg.cargo, 36)
|
|
568
|
+
assert.strictEqual(msg.callsign, 'XYZ')
|
|
569
|
+
assert.strictEqual(msg.dimA, 0)
|
|
570
|
+
assert.strictEqual(msg.dimB, 12)
|
|
571
|
+
assert.strictEqual(msg.dimC, 4)
|
|
572
|
+
assert.strictEqual(msg.dimD, 4)
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
// ============================================================
|
|
577
|
+
// isDataFresh tests
|
|
578
|
+
// ============================================================
|
|
579
|
+
describe('isDataFresh', function () {
|
|
580
|
+
it('returns false for null timestamp', function () {
|
|
581
|
+
assert.strictEqual(isDataFresh(null, 60), false)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('returns false for undefined timestamp', function () {
|
|
585
|
+
assert.strictEqual(isDataFresh(undefined, 60), false)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('returns true for recent timestamp', function () {
|
|
589
|
+
const recentTime = new Date(Date.now() - 10000).toISOString() // 10 seconds ago
|
|
590
|
+
assert.strictEqual(isDataFresh(recentTime, 60), true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('returns false for old timestamp', function () {
|
|
594
|
+
const oldTime = new Date(Date.now() - 120000).toISOString() // 2 minutes ago
|
|
595
|
+
assert.strictEqual(isDataFresh(oldTime, 60), false)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('returns true for timestamp exactly at boundary', function () {
|
|
599
|
+
const boundaryTime = new Date(Date.now() - 30000).toISOString() // 30 seconds ago
|
|
600
|
+
assert.strictEqual(isDataFresh(boundaryTime, 60), true)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('respects different maxAge values', function () {
|
|
604
|
+
const time = new Date(Date.now() - 45000).toISOString() // 45 seconds ago
|
|
605
|
+
assert.strictEqual(isDataFresh(time, 60), true) // 60s max -> fresh
|
|
606
|
+
assert.strictEqual(isDataFresh(time, 30), false) // 30s max -> stale
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
})
|