signalk-instrument-widgets 0.5.0
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 +32 -0
- package/docs/screenshots/1_freeboard-widget.png +0 -0
- package/package.json +48 -0
- package/plugin/index.js +201 -0
- package/public/config.html +13 -0
- package/public/display.html +13 -0
- package/public/gauge.html +13 -0
- package/public/index.html +16 -0
- package/public/instruments.css +248 -0
- package/public/js/config.js +681 -0
- package/public/js/config.js.map +7 -0
- package/public/js/display.js +560 -0
- package/public/js/display.js.map +7 -0
- package/public/js/gauge.js +566 -0
- package/public/js/gauge.js.map +7 -0
- package/public/js/meter.js +536 -0
- package/public/js/meter.js.map +7 -0
- package/public/js/switch.js +519 -0
- package/public/js/switch.js.map +7 -0
- package/public/meter.html +13 -0
- package/public/switch.html +13 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
|
|
6
|
+
// ../signalk-plotterext-bus/dist/chunk-ZYQKQSOC.js
|
|
7
|
+
var BUS_ID = "plotterExt/1";
|
|
8
|
+
var RPC_ERRORS = {
|
|
9
|
+
PARSE_ERROR: -32700,
|
|
10
|
+
INVALID_REQUEST: -32600,
|
|
11
|
+
METHOD_NOT_FOUND: -32601,
|
|
12
|
+
INVALID_PARAMS: -32602,
|
|
13
|
+
INTERNAL_ERROR: -32603,
|
|
14
|
+
HOST_ERROR: -32e3,
|
|
15
|
+
TIMEOUT: -32001,
|
|
16
|
+
CONNECTION_CLOSED: -32002
|
|
17
|
+
};
|
|
18
|
+
var RpcError = class _RpcError extends Error {
|
|
19
|
+
constructor(message, opts = {}) {
|
|
20
|
+
super(message);
|
|
21
|
+
__publicField(this, "code");
|
|
22
|
+
__publicField(this, "data");
|
|
23
|
+
this.name = "RpcError";
|
|
24
|
+
this.code = opts.code ?? RPC_ERRORS.HOST_ERROR;
|
|
25
|
+
const data = { ...opts.data ?? {} };
|
|
26
|
+
if (opts.reason !== void 0) data.reason = opts.reason;
|
|
27
|
+
this.data = Object.keys(data).length > 0 ? data : void 0;
|
|
28
|
+
}
|
|
29
|
+
get reason() {
|
|
30
|
+
return typeof this.data?.reason === "string" ? this.data.reason : void 0;
|
|
31
|
+
}
|
|
32
|
+
toErrorObject() {
|
|
33
|
+
return {
|
|
34
|
+
code: this.code,
|
|
35
|
+
message: this.message,
|
|
36
|
+
...this.data ? { data: this.data } : {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
static fromErrorObject(err) {
|
|
40
|
+
return new _RpcError(err.message, { code: err.code, data: err.data });
|
|
41
|
+
}
|
|
42
|
+
/** Normalize any thrown value into an RpcError suitable for the wire. */
|
|
43
|
+
static from(err) {
|
|
44
|
+
if (err instanceof _RpcError) return err;
|
|
45
|
+
if (err instanceof Error) {
|
|
46
|
+
return new _RpcError(err.message, { code: RPC_ERRORS.INTERNAL_ERROR });
|
|
47
|
+
}
|
|
48
|
+
return new _RpcError(String(err), { code: RPC_ERRORS.INTERNAL_ERROR });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var EVENT_READY = "bus.ready";
|
|
52
|
+
var EVENT_HANDSHAKE = "bus.handshake";
|
|
53
|
+
function matchesPattern(pattern, name) {
|
|
54
|
+
if (pattern === name) return true;
|
|
55
|
+
return match(pattern.split("."), 0, name.split("."), 0);
|
|
56
|
+
}
|
|
57
|
+
function match(p, pi, n, ni) {
|
|
58
|
+
while (pi < p.length) {
|
|
59
|
+
const seg = p[pi];
|
|
60
|
+
if (seg === "**") {
|
|
61
|
+
if (pi === p.length - 1) return true;
|
|
62
|
+
for (let skip = ni; skip <= n.length; skip++) {
|
|
63
|
+
if (match(p, pi + 1, n, skip)) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (ni >= n.length) return false;
|
|
68
|
+
if (seg !== "*" && seg !== n[ni]) return false;
|
|
69
|
+
pi++;
|
|
70
|
+
ni++;
|
|
71
|
+
}
|
|
72
|
+
return ni === n.length;
|
|
73
|
+
}
|
|
74
|
+
function matchesAny(patterns, name) {
|
|
75
|
+
for (const pattern of patterns) {
|
|
76
|
+
if (matchesPattern(pattern, name)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function wrap(msg) {
|
|
81
|
+
return { bus: BUS_ID, msg };
|
|
82
|
+
}
|
|
83
|
+
function unwrap(data) {
|
|
84
|
+
if (typeof data !== "object" || data === null) return null;
|
|
85
|
+
const env = data;
|
|
86
|
+
if (env.bus !== BUS_ID) return null;
|
|
87
|
+
return isJsonRpcMessage(env.msg) ? env.msg : null;
|
|
88
|
+
}
|
|
89
|
+
function isJsonRpcMessage(v) {
|
|
90
|
+
if (typeof v !== "object" || v === null) return false;
|
|
91
|
+
const m = v;
|
|
92
|
+
if (m.jsonrpc !== "2.0") return false;
|
|
93
|
+
if (typeof m.method === "string") {
|
|
94
|
+
return m.id === void 0 || typeof m.id === "string" || typeof m.id === "number";
|
|
95
|
+
}
|
|
96
|
+
const idOk = typeof m.id === "string" || typeof m.id === "number" || m.id === null;
|
|
97
|
+
if (!idOk) return false;
|
|
98
|
+
const hasResult = "result" in m;
|
|
99
|
+
const err = m.error;
|
|
100
|
+
const hasError = typeof err === "object" && err !== null && typeof err.code === "number" && typeof err.message === "string";
|
|
101
|
+
return hasResult ? !("error" in m) : hasError;
|
|
102
|
+
}
|
|
103
|
+
function isRequest(msg) {
|
|
104
|
+
return "method" in msg && "id" in msg && msg.id !== void 0;
|
|
105
|
+
}
|
|
106
|
+
function isNotification(msg) {
|
|
107
|
+
return "method" in msg && (!("id" in msg) || msg.id === void 0);
|
|
108
|
+
}
|
|
109
|
+
function isResponse(msg) {
|
|
110
|
+
return !("method" in msg);
|
|
111
|
+
}
|
|
112
|
+
function windowPort(peer, opts = {}) {
|
|
113
|
+
const listenWindow = opts.listenWindow ?? globalThis;
|
|
114
|
+
const origin = opts.origin ?? listenWindow.location?.origin ?? "*";
|
|
115
|
+
return {
|
|
116
|
+
post(data) {
|
|
117
|
+
peer.postMessage(data, origin);
|
|
118
|
+
},
|
|
119
|
+
listen(handler) {
|
|
120
|
+
const fn = (ev) => {
|
|
121
|
+
if (ev.source !== peer) return;
|
|
122
|
+
if (origin !== "*" && ev.origin !== origin) return;
|
|
123
|
+
handler(ev.data);
|
|
124
|
+
};
|
|
125
|
+
listenWindow.addEventListener("message", fn);
|
|
126
|
+
return () => listenWindow.removeEventListener("message", fn);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
var DEFAULT_CALL_TIMEOUT_MS = 1e4;
|
|
131
|
+
var BusEndpoint = class {
|
|
132
|
+
constructor(opts) {
|
|
133
|
+
__publicField(this, "callTimeoutMs");
|
|
134
|
+
__publicField(this, "port");
|
|
135
|
+
__publicField(this, "unlisten");
|
|
136
|
+
__publicField(this, "onError");
|
|
137
|
+
__publicField(this, "pending", /* @__PURE__ */ new Map());
|
|
138
|
+
__publicField(this, "methods", /* @__PURE__ */ new Map());
|
|
139
|
+
__publicField(this, "eventHandlers", /* @__PURE__ */ new Set());
|
|
140
|
+
__publicField(this, "idPrefix", Math.random().toString(36).slice(2, 8));
|
|
141
|
+
__publicField(this, "seq", 0);
|
|
142
|
+
__publicField(this, "closed", false);
|
|
143
|
+
this.port = opts.port;
|
|
144
|
+
this.callTimeoutMs = opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
145
|
+
this.onError = opts.onError ?? ((err) => console.warn("[plotterext-bus]", err));
|
|
146
|
+
this.unlisten = this.port.listen((data) => this.onData(data));
|
|
147
|
+
}
|
|
148
|
+
registerMethod(name, handler) {
|
|
149
|
+
this.methods.set(name, handler);
|
|
150
|
+
}
|
|
151
|
+
unregisterMethod(name) {
|
|
152
|
+
this.methods.delete(name);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handle incoming notifications whose names match any of the wildcard
|
|
156
|
+
* patterns. Returns an unsubscribe function. This is local dispatch only;
|
|
157
|
+
* telling the peer which events to forward is a separate concern
|
|
158
|
+
* (`events.subscribe`).
|
|
159
|
+
*/
|
|
160
|
+
onEvent(patterns, fn) {
|
|
161
|
+
const entry = { patterns, fn };
|
|
162
|
+
this.eventHandlers.add(entry);
|
|
163
|
+
return () => this.eventHandlers.delete(entry);
|
|
164
|
+
}
|
|
165
|
+
/** Send a notification (an event) to the peer. */
|
|
166
|
+
notify(method, params) {
|
|
167
|
+
this.send({ jsonrpc: "2.0", method, ...params !== void 0 ? { params } : {} });
|
|
168
|
+
}
|
|
169
|
+
/** Call a method on the peer; resolves with its result. */
|
|
170
|
+
call(method, params, opts = {}) {
|
|
171
|
+
if (this.closed) {
|
|
172
|
+
return Promise.reject(
|
|
173
|
+
new RpcError("Bus endpoint is closed", {
|
|
174
|
+
code: RPC_ERRORS.CONNECTION_CLOSED,
|
|
175
|
+
reason: "CLOSED"
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const id = `${this.idPrefix}-${++this.seq}`;
|
|
180
|
+
const timeoutMs = opts.timeoutMs ?? this.callTimeoutMs;
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
183
|
+
this.pending.delete(id);
|
|
184
|
+
reject(
|
|
185
|
+
new RpcError(`Call timed out after ${timeoutMs}ms: ${method}`, {
|
|
186
|
+
code: RPC_ERRORS.TIMEOUT,
|
|
187
|
+
reason: "TIMEOUT"
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
}, timeoutMs) : null;
|
|
191
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
192
|
+
this.send({
|
|
193
|
+
jsonrpc: "2.0",
|
|
194
|
+
id,
|
|
195
|
+
method,
|
|
196
|
+
...params !== void 0 ? { params } : {}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
close() {
|
|
201
|
+
if (this.closed) return;
|
|
202
|
+
this.closed = true;
|
|
203
|
+
this.unlisten();
|
|
204
|
+
for (const [, p] of this.pending) {
|
|
205
|
+
if (p.timer) clearTimeout(p.timer);
|
|
206
|
+
p.reject(
|
|
207
|
+
new RpcError("Bus endpoint closed", {
|
|
208
|
+
code: RPC_ERRORS.CONNECTION_CLOSED,
|
|
209
|
+
reason: "CLOSED"
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
this.pending.clear();
|
|
214
|
+
this.eventHandlers.clear();
|
|
215
|
+
}
|
|
216
|
+
send(msg) {
|
|
217
|
+
if (this.closed) return;
|
|
218
|
+
this.port.post(wrap(msg));
|
|
219
|
+
}
|
|
220
|
+
onData(data) {
|
|
221
|
+
const msg = unwrap(data);
|
|
222
|
+
if (!msg) return;
|
|
223
|
+
if (isResponse(msg)) {
|
|
224
|
+
this.onResponse(msg);
|
|
225
|
+
} else if (isRequest(msg)) {
|
|
226
|
+
void this.onRequest(msg);
|
|
227
|
+
} else if (isNotification(msg)) {
|
|
228
|
+
this.onNotification(msg.method, msg.params);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
onResponse(msg) {
|
|
232
|
+
if (msg.id === null) return;
|
|
233
|
+
const p = this.pending.get(msg.id);
|
|
234
|
+
if (!p) return;
|
|
235
|
+
this.pending.delete(msg.id);
|
|
236
|
+
if (p.timer) clearTimeout(p.timer);
|
|
237
|
+
if ("error" in msg) {
|
|
238
|
+
p.reject(RpcError.fromErrorObject(msg.error));
|
|
239
|
+
} else {
|
|
240
|
+
p.resolve(msg.result);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async onRequest(msg) {
|
|
244
|
+
const handler = this.methods.get(msg.method);
|
|
245
|
+
if (!handler) {
|
|
246
|
+
this.send({
|
|
247
|
+
jsonrpc: "2.0",
|
|
248
|
+
id: msg.id,
|
|
249
|
+
error: {
|
|
250
|
+
code: RPC_ERRORS.METHOD_NOT_FOUND,
|
|
251
|
+
message: `Method not found: ${msg.method}`
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const result = await handler(msg.params, { endpoint: this });
|
|
258
|
+
this.send({
|
|
259
|
+
jsonrpc: "2.0",
|
|
260
|
+
id: msg.id,
|
|
261
|
+
result: result === void 0 ? null : result
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.send({
|
|
265
|
+
jsonrpc: "2.0",
|
|
266
|
+
id: msg.id,
|
|
267
|
+
error: RpcError.from(err).toErrorObject()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
onNotification(name, params) {
|
|
272
|
+
for (const entry of [...this.eventHandlers]) {
|
|
273
|
+
if (matchesAny(entry.patterns, name)) {
|
|
274
|
+
try {
|
|
275
|
+
entry.fn(name, params);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.onError(err);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ../signalk-plotterext-bus/dist/chunk-EGWZMA5J.js
|
|
285
|
+
var ExtensionClient = class {
|
|
286
|
+
constructor(endpoint, handshake) {
|
|
287
|
+
__publicField(this, "handshake");
|
|
288
|
+
__publicField(this, "endpoint");
|
|
289
|
+
/** Host-persisted key/value state (see spec: State Storage). */
|
|
290
|
+
__publicField(this, "state", {
|
|
291
|
+
get: async (keys, scope) => {
|
|
292
|
+
const result = await this.call("state.get", {
|
|
293
|
+
...scope ? { scope } : {},
|
|
294
|
+
...keys ? { keys } : {}
|
|
295
|
+
});
|
|
296
|
+
return result.values ?? {};
|
|
297
|
+
},
|
|
298
|
+
set: async (values, scope) => {
|
|
299
|
+
await this.call("state.set", {
|
|
300
|
+
...scope ? { scope } : {},
|
|
301
|
+
values
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
/** Signal K data relayed by the host (capabilities signalk.stream / .put). */
|
|
306
|
+
__publicField(this, "signalk", {
|
|
307
|
+
/**
|
|
308
|
+
* Subscribe to Signal K path values. The host publishes them as
|
|
309
|
+
* `sk.<path>` events; this helper hides the event-name mapping and
|
|
310
|
+
* establishes both the event-forwarding subscription and the host's
|
|
311
|
+
* upstream Signal K subscription.
|
|
312
|
+
*/
|
|
313
|
+
subscribe: async (paths, handler) => {
|
|
314
|
+
const patterns = paths.map((p) => `sk.${p}`);
|
|
315
|
+
const offEvents = await this.subscribe(
|
|
316
|
+
patterns,
|
|
317
|
+
(_name, params) => handler(params)
|
|
318
|
+
);
|
|
319
|
+
let subscriptionId;
|
|
320
|
+
try {
|
|
321
|
+
const result = await this.call("signalk.subscribe", { paths });
|
|
322
|
+
subscriptionId = result.subscriptionId;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
await offEvents();
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
return async () => {
|
|
328
|
+
await offEvents();
|
|
329
|
+
await this.call("signalk.unsubscribe", { subscriptionId }).catch(
|
|
330
|
+
() => {
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
put: (path, value) => {
|
|
336
|
+
return this.call("signalk.put", { path, value });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
this.endpoint = endpoint;
|
|
340
|
+
this.handshake = handshake;
|
|
341
|
+
}
|
|
342
|
+
get context() {
|
|
343
|
+
return this.handshake.context;
|
|
344
|
+
}
|
|
345
|
+
get apiVersion() {
|
|
346
|
+
return this.handshake.apiVersion;
|
|
347
|
+
}
|
|
348
|
+
get capabilities() {
|
|
349
|
+
return this.handshake.capabilities;
|
|
350
|
+
}
|
|
351
|
+
hasCapability(id) {
|
|
352
|
+
return this.handshake.capabilities.includes(id);
|
|
353
|
+
}
|
|
354
|
+
/** Call a host API method. */
|
|
355
|
+
call(method, params, opts) {
|
|
356
|
+
return this.endpoint.call(method, params, opts);
|
|
357
|
+
}
|
|
358
|
+
/** Send a notification to the host. */
|
|
359
|
+
notify(method, params) {
|
|
360
|
+
this.endpoint.notify(method, params);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Subscribe to host events matching wildcard patterns. Registers both the
|
|
364
|
+
* host-side forwarding subscription and local dispatch; the returned
|
|
365
|
+
* function tears down both.
|
|
366
|
+
*/
|
|
367
|
+
async subscribe(patterns, handler) {
|
|
368
|
+
const off = this.endpoint.onEvent(patterns, handler);
|
|
369
|
+
let subscriptionId;
|
|
370
|
+
try {
|
|
371
|
+
const result = await this.call("events.subscribe", { patterns });
|
|
372
|
+
subscriptionId = result.subscriptionId;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
off();
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
return async () => {
|
|
378
|
+
off();
|
|
379
|
+
await this.call("events.unsubscribe", { subscriptionId }).catch(() => {
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
close() {
|
|
384
|
+
this.endpoint.close();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
function connectExtension(opts = {}) {
|
|
388
|
+
const port = opts.port ?? windowPort(globalThis.parent, {
|
|
389
|
+
origin: "*"
|
|
390
|
+
});
|
|
391
|
+
const endpoint = new BusEndpoint({
|
|
392
|
+
port,
|
|
393
|
+
callTimeoutMs: opts.callTimeoutMs,
|
|
394
|
+
onError: opts.onError
|
|
395
|
+
});
|
|
396
|
+
return new Promise((resolve, reject) => {
|
|
397
|
+
let done = false;
|
|
398
|
+
const off = endpoint.onEvent([EVENT_HANDSHAKE], (_name, params) => {
|
|
399
|
+
if (done) return;
|
|
400
|
+
done = true;
|
|
401
|
+
cleanup();
|
|
402
|
+
resolve(new ExtensionClient(endpoint, params));
|
|
403
|
+
});
|
|
404
|
+
const interval = setInterval(
|
|
405
|
+
() => endpoint.notify(EVENT_READY),
|
|
406
|
+
opts.readyIntervalMs ?? 250
|
|
407
|
+
);
|
|
408
|
+
const timeout = setTimeout(() => {
|
|
409
|
+
if (done) return;
|
|
410
|
+
done = true;
|
|
411
|
+
cleanup();
|
|
412
|
+
endpoint.close();
|
|
413
|
+
reject(
|
|
414
|
+
new RpcError("Timed out waiting for host handshake", {
|
|
415
|
+
code: RPC_ERRORS.TIMEOUT,
|
|
416
|
+
reason: "HANDSHAKE_TIMEOUT"
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
}, opts.timeoutMs ?? 1e4);
|
|
420
|
+
const cleanup = () => {
|
|
421
|
+
off();
|
|
422
|
+
clearInterval(interval);
|
|
423
|
+
clearTimeout(timeout);
|
|
424
|
+
};
|
|
425
|
+
endpoint.notify(EVENT_READY);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/web/common.js
|
|
430
|
+
var CONVERSIONS = {
|
|
431
|
+
none: { label: "Raw value", units: "", fn: (v) => v },
|
|
432
|
+
"ms-kn": { label: "m/s \u2192 knots", units: "kn", fn: (v) => v * 1.943844 },
|
|
433
|
+
"ms-kmh": { label: "m/s \u2192 km/h", units: "km/h", fn: (v) => v * 3.6 },
|
|
434
|
+
"ms-mph": { label: "m/s \u2192 mph", units: "mph", fn: (v) => v * 2.236936 },
|
|
435
|
+
"k-c": { label: "K \u2192 \xB0C", units: "\xB0C", fn: (v) => v - 273.15 },
|
|
436
|
+
"k-f": {
|
|
437
|
+
label: "K \u2192 \xB0F",
|
|
438
|
+
units: "\xB0F",
|
|
439
|
+
fn: (v) => (v - 273.15) * 1.8 + 32
|
|
440
|
+
},
|
|
441
|
+
"rad-deg": {
|
|
442
|
+
label: "rad \u2192 \xB0",
|
|
443
|
+
units: "\xB0",
|
|
444
|
+
fn: (v) => v * 180 / Math.PI
|
|
445
|
+
},
|
|
446
|
+
"ratio-pct": { label: "ratio \u2192 %", units: "%", fn: (v) => v * 100 },
|
|
447
|
+
"m-ft": { label: "m \u2192 ft", units: "ft", fn: (v) => v * 3.28084 },
|
|
448
|
+
"m-nm": { label: "m \u2192 nm", units: "nm", fn: (v) => v / 1852 },
|
|
449
|
+
"m-km": { label: "m \u2192 km", units: "km", fn: (v) => v / 1e3 },
|
|
450
|
+
"pa-hpa": { label: "Pa \u2192 hPa", units: "hPa", fn: (v) => v / 100 }
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/web/units.mjs
|
|
454
|
+
var VALID_BY_UNIT = {
|
|
455
|
+
"m/s": ["none", "ms-kn", "ms-kmh", "ms-mph"],
|
|
456
|
+
K: ["none", "k-c", "k-f"],
|
|
457
|
+
rad: ["none", "rad-deg"],
|
|
458
|
+
ratio: ["none", "ratio-pct"],
|
|
459
|
+
m: ["none", "m-ft", "m-nm", "m-km"],
|
|
460
|
+
Pa: ["none", "pa-hpa"]
|
|
461
|
+
};
|
|
462
|
+
function validConversions(units, allKeys) {
|
|
463
|
+
return units && VALID_BY_UNIT[units] || allKeys;
|
|
464
|
+
}
|
|
465
|
+
function defaultConversion(units, path, prefs) {
|
|
466
|
+
switch (units) {
|
|
467
|
+
case "m/s": {
|
|
468
|
+
const speed = prefs?.speed;
|
|
469
|
+
if (speed === "km/h") return "ms-kmh";
|
|
470
|
+
if (speed === "mph") return "ms-mph";
|
|
471
|
+
if (speed === "m/s") return "none";
|
|
472
|
+
return "ms-kn";
|
|
473
|
+
}
|
|
474
|
+
case "K":
|
|
475
|
+
return prefs?.temperature === "F" ? "k-f" : "k-c";
|
|
476
|
+
case "rad":
|
|
477
|
+
return "rad-deg";
|
|
478
|
+
case "ratio":
|
|
479
|
+
return "ratio-pct";
|
|
480
|
+
case "Pa":
|
|
481
|
+
return "pa-hpa";
|
|
482
|
+
case "m": {
|
|
483
|
+
const p = path ?? "";
|
|
484
|
+
if (/depth/i.test(p)) {
|
|
485
|
+
return prefs?.depth === "foot" ? "m-ft" : "none";
|
|
486
|
+
}
|
|
487
|
+
if (/(distance|log|range)/i.test(p)) {
|
|
488
|
+
return prefs?.distance === "naut-mile" ? "m-nm" : "m-km";
|
|
489
|
+
}
|
|
490
|
+
return prefs?.length === "foot" ? "m-ft" : "none";
|
|
491
|
+
}
|
|
492
|
+
default:
|
|
493
|
+
return "none";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/web/config.js
|
|
498
|
+
var NUMERIC = "numeric";
|
|
499
|
+
var BOOLEAN = "boolean";
|
|
500
|
+
var WIDGET_FIELDS = {
|
|
501
|
+
gauge: { pathKind: NUMERIC, fields: ["label", "convert", "min", "max", "decimals"] },
|
|
502
|
+
meter: { pathKind: NUMERIC, fields: ["label", "convert", "decimals"] },
|
|
503
|
+
switch: { pathKind: BOOLEAN, fields: ["label"] },
|
|
504
|
+
display: {
|
|
505
|
+
pathKind: NUMERIC,
|
|
506
|
+
fields: ["topLabel", "bottomLabel", "convert", "decimals"]
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function flattenTree(node, prefix = "", out = []) {
|
|
510
|
+
if (node === null || typeof node !== "object") return out;
|
|
511
|
+
if ("value" in node && (typeof node.value !== "object" || node.value === null)) {
|
|
512
|
+
out.push([prefix, node.value, node.meta?.units]);
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
for (const [key, child] of Object.entries(node)) {
|
|
516
|
+
if (key === "meta" || key === "timestamp" || key === "$source" || key === "values") {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
flattenTree(child, prefix ? `${prefix}.${key}` : key, out);
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
function kindOf(value) {
|
|
524
|
+
if (typeof value === "number") return NUMERIC;
|
|
525
|
+
if (typeof value === "boolean") return BOOLEAN;
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
async function fetchPaths(pathKind) {
|
|
529
|
+
const res = await fetch("/signalk/v1/api/vessels/self", {
|
|
530
|
+
credentials: "include"
|
|
531
|
+
});
|
|
532
|
+
if (!res.ok) throw new Error(`vessels/self fetch failed: ${res.status}`);
|
|
533
|
+
const tree = await res.json();
|
|
534
|
+
const leaves = flattenTree(tree);
|
|
535
|
+
const paths = [];
|
|
536
|
+
const unitsByPath = {};
|
|
537
|
+
for (const [path, value, metaUnits] of leaves) {
|
|
538
|
+
if (metaUnits) unitsByPath[path] = metaUnits;
|
|
539
|
+
const kind = kindOf(value);
|
|
540
|
+
if (kind === pathKind) paths.push(path);
|
|
541
|
+
if (pathKind === BOOLEAN && kind === NUMERIC && /(switches|\.state$)/.test(path)) {
|
|
542
|
+
paths.push(path);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return { paths: [...new Set(paths)].sort(), unitsByPath };
|
|
546
|
+
}
|
|
547
|
+
function fieldRow(id, label, control) {
|
|
548
|
+
return `<label class="row"><span>${label}</span>${control}</label>`;
|
|
549
|
+
}
|
|
550
|
+
function buildForm(widgetType, paths, state) {
|
|
551
|
+
const spec = WIDGET_FIELDS[widgetType] ?? WIDGET_FIELDS.gauge;
|
|
552
|
+
const rows = [];
|
|
553
|
+
rows.push(
|
|
554
|
+
fieldRow(
|
|
555
|
+
"path",
|
|
556
|
+
"Signal K path",
|
|
557
|
+
`<input id="path" list="paths" value="${state.path ?? ""}" placeholder="Type to search...">
|
|
558
|
+
<datalist id="paths">${paths.map((p) => `<option value="${p}">`).join("")}</datalist>`
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
if (spec.fields.includes("label")) {
|
|
562
|
+
rows.push(
|
|
563
|
+
fieldRow("label", "Label", `<input id="label" value="${state.label ?? ""}" placeholder="Display name">`)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
if (spec.fields.includes("topLabel")) {
|
|
567
|
+
rows.push(
|
|
568
|
+
fieldRow(
|
|
569
|
+
"topLabel",
|
|
570
|
+
"Top label",
|
|
571
|
+
`<input id="topLabel" value="${state.topLabel ?? ""}" placeholder="Small title (blank = hidden)">`
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (spec.fields.includes("bottomLabel")) {
|
|
576
|
+
rows.push(
|
|
577
|
+
fieldRow(
|
|
578
|
+
"bottomLabel",
|
|
579
|
+
"Bottom label",
|
|
580
|
+
`<input id="bottomLabel" value="${state.bottomLabel ?? ""}" placeholder="Large label (blank = hidden)">`
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (spec.fields.includes("convert")) {
|
|
585
|
+
rows.push(
|
|
586
|
+
fieldRow("convert", "Conversion", `<select id="convert"></select>`)
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (spec.fields.includes("min")) {
|
|
590
|
+
rows.push(fieldRow("min", "Minimum", `<input id="min" type="number" step="any" value="${state.min ?? 0}">`));
|
|
591
|
+
rows.push(fieldRow("max", "Maximum", `<input id="max" type="number" step="any" value="${state.max ?? 10}">`));
|
|
592
|
+
}
|
|
593
|
+
if (spec.fields.includes("decimals")) {
|
|
594
|
+
rows.push(
|
|
595
|
+
fieldRow("decimals", "Decimals", `<input id="decimals" type="number" min="0" max="4" value="${state.decimals ?? 1}">`)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return rows.join("");
|
|
599
|
+
}
|
|
600
|
+
function readForm(widgetType) {
|
|
601
|
+
const spec = WIDGET_FIELDS[widgetType] ?? WIDGET_FIELDS.gauge;
|
|
602
|
+
const get = (id) => document.getElementById(id);
|
|
603
|
+
const values = { path: get("path").value.trim() };
|
|
604
|
+
if (spec.fields.includes("label")) values.label = get("label").value.trim();
|
|
605
|
+
if (spec.fields.includes("topLabel")) {
|
|
606
|
+
values.topLabel = get("topLabel").value.trim();
|
|
607
|
+
}
|
|
608
|
+
if (spec.fields.includes("bottomLabel")) {
|
|
609
|
+
values.bottomLabel = get("bottomLabel").value.trim();
|
|
610
|
+
}
|
|
611
|
+
if (spec.fields.includes("convert")) values.convert = get("convert").value;
|
|
612
|
+
if (spec.fields.includes("min")) {
|
|
613
|
+
values.min = Number(get("min").value);
|
|
614
|
+
values.max = Number(get("max").value);
|
|
615
|
+
}
|
|
616
|
+
if (spec.fields.includes("decimals")) {
|
|
617
|
+
values.decimals = Number(get("decimals").value);
|
|
618
|
+
}
|
|
619
|
+
return values;
|
|
620
|
+
}
|
|
621
|
+
function refreshConversionOptions(unitsByPath, prefs, savedPath, savedConvert) {
|
|
622
|
+
const select = document.getElementById("convert");
|
|
623
|
+
if (!select) return;
|
|
624
|
+
const path = document.getElementById("path").value.trim();
|
|
625
|
+
const units = unitsByPath[path];
|
|
626
|
+
const valid = validConversions(units, Object.keys(CONVERSIONS));
|
|
627
|
+
const selected = path === savedPath && savedConvert && valid.includes(savedConvert) ? savedConvert : defaultConversion(units, path, prefs);
|
|
628
|
+
select.innerHTML = valid.map(
|
|
629
|
+
(key) => `<option value="${key}" ${key === selected ? "selected" : ""}>${CONVERSIONS[key].label}</option>`
|
|
630
|
+
).join("");
|
|
631
|
+
}
|
|
632
|
+
async function main() {
|
|
633
|
+
const root = document.getElementById("root");
|
|
634
|
+
const client = await connectExtension();
|
|
635
|
+
const widgetType = client.context.targetWidget ?? "gauge";
|
|
636
|
+
const spec = WIDGET_FIELDS[widgetType] ?? WIDGET_FIELDS.gauge;
|
|
637
|
+
root.innerHTML = '<p class="status">Loading Signal K paths\u2026</p>';
|
|
638
|
+
const [{ paths, unitsByPath }, state, prefs] = await Promise.all([
|
|
639
|
+
fetchPaths(spec.pathKind).catch(() => ({ paths: [], unitsByPath: {} })),
|
|
640
|
+
client.state.get(),
|
|
641
|
+
client.hasCapability("units") ? client.call("units.get").then((r) => r.units).catch(() => null) : Promise.resolve(null)
|
|
642
|
+
]);
|
|
643
|
+
root.innerHTML = `
|
|
644
|
+
<h2>Configure ${widgetType}</h2>
|
|
645
|
+
<form id="form">${buildForm(widgetType, paths, state)}</form>
|
|
646
|
+
<p class="status" id="status"></p>
|
|
647
|
+
<div class="actions">
|
|
648
|
+
<button type="button" id="cancel">Cancel</button>
|
|
649
|
+
<button type="button" id="save" class="primary">Save</button>
|
|
650
|
+
</div>`;
|
|
651
|
+
if (spec.fields.includes("convert")) {
|
|
652
|
+
const refresh = () => refreshConversionOptions(unitsByPath, prefs, state.path, state.convert);
|
|
653
|
+
refresh();
|
|
654
|
+
const pathInput = document.getElementById("path");
|
|
655
|
+
pathInput.addEventListener("change", refresh);
|
|
656
|
+
pathInput.addEventListener("input", () => {
|
|
657
|
+
if (unitsByPath[pathInput.value.trim()] !== void 0) refresh();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const status = document.getElementById("status");
|
|
661
|
+
document.getElementById("save").addEventListener("click", async () => {
|
|
662
|
+
try {
|
|
663
|
+
await client.state.set(readForm(widgetType));
|
|
664
|
+
status.textContent = "Saved.";
|
|
665
|
+
await client.call("ui.closePanel").catch(() => {
|
|
666
|
+
});
|
|
667
|
+
} catch (err) {
|
|
668
|
+
status.textContent = `Save failed: ${err.message}`;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
document.getElementById("cancel").addEventListener("click", () => {
|
|
672
|
+
client.call("ui.closePanel").catch(() => {
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
main().catch((err) => {
|
|
677
|
+
document.getElementById("root").textContent = `Host connection failed: ${err.message}`;
|
|
678
|
+
console.error(err);
|
|
679
|
+
});
|
|
680
|
+
})();
|
|
681
|
+
//# sourceMappingURL=config.js.map
|