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 +1 -1
- package/plugin/index.js +6 -0
- package/plugin/mayara.js +108 -16
package/package.json
CHANGED
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;
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
102
|
+
this._log("discover_ok", { radarCount: this.radarIds.length });
|
|
69
103
|
} catch (err) {
|
|
70
|
-
this.
|
|
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.
|
|
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.
|
|
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")
|
|
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))
|
|
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)
|
|
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)
|
|
172
|
-
|
|
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.
|
|
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)
|
|
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))
|
|
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.
|
|
290
|
+
this._log("http_poll_error", { radarId, error: err.message });
|
|
199
291
|
}
|
|
200
292
|
}
|
|
201
293
|
}, this.pollIntervalMs);
|