@sailingnaturali/signalk-dsc 0.4.0 → 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 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
@@ -85,6 +95,7 @@ For every DSC call heard by a connected radio:
85
95
  | Option | Default | Notes |
86
96
  | --- | --- | --- |
87
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. |
88
99
  | `logbookEnabled` | `true` | Requires signalk-logbook and a token. |
89
100
  | `logbookRoutine` | `false` | Also log routine calls. |
90
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)',
@@ -336,6 +344,7 @@ module.exports = function makePlugin(app) {
336
344
  plugin.start = function (opts) {
337
345
  options = {
338
346
  maxEvents: 1000,
347
+ markerWindowHours: 24,
339
348
  logbookEnabled: true,
340
349
  logbookRoutine: false,
341
350
  logbookUrl: 'http://localhost:3000/plugins/signalk-logbook/logs',
@@ -370,6 +379,33 @@ module.exports = function makePlugin(app) {
370
379
  },
371
380
  });
372
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
+
373
409
  app.emitPropertyValue('nmea0183sentenceParser', { sentence: 'DSC', parser: dscParser });
374
410
  app.emitPropertyValue('nmea0183sentenceParser', { sentence: 'DSE', parser: dseParser });
375
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.4.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": {