kahu-signalk 0.0.10 → 0.0.12

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
 
@@ -6,7 +6,7 @@ create table if not exists target (
6
6
  create table if not exists target_position (
7
7
  id integer primary key autoincrement,
8
8
  timestamp datetime default current_timestamp,
9
- target_id integer references target(id),
9
+ target_id integer references target(target_id),
10
10
  target_distance float,
11
11
  target_bearing float,
12
12
  target_bearing_unit text,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kahu-signalk",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
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,43 +1,47 @@
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
15
  const parseOptionalFloat = (value) => {
14
- if (value === undefined || value === null || value === '') return undefined;
16
+ if (value === undefined || value === null || value === "") return undefined;
15
17
  const parsed = parseFloat(value);
16
18
  return Number.isNaN(parsed) ? undefined : parsed;
17
19
  };
18
20
 
19
21
  const parseRattmSentence = (sentence) => {
20
- const [payload] = sentence.split('*');
22
+ const [payload] = sentence.split("*");
21
23
  if (!payload) return null;
22
24
 
23
- const parts = payload.trim().split(',');
24
- if (parts[0] !== '$RATTM') return null;
25
+ const parts = payload.trim().split(",");
26
+ if (parts[0] !== "$RATTM") return null;
25
27
  if (parts.length < 16) return null;
26
28
 
27
29
  const target_id = parseInt(parts[1], 10);
28
30
  const target_distance = parseOptionalFloat(parts[2]);
29
31
  const target_bearing = parseOptionalFloat(parts[3]);
30
- const target_bearing_unit = parts[4] || '';
32
+ const target_bearing_unit = parts[4] || "";
31
33
  const target_speed = parseOptionalFloat(parts[5]);
32
34
  const target_course = parseOptionalFloat(parts[6]);
33
- const target_course_unit = parts[7] || '';
34
- const target_distance_unit = parts[10] || '';
35
- const target_name = parts[12] || '';
36
- const target_status = parts[13] || '';
37
-
38
- if (!Number.isFinite(target_id) ||
39
- !Number.isFinite(target_distance) ||
40
- !Number.isFinite(target_bearing)) {
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
+ ) {
41
45
  return null;
42
46
  }
43
47
 
@@ -59,41 +63,91 @@ const deg2rad = (degrees) => degrees * (Math.PI / 180);
59
63
 
60
64
  const polar2Pos = (ownPos, bearing, distance) => {
61
65
  return {
62
- latitude: ownPos.latitude + distance * Math.cos(deg2rad(bearing)) / 60. / 1852.,
63
- longitude: ownPos.longitude + distance * Math.sin(deg2rad(bearing)) / Math.cos(deg2rad(ownPos.latitude)) / 60. / 1852.};
64
- }
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
+ };
65
76
 
66
- 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
+ };
67
111
 
112
+ module.exports = (app) => {
68
113
  const plugin = {
69
- id: 'radarhub',
70
- name: 'KAHU Radar Hub',
114
+ id: "radarhub",
115
+ name: "KAHU Radar Hub",
71
116
  start: async (settings, restartPlugin) => {
72
- 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;
73
121
 
74
122
  plugin.cache = new Routecache(
75
123
  path.join(packageDir, "data", "protocol", "migrations"),
76
- path.join(app.getDataDirPath(), "routecache.sqlite3"));
124
+ path.join(app.getDataDirPath(), "routecache.sqlite3"),
125
+ );
77
126
  await plugin.cache.init();
78
-
127
+
79
128
  plugin.connector = new Connector({
80
129
  routecache: plugin.cache,
81
130
  plugin_dir: packageDir,
82
131
  config: settings,
83
- status_function: app.setPluginStatus.bind(app)});
84
-
132
+ status_function: app.setPluginStatus.bind(app),
133
+ });
134
+
85
135
  const now = new Date(1970, 1, 1);
86
136
  plugin.route_updates = Array.from(Array(100)).map(() => now);
87
137
  plugin.route_ids = Array.from(Array(100));
88
-
89
- app.emitPropertyValue('nmea0183sentenceParser', {
90
- sentence: 'TTM',
138
+
139
+ app.emitPropertyValue("nmea0183sentenceParser", {
140
+ sentence: "TTM",
91
141
  parser: ({ id, sentence, parts, tags }, session) => {
92
142
  if (sentence.startsWith("$RATTL")) {
93
143
  } else if (sentence.startsWith("$RATTM")) {
94
144
  const parsed = parseRattmSentence(sentence);
95
145
  if (!parsed) {
96
- console.error("Failed to parse RATTM NMEA sentence: [", sentence, "]");
146
+ console.error(
147
+ "Failed to parse RATTM NMEA sentence: [",
148
+ sentence,
149
+ "]",
150
+ );
97
151
  return;
98
152
  }
99
153
 
@@ -110,96 +164,136 @@ module.exports = (app) => {
110
164
  target_status,
111
165
  } = parsed;
112
166
 
113
- if (target_bearing_unit === 'R') {
114
- console.warn("Relative bearings not yet supported, skipping RATTM sentence");
167
+ if (target_bearing_unit === "R") {
168
+ console.warn(
169
+ "Relative bearings not yet supported, skipping RATTM sentence",
170
+ );
115
171
  return;
116
172
  }
117
-
173
+
118
174
  const ownPos = app.getSelfPath("navigation.position")?.value;
119
175
  if (!ownPos) {
120
- 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
+ }
121
187
  return;
122
188
  }
123
- const targetPos = polar2Pos(ownPos, target_bearing, target_distance);
124
-
125
- const relative = {
126
- position: ownPos,
127
- distance: target_distance,
128
- bearing: target_bearing,
129
- bearing_unit: target_bearing_unit,
130
- distance_unit: target_distance_unit,
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
+ }
131
230
  }
231
+ const targetPos = polar2Pos(
232
+ ownPos,
233
+ target_bearing,
234
+ target_distance,
235
+ );
132
236
 
133
237
  // target_distance_closes_point_of_approac: parseInt(match[8]),
134
238
  // target_time_closes_point_of_approac: parseInt(match[9]),
135
-
239
+
136
240
  const now = new Date();
137
241
  if (now - plugin.route_updates[target_id] > 60000) {
138
242
  plugin.route_updates[target_id] = now;
139
243
  plugin.route_ids[target_id] = uuidv4();
140
244
  }
141
-
142
- return {
143
- context: 'vessels.urn:mrn:signalk:uuid:' + plugin.route_ids[target_id],
144
- updates: [
145
- {
146
- values: [
147
- { path: 'name',
148
- value: target_name
149
- },
150
- { path: 'navigation.speedOverGround',
151
- value: target_speed
152
- },
153
- { path: 'navigation.courseOverGroundTrue',
154
- value: target_course
155
- },
156
- { path: 'navigation.position',
157
- value: {...targetPos, relative}
158
- },
159
- ]
160
- }
161
- ]
162
- };
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
+ });
163
258
  }
164
- }
259
+ },
165
260
  });
166
261
 
167
262
  app.streambundle
168
- .getBus('navigation.position')
263
+ .getBus("navigation.position")
169
264
  .forEach(plugin.updatePosition);
170
-
171
265
  },
172
266
  stop: async () => {
173
267
  await plugin.connector?.destroy?.();
174
268
  await plugin.cache?.destroy?.();
175
- console.log("Stopped KAHU radar Hub")
269
+ console.log("Stopped KAHU radar Hub");
176
270
  },
177
271
  schema: () => {
178
272
  return {
179
273
  properties: {
180
- server: {type: "string", default: "crowdsource.kahu.earth"},
181
- port: {type: "number", default: 9900},
182
- api_key: {type: "string"},
183
- min_reconnect_time: {type: "number", default: 100.0},
184
- max_reconnect_time: {type: "number", default: 600.0}
185
- }
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
+ },
186
280
  };
187
281
  },
188
282
  updatePosition: (pos) => {
189
283
  if (pos.source.sentence != "TTM") return;
190
284
 
191
285
  const rest = app.getPath(pos.context);
192
-
286
+
193
287
  const target_id = pos.context.split("vessels.urn:mrn:signalk:uuid:")[1];
194
-
288
+
195
289
  plugin.cache.insert({
196
290
  target_id: target_id,
197
291
  position: pos.value,
198
292
  speedOverGround: rest?.navigation?.speedOverGround?.value,
199
293
  courseOverGroundTrue: rest?.navigation?.courseOverGroundTrue?.value,
200
294
  name: rest?.name?.value,
201
- });
202
- }
295
+ });
296
+ },
203
297
  };
204
298
 
205
299
  return plugin;