@sailingnaturali/signalk-dsc 0.5.1 → 0.5.3

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 CHANGED
@@ -43,6 +43,9 @@ For every DSC call heard by a connected radio:
43
43
  (default 24); an active (un-cleared) distress call stays until you clear the alarm.
44
44
  This is the *detail* layer — distinct from the prominent live SaR marker a
45
45
  distress call also draws via the `sar.` context (see Remote-vessel deltas).
46
+
47
+ ![DSC distress markers in Freeboard-SK](docs/screenshots/freeboard-distress-markers.png)
48
+
46
49
  - **Alarms under your own vessel** — `notifications.dsc.distress` (state
47
50
  `emergency`), `notifications.dsc.urgency` (`alarm`), `notifications.dsc.safety`
48
51
  (`alert`). Routine calls never alarm. Repeated re-transmissions of the same
package/lib/dsc.js CHANGED
@@ -56,6 +56,32 @@ const NATURES = {
56
56
  // Telecommand 21 on a non-distress call = "ship position" — field 5 holds a position.
57
57
  const TELECOMMAND_SHIP_POSITION = '21';
58
58
 
59
+ // AIS device-beacon MMSI prefixes (ITU 97x). A modern SART / MOB / EPIRB can
60
+ // send a DSC distress directly; the prefix lets a consumer correlate it with
61
+ // the matching AIS target.
62
+ const DEVICE_BEACONS = {
63
+ '970': 'sart',
64
+ '972': 'mob',
65
+ '974': 'epirb',
66
+ };
67
+
68
+ // Tag the AIS device class (sart/mob/epirb) for a 97x device-beacon MMSI, else
69
+ // undefined. Shared with the N2K (PGN 129808) path so both transports agree.
70
+ function deviceBeaconFor(mmsi) {
71
+ if (typeof mmsi !== 'string') return undefined;
72
+ return DEVICE_BEACONS[mmsi.substring(0, 3)];
73
+ }
74
+
75
+ // Resolve a nature-of-distress code to its name. The code arrives over the air
76
+ // unsanitised: only a clean 1–2 digit numeric code may reach the NATURES lookup
77
+ // or the code-NN fallback. A key like "__proto__" would otherwise resolve to
78
+ // Object.prototype, and any dotted value would inject extra segments into the
79
+ // notification path the server walks unguarded (SignalK/signalk-server#2768).
80
+ // Anything else collapses to a safe constant.
81
+ function parseNature(raw) {
82
+ return /^\d{1,2}$/.test(raw) ? NATURES[raw] || `code-${raw}` : 'undesignated';
83
+ }
84
+
59
85
  function field(parts, i) {
60
86
  const v = parts[i];
61
87
  return typeof v === 'string' ? v.trim() : '';
@@ -126,18 +152,22 @@ function parseDsc(parts) {
126
152
  mmsi: parseMmsi(parts[1]),
127
153
  };
128
154
 
155
+ const beacon = deviceBeaconFor(event.mmsi);
156
+ if (beacon) event.deviceBeacon = beacon;
157
+
129
158
  const distress = event.category === 'distress';
130
159
  if (distress) {
131
- const natureCode = field(parts, 3);
132
- // natureCode arrives over the air, unsanitised. Only a clean numeric code
133
- // may reach the NATURES lookup or the code-NN fallback: a key like
134
- // "__proto__" would otherwise resolve to Object.prototype, and any dotted
135
- // value would inject extra segments into the notification path that the
136
- // server walks unguarded (SignalK/signalk-server#2768). Anything else
137
- // collapses to a safe constant.
138
- event.natureOfDistress = /^\d{1,2}$/.test(natureCode)
139
- ? NATURES[natureCode] || `code-${natureCode}`
140
- : 'undesignated';
160
+ // A distress *relay* (all-ships / area / individual format carrying a
161
+ // distress category) reports the casualty in field 7 and the nature of
162
+ // distress in field 8 field 3 holds the relay telecommand, not a nature
163
+ // code. A first-party alert (format distressAlert) carries the nature in
164
+ // field 3.
165
+ if (event.format !== 'distressAlert') {
166
+ event.relay = true;
167
+ event.natureOfDistress = parseNature(field(parts, 8));
168
+ } else {
169
+ event.natureOfDistress = parseNature(field(parts, 3));
170
+ }
141
171
  event.distressedMmsi = parseMmsi(parts[7]);
142
172
  }
143
173
 
@@ -156,4 +186,4 @@ function parseDsc(parts) {
156
186
  return event;
157
187
  }
158
188
 
159
- module.exports = { parseDsc, parsePosition, parseMmsi, parseUtcTime, parseChannel, NATURES };
189
+ module.exports = { parseDsc, parsePosition, parseMmsi, parseUtcTime, parseChannel, deviceBeaconFor, NATURES };
package/lib/format.js CHANGED
@@ -64,7 +64,8 @@ function buildMessage(event, { ownPosition, vesselName } = {}) {
64
64
  const where = wherePhrase(event, ownPosition, { spoken: true });
65
65
  if (event.category === 'distress') {
66
66
  const nature = NATURE_TEXT[event.natureOfDistress] || event.natureOfDistress || 'undesignated distress';
67
- return `DSC distress alert: ${who}, ${nature}, ${where}. Monitor channel 16.`;
67
+ const lead = event.relay ? 'DSC distress relay' : 'DSC distress alert';
68
+ return `${lead}: ${who}, ${nature}, ${where}. Monitor channel 16.`;
68
69
  }
69
70
  const kind = event.category === 'unknown' ? 'call' : `${event.category} call`;
70
71
  return `DSC ${kind}: ${who}, ${where}.`;
@@ -75,7 +76,13 @@ function buildLogbookText(event, { ownPosition, vesselName } = {}) {
75
76
  const name = vesselName ? `${vesselName} (MMSI ${event.mmsi || 'unknown'})` : `MMSI ${event.mmsi || 'unknown'}`;
76
77
  if (event.category === 'distress') {
77
78
  const nature = NATURE_TEXT[event.natureOfDistress] || event.natureOfDistress || 'undesignated distress';
78
- parts.push(`DISTRESS alert from ${name}: ${nature}`);
79
+ if (event.relay) {
80
+ // Name the casualty (distressedMmsi) and the relaying station (name).
81
+ const casualty = event.distressedMmsi ? `MMSI ${event.distressedMmsi}` : 'an unidentified vessel';
82
+ parts.push(`DISTRESS RELAY from ${name} reporting ${casualty}: ${nature}`);
83
+ } else {
84
+ parts.push(`DISTRESS alert from ${name}: ${nature}`);
85
+ }
79
86
  } else {
80
87
  parts.push(`${event.category} call from ${name}`);
81
88
  }
package/lib/pgn129808.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * ITU symbol number (1xx) comes through instead — handle both.
10
10
  */
11
11
 
12
+ const { deviceBeaconFor } = require('./dsc');
13
+
12
14
  // Lookup-name → canonical (lowercased keys); ITU symbols as numeric fallback.
13
15
  const FORMATS = new Map([
14
16
  ['geographical area', 'area'],
@@ -109,9 +111,16 @@ function normalizePgn129808(pgnData) {
109
111
  mmsi: normalizeMmsi(f.dscMessageAddress),
110
112
  };
111
113
 
114
+ const beacon = deviceBeaconFor(event.mmsi);
115
+ if (beacon) event.deviceBeacon = beacon;
116
+
112
117
  if (event.category === 'distress') {
113
118
  const nature = f.natureOfDistress ?? f['1stTelecommand'];
114
119
  event.natureOfDistress = lookup(NATURES, nature) || 'undesignated';
120
+ // A distress *relay* arrives under a calling format (all-ships / area /
121
+ // individual), not the distressAlert format of a first-party alert. The
122
+ // PGN already carries the casualty's nature + MMSI in dedicated fields.
123
+ if (event.format !== 'distressAlert') event.relay = true;
115
124
  }
116
125
 
117
126
  const lat = f.latitudeOfVesselReported;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingnaturali/signalk-dsc",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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": {
@@ -29,6 +29,9 @@
29
29
  },
30
30
  "signalk": {
31
31
  "displayName": "DSC",
32
+ "recommends": [
33
+ "@meri-imperiumi/signalk-logbook"
34
+ ],
32
35
  "screenshots": [
33
36
  "./docs/screenshots/config.png",
34
37
  "./docs/screenshots/data-table.png"