@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +37 -8
  3. package/dist/cli/index.js +130 -96
  4. package/dist/cli/liveContext.d.ts +2 -2
  5. package/dist/cli/liveContext.js +15 -15
  6. package/dist/cli/log.d.ts +1 -1
  7. package/dist/cli/log.js +4 -4
  8. package/dist/config.d.ts +12 -3
  9. package/dist/config.js +71 -49
  10. package/dist/devices/bleDiscovery.d.ts +1 -1
  11. package/dist/devices/bleDiscovery.js +9 -3
  12. package/dist/devices/registry.d.ts +1 -1
  13. package/dist/devices/types.d.ts +4 -4
  14. package/dist/devices/zhsunyco/encode.d.ts +2 -2
  15. package/dist/devices/zhsunyco/index.d.ts +3 -3
  16. package/dist/devices/zhsunyco/index.js +23 -11
  17. package/dist/devices/zhsunyco/metadata.d.ts +1 -1
  18. package/dist/devices/zhsunyco/metadata.js +12 -12
  19. package/dist/devices/zhsunyco/protocol.js +10 -10
  20. package/dist/httpJson.js +3 -3
  21. package/dist/index.d.ts +3 -3
  22. package/dist/pathMeta.d.ts +1 -1
  23. package/dist/pathMeta.js +1 -1
  24. package/dist/plugin.d.ts +1 -1
  25. package/dist/plugin.js +16 -13
  26. package/dist/pluginVersion.d.ts +9 -0
  27. package/dist/pluginVersion.js +12 -2
  28. package/dist/render/assets.d.ts +14 -6
  29. package/dist/render/assets.js +65 -10
  30. package/dist/render/binding.d.ts +9 -2
  31. package/dist/render/binding.js +23 -23
  32. package/dist/render/fonts.js +6 -6
  33. package/dist/render/formatters.d.ts +1 -1
  34. package/dist/render/formatters.js +25 -22
  35. package/dist/render/png.d.ts +1 -1
  36. package/dist/render/svgRenderer.d.ts +9 -8
  37. package/dist/render/svgRenderer.js +33 -27
  38. package/dist/render/types.d.ts +1 -1
  39. package/dist/repaintScheduler.d.ts +2 -2
  40. package/dist/repaintScheduler.js +26 -22
  41. package/dist/resolveApiUrl.js +3 -3
  42. package/dist/unitCategories.d.ts +1 -1
  43. package/dist/unitCategories.js +3 -3
  44. package/docs/assets/screenshots/plugin_config.png +0 -0
  45. package/package.json +42 -39
  46. package/templates/assets/lunar_phases/README.md +18 -0
  47. package/templates/tide.svg +11 -11
  48. /package/docs/assets/{inkscape_desc.png → screenshots/inkscape_desc.png} +0 -0
  49. /package/{resources/svg → templates/assets}/lunar_phases/first_quarter.svg +0 -0
  50. /package/{resources/svg → templates/assets}/lunar_phases/full_moon.svg +0 -0
  51. /package/{resources/svg → templates/assets}/lunar_phases/new_moon.svg +0 -0
  52. /package/{resources/svg → templates/assets}/lunar_phases/third_quarter.svg +0 -0
  53. /package/{resources/svg → templates/assets}/lunar_phases/waning_crescent.svg +0 -0
  54. /package/{resources/svg → templates/assets}/lunar_phases/waning_gibbous.svg +0 -0
  55. /package/{resources/svg → templates/assets}/lunar_phases/waxing_crescent.svg +0 -0
  56. /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('@resvg/resvg-wasm/index_bg.wasm')).then((buffer) => (0, resvg_wasm_1.initWasm)(buffer));
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 that
57
- * directory - see `./assets.ts`) rather than substituting text, and the element is dropped entirely
58
- * (rather than showing an error placeholder) whenever the value is unavailable or doesn't map to a
59
- * file - e.g. a device without the `derived-data` plugin installed simply shows no moon-phase icon.
60
- * The matched file's contents are inlined as a `data:image/svg+xml;base64,...` URI so no filesystem
61
- * access happens at resvg-wasm's render step.
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('SvgRenderer requires at least one font path - resvg-wasm cannot use host system fonts');
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, 'utf-8'));
86
- const doc = new xmldom_1.DOMParser().parseFromString(svgSource, 'image/svg+xml');
87
- const elements = doc.getElementsByTagName('text');
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('desc').item(0);
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(`field "${descElement.textContent}" failed to render: ${err.message}`);
103
- element.textContent = 'ERROR';
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('image');
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('desc').item(0);
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)(svgTemplatePath, binding.assets, key);
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
- ? `image "${descElement.textContent}" has no asset file for value "${key}" in "${binding.assets}"`
133
- : `image "${descElement.textContent}" resolved to no usable value to pick an asset file with`);
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, 'utf-8');
138
- const dataUri = `data:image/svg+xml;base64,${Buffer.from(assetSource).toString('base64')}`;
139
- const hrefAttr = element.getAttribute('href') !== null ? 'href' : 'xlink:href';
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(`image "${descElement.textContent}" failed to render: ${err.message}`);
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: 'width', value: width },
155
+ fitTo: { mode: "width", value: width },
150
156
  font: { fontBuffers },
151
157
  });
152
158
  const rendered = resvg.render();
@@ -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
  }
@@ -1,5 +1,5 @@
1
- import { ServerAPI } from '@signalk/server-api';
2
- import { PluginConfig } from './config';
1
+ import { ServerAPI } from "@signalk/server-api";
2
+ import { PluginConfig } from "./config";
3
3
  export interface RepaintScheduler {
4
4
  stop(): void;
5
5
  }
@@ -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(), 'repaint-state.json');
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), 'utf-8'));
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 === 'object') {
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)('sha1').update(stableStringify(context)).digest('hex');
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 === 'object' && next !== null ? 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 !== 'signalk')
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 === 'self' ? app.getSelfPath(binding.path) : app.getPath(`${binding.context}.${binding.path}`);
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 === 'signalk').map((binding) => binding.context));
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 === 'resources').map((binding) => binding.resource));
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 ? 'ies' : 'y'} "${[...categoryNames].join(', ')}" but no SignalK API base URL is configured`);
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 templatePath = (0, config_1.resolveTemplatePath)((0, config_1.resolveTemplatesDir)(config.templatesDir), device.templateName);
130
- const bindings = (0, binding_1.findBindings)((0, fs_1.readFileSync)(templatePath, 'utf-8'));
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 attempt ${attempt}/${config.paintRetries}`);
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 === 'interval');
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 === 'subscription' && device.triggerPath) {
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() {
@@ -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 = ['http://localhost:3000', 'http://localhost', 'https://localhost'];
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 = '/signalk/v1/unitpreferences/categories';
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(', ')} - it may be on a different port, or anonymous read access may not be enabled`);
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
@@ -1,4 +1,4 @@
1
- import { DisplayUnits } from './render/formatters';
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.
@@ -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 = '/signalk/v1/unitpreferences/categories';
6
- const ACTIVE_PRESET_PATH = '/signalk/v1/unitpreferences/active';
7
- const DEFINITIONS_PATH = '/signalk/v1/unitpreferences/definitions';
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.
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Display SignalK data on eInk Electronic Shelf Labels",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
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
- "prettier:check": "prettier --check .",
25
- "prettier:write": "prettier --write .",
26
- "prepublishOnly": "npm run build"
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
- "prettier": "^3.8.5",
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
- "license": "APACHE-2.0",
73
- "repository": {
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
- "homepage": "https://github.com/rhizomatics/signalk-einklabel-plugin#readme"
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.
@@ -23,8 +23,8 @@
23
23
  inkscape:pagecheckerboard="0"
24
24
  inkscape:deskcolor="#d1d1d1"
25
25
  inkscape:zoom="1.3906433"
26
- inkscape:cx="144.89697"
27
- inkscape:cy="197.39066"
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="194.47879"
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="328.79895"
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="328.79895"
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="187.55594"
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="322.34219"
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="322.34219"
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=../resources/svg/lunar_phases</desc></image></svg>
285
+ id="desc22">path=environment.moon.phaseName,assets=lunar_phases</desc></image></svg>