@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
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ServerAPI, Plugin } from
|
|
2
|
-
import { registerDriver, getDriver, allDrivers } from
|
|
3
|
-
import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadataType, DiscoveredDevice as DiscoveredDeviceType, VendorDeviceConfig as VendorDeviceConfigType, Colour as ColourType } from
|
|
1
|
+
import { ServerAPI, Plugin } from "@signalk/server-api";
|
|
2
|
+
import { registerDriver, getDriver, allDrivers } from "./devices/registry";
|
|
3
|
+
import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadataType, DiscoveredDevice as DiscoveredDeviceType, VendorDeviceConfig as VendorDeviceConfigType, Colour as ColourType } from "./devices/types";
|
|
4
4
|
/**
|
|
5
5
|
* Public extension point for vendor packages. A package that adds support for a new
|
|
6
6
|
* ESL vendor (e.g. `signalk-esl-shoplabelcorp-plugin`) imports this module and calls
|
package/dist/pathMeta.d.ts
CHANGED
package/dist/pathMeta.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.fetchPathMeta = fetchPathMeta;
|
|
|
4
4
|
const httpJson_1 = require("./httpJson");
|
|
5
5
|
/** `self` -> `vessels/self`, `vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's whole-vessel metadata. */
|
|
6
6
|
function metaContextPath(context) {
|
|
7
|
-
return context ===
|
|
7
|
+
return context === "self" ? "vessels/self" : context.replace(/\./g, "/");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Fetches all of a vessel's per-path metadata in one request - `GET .../vessels/<context>/meta` returns
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Plugin, ServerAPI } from
|
|
1
|
+
import { Plugin, ServerAPI } from "@signalk/server-api";
|
|
2
2
|
export declare function createPlugin(app: ServerAPI): Plugin;
|
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,
|
|
32
|
-
const hwid = found.hwVersion ??
|
|
33
|
-
app.debug(`discovered ${driver.vendor} device "${found.name ??
|
|
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:
|
|
67
|
-
name:
|
|
68
|
-
description:
|
|
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 = {
|
|
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(
|
|
99
|
+
app.debug("stopped");
|
|
97
100
|
},
|
|
98
101
|
};
|
|
99
102
|
return plugin;
|
package/dist/pluginVersion.d.ts
CHANGED
|
@@ -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;
|
package/dist/pluginVersion.js
CHANGED
|
@@ -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 =
|
|
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(/^@[^/]+\//, "");
|
package/dist/render/assets.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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(
|
|
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;
|
package/dist/render/assets.js
CHANGED
|
@@ -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 !==
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
31
|
-
const
|
|
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
|
+
}
|
package/dist/render/binding.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TemplateContext } from
|
|
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
|
-
/**
|
|
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
|
/**
|
package/dist/render/binding.js
CHANGED
|
@@ -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 = [
|
|
10
|
-
const KNOWN_KEYS = new Set([
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
45
|
-
if (source ===
|
|
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,
|
|
70
|
+
const doc = new xmldom_1.DOMParser().parseFromString(svgSource, "image/svg+xml");
|
|
71
71
|
const bindings = [];
|
|
72
|
-
for (const tagName of [
|
|
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(
|
|
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,
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
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 !==
|
|
164
|
+
if (binding.format && binding.format !== "raw")
|
|
165
165
|
return (0, formatters_1.applyFormat)(binding.format, value, context, binding.round);
|
|
166
|
-
if (typeof value ===
|
|
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 ===
|
|
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 ===
|
|
176
|
+
return "";
|
|
177
|
+
if (typeof value === "object")
|
|
178
178
|
return JSON.stringify(value);
|
|
179
179
|
return String(value);
|
|
180
180
|
}
|
package/dist/render/fonts.js
CHANGED
|
@@ -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(
|
|
28
|
-
require.resolve(
|
|
29
|
-
require.resolve(
|
|
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
|
-
|
|
40
|
-
monospace:
|
|
41
|
-
serif:
|
|
39
|
+
"sans-serif": "Roboto",
|
|
40
|
+
monospace: "Roboto Mono",
|
|
41
|
+
serif: "Roboto Serif 20pt",
|
|
42
42
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TemplateContext } from
|
|
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(
|
|
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, {
|
|
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 ===
|
|
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:
|
|
55
|
-
if (typeof value ===
|
|
56
|
-
return luxon_1.DateTime.fromISO(value, { zone:
|
|
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(
|
|
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(
|
|
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(
|
|
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 !==
|
|
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(
|
|
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 !==
|
|
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 ?
|
|
96
|
-
const lonHemisphere = position.longitude >= 0 ?
|
|
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
|
|
105
|
+
case "local_time":
|
|
103
106
|
return formatLocalTime(value, context);
|
|
104
|
-
case
|
|
107
|
+
case "day_mon":
|
|
105
108
|
return formatDayMonth(value, context);
|
|
106
|
-
case
|
|
109
|
+
case "local_datetime_short":
|
|
107
110
|
return formatLocalDatetimeShort(value, context);
|
|
108
|
-
case
|
|
111
|
+
case "utc_offset":
|
|
109
112
|
return formatUtcOffset(value);
|
|
110
|
-
case
|
|
113
|
+
case "position":
|
|
111
114
|
return formatPosition(value, round);
|
|
112
115
|
default:
|
|
113
116
|
throw new Error(`unknown format "${name}"`);
|
package/dist/render/png.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bitmap, Renderer, TemplateContext } from
|
|
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
|
|
21
|
-
* directory
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
}
|