@sailingnaturali/signalk-dsc 0.3.1 → 0.5.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/README.md +17 -2
- package/index.js +51 -6
- package/lib/markers.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,16 @@ For every DSC call heard by a connected radio:
|
|
|
33
33
|
`GET /signalk/v2/api/resources/dsc-calls` (anonymously readable when the server
|
|
34
34
|
allows read-only access). Raw sentence/PGN is always kept alongside the parsed
|
|
35
35
|
fields: time, MMSI, category, nature of distress, position, UTC time.
|
|
36
|
+
- **A chart-marker layer** — `GET /signalk/v2/api/resources/dsc-call-markers`
|
|
37
|
+
serves logged calls as [Freeboard-SK](https://github.com/SignalK/freeboard-sk)
|
|
38
|
+
ResourceSets, one per category (distress/urgency/safety/routine), each a
|
|
39
|
+
GeoJSON `FeatureCollection` of Point markers whose popup carries the nature,
|
|
40
|
+
caller name/MMSI, and times. To use it in Freeboard: Settings → Resources
|
|
41
|
+
(Custom) → add resource type `dsc-call-markers`, reload, then toggle the
|
|
42
|
+
per-category layers. Non-distress calls drop off after `markerWindowHours`
|
|
43
|
+
(default 24); an active (un-cleared) distress call stays until you clear the alarm.
|
|
44
|
+
This is the *detail* layer — distinct from the prominent live SaR marker a
|
|
45
|
+
distress call also draws via the `sar.` context (see Remote-vessel deltas).
|
|
36
46
|
- **Alarms under your own vessel** — `notifications.dsc.distress` (state
|
|
37
47
|
`emergency`), `notifications.dsc.urgency` (`alarm`), `notifications.dsc.safety`
|
|
38
48
|
(`alert`). Routine calls never alarm. Repeated re-transmissions of the same
|
|
@@ -48,8 +58,12 @@ For every DSC call heard by a connected radio:
|
|
|
48
58
|
Full detail (MMSI, coordinates, reported time, transport) goes to the call
|
|
49
59
|
log and the logbook entry instead, so TTS pipelines stay terse.
|
|
50
60
|
- **Remote-vessel deltas** — the caller's `navigation.position` (and a distress
|
|
51
|
-
notification) under
|
|
52
|
-
|
|
61
|
+
notification) under the caller's context, so chartplotters can show where the
|
|
62
|
+
call came from. A **distress** caller is emitted under the Search-and-Rescue
|
|
63
|
+
context `sar.urn:mrn:imo:mmsi:<caller>` — which plotters like
|
|
64
|
+
[Freeboard-SK](https://github.com/SignalK/freeboard-sk) render as a distress
|
|
65
|
+
(SaR) target rather than an ordinary AIS vessel; every other category stays
|
|
66
|
+
under `vessels.urn:mrn:imo:mmsi:<caller>`.
|
|
53
67
|
- Every stored call carries an `ownShip` snapshot of the moment it arrived —
|
|
54
68
|
position, course, heading, speed, wind, pressure, and (when a source publishes them)
|
|
55
69
|
sea state, visibility, and cloud coverage. Absent sensor, absent field.
|
|
@@ -81,6 +95,7 @@ For every DSC call heard by a connected radio:
|
|
|
81
95
|
| Option | Default | Notes |
|
|
82
96
|
| --- | --- | --- |
|
|
83
97
|
| `maxEvents` | `1000` | Oldest calls dropped beyond this. |
|
|
98
|
+
| `markerWindowHours` | `24` | Non-distress calls leave the `dsc-call-markers` chart layer after this many hours; active distress stays until cleared. |
|
|
84
99
|
| `logbookEnabled` | `true` | Requires signalk-logbook and a token. |
|
|
85
100
|
| `logbookRoutine` | `false` | Also log routine calls. |
|
|
86
101
|
| `logbookUrl` | `http://localhost:3000/plugins/signalk-logbook/logs` | |
|
package/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const { parseDsc } = require('./lib/dsc');
|
|
|
28
28
|
const { parseDse, refinePosition } = require('./lib/dse');
|
|
29
29
|
const { normalizePgn129808 } = require('./lib/pgn129808');
|
|
30
30
|
const { EventStore } = require('./lib/store');
|
|
31
|
+
const { buildMarkerResourceSets } = require('./lib/markers');
|
|
31
32
|
const { buildMessage, buildLogbookText } = require('./lib/format');
|
|
32
33
|
const { captureOwnShip, buildObservations, unwrap } = require('./lib/snapshot');
|
|
33
34
|
|
|
@@ -56,6 +57,13 @@ module.exports = function makePlugin(app) {
|
|
|
56
57
|
description: 'Oldest calls are dropped beyond this count.',
|
|
57
58
|
default: 1000,
|
|
58
59
|
},
|
|
60
|
+
markerWindowHours: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
title: 'Chart marker window (hours)',
|
|
63
|
+
description:
|
|
64
|
+
'Non-distress calls drop off the dsc-call-markers chart layer after this many hours. Active (un-cleared) distress calls always remain.',
|
|
65
|
+
default: 24,
|
|
66
|
+
},
|
|
59
67
|
logbookEnabled: {
|
|
60
68
|
type: 'boolean',
|
|
61
69
|
title: 'Write received calls to the ship\'s log (signalk-logbook)',
|
|
@@ -244,8 +252,14 @@ module.exports = function makePlugin(app) {
|
|
|
244
252
|
return event;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
// The SignalK context the caller's position is emitted under. A distress
|
|
256
|
+
// caller is a vessel in extremis, so it goes under the Search-and-Rescue
|
|
257
|
+
// context (`sar.`) — which chartplotters (e.g. Freeboard-SK) render as a
|
|
258
|
+
// distress/SaR target rather than an ordinary AIS vessel. Every other
|
|
259
|
+
// category stays under `vessels.` like any AIS contact.
|
|
260
|
+
function callerContext(category, mmsi) {
|
|
261
|
+
const prefix = category === 'distress' ? 'sar' : 'vessels';
|
|
262
|
+
return `${prefix}.urn:mrn:imo:mmsi:${mmsi}`;
|
|
249
263
|
}
|
|
250
264
|
|
|
251
265
|
// Custom sentence parsers override the stock hooks (a superset of the
|
|
@@ -263,8 +277,8 @@ module.exports = function makePlugin(app) {
|
|
|
263
277
|
receivedAt: input.tags && input.tags.timestamp,
|
|
264
278
|
});
|
|
265
279
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
280
|
+
// Delta under the caller's context so chartplotters and AIS-style
|
|
281
|
+
// consumers see the caller's position (distress → SaR target).
|
|
268
282
|
if (parsed.mmsi) {
|
|
269
283
|
const values = [];
|
|
270
284
|
if (parsed.position) {
|
|
@@ -281,7 +295,10 @@ module.exports = function makePlugin(app) {
|
|
|
281
295
|
});
|
|
282
296
|
}
|
|
283
297
|
if (values.length) {
|
|
284
|
-
return {
|
|
298
|
+
return {
|
|
299
|
+
context: callerContext(parsed.category, parsed.mmsi),
|
|
300
|
+
updates: [{ values }],
|
|
301
|
+
};
|
|
285
302
|
}
|
|
286
303
|
}
|
|
287
304
|
} catch (err) {
|
|
@@ -306,7 +323,7 @@ module.exports = function makePlugin(app) {
|
|
|
306
323
|
const refined = refinePosition(target.position, ext);
|
|
307
324
|
store.update(target.id, { position: refined, positionResolution: 'enhanced' });
|
|
308
325
|
return {
|
|
309
|
-
context:
|
|
326
|
+
context: callerContext(target.category, ext.mmsi),
|
|
310
327
|
updates: [{ values: [{ path: 'navigation.position', value: refined }] }],
|
|
311
328
|
};
|
|
312
329
|
} catch (err) {
|
|
@@ -327,6 +344,7 @@ module.exports = function makePlugin(app) {
|
|
|
327
344
|
plugin.start = function (opts) {
|
|
328
345
|
options = {
|
|
329
346
|
maxEvents: 1000,
|
|
347
|
+
markerWindowHours: 24,
|
|
330
348
|
logbookEnabled: true,
|
|
331
349
|
logbookRoutine: false,
|
|
332
350
|
logbookUrl: 'http://localhost:3000/plugins/signalk-logbook/logs',
|
|
@@ -361,6 +379,33 @@ module.exports = function makePlugin(app) {
|
|
|
361
379
|
},
|
|
362
380
|
});
|
|
363
381
|
|
|
382
|
+
const buildSets = () =>
|
|
383
|
+
buildMarkerResourceSets(store.list(), {
|
|
384
|
+
now: Date.now(),
|
|
385
|
+
windowHours: options.markerWindowHours,
|
|
386
|
+
nameFor: vesselName,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
app.registerResourceProvider({
|
|
390
|
+
type: 'dsc-call-markers',
|
|
391
|
+
methods: {
|
|
392
|
+
async listResources() {
|
|
393
|
+
return buildSets();
|
|
394
|
+
},
|
|
395
|
+
async getResource(id) {
|
|
396
|
+
const sets = buildSets();
|
|
397
|
+
if (!sets[id]) throw new Error(`No DSC calls in category: ${id}`);
|
|
398
|
+
return sets[id];
|
|
399
|
+
},
|
|
400
|
+
setResource() {
|
|
401
|
+
throw new Error('dsc-call-markers is read-only');
|
|
402
|
+
},
|
|
403
|
+
deleteResource() {
|
|
404
|
+
throw new Error('dsc-call-markers is read-only');
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
364
409
|
app.emitPropertyValue('nmea0183sentenceParser', { sentence: 'DSC', parser: dscParser });
|
|
365
410
|
app.emitPropertyValue('nmea0183sentenceParser', { sentence: 'DSE', parser: dseParser });
|
|
366
411
|
app.on('N2KAnalyzerOut', onPgn);
|
package/lib/markers.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Per-category marker colour, emitted as the ResourceSet `styles.default`.
|
|
4
|
+
// Freeboard styles a feature from styles[properties.styleRef] and falls back to
|
|
5
|
+
// styles.default, so one default per category colours all of that set's markers.
|
|
6
|
+
const CATEGORY_COLORS = {
|
|
7
|
+
distress: 'rgba(211,47,47,1)', // red
|
|
8
|
+
urgency: 'rgba(245,124,0,1)', // orange
|
|
9
|
+
safety: 'rgba(251,192,45,1)', // amber
|
|
10
|
+
routine: 'rgba(117,117,117,1)', // grey
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
// Should this call appear at `now`? Un-cleared distress is always shown — a
|
|
16
|
+
// MAYDAY must not age off the chart. Everything else (and cleared distress)
|
|
17
|
+
// must fall within `windowHours` of receipt.
|
|
18
|
+
function withinWindow(event, now, windowHours) {
|
|
19
|
+
if (event.category === 'distress' && !event.clearedAt) return true;
|
|
20
|
+
const received = Date.parse(event.receivedAt);
|
|
21
|
+
if (Number.isNaN(received)) return false;
|
|
22
|
+
return now - received <= windowHours * HOUR_MS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toFeature(event, nameFor) {
|
|
26
|
+
const mmsi = event.distressedMmsi || event.mmsi;
|
|
27
|
+
const vesselName = nameFor ? nameFor(mmsi) : undefined;
|
|
28
|
+
const properties = {
|
|
29
|
+
name: event.natureOfDistress
|
|
30
|
+
? `${event.category}: ${event.natureOfDistress}`
|
|
31
|
+
: event.category,
|
|
32
|
+
category: event.category,
|
|
33
|
+
mmsi,
|
|
34
|
+
utcTime: event.utcTime,
|
|
35
|
+
receivedAt: event.receivedAt,
|
|
36
|
+
};
|
|
37
|
+
if (event.natureOfDistress) properties.natureOfDistress = event.natureOfDistress;
|
|
38
|
+
if (vesselName) properties.vesselName = vesselName;
|
|
39
|
+
return {
|
|
40
|
+
type: 'Feature',
|
|
41
|
+
geometry: {
|
|
42
|
+
type: 'Point',
|
|
43
|
+
coordinates: [event.position.longitude, event.position.latitude],
|
|
44
|
+
},
|
|
45
|
+
properties,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Turn stored DSC call events into freeboard ResourceSets keyed by category.
|
|
50
|
+
// Empty categories are omitted.
|
|
51
|
+
function buildMarkerResourceSets(events, { now, windowHours = 24, nameFor } = {}) {
|
|
52
|
+
const buckets = {};
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (
|
|
55
|
+
!event.position ||
|
|
56
|
+
typeof event.position.latitude !== 'number' ||
|
|
57
|
+
typeof event.position.longitude !== 'number'
|
|
58
|
+
)
|
|
59
|
+
continue;
|
|
60
|
+
if (!withinWindow(event, now, windowHours)) continue;
|
|
61
|
+
const category = event.category || 'routine';
|
|
62
|
+
(buckets[category] = buckets[category] || []).push(toFeature(event, nameFor));
|
|
63
|
+
}
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [category, features] of Object.entries(buckets)) {
|
|
66
|
+
const color = CATEGORY_COLORS[category] || CATEGORY_COLORS.routine;
|
|
67
|
+
out[category] = {
|
|
68
|
+
name: `DSC — ${category}`,
|
|
69
|
+
description: `DSC ${category} calls heard on channel 70`,
|
|
70
|
+
styles: { default: { width: 2, stroke: color, fill: color } },
|
|
71
|
+
values: { type: 'FeatureCollection', features },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { buildMarkerResourceSets, CATEGORY_COLORS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailingnaturali/signalk-dsc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Receive, log, and alert on DSC (VHF digital selective calling) calls — distress, urgency, safety, routine — from NMEA 0183 ($CDDSC/$CDDSE) and NMEA 2000 (PGN 129808).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|