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 +1 -1
- package/plugin/connector.js +1 -1
- package/plugin/index.js +6 -0
- package/plugin/mayara.js +164 -20
package/package.json
CHANGED
package/plugin/connector.js
CHANGED
|
@@ -75,7 +75,7 @@ class Connector {
|
|
|
75
75
|
const content = response.Response;
|
|
76
76
|
|
|
77
77
|
if (content["kahu.ErrorResponseMessage"] !== undefined) {
|
|
78
|
-
throw content.
|
|
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({
|
|
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.
|
|
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.
|
|
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.
|
|
140
|
+
this._log("discover_ok", { radarCount: this.radarIds.length });
|
|
69
141
|
} catch (err) {
|
|
70
|
-
this.
|
|
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.
|
|
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) =>
|
|
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")
|
|
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))
|
|
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)
|
|
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)
|
|
172
|
-
|
|
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.
|
|
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)
|
|
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))
|
|
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.
|
|
342
|
+
this._log("http_poll_error", { radarId, error: err.message });
|
|
199
343
|
}
|
|
200
344
|
}
|
|
201
345
|
}, this.pollIntervalMs);
|