kahu-signalk 0.0.9 → 0.0.11

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
@@ -207,7 +207,7 @@ You may see an **ExperimentalWarning** about SQLite from Node; that is expected
207
207
 
208
208
  Use `--config-dir` (with an **n**), not `--confic-dir`:
209
209
  ```bash
210
- npm start -- --config-dir /home/bs01743/Projects/KAHU/signalk-server/signalk-server/config
210
+ npm start -- --config-dir /home/signalk-server/config
211
211
  ```
212
212
  The `--` is needed so npm passes `--config-dir` to the start script.
213
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kahu-signalk",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Contribute AIS and ARPA targets from your vessel to crowdsourcing for marine safety!",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
package/plugin/index.js CHANGED
@@ -1,164 +1,299 @@
1
- const { v4: uuidv4 } = require('uuid');
1
+ const { v4: uuidv4 } = require("uuid");
2
2
  const { Routecache } = require("./routecache");
3
3
  const { Connector } = require("./connector");
4
- const path = require('path');
5
- const fs = require('fs');
4
+ const path = require("path");
5
+ const fs = require("fs");
6
6
 
7
7
  let packageDir = __dirname;
8
- while (packageDir !== path.dirname(packageDir)
9
- && !fs.existsSync(path.join(packageDir, 'package.json'))) {
8
+ while (
9
+ packageDir !== path.dirname(packageDir) &&
10
+ !fs.existsSync(path.join(packageDir, "package.json"))
11
+ ) {
10
12
  packageDir = path.dirname(packageDir);
11
13
  }
12
14
 
13
- const nmeaRattmRegex = /\$RATTM,(\d{2}),([\d\.\-]+),([\d\.\-]+),([^,]*),([\d\.\-]+),([\d\.\-]+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),(..*)\*([A-Fa-f0-9]{2})\s*/;
15
+ const parseOptionalFloat = (value) => {
16
+ if (value === undefined || value === null || value === "") return undefined;
17
+ const parsed = parseFloat(value);
18
+ return Number.isNaN(parsed) ? undefined : parsed;
19
+ };
20
+
21
+ const parseRattmSentence = (sentence) => {
22
+ const [payload] = sentence.split("*");
23
+ if (!payload) return null;
24
+
25
+ const parts = payload.trim().split(",");
26
+ if (parts[0] !== "$RATTM") return null;
27
+ if (parts.length < 16) return null;
28
+
29
+ const target_id = parseInt(parts[1], 10);
30
+ const target_distance = parseOptionalFloat(parts[2]);
31
+ const target_bearing = parseOptionalFloat(parts[3]);
32
+ const target_bearing_unit = parts[4] || "";
33
+ const target_speed = parseOptionalFloat(parts[5]);
34
+ const target_course = parseOptionalFloat(parts[6]);
35
+ const target_course_unit = parts[7] || "";
36
+ const target_distance_unit = parts[10] || "";
37
+ const target_name = parts[11] || "";
38
+ const target_status = parts[12] || "";
39
+
40
+ if (
41
+ !Number.isFinite(target_id) ||
42
+ !Number.isFinite(target_distance) ||
43
+ !Number.isFinite(target_bearing)
44
+ ) {
45
+ return null;
46
+ }
47
+
48
+ return {
49
+ target_id,
50
+ target_distance,
51
+ target_bearing,
52
+ target_bearing_unit,
53
+ target_speed,
54
+ target_course,
55
+ target_course_unit,
56
+ target_distance_unit,
57
+ target_name,
58
+ target_status,
59
+ };
60
+ };
14
61
 
15
62
  const deg2rad = (degrees) => degrees * (Math.PI / 180);
16
63
 
17
64
  const polar2Pos = (ownPos, bearing, distance) => {
18
65
  return {
19
- latitude: ownPos.latitude + distance * Math.cos(deg2rad(bearing)) / 60. / 1852.,
20
- longitude: ownPos.longitude + distance * Math.sin(deg2rad(bearing)) / Math.cos(deg2rad(ownPos.latitude)) / 60. / 1852.};
21
- }
66
+ latitude:
67
+ ownPos.latitude + (distance * Math.cos(deg2rad(bearing))) / 60 / 1852,
68
+ longitude:
69
+ ownPos.longitude +
70
+ (distance * Math.sin(deg2rad(bearing))) /
71
+ Math.cos(deg2rad(ownPos.latitude)) /
72
+ 60 /
73
+ 1852,
74
+ };
75
+ };
22
76
 
23
- module.exports = (app) => {
77
+ const buildRattmDelta = ({
78
+ route_id,
79
+ target_distance,
80
+ target_bearing,
81
+ target_bearing_unit,
82
+ target_distance_unit,
83
+ target_name,
84
+ target_speed,
85
+ target_course,
86
+ targetPos,
87
+ ownPos,
88
+ }) => {
89
+ const relative = {
90
+ position: ownPos,
91
+ distance: target_distance,
92
+ bearing: target_bearing,
93
+ bearing_unit: target_bearing_unit,
94
+ distance_unit: target_distance_unit,
95
+ };
96
+
97
+ return {
98
+ context: "vessels.urn:mrn:signalk:uuid:" + route_id,
99
+ updates: [
100
+ {
101
+ values: [
102
+ { path: "name", value: target_name },
103
+ { path: "navigation.speedOverGround", value: target_speed },
104
+ { path: "navigation.courseOverGroundTrue", value: target_course },
105
+ { path: "navigation.position", value: { ...targetPos, relative } },
106
+ ],
107
+ },
108
+ ],
109
+ };
110
+ };
24
111
 
112
+ module.exports = (app) => {
25
113
  const plugin = {
26
- id: 'radarhub',
27
- name: 'KAHU Radar Hub',
114
+ id: "radarhub",
115
+ name: "KAHU Radar Hub",
28
116
  start: async (settings, restartPlugin) => {
29
- console.log("Started KAHU radar Hub")
117
+ console.log("Started KAHU radar Hub");
118
+
119
+ plugin.pending_rattm = [];
120
+ plugin.last_no_ownpos_warning_at = 0;
30
121
 
31
122
  plugin.cache = new Routecache(
32
123
  path.join(packageDir, "data", "protocol", "migrations"),
33
- path.join(app.getDataDirPath(), "routecache.sqlite3"));
124
+ path.join(app.getDataDirPath(), "routecache.sqlite3"),
125
+ );
34
126
  await plugin.cache.init();
35
-
127
+
36
128
  plugin.connector = new Connector({
37
129
  routecache: plugin.cache,
38
130
  plugin_dir: packageDir,
39
131
  config: settings,
40
- status_function: app.setPluginStatus.bind(app)});
41
-
132
+ status_function: app.setPluginStatus.bind(app),
133
+ });
134
+
42
135
  const now = new Date(1970, 1, 1);
43
136
  plugin.route_updates = Array.from(Array(100)).map(() => now);
44
137
  plugin.route_ids = Array.from(Array(100));
45
-
46
- app.emitPropertyValue('nmea0183sentenceParser', {
47
- sentence: 'TTM',
138
+
139
+ app.emitPropertyValue("nmea0183sentenceParser", {
140
+ sentence: "TTM",
48
141
  parser: ({ id, sentence, parts, tags }, session) => {
49
142
  if (sentence.startsWith("$RATTL")) {
50
143
  } else if (sentence.startsWith("$RATTM")) {
51
- const match = nmeaRattmRegex.exec(sentence);
52
-
53
- if (!match) {
54
- console.error("Failed to parse RATTM NMEA sentence: [", sentence, "]");
55
- return;
56
- }
57
- if (match.length - 1 != 14) {
58
- console.log("Only parsed ", (match.length - 1), " fields of RATTM NMEA sentence: [", sentence, "]");
144
+ const parsed = parseRattmSentence(sentence);
145
+ if (!parsed) {
146
+ console.error(
147
+ "Failed to parse RATTM NMEA sentence: [",
148
+ sentence,
149
+ "]",
150
+ );
59
151
  return;
60
152
  }
61
-
62
- const target_id = parseInt(match[1]);
63
- const target_distance = parseFloat(match[2]);
64
- const target_bearing = parseFloat(match[3]);
65
- const target_bearing_unit = match[4];
66
-
67
- if (target_bearing_unit === 'R') {
68
- console.warn("Relative bearings not yet supported, skipping RATTM sentence");
153
+
154
+ const {
155
+ target_id,
156
+ target_distance,
157
+ target_bearing,
158
+ target_bearing_unit,
159
+ target_speed,
160
+ target_course,
161
+ target_course_unit,
162
+ target_distance_unit,
163
+ target_name,
164
+ target_status,
165
+ } = parsed;
166
+
167
+ if (target_bearing_unit === "R") {
168
+ console.warn(
169
+ "Relative bearings not yet supported, skipping RATTM sentence",
170
+ );
69
171
  return;
70
172
  }
71
-
173
+
72
174
  const ownPos = app.getSelfPath("navigation.position")?.value;
73
175
  if (!ownPos) {
74
- console.warn("No own-ship position available, skipping RATTM sentence");
176
+ // Common at startup / in mixed streams: RATTM may arrive before the first GPS fix.
177
+ plugin.pending_rattm.push({ parsed, received_at: Date.now() });
178
+ if (plugin.pending_rattm.length > 500) plugin.pending_rattm.shift();
179
+
180
+ const nowMs = Date.now();
181
+ if (nowMs - plugin.last_no_ownpos_warning_at > 5000) {
182
+ plugin.last_no_ownpos_warning_at = nowMs;
183
+ console.warn(
184
+ "No own-ship position available yet; buffering RATTM sentences until GPS fix is available",
185
+ );
186
+ }
75
187
  return;
76
188
  }
77
- const targetPos = polar2Pos(ownPos, target_bearing, target_distance);
78
-
79
- const relative = {
80
- position: ownPos,
81
- distance: target_distance,
82
- bearing: target_bearing,
83
- bearing_unit: target_bearing_unit,
84
- distance_unit: match[10],
189
+
190
+ // If GPS is available now, flush any buffered RATTM sentences.
191
+ if (plugin.pending_rattm?.length) {
192
+ if (typeof app.handleMessage === "function") {
193
+ const buffered = plugin.pending_rattm.splice(0);
194
+ for (const item of buffered) {
195
+ const b = item?.parsed;
196
+ if (!b) continue;
197
+ if (b.target_bearing_unit === "R") continue;
198
+ const bTargetPos = polar2Pos(
199
+ ownPos,
200
+ b.target_bearing,
201
+ b.target_distance,
202
+ );
203
+
204
+ const bNow = new Date();
205
+ if (bNow - plugin.route_updates[b.target_id] > 60000) {
206
+ plugin.route_updates[b.target_id] = bNow;
207
+ plugin.route_ids[b.target_id] = uuidv4();
208
+ }
209
+
210
+ app.handleMessage(
211
+ plugin.id,
212
+ buildRattmDelta({
213
+ route_id: plugin.route_ids[b.target_id],
214
+ target_distance: b.target_distance,
215
+ target_bearing: b.target_bearing,
216
+ target_bearing_unit: b.target_bearing_unit,
217
+ target_distance_unit: b.target_distance_unit,
218
+ target_name: b.target_name,
219
+ target_speed: b.target_speed,
220
+ target_course: b.target_course,
221
+ targetPos: bTargetPos,
222
+ ownPos,
223
+ }),
224
+ );
225
+ }
226
+ } else {
227
+ // Can't publish buffered deltas in this environment; drop to prevent leaks.
228
+ plugin.pending_rattm.length = 0;
229
+ }
85
230
  }
231
+ const targetPos = polar2Pos(
232
+ ownPos,
233
+ target_bearing,
234
+ target_distance,
235
+ );
86
236
 
87
- const target_speed = parseFloat(match[5]);
88
- const target_course = parseFloat(match[6]);
89
- const target_course_unit = match[7];
90
237
  // target_distance_closes_point_of_approac: parseInt(match[8]),
91
238
  // target_time_closes_point_of_approac: parseInt(match[9]),
92
- const target_name = match[11];
93
- const target_status = match[12];
94
-
239
+
95
240
  const now = new Date();
96
241
  if (now - plugin.route_updates[target_id] > 60000) {
97
242
  plugin.route_updates[target_id] = now;
98
243
  plugin.route_ids[target_id] = uuidv4();
99
244
  }
100
-
101
- return {
102
- context: 'vessels.urn:mrn:signalk:uuid:' + plugin.route_ids[target_id],
103
- updates: [
104
- {
105
- values: [
106
- { path: 'name',
107
- value: target_name
108
- },
109
- { path: 'navigation.speedOverGround',
110
- value: target_speed
111
- },
112
- { path: 'navigation.courseOverGroundTrue',
113
- value: target_course
114
- },
115
- { path: 'navigation.position',
116
- value: {...targetPos, relative}
117
- },
118
- ]
119
- }
120
- ]
121
- };
245
+
246
+ return buildRattmDelta({
247
+ route_id: plugin.route_ids[target_id],
248
+ target_distance,
249
+ target_bearing,
250
+ target_bearing_unit,
251
+ target_distance_unit,
252
+ target_name,
253
+ target_speed,
254
+ target_course,
255
+ targetPos,
256
+ ownPos,
257
+ });
122
258
  }
123
- }
259
+ },
124
260
  });
125
261
 
126
262
  app.streambundle
127
- .getBus('navigation.position')
263
+ .getBus("navigation.position")
128
264
  .forEach(plugin.updatePosition);
129
-
130
265
  },
131
266
  stop: async () => {
132
267
  await plugin.connector?.destroy?.();
133
268
  await plugin.cache?.destroy?.();
134
- console.log("Stopped KAHU radar Hub")
269
+ console.log("Stopped KAHU radar Hub");
135
270
  },
136
271
  schema: () => {
137
272
  return {
138
273
  properties: {
139
- server: {type: "string", default: "crowdsource.kahu.earth"},
140
- port: {type: "number", default: 9900},
141
- api_key: {type: "string"},
142
- min_reconnect_time: {type: "number", default: 100.0},
143
- max_reconnect_time: {type: "number", default: 600.0}
144
- }
274
+ server: { type: "string", default: "crowdsource.kahu.earth" },
275
+ port: { type: "number", default: 9900 },
276
+ api_key: { type: "string" },
277
+ min_reconnect_time: { type: "number", default: 100.0 },
278
+ max_reconnect_time: { type: "number", default: 600.0 },
279
+ },
145
280
  };
146
281
  },
147
282
  updatePosition: (pos) => {
148
283
  if (pos.source.sentence != "TTM") return;
149
284
 
150
285
  const rest = app.getPath(pos.context);
151
-
286
+
152
287
  const target_id = pos.context.split("vessels.urn:mrn:signalk:uuid:")[1];
153
-
288
+
154
289
  plugin.cache.insert({
155
290
  target_id: target_id,
156
291
  position: pos.value,
157
292
  speedOverGround: rest?.navigation?.speedOverGround?.value,
158
293
  courseOverGroundTrue: rest?.navigation?.courseOverGroundTrue?.value,
159
294
  name: rest?.name?.value,
160
- });
161
- }
295
+ });
296
+ },
162
297
  };
163
298
 
164
299
  return plugin;