@rhizomatics/signalk-einklabel-plugin 0.6.3 → 0.7.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/CHANGELOG.md +23 -0
- package/README.md +37 -8
- package/dist/cli/index.js +130 -96
- package/dist/cli/liveContext.d.ts +2 -2
- package/dist/cli/liveContext.js +15 -15
- package/dist/cli/log.d.ts +1 -1
- package/dist/cli/log.js +4 -4
- package/dist/config.d.ts +12 -3
- package/dist/config.js +71 -49
- package/dist/devices/bleDiscovery.d.ts +1 -1
- package/dist/devices/bleDiscovery.js +9 -3
- package/dist/devices/registry.d.ts +1 -1
- package/dist/devices/types.d.ts +4 -4
- package/dist/devices/zhsunyco/encode.d.ts +2 -2
- package/dist/devices/zhsunyco/index.d.ts +3 -3
- package/dist/devices/zhsunyco/index.js +23 -11
- package/dist/devices/zhsunyco/metadata.d.ts +1 -1
- package/dist/devices/zhsunyco/metadata.js +12 -12
- package/dist/devices/zhsunyco/protocol.js +10 -10
- package/dist/httpJson.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/pathMeta.d.ts +1 -1
- package/dist/pathMeta.js +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +16 -13
- package/dist/pluginVersion.d.ts +9 -0
- package/dist/pluginVersion.js +12 -2
- package/dist/render/assets.d.ts +14 -6
- package/dist/render/assets.js +65 -10
- package/dist/render/binding.d.ts +9 -2
- package/dist/render/binding.js +23 -23
- package/dist/render/fonts.js +6 -6
- package/dist/render/formatters.d.ts +1 -1
- package/dist/render/formatters.js +25 -22
- package/dist/render/png.d.ts +1 -1
- package/dist/render/svgRenderer.d.ts +9 -8
- package/dist/render/svgRenderer.js +33 -27
- package/dist/render/types.d.ts +1 -1
- package/dist/repaintScheduler.d.ts +2 -2
- package/dist/repaintScheduler.js +26 -22
- package/dist/resolveApiUrl.js +3 -3
- package/dist/unitCategories.d.ts +1 -1
- package/dist/unitCategories.js +3 -3
- package/docs/assets/screenshots/plugin_config.png +0 -0
- package/package.json +42 -39
- package/templates/assets/lunar_phases/README.md +18 -0
- package/templates/tide.svg +11 -11
- /package/docs/assets/{inkscape_desc.png → screenshots/inkscape_desc.png} +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/first_quarter.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/full_moon.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/new_moon.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/third_quarter.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waning_crescent.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waning_gibbous.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waxing_crescent.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waxing_gibbous.svg +0 -0
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SvgRenderer = void 0;
|
|
4
4
|
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
5
6
|
const xmldom_1 = require("@xmldom/xmldom");
|
|
6
7
|
const resvg_wasm_1 = require("@resvg/resvg-wasm");
|
|
7
8
|
const binding_1 = require("./binding");
|
|
8
9
|
const assets_1 = require("./assets");
|
|
9
10
|
const fonts_1 = require("./fonts");
|
|
11
|
+
const pluginVersion_1 = require("../pluginVersion");
|
|
12
|
+
const config_1 = require("../config");
|
|
10
13
|
let wasmReady;
|
|
11
14
|
function ensureWasmInitialized() {
|
|
12
15
|
if (!wasmReady) {
|
|
13
|
-
wasmReady = (0, promises_1.readFile)(require.resolve(
|
|
16
|
+
wasmReady = (0, promises_1.readFile)(require.resolve("@resvg/resvg-wasm/index_bg.wasm")).then((buffer) => (0, resvg_wasm_1.initWasm)(buffer));
|
|
14
17
|
}
|
|
15
18
|
return wasmReady;
|
|
16
19
|
}
|
|
@@ -53,12 +56,13 @@ function expandGenericFontFamilies(svgSource) {
|
|
|
53
56
|
* whole tree and rendered blank.
|
|
54
57
|
*
|
|
55
58
|
* Non-textual inclusion: an `<image>` element with a `<desc>` child works the same way, except the
|
|
56
|
-
* resolved value picks one of a directory of `.svg` files (an `assets=` key, required, names
|
|
57
|
-
* directory
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* resolved value picks one of a directory of `.svg` files (an `assets=` key, required, names an
|
|
60
|
+
* `assets/<name>` directory looked up under `templatesDir` first and `BUNDLED_TEMPLATES_DIR` otherwise
|
|
61
|
+
* - see `resolveAssetPath` in `./assets.ts`) rather than substituting text, and the element is dropped
|
|
62
|
+
* entirely (rather than showing an error placeholder) whenever the value is unavailable or doesn't map
|
|
63
|
+
* to a file - e.g. a device without the `derived-data` plugin installed simply shows no moon-phase
|
|
64
|
+
* icon. The matched file's contents are inlined as a `data:image/svg+xml;base64,...` URI so no
|
|
65
|
+
* filesystem access happens at resvg-wasm's render step.
|
|
62
66
|
*
|
|
63
67
|
* resvg-wasm cannot see the host's installed fonts (`loadSystemFonts`/`fontFiles`
|
|
64
68
|
* are silently no-ops under plain Node) - it only renders text if given font
|
|
@@ -71,7 +75,7 @@ class SvgRenderer {
|
|
|
71
75
|
constructor(fontPaths = fonts_1.DEFAULT_FONT_PATHS) {
|
|
72
76
|
this.fontPaths = fontPaths;
|
|
73
77
|
if (fontPaths.length === 0) {
|
|
74
|
-
throw new Error(
|
|
78
|
+
throw new Error("SvgRenderer requires at least one font path - resvg-wasm cannot use host system fonts");
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
loadFontBuffers() {
|
|
@@ -80,73 +84,75 @@ class SvgRenderer {
|
|
|
80
84
|
}
|
|
81
85
|
return this.fontBuffers;
|
|
82
86
|
}
|
|
83
|
-
async render(svgTemplatePath, context, width, height) {
|
|
87
|
+
async render(svgTemplatePath, context, width, height, templatesDir = (0, path_1.dirname)(svgTemplatePath), bundledTemplatesDir = config_1.BUNDLED_TEMPLATES_DIR) {
|
|
84
88
|
const [, fontBuffers] = await Promise.all([ensureWasmInitialized(), this.loadFontBuffers()]);
|
|
85
|
-
const svgSource = expandGenericFontFamilies(await (0, promises_1.readFile)(svgTemplatePath,
|
|
86
|
-
const doc = new xmldom_1.DOMParser().parseFromString(svgSource,
|
|
87
|
-
const elements = doc.getElementsByTagName(
|
|
89
|
+
const svgSource = expandGenericFontFamilies(await (0, promises_1.readFile)(svgTemplatePath, "utf-8"));
|
|
90
|
+
const doc = new xmldom_1.DOMParser().parseFromString(svgSource, "image/svg+xml");
|
|
91
|
+
const elements = doc.getElementsByTagName("text");
|
|
88
92
|
for (let i = 0; i < elements.length; i++) {
|
|
89
93
|
const element = elements.item(i);
|
|
90
94
|
if (!element)
|
|
91
95
|
continue;
|
|
92
|
-
const descElement = element.getElementsByTagName(
|
|
96
|
+
const descElement = element.getElementsByTagName("desc").item(0);
|
|
93
97
|
if (!descElement)
|
|
94
98
|
continue;
|
|
95
99
|
// Scoped to this one element - a single bad/unavailable binding (e.g. a resource that hasn't
|
|
96
100
|
// loaded yet) must not blank out every other field on the label, nor abort the whole repaint.
|
|
97
101
|
try {
|
|
98
|
-
const binding = (0, binding_1.parseBinding)(descElement.textContent ??
|
|
102
|
+
const binding = (0, binding_1.parseBinding)(descElement.textContent ?? "");
|
|
99
103
|
element.textContent = (0, binding_1.renderBinding)(binding, context);
|
|
100
104
|
}
|
|
101
105
|
catch (err) {
|
|
102
|
-
console.error(
|
|
103
|
-
element.textContent =
|
|
106
|
+
console.error(`${pluginVersion_1.PLUGIN_NAME}: field "${descElement.textContent}" failed to render: ${err.message}`);
|
|
107
|
+
element.textContent = "ERROR";
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
// Collected up front, rather than iterated live like the `<text>` loop above - some of these get
|
|
107
111
|
// removed outright (see below), and removing from a live NodeList while iterating it by index would
|
|
108
112
|
// skip whatever shifts into the removed slot.
|
|
109
113
|
const imageElements = [];
|
|
110
|
-
const rawImageElements = doc.getElementsByTagName(
|
|
114
|
+
const rawImageElements = doc.getElementsByTagName("image");
|
|
111
115
|
for (let i = 0; i < rawImageElements.length; i++) {
|
|
112
116
|
const element = rawImageElements.item(i);
|
|
113
117
|
if (element)
|
|
114
118
|
imageElements.push(element);
|
|
115
119
|
}
|
|
116
120
|
for (const element of imageElements) {
|
|
117
|
-
const descElement = element.getElementsByTagName(
|
|
121
|
+
const descElement = element.getElementsByTagName("desc").item(0);
|
|
118
122
|
if (!descElement)
|
|
119
123
|
continue;
|
|
120
124
|
// Same one-field-at-a-time isolation as the `<text>` loop - and the same "no value" outcome
|
|
121
125
|
// (here, dropping the element rather than blanking to 'ERROR' text) for a value that isn't
|
|
122
126
|
// available at all as for one that doesn't map to any asset file, per the binding's contract.
|
|
123
127
|
try {
|
|
124
|
-
const binding = (0, binding_1.parseBinding)(descElement.textContent ??
|
|
128
|
+
const binding = (0, binding_1.parseBinding)(descElement.textContent ?? "");
|
|
125
129
|
if (!binding.assets) {
|
|
126
130
|
throw new Error('an <image> binding requires an "assets" key naming the directory to pick a file from');
|
|
127
131
|
}
|
|
128
132
|
const key = (0, assets_1.normalizeAssetKey)((0, binding_1.resolveBinding)(binding, context));
|
|
129
|
-
const assetPath = key && (0, assets_1.resolveAssetPath)(
|
|
133
|
+
const assetPath = key && (0, assets_1.resolveAssetPath)(templatesDir, bundledTemplatesDir, binding.assets, key);
|
|
130
134
|
if (!assetPath) {
|
|
135
|
+
const dirProblem = (0, assets_1.describeAssetsDirProblem)(templatesDir, bundledTemplatesDir, binding.assets);
|
|
131
136
|
console.error(key
|
|
132
|
-
?
|
|
133
|
-
:
|
|
137
|
+
? `${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" has no asset file for value "${key}" in "${binding.assets}"`
|
|
138
|
+
: `${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" resolved to no usable value to pick an asset file with`);
|
|
139
|
+
console.error(`${pluginVersion_1.PLUGIN_NAME}: ${dirProblem ?? `assets directory for "${binding.assets}" checked out fine - the miss is just this value's`}`);
|
|
134
140
|
element.parentNode?.removeChild(element);
|
|
135
141
|
continue;
|
|
136
142
|
}
|
|
137
|
-
const assetSource = await (0, promises_1.readFile)(assetPath,
|
|
138
|
-
const dataUri = `data:image/svg+xml;base64,${Buffer.from(assetSource).toString(
|
|
139
|
-
const hrefAttr = element.getAttribute(
|
|
143
|
+
const assetSource = await (0, promises_1.readFile)(assetPath, "utf-8");
|
|
144
|
+
const dataUri = `data:image/svg+xml;base64,${Buffer.from(assetSource).toString("base64")}`;
|
|
145
|
+
const hrefAttr = element.getAttribute("href") !== null ? "href" : "xlink:href";
|
|
140
146
|
element.setAttribute(hrefAttr, dataUri);
|
|
141
147
|
}
|
|
142
148
|
catch (err) {
|
|
143
|
-
console.error(
|
|
149
|
+
console.error(`${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" failed to render: ${err.message}`);
|
|
144
150
|
element.parentNode?.removeChild(element);
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
const svgOutput = new xmldom_1.XMLSerializer().serializeToString(doc);
|
|
148
154
|
const resvg = new resvg_wasm_1.Resvg(svgOutput, {
|
|
149
|
-
fitTo: { mode:
|
|
155
|
+
fitTo: { mode: "width", value: width },
|
|
150
156
|
font: { fontBuffers },
|
|
151
157
|
});
|
|
152
158
|
const rendered = resvg.render();
|
package/dist/render/types.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export interface Bitmap {
|
|
|
22
22
|
*/
|
|
23
23
|
export type TemplateContext = Record<string, unknown>;
|
|
24
24
|
export interface Renderer {
|
|
25
|
-
render(svgTemplatePath: string, context: TemplateContext, width: number, height: number): Promise<Bitmap>;
|
|
25
|
+
render(svgTemplatePath: string, context: TemplateContext, width: number, height: number, templatesDir?: string, bundledTemplatesDir?: string): Promise<Bitmap>;
|
|
26
26
|
}
|
package/dist/repaintScheduler.js
CHANGED
|
@@ -17,11 +17,11 @@ const pluginVersion_1 = require("./pluginVersion");
|
|
|
17
17
|
const INTERVAL_POLL_MS = 60000;
|
|
18
18
|
const SUBSCRIPTION_DEBOUNCE_MS = 2000;
|
|
19
19
|
function statePath(app) {
|
|
20
|
-
return (0, path_1.join)(app.getDataDirPath(),
|
|
20
|
+
return (0, path_1.join)(app.getDataDirPath(), "repaint-state.json");
|
|
21
21
|
}
|
|
22
22
|
function loadState(app) {
|
|
23
23
|
try {
|
|
24
|
-
return JSON.parse((0, fs_1.readFileSync)(statePath(app),
|
|
24
|
+
return JSON.parse((0, fs_1.readFileSync)(statePath(app), "utf-8"));
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
27
|
return {};
|
|
@@ -33,24 +33,24 @@ function saveState(app, state) {
|
|
|
33
33
|
/** Deterministic JSON serialisation (sorted keys) so re-ordered object keys don't change the hash. */
|
|
34
34
|
function stableStringify(value) {
|
|
35
35
|
if (Array.isArray(value)) {
|
|
36
|
-
return `[${value.map(stableStringify).join(
|
|
36
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
37
37
|
}
|
|
38
|
-
if (value !== null && typeof value ===
|
|
38
|
+
if (value !== null && typeof value === "object") {
|
|
39
39
|
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
40
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(
|
|
40
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
41
41
|
}
|
|
42
42
|
return JSON.stringify(value);
|
|
43
43
|
}
|
|
44
44
|
function hashContext(context) {
|
|
45
|
-
return (0, crypto_1.createHash)(
|
|
45
|
+
return (0, crypto_1.createHash)("sha1").update(stableStringify(context)).digest("hex");
|
|
46
46
|
}
|
|
47
47
|
/** Merges `value` into `target` at the nested location described by a dotted SignalK path. */
|
|
48
48
|
function setAtPath(target, path, value) {
|
|
49
|
-
const segments = path.split(
|
|
49
|
+
const segments = path.split(".");
|
|
50
50
|
let node = target;
|
|
51
51
|
for (const segment of segments.slice(0, -1)) {
|
|
52
52
|
const next = node[segment];
|
|
53
|
-
node[segment] = typeof next ===
|
|
53
|
+
node[segment] = typeof next === "object" && next !== null ? next : {};
|
|
54
54
|
node = node[segment];
|
|
55
55
|
}
|
|
56
56
|
node[segments[segments.length - 1]] = value;
|
|
@@ -71,19 +71,19 @@ async function assembleRawContext(app, apiUrl, bindings) {
|
|
|
71
71
|
const signalk = {};
|
|
72
72
|
const seenSignalk = new Set();
|
|
73
73
|
for (const binding of bindings) {
|
|
74
|
-
if (binding.source !==
|
|
74
|
+
if (binding.source !== "signalk")
|
|
75
75
|
continue;
|
|
76
76
|
const key = `${binding.context} ${binding.path}`;
|
|
77
77
|
if (seenSignalk.has(key))
|
|
78
78
|
continue;
|
|
79
79
|
seenSignalk.add(key);
|
|
80
|
-
const value = binding.context ===
|
|
80
|
+
const value = binding.context === "self" ? app.getSelfPath(binding.path) : app.getPath(`${binding.context}.${binding.path}`);
|
|
81
81
|
const namespace = (signalk[_a = binding.context] ?? (signalk[_a] = {}));
|
|
82
82
|
setAtPath(namespace, binding.path, value);
|
|
83
83
|
}
|
|
84
84
|
signalk.self ?? (signalk.self = {});
|
|
85
85
|
const pathMeta = {};
|
|
86
|
-
const signalkContexts = new Set(bindings.filter((binding) => binding.source ===
|
|
86
|
+
const signalkContexts = new Set(bindings.filter((binding) => binding.source === "signalk").map((binding) => binding.context));
|
|
87
87
|
if (apiUrl) {
|
|
88
88
|
for (const ctx of signalkContexts) {
|
|
89
89
|
try {
|
|
@@ -95,7 +95,7 @@ async function assembleRawContext(app, apiUrl, bindings) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
const resources = {};
|
|
98
|
-
const resourceNames = new Set(bindings.filter((binding) => binding.source ===
|
|
98
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
|
|
99
99
|
for (const name of resourceNames) {
|
|
100
100
|
// `listResources`'s type only allows the standard SignalKResourceType union, but the underlying
|
|
101
101
|
// Resources API (and a custom provider like signalk-tides, registered under the non-standard
|
|
@@ -105,7 +105,7 @@ async function assembleRawContext(app, apiUrl, bindings) {
|
|
|
105
105
|
}
|
|
106
106
|
const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
|
|
107
107
|
if (categoryNames.size > 0 && !apiUrl) {
|
|
108
|
-
throw new Error(`binding references categor${categoryNames.size > 1 ?
|
|
108
|
+
throw new Error(`binding references categor${categoryNames.size > 1 ? "ies" : "y"} "${[...categoryNames].join(", ")}" but no SignalK API base URL is configured`);
|
|
109
109
|
}
|
|
110
110
|
const categories = apiUrl ? await (0, unitCategories_1.fetchCategoryDisplayUnits)(apiUrl, categoryNames) : {};
|
|
111
111
|
return { signalk, resources, pathMeta, categories };
|
|
@@ -126,8 +126,9 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
|
|
|
126
126
|
app.debug(`"${device.friendlyName}": no driver/metadata for device "${device.device}", skipping`);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
-
const
|
|
130
|
-
const
|
|
129
|
+
const templatesDir = (0, config_1.resolveTemplatesDir)(config.templatesDir);
|
|
130
|
+
const templatePath = (0, config_1.resolveTemplatePath)(templatesDir, device.templateName);
|
|
131
|
+
const bindings = (0, binding_1.findBindings)((0, fs_1.readFileSync)(templatePath, "utf-8"));
|
|
131
132
|
const apiUrl = await getApiUrl().catch((err) => {
|
|
132
133
|
app.debug(`"${device.friendlyName}": ${err.message}`);
|
|
133
134
|
return undefined;
|
|
@@ -147,27 +148,30 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
|
|
|
147
148
|
},
|
|
148
149
|
};
|
|
149
150
|
const renderer = new svgRenderer_1.SvgRenderer();
|
|
150
|
-
const bitmap = await renderer.render(templatePath, renderContext, metadata.width, metadata.height - metadata.voffset);
|
|
151
|
+
const bitmap = await renderer.render(templatePath, renderContext, metadata.width, metadata.height - metadata.voffset, templatesDir, config_1.BUNDLED_TEMPLATES_DIR);
|
|
151
152
|
const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
|
|
153
|
+
let paintDurationMs = 0;
|
|
152
154
|
await (0, bleDiscovery_1.withRetries)(config.paintRetries, async (attempt) => {
|
|
153
155
|
if (attempt > 1) {
|
|
154
|
-
app.debug(`"${device.friendlyName}": paint
|
|
156
|
+
app.debug(`"${device.friendlyName}": attempting paint ${attempt}/${config.paintRetries}`);
|
|
155
157
|
}
|
|
158
|
+
const startedAt = Date.now();
|
|
156
159
|
await driver.paint(bitmap, { address: model.address, aesKey: device.aesKey, connectTimeoutMs });
|
|
160
|
+
paintDurationMs = Date.now() - startedAt;
|
|
157
161
|
});
|
|
158
162
|
state[device.friendlyName] = { hash };
|
|
159
163
|
saveState(app, state);
|
|
160
164
|
if (device.forceRepaint) {
|
|
161
165
|
clearForceRepaint(app, device.friendlyName);
|
|
162
166
|
}
|
|
163
|
-
app.debug(`"${device.friendlyName}": repainted`);
|
|
167
|
+
app.debug(`"${device.friendlyName}": repainted (paint took ${paintDurationMs}ms)`);
|
|
164
168
|
}
|
|
165
169
|
function startRepaintScheduler(app, config) {
|
|
166
170
|
const state = loadState(app);
|
|
167
171
|
const unsubscribes = [];
|
|
168
172
|
const getApiUrl = (0, resolveApiUrl_1.createApiUrlResolver)(config.signalkApiUrl);
|
|
169
173
|
const repaint = (device) => considerRepaint(app, config, device, state, getApiUrl).catch((err) => app.debug(`"${device.friendlyName}": repaint failed: ${err.message}`));
|
|
170
|
-
const intervalDevices = config.devices.filter((device) => device.repaintTrigger ===
|
|
174
|
+
const intervalDevices = config.devices.filter((device) => device.repaintTrigger === "interval");
|
|
171
175
|
if (intervalDevices.length > 0) {
|
|
172
176
|
const timer = setInterval(() => {
|
|
173
177
|
const now = new Date();
|
|
@@ -175,14 +179,14 @@ function startRepaintScheduler(app, config) {
|
|
|
175
179
|
const hours = device.intervalHours ?? 1;
|
|
176
180
|
const minute = device.intervalMinute ?? 0;
|
|
177
181
|
if (now.getHours() % hours === 0 && now.getMinutes() === minute) {
|
|
178
|
-
repaint(device);
|
|
182
|
+
void repaint(device);
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
}, INTERVAL_POLL_MS);
|
|
182
186
|
unsubscribes.push(() => clearInterval(timer));
|
|
183
187
|
}
|
|
184
188
|
for (const device of config.devices) {
|
|
185
|
-
if (device.repaintTrigger ===
|
|
189
|
+
if (device.repaintTrigger === "subscription" && device.triggerPath) {
|
|
186
190
|
const stream = app.streambundle.getSelfStream(device.triggerPath).debounce(SUBSCRIPTION_DEBOUNCE_MS);
|
|
187
191
|
const unsub = stream.onValue(() => repaint(device));
|
|
188
192
|
unsubscribes.push(unsub);
|
|
@@ -191,7 +195,7 @@ function startRepaintScheduler(app, config) {
|
|
|
191
195
|
// Check every device once at startup - harmless given hash dedup, and covers newly-added
|
|
192
196
|
// devices or a forceRepaint left set from before a restart.
|
|
193
197
|
for (const device of config.devices) {
|
|
194
|
-
repaint(device);
|
|
198
|
+
void repaint(device);
|
|
195
199
|
}
|
|
196
200
|
return {
|
|
197
201
|
stop() {
|
package/dist/resolveApiUrl.js
CHANGED
|
@@ -18,9 +18,9 @@ const httpJson_1 = require("./httpJson");
|
|
|
18
18
|
* entirely by install method: a bare `npm install` defaults to 3000; container/systemd installs
|
|
19
19
|
* commonly default to 80, or 443 if TLS-terminated locally.
|
|
20
20
|
*/
|
|
21
|
-
exports.SIGNALK_API_URL_OPTIONS = [
|
|
21
|
+
exports.SIGNALK_API_URL_OPTIONS = ["http://localhost:3000", "http://localhost", "https://localhost"];
|
|
22
22
|
/** Cheap, always-required (for `category=` resolution) and read-only, so safe to use as a connectivity+access probe. */
|
|
23
|
-
const PROBE_PATH =
|
|
23
|
+
const PROBE_PATH = "/signalk/v1/unitpreferences/categories";
|
|
24
24
|
async function probe(url) {
|
|
25
25
|
try {
|
|
26
26
|
await (0, httpJson_1.fetchJson)(`${url}${PROBE_PATH}`);
|
|
@@ -44,7 +44,7 @@ async function resolveSignalkApiUrl(configuredUrl) {
|
|
|
44
44
|
}
|
|
45
45
|
throw new Error(configuredUrl
|
|
46
46
|
? `configured SignalK API base URL "${configuredUrl}" did not respond to ${PROBE_PATH} - check the port, and that anonymous read access is enabled`
|
|
47
|
-
: `could not reach this server's API on any of ${exports.SIGNALK_API_URL_OPTIONS.join(
|
|
47
|
+
: `could not reach this server's API on any of ${exports.SIGNALK_API_URL_OPTIONS.join(", ")} - it may be on a different port, or anonymous read access may not be enabled`);
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Memoizes a successful resolution only - a failure (server not ready yet, anonymous access
|
package/dist/unitCategories.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DisplayUnits } from
|
|
1
|
+
import { DisplayUnits } from "./render/formatters";
|
|
2
2
|
/**
|
|
3
3
|
* Resolves `category=` bindings (e.g. `category=depth` on a `source=resources` value with no per-path
|
|
4
4
|
* metadata of its own to auto-convert from) against this server's unit-preferences setup.
|
package/dist/unitCategories.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchCategoryDisplayUnits = fetchCategoryDisplayUnits;
|
|
4
4
|
const httpJson_1 = require("./httpJson");
|
|
5
|
-
const CATEGORIES_PATH =
|
|
6
|
-
const ACTIVE_PRESET_PATH =
|
|
7
|
-
const DEFINITIONS_PATH =
|
|
5
|
+
const CATEGORIES_PATH = "/signalk/v1/unitpreferences/categories";
|
|
6
|
+
const ACTIVE_PRESET_PATH = "/signalk/v1/unitpreferences/active";
|
|
7
|
+
const DEFINITIONS_PATH = "/signalk/v1/unitpreferences/definitions";
|
|
8
8
|
/**
|
|
9
9
|
* Resolves `category=` bindings (e.g. `category=depth` on a `source=resources` value with no per-path
|
|
10
10
|
* metadata of its own to auto-convert from) against this server's unit-preferences setup.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Display SignalK data on eInk Electronic Shelf Labels",
|
|
5
|
-
"
|
|
6
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ble",
|
|
7
|
+
"display",
|
|
8
|
+
"eink",
|
|
9
|
+
"esl",
|
|
10
|
+
"instrument",
|
|
11
|
+
"signalk",
|
|
12
|
+
"signalk-category-hardware",
|
|
13
|
+
"signalk-category-instruments",
|
|
14
|
+
"signalk-node-server-plugin",
|
|
15
|
+
"tides"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/rhizomatics/signalk-einklabel-plugin#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/rhizomatics/signalk-einklabel-plugin/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "APACHE-2.0",
|
|
22
|
+
"author": "jey burrows",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/rhizomatics/signalk-einklabel-plugin.git"
|
|
26
|
+
},
|
|
7
27
|
"bin": {
|
|
8
28
|
"esl-cli": "dist/cli/index.js"
|
|
9
29
|
},
|
|
10
30
|
"files": [
|
|
11
31
|
"dist",
|
|
12
32
|
"templates",
|
|
13
|
-
"resources",
|
|
14
33
|
"docs",
|
|
15
34
|
"CHANGELOG.md",
|
|
16
35
|
"README.md"
|
|
17
36
|
],
|
|
37
|
+
"main": "dist/index.js",
|
|
38
|
+
"types": "dist/index.d.ts",
|
|
18
39
|
"scripts": {
|
|
19
40
|
"build": "tsc",
|
|
20
41
|
"watch": "tsc --watch",
|
|
21
42
|
"cli": "ts-node src/cli/index.ts",
|
|
22
43
|
"test": "node scripts/run-tests.js",
|
|
23
44
|
"coverage": "node scripts/run-tests.js --coverage",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"signalk": {
|
|
29
|
-
"displayName": "eInk Label Displays",
|
|
30
|
-
"appIcon": "docs/assets/icons/icon_tmp.png",
|
|
31
|
-
"screenshots": [
|
|
32
|
-
"docs/assets/screenshots/example_tidal_clock.png"
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"signalk-node-server-plugin",
|
|
37
|
-
"signalk-category-instruments",
|
|
38
|
-
"signalk-category-hardware",
|
|
39
|
-
"signalk",
|
|
40
|
-
"eink",
|
|
41
|
-
"esl",
|
|
42
|
-
"display",
|
|
43
|
-
"instrument",
|
|
44
|
-
"ble",
|
|
45
|
-
"tides"
|
|
46
|
-
],
|
|
47
|
-
"engines": {
|
|
48
|
-
"node": ">=18"
|
|
45
|
+
"prepublishOnly": "npm run build",
|
|
46
|
+
"fmt": "oxfmt",
|
|
47
|
+
"fmt:check": "oxfmt --check",
|
|
48
|
+
"lint": "oxlint"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@fontsource/roboto": "^5.2.10",
|
|
@@ -65,18 +65,21 @@
|
|
|
65
65
|
"@types/luxon": "^3.7.1",
|
|
66
66
|
"@types/node": "^20.14.0",
|
|
67
67
|
"@types/pngjs": "^6.0.5",
|
|
68
|
-
"
|
|
68
|
+
"oxfmt": "^0.56.0",
|
|
69
|
+
"oxlint": "^1.71.0",
|
|
70
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
69
71
|
"ts-node": "^10.9.2",
|
|
70
72
|
"typescript": "^5.5.0"
|
|
71
73
|
},
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"type": "git",
|
|
75
|
-
"url": "git+https://github.com/rhizomatics/signalk-einklabel-plugin.git"
|
|
76
|
-
},
|
|
77
|
-
"author": "jey burrows",
|
|
78
|
-
"bugs": {
|
|
79
|
-
"url": "https://github.com/rhizomatics/signalk-einklabel-plugin/issues"
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18"
|
|
80
76
|
},
|
|
81
|
-
"
|
|
77
|
+
"signalk": {
|
|
78
|
+
"displayName": "eInk Label Displays",
|
|
79
|
+
"appIcon": "docs/assets/icons/icon_tmp.png",
|
|
80
|
+
"screenshots": [
|
|
81
|
+
"docs/assets/screenshots/example_tidal_clock.png",
|
|
82
|
+
"docs/assets/screenshots/plugin_config.png"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
82
85
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Lunar Phases
|
|
2
|
+
|
|
3
|
+
## Source
|
|
4
|
+
|
|
5
|
+
https://commons.wikimedia.org/wiki/Category:SVG_lunar_phases_icons
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Embed any of these in a template as a placeholder and set the `description`
|
|
10
|
+
property to `path=environment.moon.phaseName,assets=lunar_phases`
|
|
11
|
+
|
|
12
|
+
The image will be replaced at rendering time by the one matching the phase name.
|
|
13
|
+
This will automatically adapt from `Waxing Gibbous` to `waxing_gibbous.svg`
|
|
14
|
+
|
|
15
|
+
## Replacing
|
|
16
|
+
|
|
17
|
+
Define your own template directory in the plugin configuration, and add an `assets/lunar_phases` directory path beneath it. You will need a complete set of phases in
|
|
18
|
+
the new directory, its not possible to override only one of them.
|
package/templates/tide.svg
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
inkscape:pagecheckerboard="0"
|
|
24
24
|
inkscape:deskcolor="#d1d1d1"
|
|
25
25
|
inkscape:zoom="1.3906433"
|
|
26
|
-
inkscape:cx="
|
|
27
|
-
inkscape:cy="
|
|
26
|
+
inkscape:cx="180.85155"
|
|
27
|
+
inkscape:cy="134.11059"
|
|
28
28
|
inkscape:window-width="1488"
|
|
29
29
|
inkscape:window-height="674"
|
|
30
30
|
inkscape:window-x="24"
|
|
@@ -182,16 +182,16 @@
|
|
|
182
182
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.78893">3.3m</tspan><desc
|
|
183
183
|
id="desc6">source=resources,resource=tides,path=extremes[2].level,category=depth</desc></text><text
|
|
184
184
|
id="extremes.2.time"
|
|
185
|
-
x="
|
|
185
|
+
x="203.77321"
|
|
186
186
|
y="146.1584"
|
|
187
187
|
font-size="14"
|
|
188
188
|
font-family="monospace"
|
|
189
189
|
fill="red"
|
|
190
|
-
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.51352"
|
|
190
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.51352"
|
|
191
191
|
transform="scale(0.70226239,1.4239692)">15:53<desc
|
|
192
192
|
id="desc10">source=resources,resource=tides,path=extremes[2].time,format=local_time</desc></text><text
|
|
193
193
|
id="extremes.1.level"
|
|
194
|
-
x="
|
|
194
|
+
x="334.30084"
|
|
195
195
|
y="109.17664"
|
|
196
196
|
font-size="14"
|
|
197
197
|
font-family="monospace"
|
|
@@ -202,17 +202,17 @@
|
|
|
202
202
|
inkscape:transform-center-x="-0.00038058817"><tspan
|
|
203
203
|
sodipodi:role="line"
|
|
204
204
|
id="tspan2-1"
|
|
205
|
-
x="
|
|
205
|
+
x="334.30084"
|
|
206
206
|
y="109.17664"
|
|
207
207
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.701">1.6m</tspan><desc
|
|
208
208
|
id="desc5">source=resources,resource=tides,path=extremes[1].level,category=depth</desc></text><text
|
|
209
209
|
id="extremes.1.time"
|
|
210
|
-
x="
|
|
210
|
+
x="188.63072"
|
|
211
211
|
y="108.65867"
|
|
212
212
|
font-size="14"
|
|
213
213
|
font-family="monospace"
|
|
214
214
|
fill="red"
|
|
215
|
-
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.48364"
|
|
215
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.48364"
|
|
216
216
|
transform="scale(0.7345432,1.3613903)">09:36<desc
|
|
217
217
|
id="desc8">source=resources,resource=tides,path=extremes[1].time,format=local_time</desc></text><text
|
|
218
218
|
id="extremes.0.time"
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
transform="scale(0.74623156,1.3400666)"><desc
|
|
253
253
|
id="desc9">source=resources,resource=tides,path=extremes[2].time,format=day_mon</desc>27 Jun</text><text
|
|
254
254
|
id="extremes.0.level"
|
|
255
|
-
x="
|
|
255
|
+
x="324.0769"
|
|
256
256
|
y="64.82357"
|
|
257
257
|
font-size="14"
|
|
258
258
|
font-family="monospace"
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
transform="scale(0.7703174,1.2981662)"><tspan
|
|
262
262
|
sodipodi:role="line"
|
|
263
263
|
id="tspan2"
|
|
264
|
-
x="
|
|
264
|
+
x="324.0769"
|
|
265
265
|
y="64.82357"
|
|
266
266
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.71992">3.4m</tspan><desc
|
|
267
267
|
id="desc4">source=resources,resource=tides,path=extremes[0].level,category=depth</desc></text><text
|
|
@@ -282,4 +282,4 @@
|
|
|
282
282
|
id="moon_phase"
|
|
283
283
|
x="340.3974"
|
|
284
284
|
y="90.153488"><desc
|
|
285
|
-
id="desc22">path=environment.moon.phaseName,assets
|
|
285
|
+
id="desc22">path=environment.moon.phaseName,assets=lunar_phases</desc></image></svg>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|