kahu-signalk 0.0.16 → 0.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kahu-signalk",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
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
@@ -149,6 +149,7 @@ module.exports = (app) => {
149
149
  getOwnPosition: () => app.getSelfPath("navigation.position")?.value,
150
150
  statusFn: (msg) => app.setPluginStatus(`Mayara: ${msg}`),
151
151
  pollIntervalMs: plugin.settings.mayara.poll_interval_ms || 2000,
152
+ verboseLogs: plugin.settings.mayara.verbose_logs === true,
152
153
  });
153
154
  plugin.mayaraIngestor.start();
154
155
  }
@@ -345,6 +346,11 @@ module.exports = (app) => {
345
346
  default: 2000,
346
347
  title: "Mayara HTTP fallback poll interval (ms)",
347
348
  },
349
+ verbose_logs: {
350
+ type: "boolean",
351
+ default: false,
352
+ title: "Enable verbose Mayara logs",
353
+ },
348
354
  },
349
355
  },
350
356
  },
package/plugin/mayara.js CHANGED
@@ -4,12 +4,20 @@ const WebSocket = require("ws");
4
4
  const rad2deg = (rad) => (Number.isFinite(rad) ? (rad * 180) / Math.PI : null);
5
5
 
6
6
  class MayaraIngestor {
7
- constructor({ baseUrl, routecache, getOwnPosition, statusFn, pollIntervalMs }) {
7
+ constructor({
8
+ baseUrl,
9
+ routecache,
10
+ getOwnPosition,
11
+ statusFn,
12
+ pollIntervalMs,
13
+ verboseLogs,
14
+ }) {
8
15
  this.baseUrl = (baseUrl || "http://localhost:6502").replace(/\/+$/, "");
9
16
  this.routecache = routecache;
10
17
  this.getOwnPosition = getOwnPosition || (() => null);
11
18
  this.statusFn = statusFn || (() => {});
12
19
  this.pollIntervalMs = pollIntervalMs || 2000;
20
+ this.verboseLogs = Boolean(verboseLogs);
13
21
 
14
22
  this.ws = null;
15
23
  this.pollTimer = null;
@@ -20,14 +28,38 @@ class MayaraIngestor {
20
28
 
21
29
  this.radarIds = [];
22
30
  this.targetUuids = new Map();
31
+ this._log("init", {
32
+ baseUrl: this.baseUrl,
33
+ pollIntervalMs: this.pollIntervalMs,
34
+ verboseLogs: this.verboseLogs,
35
+ });
36
+ }
37
+
38
+ _log(state, details = {}) {
39
+ const pairs =
40
+ details && typeof details === "object"
41
+ ? Object.entries(details)
42
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
43
+ .join(" ")
44
+ : `message=${JSON.stringify(String(details))}`;
45
+ const line = `[MayaraIngestor] state=${state}${pairs ? ` ${pairs}` : ""}`;
46
+ this.statusFn(line);
47
+ console.error(line);
48
+ }
49
+
50
+ _logVerbose(state, details = {}) {
51
+ if (!this.verboseLogs) return;
52
+ this._log(state, details);
23
53
  }
24
54
 
25
55
  async start() {
56
+ this._log("start", { message: "starting ingestor" });
26
57
  await this._discoverRadars();
27
58
  this._connectWebSocket();
28
59
  }
29
60
 
30
61
  async destroy() {
62
+ this._log("stop", { message: "destroy requested" });
31
63
  this.destroyed = true;
32
64
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
33
65
  if (this.pollTimer) clearInterval(this.pollTimer);
@@ -38,19 +70,21 @@ class MayaraIngestor {
38
70
  try {
39
71
  this.ws.close();
40
72
  } catch (err) {
41
- this.statusFn(`Mayara WS close error: ${err.message}`);
73
+ this._log("ws_close_error", { error: err.message });
42
74
  }
43
75
  }
44
76
  this.ws = null;
77
+ this._log("stop", { message: "ingestor destroyed" });
45
78
  }
46
79
 
47
80
  async _discoverRadars() {
81
+ this._log("discover", { message: "requesting radar list" });
48
82
  try {
49
83
  const res = await fetch(
50
84
  `${this.baseUrl}/signalk/v2/api/vessels/self/radars`,
51
85
  );
52
86
  if (!res.ok) {
53
- this.statusFn(`Mayara radar discovery failed: HTTP ${res.status}`);
87
+ this._log("discover_http_error", { status: res.status });
54
88
  return;
55
89
  }
56
90
  const payload = await res.json();
@@ -65,9 +99,9 @@ class MayaraIngestor {
65
99
  this.radarIds = [];
66
100
  }
67
101
 
68
- this.statusFn(`Mayara discovered ${this.radarIds.length} radar(s)`);
102
+ this._log("discover_ok", { radarCount: this.radarIds.length });
69
103
  } catch (err) {
70
- this.statusFn(`Mayara radar discovery error: ${err.message}`);
104
+ this._log("discover_error", { error: err.message });
71
105
  }
72
106
  }
73
107
 
@@ -82,6 +116,7 @@ class MayaraIngestor {
82
116
  _connectWebSocket() {
83
117
  if (this.destroyed) return;
84
118
  const wsUrl = this._buildWsUrl();
119
+ this._log("ws_connecting", { wsUrl });
85
120
 
86
121
  this.ws = new WebSocket(wsUrl);
87
122
 
@@ -92,23 +127,28 @@ class MayaraIngestor {
92
127
  clearInterval(this.pollTimer);
93
128
  this.pollTimer = null;
94
129
  }
95
- this.statusFn("Mayara WS connected");
130
+ this._log("ws_open", { message: "connected" });
96
131
  this.ws.send(
97
132
  JSON.stringify({
98
133
  context: "vessels.self",
99
134
  subscribe: [{ path: "radars.*.targets.*", policy: "instant" }],
100
135
  }),
101
136
  );
137
+ this._log("ws_subscribe", { path: "radars.*.targets.*", policy: "instant" });
102
138
  });
103
139
 
104
140
  this.ws.on("message", (raw) => this._handleDelta(raw));
105
- this.ws.on("error", (err) => this.statusFn(`Mayara WS error: ${err.message}`));
141
+ this.ws.on("error", (err) => this._log("ws_error", { error: err.message }));
106
142
  this.ws.on("close", () => this._scheduleReconnect());
107
143
  }
108
144
 
109
145
  _scheduleReconnect() {
110
146
  if (this.destroyed) return;
111
147
  this.wsFailCount += 1;
148
+ this._log(
149
+ "ws_close",
150
+ { failCount: this.wsFailCount, reconnectMs: this.reconnectMs },
151
+ );
112
152
 
113
153
  if (this.wsFailCount >= 3) this._startHttpFallback();
114
154
 
@@ -126,13 +166,37 @@ class MayaraIngestor {
126
166
  }
127
167
 
128
168
  _insertTarget(radarId, targetId, target) {
129
- if (!target || target.status !== "tracking") return;
169
+ if (!target || target.status !== "tracking") {
170
+ this._logVerbose("target_skip", {
171
+ radarId,
172
+ targetId,
173
+ reason: "non-tracking",
174
+ status: target?.status,
175
+ });
176
+ return;
177
+ }
130
178
  const lat = target.position?.latitude;
131
179
  const lon = target.position?.longitude;
132
- if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
180
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
181
+ this._logVerbose("target_skip", {
182
+ radarId,
183
+ targetId,
184
+ reason: "invalid-lat-lon",
185
+ latitude: lat,
186
+ longitude: lon,
187
+ });
188
+ return;
189
+ }
133
190
 
134
191
  const ownPos = this.getOwnPosition?.();
135
- if (!ownPos?.latitude || !ownPos?.longitude) return;
192
+ if (!ownPos?.latitude || !ownPos?.longitude) {
193
+ this._logVerbose("target_skip", {
194
+ radarId,
195
+ targetId,
196
+ reason: "no-own-position",
197
+ });
198
+ return;
199
+ }
136
200
 
137
201
  this.routecache.insert({
138
202
  target_id: this._getTargetUuid(radarId, targetId),
@@ -154,6 +218,16 @@ class MayaraIngestor {
154
218
  courseOverGroundTrue: rad2deg(target.motion?.course),
155
219
  name: `ARPA-${targetId}`,
156
220
  });
221
+ this._logVerbose("target_insert", {
222
+ radarId,
223
+ targetId,
224
+ latitude: lat,
225
+ longitude: lon,
226
+ distance: target.position?.distance ?? null,
227
+ bearingRad: target.position?.bearing ?? null,
228
+ speed: target.motion?.speed ?? null,
229
+ courseRad: target.motion?.course ?? null,
230
+ });
157
231
  }
158
232
 
159
233
  _handleDelta(raw) {
@@ -161,15 +235,26 @@ class MayaraIngestor {
161
235
  try {
162
236
  msg = JSON.parse(raw.toString());
163
237
  } catch (err) {
238
+ this._log("delta_parse_error", { error: err.message });
164
239
  return;
165
240
  }
241
+ this._logVerbose("delta_received", { updates: msg?.updates?.length || 0 });
166
242
 
167
243
  for (const update of msg?.updates || []) {
168
244
  for (const value of update?.values || []) {
169
245
  const path = value?.path || "";
170
246
  const match = path.match(/^radars\.([^.]+)\.targets\.([^.]+)$/);
171
- if (!match) continue;
172
- if (!value.value) continue;
247
+ if (!match) {
248
+ this._logVerbose("delta_skip", {
249
+ reason: "path-not-radar-target",
250
+ path: path || "<empty>",
251
+ });
252
+ continue;
253
+ }
254
+ if (!value.value) {
255
+ this._logVerbose("delta_skip", { reason: "missing-value", path });
256
+ continue;
257
+ }
173
258
  const radarId = match[1];
174
259
  const targetId = match[2];
175
260
  this._insertTarget(radarId, targetId, value.value);
@@ -179,7 +264,7 @@ class MayaraIngestor {
179
264
 
180
265
  _startHttpFallback() {
181
266
  if (this.pollTimer || this.destroyed) return;
182
- this.statusFn("Mayara WS unstable, enabling HTTP fallback polling");
267
+ this._log("http_fallback_start", { reason: "ws-unstable" });
183
268
  this.pollTimer = setInterval(async () => {
184
269
  if (this.destroyed) return;
185
270
  if (!this.radarIds.length) await this._discoverRadars();
@@ -188,14 +273,21 @@ class MayaraIngestor {
188
273
  const res = await fetch(
189
274
  `${this.baseUrl}/signalk/v2/api/vessels/self/radars/${radarId}/targets`,
190
275
  );
191
- if (!res.ok) continue;
276
+ if (!res.ok) {
277
+ this._log("http_poll_http_error", { radarId, status: res.status });
278
+ continue;
279
+ }
192
280
  const targets = await res.json();
193
- if (!Array.isArray(targets)) continue;
281
+ if (!Array.isArray(targets)) {
282
+ this._log("http_poll_skip", { radarId, reason: "response-not-array" });
283
+ continue;
284
+ }
285
+ this._logVerbose("http_poll_ok", { radarId, targetCount: targets.length });
194
286
  for (const target of targets) {
195
287
  this._insertTarget(radarId, target?.id, target);
196
288
  }
197
289
  } catch (err) {
198
- this.statusFn(`Mayara HTTP poll error: ${err.message}`);
290
+ this._log("http_poll_error", { radarId, error: err.message });
199
291
  }
200
292
  }
201
293
  }, this.pollIntervalMs);