kahu-signalk 0.0.16 → 0.0.18

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.18",
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",
@@ -75,7 +75,7 @@ class Connector {
75
75
  const content = response.Response;
76
76
 
77
77
  if (content["kahu.ErrorResponseMessage"] !== undefined) {
78
- throw content.Error.exception;
78
+ throw content["kahu.ErrorResponseMessage"].exception;
79
79
  } else if (type !== undefined && content[type] === undefined) {
80
80
  throw (
81
81
  "Received response for wrong method: expected " +
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;
@@ -17,17 +25,43 @@ class MayaraIngestor {
17
25
  this.reconnectMs = 1000;
18
26
  this.wsFailCount = 0;
19
27
  this.destroyed = false;
28
+ this.halted = false;
20
29
 
21
30
  this.radarIds = [];
22
31
  this.targetUuids = new Map();
32
+ this._log("init", {
33
+ baseUrl: this.baseUrl,
34
+ pollIntervalMs: this.pollIntervalMs,
35
+ verboseLogs: this.verboseLogs,
36
+ });
37
+ }
38
+
39
+ _log(state, details = {}) {
40
+ const pairs =
41
+ details && typeof details === "object"
42
+ ? Object.entries(details)
43
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
44
+ .join(" ")
45
+ : `message=${JSON.stringify(String(details))}`;
46
+ const line = `[MayaraIngestor] state=${state}${pairs ? ` ${pairs}` : ""}`;
47
+ this.statusFn(line);
48
+ console.error(line);
49
+ }
50
+
51
+ _logVerbose(state, details = {}) {
52
+ if (!this.verboseLogs) return;
53
+ this._log(state, details);
23
54
  }
24
55
 
25
56
  async start() {
57
+ if (this.halted) return;
58
+ this._log("start", { message: "starting ingestor" });
26
59
  await this._discoverRadars();
27
- this._connectWebSocket();
60
+ if (!this.halted) this._connectWebSocket();
28
61
  }
29
62
 
30
63
  async destroy() {
64
+ this._log("stop", { message: "destroy requested" });
31
65
  this.destroyed = true;
32
66
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
33
67
  if (this.pollTimer) clearInterval(this.pollTimer);
@@ -38,19 +72,57 @@ class MayaraIngestor {
38
72
  try {
39
73
  this.ws.close();
40
74
  } catch (err) {
41
- this.statusFn(`Mayara WS close error: ${err.message}`);
75
+ this._log("ws_close_error", { error: err.message });
76
+ }
77
+ }
78
+ this.ws = null;
79
+ this._log("stop", { message: "ingestor destroyed" });
80
+ }
81
+
82
+ _halt(reason, details = {}) {
83
+ if (this.halted || this.destroyed) return;
84
+ this.halted = true;
85
+ this._log("halted", { reason, ...details });
86
+
87
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
88
+ if (this.pollTimer) clearInterval(this.pollTimer);
89
+ this.reconnectTimer = null;
90
+ this.pollTimer = null;
91
+
92
+ if (this.ws) {
93
+ try {
94
+ this.ws.close();
95
+ } catch (err) {
96
+ this._log("ws_close_error", { error: err.message });
42
97
  }
43
98
  }
44
99
  this.ws = null;
45
100
  }
46
101
 
102
+ _isServerUnreachable(err) {
103
+ if (!err) return false;
104
+ const message = String(err?.message || "").toLowerCase();
105
+ const code = String(err?.code || err?.cause?.code || "").toUpperCase();
106
+ return (
107
+ code === "ECONNREFUSED" ||
108
+ code === "ENOTFOUND" ||
109
+ code === "EHOSTUNREACH" ||
110
+ code === "ETIMEDOUT" ||
111
+ message.includes("fetch failed") ||
112
+ message.includes("connection refused") ||
113
+ message.includes("host unreachable")
114
+ );
115
+ }
116
+
47
117
  async _discoverRadars() {
118
+ if (this.destroyed || this.halted) return;
119
+ this._log("discover", { message: "requesting radar list" });
48
120
  try {
49
121
  const res = await fetch(
50
122
  `${this.baseUrl}/signalk/v2/api/vessels/self/radars`,
51
123
  );
52
124
  if (!res.ok) {
53
- this.statusFn(`Mayara radar discovery failed: HTTP ${res.status}`);
125
+ this._log("discover_http_error", { status: res.status });
54
126
  return;
55
127
  }
56
128
  const payload = await res.json();
@@ -65,9 +137,15 @@ class MayaraIngestor {
65
137
  this.radarIds = [];
66
138
  }
67
139
 
68
- this.statusFn(`Mayara discovered ${this.radarIds.length} radar(s)`);
140
+ this._log("discover_ok", { radarCount: this.radarIds.length });
69
141
  } catch (err) {
70
- this.statusFn(`Mayara radar discovery error: ${err.message}`);
142
+ this._log("discover_error", { error: err.message });
143
+ if (this._isServerUnreachable(err)) {
144
+ this._halt("discover-unreachable", {
145
+ error: err.message,
146
+ code: err?.code || err?.cause?.code || null,
147
+ });
148
+ }
71
149
  }
72
150
  }
73
151
 
@@ -80,8 +158,9 @@ class MayaraIngestor {
80
158
  }
81
159
 
82
160
  _connectWebSocket() {
83
- if (this.destroyed) return;
161
+ if (this.destroyed || this.halted) return;
84
162
  const wsUrl = this._buildWsUrl();
163
+ this._log("ws_connecting", { wsUrl });
85
164
 
86
165
  this.ws = new WebSocket(wsUrl);
87
166
 
@@ -92,23 +171,36 @@ class MayaraIngestor {
92
171
  clearInterval(this.pollTimer);
93
172
  this.pollTimer = null;
94
173
  }
95
- this.statusFn("Mayara WS connected");
174
+ this._log("ws_open", { message: "connected" });
96
175
  this.ws.send(
97
176
  JSON.stringify({
98
177
  context: "vessels.self",
99
178
  subscribe: [{ path: "radars.*.targets.*", policy: "instant" }],
100
179
  }),
101
180
  );
181
+ this._log("ws_subscribe", { path: "radars.*.targets.*", policy: "instant" });
102
182
  });
103
183
 
104
184
  this.ws.on("message", (raw) => this._handleDelta(raw));
105
- this.ws.on("error", (err) => this.statusFn(`Mayara WS error: ${err.message}`));
185
+ this.ws.on("error", (err) => {
186
+ this._log("ws_error", { error: err.message });
187
+ if (this._isServerUnreachable(err)) {
188
+ this._halt("ws-unreachable", {
189
+ error: err.message,
190
+ code: err?.code || err?.cause?.code || null,
191
+ });
192
+ }
193
+ });
106
194
  this.ws.on("close", () => this._scheduleReconnect());
107
195
  }
108
196
 
109
197
  _scheduleReconnect() {
110
- if (this.destroyed) return;
198
+ if (this.destroyed || this.halted) return;
111
199
  this.wsFailCount += 1;
200
+ this._log(
201
+ "ws_close",
202
+ { failCount: this.wsFailCount, reconnectMs: this.reconnectMs },
203
+ );
112
204
 
113
205
  if (this.wsFailCount >= 3) this._startHttpFallback();
114
206
 
@@ -126,13 +218,37 @@ class MayaraIngestor {
126
218
  }
127
219
 
128
220
  _insertTarget(radarId, targetId, target) {
129
- if (!target || target.status !== "tracking") return;
221
+ if (!target || target.status !== "tracking") {
222
+ this._logVerbose("target_skip", {
223
+ radarId,
224
+ targetId,
225
+ reason: "non-tracking",
226
+ status: target?.status,
227
+ });
228
+ return;
229
+ }
130
230
  const lat = target.position?.latitude;
131
231
  const lon = target.position?.longitude;
132
- if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
232
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
233
+ this._logVerbose("target_skip", {
234
+ radarId,
235
+ targetId,
236
+ reason: "invalid-lat-lon",
237
+ latitude: lat,
238
+ longitude: lon,
239
+ });
240
+ return;
241
+ }
133
242
 
134
243
  const ownPos = this.getOwnPosition?.();
135
- if (!ownPos?.latitude || !ownPos?.longitude) return;
244
+ if (!ownPos?.latitude || !ownPos?.longitude) {
245
+ this._logVerbose("target_skip", {
246
+ radarId,
247
+ targetId,
248
+ reason: "no-own-position",
249
+ });
250
+ return;
251
+ }
136
252
 
137
253
  this.routecache.insert({
138
254
  target_id: this._getTargetUuid(radarId, targetId),
@@ -154,6 +270,16 @@ class MayaraIngestor {
154
270
  courseOverGroundTrue: rad2deg(target.motion?.course),
155
271
  name: `ARPA-${targetId}`,
156
272
  });
273
+ this._logVerbose("target_insert", {
274
+ radarId,
275
+ targetId,
276
+ latitude: lat,
277
+ longitude: lon,
278
+ distance: target.position?.distance ?? null,
279
+ bearingRad: target.position?.bearing ?? null,
280
+ speed: target.motion?.speed ?? null,
281
+ courseRad: target.motion?.course ?? null,
282
+ });
157
283
  }
158
284
 
159
285
  _handleDelta(raw) {
@@ -161,15 +287,26 @@ class MayaraIngestor {
161
287
  try {
162
288
  msg = JSON.parse(raw.toString());
163
289
  } catch (err) {
290
+ this._log("delta_parse_error", { error: err.message });
164
291
  return;
165
292
  }
293
+ this._logVerbose("delta_received", { updates: msg?.updates?.length || 0 });
166
294
 
167
295
  for (const update of msg?.updates || []) {
168
296
  for (const value of update?.values || []) {
169
297
  const path = value?.path || "";
170
298
  const match = path.match(/^radars\.([^.]+)\.targets\.([^.]+)$/);
171
- if (!match) continue;
172
- if (!value.value) continue;
299
+ if (!match) {
300
+ this._logVerbose("delta_skip", {
301
+ reason: "path-not-radar-target",
302
+ path: path || "<empty>",
303
+ });
304
+ continue;
305
+ }
306
+ if (!value.value) {
307
+ this._logVerbose("delta_skip", { reason: "missing-value", path });
308
+ continue;
309
+ }
173
310
  const radarId = match[1];
174
311
  const targetId = match[2];
175
312
  this._insertTarget(radarId, targetId, value.value);
@@ -178,8 +315,8 @@ class MayaraIngestor {
178
315
  }
179
316
 
180
317
  _startHttpFallback() {
181
- if (this.pollTimer || this.destroyed) return;
182
- this.statusFn("Mayara WS unstable, enabling HTTP fallback polling");
318
+ if (this.pollTimer || this.destroyed || this.halted) return;
319
+ this._log("http_fallback_start", { reason: "ws-unstable" });
183
320
  this.pollTimer = setInterval(async () => {
184
321
  if (this.destroyed) return;
185
322
  if (!this.radarIds.length) await this._discoverRadars();
@@ -188,14 +325,21 @@ class MayaraIngestor {
188
325
  const res = await fetch(
189
326
  `${this.baseUrl}/signalk/v2/api/vessels/self/radars/${radarId}/targets`,
190
327
  );
191
- if (!res.ok) continue;
328
+ if (!res.ok) {
329
+ this._log("http_poll_http_error", { radarId, status: res.status });
330
+ continue;
331
+ }
192
332
  const targets = await res.json();
193
- if (!Array.isArray(targets)) continue;
333
+ if (!Array.isArray(targets)) {
334
+ this._log("http_poll_skip", { radarId, reason: "response-not-array" });
335
+ continue;
336
+ }
337
+ this._logVerbose("http_poll_ok", { radarId, targetCount: targets.length });
194
338
  for (const target of targets) {
195
339
  this._insertTarget(radarId, target?.id, target);
196
340
  }
197
341
  } catch (err) {
198
- this.statusFn(`Mayara HTTP poll error: ${err.message}`);
342
+ this._log("http_poll_error", { radarId, error: err.message });
199
343
  }
200
344
  }
201
345
  }, this.pollIntervalMs);