@rhizomatics/signalk-einklabel-plugin 0.6.4 → 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 +18 -0
  2. package/README.md +19 -10
  3. package/dist/cli/index.js +129 -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 +10 -9
  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 +21 -20
  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
package/dist/plugin.js CHANGED
@@ -21,16 +21,16 @@ async function runStartupScan(app, discovered, durationSeconds) {
21
21
  }
22
22
  const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData).catch((err) => {
23
23
  scanError = `${driver.vendor} scan failed: ${err.message}`;
24
- app.debug(`${scanError}\n${err.stack ?? ''}`);
24
+ app.debug(`${scanError}\n${err.stack ?? ""}`);
25
25
  return undefined;
26
26
  });
27
27
  if (!found) {
28
28
  return;
29
29
  }
30
30
  discovered.push(found);
31
- const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, '0')}` : 'unknown';
32
- const hwid = found.hwVersion ?? 'unknown';
33
- app.debug(`discovered ${driver.vendor} device "${found.name ?? ''}" [${found.address}] pid=${pid} hwid=${hwid}`);
31
+ const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "unknown";
32
+ const hwid = found.hwVersion ?? "unknown";
33
+ app.debug(`discovered ${driver.vendor} device "${found.name ?? ""}" [${found.address}] pid=${pid} hwid=${hwid}`);
34
34
  });
35
35
  });
36
36
  }
@@ -39,17 +39,17 @@ async function runStartupScan(app, discovered, durationSeconds) {
39
39
  // D-Bus connection) skips every line below, leaving the admin UI stuck on "Scanning..."
40
40
  // forever even though `discovered` may already hold devices found before the failure.
41
41
  scanError = `scan failed: ${err.message}`;
42
- app.debug(`${scanError}\n${err.stack ?? ''}`);
42
+ app.debug(`${scanError}\n${err.stack ?? ""}`);
43
43
  }
44
44
  // Surfaces the real cause in the admin UI (instead of only the debug log) - a scan that
45
45
  // ends in well under its configured duration is almost always this, not "no devices nearby".
46
- app.setPluginError(scanError ?? '');
46
+ app.setPluginError(scanError ?? "");
47
47
  const elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1);
48
48
  if (discovered.length === 0) {
49
49
  app.setPluginStatus(`Scan complete - no ESL devices found nearby after ${elapsedSeconds} seconds.`);
50
50
  return;
51
51
  }
52
- const summary = discovered.map((device) => `${device.name ?? device.vendor} [${device.address}]`).join(', ');
52
+ const summary = discovered.map((device) => `${device.name ?? device.vendor} [${device.address}]`).join(", ");
53
53
  app.setPluginStatus(`Scan complete - found ${discovered.length} device(s) in ${elapsedSeconds}s: ${summary} - pick one from a device's "Device" field below`);
54
54
  }
55
55
  function createPlugin(app) {
@@ -63,13 +63,16 @@ function createPlugin(app) {
63
63
  let scanInProgress;
64
64
  let scanStartedAt;
65
65
  const plugin = {
66
- id: 'signalk-einklabel-plugin',
67
- name: 'eInk ESL (Electronic Shelf Label)',
68
- description: 'Renders selected SignalK data to BLE eInk Electronic Shelf Labels',
66
+ id: "signalk-einklabel-plugin",
67
+ name: "eInk ESL (Electronic Shelf Label)",
68
+ description: "Renders selected SignalK data to BLE eInk Electronic Shelf Labels",
69
69
  schema: () => (0, config_1.configSchema)(app, lastDiscovered),
70
70
  uiSchema: () => (0, config_1.configUiSchema)(),
71
71
  start(config) {
72
- const pluginConfig = { ...(0, config_1.defaultConfig)(), ...config };
72
+ const pluginConfig = {
73
+ ...(0, config_1.defaultConfig)(),
74
+ ...config,
75
+ };
73
76
  app.debug(`starting with ${pluginConfig.devices.length} configured device(s)`);
74
77
  if (pluginConfig.scanOnStart) {
75
78
  if (scanInProgress) {
@@ -82,7 +85,7 @@ function createPlugin(app) {
82
85
  scanStartedAt = Date.now();
83
86
  const scan = runStartupScan(app, lastDiscovered, pluginConfig.scanDurationSeconds).catch((err) => app.debug(`startup scan failed: ${err.message}`));
84
87
  scanInProgress = scan;
85
- scan.finally(() => {
88
+ void scan.finally(() => {
86
89
  scanInProgress = undefined;
87
90
  scanStartedAt = undefined;
88
91
  });
@@ -93,7 +96,7 @@ function createPlugin(app) {
93
96
  stop() {
94
97
  scheduler?.stop();
95
98
  scheduler = undefined;
96
- app.debug('stopped');
99
+ app.debug("stopped");
97
100
  },
98
101
  };
99
102
  return plugin;
@@ -4,3 +4,12 @@
4
4
  * `cli/liveContext.ts`), e.g. to show a build/version marker on a label for support purposes.
5
5
  */
6
6
  export declare const PLUGIN_VERSION: string;
7
+ /**
8
+ * The installed plugin's own package name, with any npm scope (`@rhizomatics/`) stripped - prefixed
9
+ * onto `console.error` lines from code shared with the CLI (rendering/device-driver modules with no
10
+ * `ServerAPI` to call `app.debug` on). SignalK server's own `app.debug` calls are auto-namespaced
11
+ * with the plugin id (unscoped, e.g. `signalk-einklabel-plugin`) by the `debug` module underneath
12
+ * it, so without this prefix those `console.error` lines would show up in the server log
13
+ * unidentified next to ones that are - matching the unscoped form keeps the two consistent.
14
+ */
15
+ export declare const PLUGIN_NAME: string;
@@ -1,11 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PLUGIN_VERSION = void 0;
3
+ exports.PLUGIN_NAME = exports.PLUGIN_VERSION = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const path_1 = require("path");
6
+ const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, "..", "package.json"), "utf-8"));
6
7
  /**
7
8
  * The installed plugin's own version, read from `package.json` once at module load - exposed to
8
9
  * templates as `source=einklabel,path=plugin_version` (see `meta` in repaintScheduler.ts and
9
10
  * `cli/liveContext.ts`), e.g. to show a build/version marker on a label for support purposes.
10
11
  */
11
- exports.PLUGIN_VERSION = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf-8')).version;
12
+ exports.PLUGIN_VERSION = packageJson.version;
13
+ /**
14
+ * The installed plugin's own package name, with any npm scope (`@rhizomatics/`) stripped - prefixed
15
+ * onto `console.error` lines from code shared with the CLI (rendering/device-driver modules with no
16
+ * `ServerAPI` to call `app.debug` on). SignalK server's own `app.debug` calls are auto-namespaced
17
+ * with the plugin id (unscoped, e.g. `signalk-einklabel-plugin`) by the `debug` module underneath
18
+ * it, so without this prefix those `console.error` lines would show up in the server log
19
+ * unidentified next to ones that are - matching the unscoped form keeps the two consistent.
20
+ */
21
+ exports.PLUGIN_NAME = packageJson.name.replace(/^@[^/]+\//, "");
@@ -6,10 +6,18 @@
6
6
  */
7
7
  export declare function normalizeAssetKey(value: unknown): string | undefined;
8
8
  /**
9
- * Resolves an `assets=` binding to an actual `.svg` file, looked up as `<key>.svg` inside `assetsDir`
10
- * resolved relative to the template's own directory (mirroring how a bundled template's relative asset
11
- * references keep working whether the template itself is the bundled copy or a user override elsewhere) -
12
- * `undefined` if no matching file exists, which callers treat as "no image" rather than an error, since an
13
- * unmapped value (e.g. a phase name the asset set doesn't cover) is an expected, not exceptional, case.
9
+ * Resolves an `assets=` binding to an actual `.svg` file, looked up as `<key>.svg` inside whichever
10
+ * `assets/<assetsName>` directory `selectAssetsDir` picks. `undefined` if that directory has no
11
+ * matching file, which callers treat as "no image" rather than an error, since an unmapped value
12
+ * (e.g. a phase name the asset set doesn't cover) is an expected, not exceptional, case.
14
13
  */
15
- export declare function resolveAssetPath(templatePath: string, assetsDir: string, key: string): string | undefined;
14
+ export declare function resolveAssetPath(templatesDir: string, bundledTemplatesDir: string, assetsName: string, key: string): string | undefined;
15
+ /**
16
+ * Diagnoses why an `assets=` directory itself might be at fault, for a caller to log when
17
+ * `resolveAssetPath` comes back empty - a wrong/misconfigured `assets=` name (typo, nothing bundled or
18
+ * user-supplied under that name) looks identical to "this value just has no icon" from
19
+ * `resolveAssetPath` alone, and the two need very different fixes. Checks the same directory
20
+ * `resolveAssetPath` would have read from; `undefined` when that directory is fine (exists, is a
21
+ * directory, is readable) - the miss is then just this value's.
22
+ */
23
+ export declare function describeAssetsDirProblem(templatesDir: string, bundledTemplatesDir: string, assetsName: string): string | undefined;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeAssetKey = normalizeAssetKey;
4
4
  exports.resolveAssetPath = resolveAssetPath;
5
+ exports.describeAssetsDirProblem = describeAssetsDirProblem;
5
6
  const fs_1 = require("fs");
6
7
  const path_1 = require("path");
7
8
  /**
@@ -11,23 +12,77 @@ const path_1 = require("path");
11
12
  * a file (non-string, empty, or all-punctuation), which callers treat as "no image".
12
13
  */
13
14
  function normalizeAssetKey(value) {
14
- if (typeof value !== 'string')
15
+ if (typeof value !== "string")
15
16
  return undefined;
16
17
  const normalized = value
17
18
  .trim()
18
19
  .toLowerCase()
19
- .replace(/[^a-z0-9]+/g, '_')
20
- .replace(/^_+|_+$/g, '');
20
+ .replace(/[^a-z0-9]+/g, "_")
21
+ .replace(/^_+|_+$/g, "");
21
22
  return normalized || undefined;
22
23
  }
23
24
  /**
24
- * Resolves an `assets=` binding to an actual `.svg` file, looked up as `<key>.svg` inside `assetsDir`
25
- * resolved relative to the template's own directory (mirroring how a bundled template's relative asset
26
- * references keep working whether the template itself is the bundled copy or a user override elsewhere) -
27
- * `undefined` if no matching file exists, which callers treat as "no image" rather than an error, since an
28
- * unmapped value (e.g. a phase name the asset set doesn't cover) is an expected, not exceptional, case.
25
+ * Picks which `assets/<assetsName>` directory an `assets=` binding should read from - the user's own
26
+ * `templatesDir/assets/<assetsName>` if it exists as a directory at all, otherwise
27
+ * `bundledTemplatesDir/assets/<assetsName>` (see `BUNDLED_TEMPLATES_DIR` in `../config.ts`). This is a
28
+ * whole-directory choice, not a per-file merge: a user who provides their own `assets/lunar_phases`
29
+ * directory (even a partial one, missing some phases) gets exactly that directory and nothing from the
30
+ * bundled set, so it's always clear which files are in play - see the module's "overrides only work at
31
+ * directory level" design note. This choice is independent of which template file (bundled or a user
32
+ * override) ended up being rendered, so a user can override just a template, just its resources, or
33
+ * both, in any combination.
29
34
  */
30
- function resolveAssetPath(templatePath, assetsDir, key) {
31
- const candidate = (0, path_1.join)((0, path_1.dirname)(templatePath), assetsDir, `${key}.svg`);
35
+ function selectAssetsDir(templatesDir, bundledTemplatesDir, assetsName) {
36
+ const userDir = (0, path_1.join)(templatesDir, "assets", assetsName);
37
+ try {
38
+ if ((0, fs_1.statSync)(userDir).isDirectory())
39
+ return userDir;
40
+ }
41
+ catch {
42
+ // doesn't exist (or isn't readable) - fall through to the bundled directory
43
+ }
44
+ return (0, path_1.join)(bundledTemplatesDir, "assets", assetsName);
45
+ }
46
+ /**
47
+ * Resolves an `assets=` binding to an actual `.svg` file, looked up as `<key>.svg` inside whichever
48
+ * `assets/<assetsName>` directory `selectAssetsDir` picks. `undefined` if that directory has no
49
+ * matching file, which callers treat as "no image" rather than an error, since an unmapped value
50
+ * (e.g. a phase name the asset set doesn't cover) is an expected, not exceptional, case.
51
+ */
52
+ function resolveAssetPath(templatesDir, bundledTemplatesDir, assetsName, key) {
53
+ const candidate = (0, path_1.join)(selectAssetsDir(templatesDir, bundledTemplatesDir, assetsName), `${key}.svg`);
32
54
  return (0, fs_1.existsSync)(candidate) ? candidate : undefined;
33
55
  }
56
+ /** `undefined` when `dir` is a readable directory - otherwise a human-readable reason it isn't. */
57
+ function describeDir(dir) {
58
+ if (!(0, fs_1.existsSync)(dir))
59
+ return `"${dir}" does not exist`;
60
+ let stats;
61
+ try {
62
+ stats = (0, fs_1.statSync)(dir);
63
+ }
64
+ catch (err) {
65
+ return `"${dir}" could not be read: ${err.message}`;
66
+ }
67
+ if (!stats.isDirectory())
68
+ return `"${dir}" is not a directory`;
69
+ try {
70
+ (0, fs_1.accessSync)(dir, fs_1.constants.R_OK);
71
+ }
72
+ catch (err) {
73
+ return `"${dir}" is not readable: ${err.message}`;
74
+ }
75
+ return undefined;
76
+ }
77
+ /**
78
+ * Diagnoses why an `assets=` directory itself might be at fault, for a caller to log when
79
+ * `resolveAssetPath` comes back empty - a wrong/misconfigured `assets=` name (typo, nothing bundled or
80
+ * user-supplied under that name) looks identical to "this value just has no icon" from
81
+ * `resolveAssetPath` alone, and the two need very different fixes. Checks the same directory
82
+ * `resolveAssetPath` would have read from; `undefined` when that directory is fine (exists, is a
83
+ * directory, is readable) - the miss is then just this value's.
84
+ */
85
+ function describeAssetsDirProblem(templatesDir, bundledTemplatesDir, assetsName) {
86
+ const problem = describeDir(selectAssetsDir(templatesDir, bundledTemplatesDir, assetsName));
87
+ return problem && `assets directory ${problem}`;
88
+ }
@@ -1,4 +1,4 @@
1
- import { TemplateContext } from './types';
1
+ import { TemplateContext } from "./types";
2
2
  declare const SOURCES: readonly ["signalk", "resources", "einklabel"];
3
3
  type Source = (typeof SOURCES)[number];
4
4
  /**
@@ -17,7 +17,14 @@ export interface Binding {
17
17
  /** Explicit unit-preferences category (e.g. `depth`, `speed`, `temperature`) for a numeric value with no path metadata of its own, e.g. a `source=resources` value - see `../unitCategories.ts`. */
18
18
  category?: string;
19
19
  round?: number;
20
- /** Only meaningful on an `<image>` element's binding (see `SvgRenderer`) - a directory, resolved relative to the template file, of `<value>.svg` files to pick from by the resolved value (see `../assets.ts`'s `normalizeAssetKey`). */
20
+ /**
21
+ * Only meaningful on an `<image>` element's binding (see `SvgRenderer`) - names an `assets/<name>`
22
+ * directory (e.g. `assets=lunar_phases` for `templates/assets/lunar_phases/`) of `<value>.svg` files
23
+ * to pick from by the resolved value (see `../assets.ts`'s `normalizeAssetKey`/`resolveAssetPath`).
24
+ * Looked up under the user's configured templates directory first, then the plugin's own bundled
25
+ * `templates/` directory - never relative to the specific template file itself, so overriding a
26
+ * template doesn't also require duplicating its bundled asset sets.
27
+ */
21
28
  assets?: string;
22
29
  }
23
30
  /**
@@ -6,8 +6,8 @@ exports.resolveBinding = resolveBinding;
6
6
  exports.renderBinding = renderBinding;
7
7
  const xmldom_1 = require("@xmldom/xmldom");
8
8
  const formatters_1 = require("./formatters");
9
- const SOURCES = ['signalk', 'resources', 'einklabel'];
10
- const KNOWN_KEYS = new Set(['source', 'context', 'resource', 'path', 'format', 'category', 'round', 'assets']);
9
+ const SOURCES = ["signalk", "resources", "einklabel"];
10
+ const KNOWN_KEYS = new Set(["source", "context", "resource", "path", "format", "category", "round", "assets"]);
11
11
  /**
12
12
  * Parses a `<desc>` element's text content into a `Binding`, e.g.
13
13
  * `source=resources,resource=tides,path=extremes[0].level,category=depth,round=2` or, using the
@@ -19,15 +19,15 @@ const KNOWN_KEYS = new Set(['source', 'context', 'resource', 'path', 'format', '
19
19
  */
20
20
  function parseBinding(desc) {
21
21
  const trimmedDesc = desc.trim();
22
- if (trimmedDesc && !trimmedDesc.includes('=')) {
23
- return { source: 'signalk', context: 'self', path: trimmedDesc };
22
+ if (trimmedDesc && !trimmedDesc.includes("=")) {
23
+ return { source: "signalk", context: "self", path: trimmedDesc };
24
24
  }
25
25
  const fields = {};
26
- for (const pair of desc.split(',')) {
26
+ for (const pair of desc.split(",")) {
27
27
  const trimmed = pair.trim();
28
28
  if (!trimmed)
29
29
  continue;
30
- const eq = trimmed.indexOf('=');
30
+ const eq = trimmed.indexOf("=");
31
31
  if (eq < 0) {
32
32
  throw new Error(`invalid binding "${desc}" - expected "key=value" pairs, got "${trimmed}"`);
33
33
  }
@@ -37,12 +37,12 @@ function parseBinding(desc) {
37
37
  }
38
38
  fields[key] = trimmed.slice(eq + 1).trim();
39
39
  }
40
- const source = (fields.source ?? 'signalk');
40
+ const source = (fields.source ?? "signalk");
41
41
  if (!SOURCES.includes(source)) {
42
42
  throw new Error(`invalid binding "${desc}" - unknown source "${source}"`);
43
43
  }
44
- const context = fields.context ?? 'self';
45
- if (source === 'resources' && !fields.resource) {
44
+ const context = fields.context ?? "self";
45
+ if (source === "resources" && !fields.resource) {
46
46
  throw new Error(`invalid binding "${desc}" - source=resources requires a "resource" key`);
47
47
  }
48
48
  if (!fields.path) {
@@ -67,12 +67,12 @@ function parseBinding(desc) {
67
67
  * way as a `<text>` binding.
68
68
  */
69
69
  function findBindings(svgSource) {
70
- const doc = new xmldom_1.DOMParser().parseFromString(svgSource, 'image/svg+xml');
70
+ const doc = new xmldom_1.DOMParser().parseFromString(svgSource, "image/svg+xml");
71
71
  const bindings = [];
72
- for (const tagName of ['text', 'image']) {
72
+ for (const tagName of ["text", "image"]) {
73
73
  const elements = doc.getElementsByTagName(tagName);
74
74
  for (let i = 0; i < elements.length; i++) {
75
- const desc = elements.item(i)?.getElementsByTagName('desc').item(0);
75
+ const desc = elements.item(i)?.getElementsByTagName("desc").item(0);
76
76
  if (desc?.textContent) {
77
77
  bindings.push(parseBinding(desc.textContent));
78
78
  }
@@ -83,12 +83,12 @@ function findBindings(svgSource) {
83
83
  /** Supports both `a.[0].b` and `a[0].b` array index notation, matching `setAtPath` in repaintScheduler.ts. */
84
84
  function getAtPath(obj, path) {
85
85
  const segments = path
86
- .replace(/\[(\d+)\]/g, '.$1')
87
- .split('.')
86
+ .replace(/\[(\d+)\]/g, ".$1")
87
+ .split(".")
88
88
  .filter((segment) => segment.length > 0);
89
89
  let node = obj;
90
90
  for (const segment of segments) {
91
- if (node === null || typeof node !== 'object')
91
+ if (node === null || typeof node !== "object")
92
92
  return undefined;
93
93
  node = node[segment];
94
94
  }
@@ -96,7 +96,7 @@ function getAtPath(obj, path) {
96
96
  }
97
97
  /** Resolves a parsed `Binding` against the render context assembled by `assembleRawContext`. */
98
98
  function resolveBinding(binding, context) {
99
- if (binding.source === 'signalk') {
99
+ if (binding.source === "signalk") {
100
100
  const signalk = context.signalk;
101
101
  const vessel = signalk?.[binding.context];
102
102
  if (vessel === undefined) {
@@ -104,7 +104,7 @@ function resolveBinding(binding, context) {
104
104
  }
105
105
  return getAtPath(vessel, binding.path);
106
106
  }
107
- if (binding.source === 'einklabel') {
107
+ if (binding.source === "einklabel") {
108
108
  const meta = context.meta;
109
109
  if (meta === undefined) {
110
110
  throw new Error('binding references source "einklabel" but no "meta" is present in the render context');
@@ -127,7 +127,7 @@ function resolveBinding(binding, context) {
127
127
  * rather than an error).
128
128
  */
129
129
  function resolveDisplayUnits(binding, context) {
130
- if (binding.source !== 'signalk')
130
+ if (binding.source !== "signalk")
131
131
  return undefined;
132
132
  const pathMeta = context.pathMeta;
133
133
  return pathMeta?.[binding.context]?.[binding.path]?.displayUnits;
@@ -161,20 +161,20 @@ function resolveCategoryDisplayUnits(binding, context) {
161
161
  */
162
162
  function renderBinding(binding, context) {
163
163
  const value = resolveBinding(binding, context);
164
- if (binding.format && binding.format !== 'raw')
164
+ if (binding.format && binding.format !== "raw")
165
165
  return (0, formatters_1.applyFormat)(binding.format, value, context, binding.round);
166
- if (typeof value === 'number') {
166
+ if (typeof value === "number") {
167
167
  if (binding.category)
168
168
  return (0, formatters_1.formatDisplayUnits)(value, resolveCategoryDisplayUnits(binding, context), binding.round);
169
- const displayUnits = binding.format === 'raw' ? undefined : resolveDisplayUnits(binding, context);
169
+ const displayUnits = binding.format === "raw" ? undefined : resolveDisplayUnits(binding, context);
170
170
  if (displayUnits)
171
171
  return (0, formatters_1.formatDisplayUnits)(value, displayUnits, binding.round);
172
172
  if (binding.round !== undefined)
173
173
  return value.toFixed(binding.round);
174
174
  }
175
175
  if (value === null || value === undefined)
176
- return '';
177
- if (typeof value === 'object')
176
+ return "";
177
+ if (typeof value === "object")
178
178
  return JSON.stringify(value);
179
179
  return String(value);
180
180
  }
@@ -24,9 +24,9 @@ exports.GENERIC_FONT_FAMILY_MAP = exports.DEFAULT_FONT_PATHS = void 0;
24
24
  */
25
25
  exports.DEFAULT_FONT_PATHS = [
26
26
  // first font is the default
27
- require.resolve('@fontsource/roboto/files/roboto-latin-400-normal.woff2'),
28
- require.resolve('@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff2'),
29
- require.resolve('@fontsource/roboto-serif/files/roboto-serif-latin-400-normal.woff2'),
27
+ require.resolve("@fontsource/roboto/files/roboto-latin-400-normal.woff2"),
28
+ require.resolve("@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff2"),
29
+ require.resolve("@fontsource/roboto-serif/files/roboto-serif-latin-400-normal.woff2"),
30
30
  ];
31
31
  /**
32
32
  * Maps each CSS generic font-family keyword to the literal embedded name (nameID1) of the
@@ -36,7 +36,7 @@ exports.DEFAULT_FONT_PATHS = [
36
36
  * - see project memory. Keep these in sync with DEFAULT_FONT_PATHS if a bucket's font ever changes.
37
37
  */
38
38
  exports.GENERIC_FONT_FAMILY_MAP = {
39
- 'sans-serif': 'Roboto',
40
- monospace: 'Roboto Mono',
41
- serif: 'Roboto Serif 20pt',
39
+ "sans-serif": "Roboto",
40
+ monospace: "Roboto Mono",
41
+ serif: "Roboto Serif 20pt",
42
42
  };
@@ -1,4 +1,4 @@
1
- import { TemplateContext } from './types';
1
+ import { TemplateContext } from "./types";
2
2
  /**
3
3
  * Matches `displayUnits` on a path's own metadata (`app.getMetadata(path).displayUnits` in the live
4
4
  * plugin, the `.../meta` tree over HTTP in the CLI) - SignalK's per-path unit-preference info, fully
@@ -8,7 +8,7 @@ const mathjs_1 = require("mathjs");
8
8
  /** Converts a base-SI value (always what SignalK paths deliver) to its metadata's preferred display unit and formats it with the unit's symbol, e.g. 3.42 -> "11.2ft". */
9
9
  function formatDisplayUnits(value, displayUnits, round) {
10
10
  const converted = displayUnits.formula ? Number((0, mathjs_1.evaluate)(displayUnits.formula, { value })) : value;
11
- const decimals = round ?? (displayUnits.displayFormat?.includes('.') ? displayUnits.displayFormat.split('.')[1].length : 0);
11
+ const decimals = round ?? (displayUnits.displayFormat?.includes(".") ? displayUnits.displayFormat.split(".")[1].length : 0);
12
12
  const symbol = displayUnits.symbol ?? displayUnits.targetUnit;
13
13
  return `${converted.toFixed(decimals)}${symbol}`;
14
14
  }
@@ -37,10 +37,13 @@ function selfTimezone(context) {
37
37
  * binding (see `considerRepaint`).
38
38
  */
39
39
  function resolveLocalZoneAbbreviation(context) {
40
- const part = new Intl.DateTimeFormat(undefined, { timeZone: selfTimezone(context), timeZoneName: 'short' })
40
+ const part = new Intl.DateTimeFormat(undefined, {
41
+ timeZone: selfTimezone(context),
42
+ timeZoneName: "short",
43
+ })
41
44
  .formatToParts(new Date())
42
- .find((p) => p.type === 'timeZoneName');
43
- return part?.value ?? '';
45
+ .find((p) => p.type === "timeZoneName");
46
+ return part?.value ?? "";
44
47
  }
45
48
  /**
46
49
  * A `resources`-sourced timestamp can arrive as either an ISO string (always true over HTTP, since
@@ -51,9 +54,9 @@ function resolveLocalZoneAbbreviation(context) {
51
54
  */
52
55
  function parseTimestamp(value) {
53
56
  if (value instanceof Date)
54
- return luxon_1.DateTime.fromJSDate(value, { zone: 'utc' });
55
- if (typeof value === 'string')
56
- return luxon_1.DateTime.fromISO(value, { zone: 'utc' });
57
+ return luxon_1.DateTime.fromJSDate(value, { zone: "utc" });
58
+ if (typeof value === "string")
59
+ return luxon_1.DateTime.fromISO(value, { zone: "utc" });
57
60
  return undefined;
58
61
  }
59
62
  /**
@@ -63,51 +66,51 @@ function parseTimestamp(value) {
63
66
  */
64
67
  function formatLocalTime(value, context) {
65
68
  const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
66
- return dt?.isValid ? dt.toFormat('HH:mm') : '';
69
+ return dt?.isValid ? dt.toFormat("HH:mm") : "";
67
70
  }
68
71
  /** e.g. "27 Jun" - day of month (no leading zero) and abbreviated month name, in the local vessel's timezone. */
69
72
  function formatDayMonth(value, context) {
70
73
  const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
71
- return dt?.isValid ? dt.toFormat('d MMM') : '';
74
+ return dt?.isValid ? dt.toFormat("d MMM") : "";
72
75
  }
73
76
  /** e.g. "21 Jun 26 18:05" - day of month (no leading zero), abbreviated month, 2-digit year, and 24h time, in the local vessel's timezone. */
74
77
  function formatLocalDatetimeShort(value, context) {
75
78
  const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
76
- return dt?.isValid ? dt.toFormat('d MMM yy HH:mm') : '';
79
+ return dt?.isValid ? dt.toFormat("d MMM yy HH:mm") : "";
77
80
  }
78
81
  /**
79
82
  * IANA region names are ambiguous about DST (e.g. "Europe/London" is UTC+00:00 in winter, UTC+01:00
80
83
  * in summer); show the numeric offset actually in effect.
81
84
  */
82
85
  function formatUtcOffset(value) {
83
- if (typeof value !== 'string' || !value)
84
- return '';
86
+ if (typeof value !== "string" || !value)
87
+ return "";
85
88
  const dt = luxon_1.DateTime.now().setZone(value);
86
- return dt.isValid ? `UTC${dt.toFormat('ZZ')}` : '';
89
+ return dt.isValid ? `UTC${dt.toFormat("ZZ")}` : "";
87
90
  }
88
91
  function formatPosition(value, round) {
89
92
  const position = value;
90
- if (typeof position?.latitude !== 'number' || typeof position?.longitude !== 'number')
91
- return '';
93
+ if (typeof position?.latitude !== "number" || typeof position?.longitude !== "number")
94
+ return "";
92
95
  const decimals = round ?? 4;
93
96
  const lat = Math.abs(position.latitude).toFixed(decimals);
94
97
  const lon = Math.abs(position.longitude).toFixed(decimals);
95
- const latHemisphere = position.latitude >= 0 ? 'N' : 'S';
96
- const lonHemisphere = position.longitude >= 0 ? 'E' : 'W';
98
+ const latHemisphere = position.latitude >= 0 ? "N" : "S";
99
+ const lonHemisphere = position.longitude >= 0 ? "E" : "W";
97
100
  return `${lat}°${latHemisphere} ${lon}°${lonHemisphere}`;
98
101
  }
99
102
  /** Applies a named `format=` formatter to a resolved binding value. */
100
103
  function applyFormat(name, value, context, round) {
101
104
  switch (name) {
102
- case 'local_time':
105
+ case "local_time":
103
106
  return formatLocalTime(value, context);
104
- case 'day_mon':
107
+ case "day_mon":
105
108
  return formatDayMonth(value, context);
106
- case 'local_datetime_short':
109
+ case "local_datetime_short":
107
110
  return formatLocalDatetimeShort(value, context);
108
- case 'utc_offset':
111
+ case "utc_offset":
109
112
  return formatUtcOffset(value);
110
- case 'position':
113
+ case "position":
111
114
  return formatPosition(value, round);
112
115
  default:
113
116
  throw new Error(`unknown format "${name}"`);
@@ -1,3 +1,3 @@
1
- import { Bitmap } from './types';
1
+ import { Bitmap } from "./types";
2
2
  /** Encodes a common Bitmap as PNG bytes, for previewing templates without a physical device. */
3
3
  export declare function bitmapToPng(bitmap: Bitmap): Buffer;
@@ -1,4 +1,4 @@
1
- import { Bitmap, Renderer, TemplateContext } from './types';
1
+ import { Bitmap, Renderer, TemplateContext } from "./types";
2
2
  /**
3
3
  * Renders an SVG+binding template to a common RGBA bitmap.
4
4
  *
@@ -17,12 +17,13 @@ import { Bitmap, Renderer, TemplateContext } from './types';
17
17
  * whole tree and rendered blank.
18
18
  *
19
19
  * Non-textual inclusion: an `<image>` element with a `<desc>` child works the same way, except the
20
- * resolved value picks one of a directory of `.svg` files (an `assets=` key, required, names that
21
- * directory - see `./assets.ts`) rather than substituting text, and the element is dropped entirely
22
- * (rather than showing an error placeholder) whenever the value is unavailable or doesn't map to a
23
- * file - e.g. a device without the `derived-data` plugin installed simply shows no moon-phase icon.
24
- * The matched file's contents are inlined as a `data:image/svg+xml;base64,...` URI so no filesystem
25
- * access happens at resvg-wasm's render step.
20
+ * resolved value picks one of a directory of `.svg` files (an `assets=` key, required, names an
21
+ * `assets/<name>` directory looked up under `templatesDir` first and `BUNDLED_TEMPLATES_DIR` otherwise
22
+ * - see `resolveAssetPath` in `./assets.ts`) rather than substituting text, and the element is dropped
23
+ * entirely (rather than showing an error placeholder) whenever the value is unavailable or doesn't map
24
+ * to a file - e.g. a device without the `derived-data` plugin installed simply shows no moon-phase
25
+ * icon. The matched file's contents are inlined as a `data:image/svg+xml;base64,...` URI so no
26
+ * filesystem access happens at resvg-wasm's render step.
26
27
  *
27
28
  * resvg-wasm cannot see the host's installed fonts (`loadSystemFonts`/`fontFiles`
28
29
  * are silently no-ops under plain Node) - it only renders text if given font
@@ -36,5 +37,5 @@ export declare class SvgRenderer implements Renderer {
36
37
  private fontBuffers?;
37
38
  constructor(fontPaths?: string[]);
38
39
  private loadFontBuffers;
39
- render(svgTemplatePath: string, context: TemplateContext, width: number, height: number): Promise<Bitmap>;
40
+ render(svgTemplatePath: string, context: TemplateContext, width: number, height: number, templatesDir?: string, bundledTemplatesDir?: string): Promise<Bitmap>;
40
41
  }