@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.
- package/CHANGELOG.md +18 -0
- package/README.md +19 -10
- package/dist/cli/index.js +129 -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 +10 -9
- 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 +21 -20
- 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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
# 0.7.0
|
|
2
|
+
|
|
3
|
+
## Logging
|
|
4
|
+
|
|
5
|
+
- Standardize all log output to show a `signalk-einklabel-plugin` prefix
|
|
6
|
+
|
|
7
|
+
## Image Fields
|
|
8
|
+
|
|
9
|
+
- Include image fields on CLI `fields` command output
|
|
10
|
+
- Better logging diagnostics if asset image resolution fails or directory not found
|
|
11
|
+
- Replace separate fixed `resources/svg` directory for images with user overridable `templates/assets`
|
|
12
|
+
- Means that the bundled lunar phase icons can be easily replaced without touching the template - use the same file names in `templates/assets/lunar_phases` directory.
|
|
13
|
+
- Added attribution to the Lunar Phases icons
|
|
14
|
+
|
|
15
|
+
## Build
|
|
16
|
+
|
|
17
|
+
- Added `pre-commit`, `oxlint` and `oxfmt` for code quality, retired `prettier`
|
|
18
|
+
|
|
1
19
|
# 0.6.4
|
|
2
20
|
|
|
3
21
|
- Log improvements for device paint requests
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@rhizomatics/signalk-einklabel-plugin)
|
|
5
5
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/actions/workflows/signalk-ci.yml)
|
|
6
6
|
[](https://codecov.io/gh/rhizomatics/signalk-einklabel-plugin)
|
|
7
|
-
[](https://github.com)
|
|
8
8
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
9
9
|
|
|
10
10
|
** Fully working but limited vendor/product support and requires Linux for device access **
|
|
@@ -26,8 +26,10 @@ Unlike some eInk projects, this plugin doesn't require any physical modification
|
|
|
26
26
|
Most of these requirements are about making SignalK work with Bluetooth Low Energy, which is good thing to have anyway, since vendors like Victron, Switchbot, Ruuvi and others have BLE enabled hardware that's useful to have on a boat. [Direct BLE support](https://github.com/SignalK/signalk-server/issues/2411) in SignalK is being planned in 2026.
|
|
27
27
|
|
|
28
28
|
1. A SignalK server, **running Linux**
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
- MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble), however can be used for template development and
|
|
30
31
|
debugging (everything except `scan` and `paint`)
|
|
32
|
+
|
|
31
33
|
2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy), which is Bluetooth v4.0 or higher
|
|
32
34
|
|
|
33
35
|
- Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
|
|
@@ -75,6 +77,12 @@ The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk
|
|
|
75
77
|
To show the lunar phase, the `environment.moon.phaseName` path is required, which can
|
|
76
78
|
be easily achieved by installing and configuring the `derived-data` plugin.
|
|
77
79
|
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Use the standard configuration option in the SignalK menu for the plugin.
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
78
86
|
## Scanning for Devices
|
|
79
87
|
|
|
80
88
|
Since these are ultra-low power devices, they don't respond instantly to either identify themselves or accept a new image. By default, both scanning and painting have time-outs to wait for a response, which can be altered in the plugin configuration or CLI argument.
|
|
@@ -88,10 +96,9 @@ The plugin will optionally re-scan whenever it starts up, although this isn't es
|
|
|
88
96
|
The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
|
|
89
97
|
|
|
90
98
|
- Scan for longer, in this example 90 seconds
|
|
91
|
-
-
|
|
99
|
+
- `npx esl-cli scan -d 90`
|
|
92
100
|
- Scan for all BLE devices, whatever they are
|
|
93
|
-
-
|
|
94
|
-
|
|
101
|
+
- `npx esl-cli scan -a`
|
|
95
102
|
|
|
96
103
|
## Templating
|
|
97
104
|
|
|
@@ -144,15 +151,17 @@ These can all be combined as in `source=resources,resource=tides,path=extremes[2
|
|
|
144
151
|
|
|
145
152
|
### Non-Textual Fields (Images)
|
|
146
153
|
|
|
147
|
-
The same `<desc>` mechanism works on an `<image>` element instead of a `<text>` element, for a value that's better shown as a picture than as text - a moon phase icon, a wind direction arrow, a weather condition glyph, and so on. Rather than substituting text, the resolved value picks one of a directory of `.svg` files to embed, by an extra required `assets=` key naming that directory
|
|
154
|
+
The same `<desc>` mechanism works on an `<image>` element instead of a `<text>` element, for a value that's better shown as a picture than as text - a moon phase icon, a wind direction arrow, a weather condition glyph, and so on. Rather than substituting text, the resolved value picks one of a directory of `.svg` files to embed, by an extra required `assets=` key naming that directory - an `assets/<name>` sub-directory looked up in your configured `templates` directory first, and the bundled `templates` directory otherwise. For example, the tide clock's moon phase icon uses:
|
|
148
155
|
|
|
149
156
|
```
|
|
150
|
-
path=environment.moon.phaseName,assets
|
|
157
|
+
path=environment.moon.phaseName,assets=lunar_phases
|
|
151
158
|
```
|
|
152
159
|
|
|
153
|
-
The resolved value (e.g. `"Waning Gibbous"`, as published by the [derived-data](https://www.npmjs.com/package/signalk-derived-data) plugin) is normalized to match a filename - lower-cased, punctuation and spaces collapsed to underscores - so `"Waning Gibbous"` picks `waning_gibbous.svg` out of that directory. If the underlying path has no value at all (e.g. the `derived-data` plugin isn't installed), or the value doesn't normalize to any file in the directory, the `<image>` element is
|
|
160
|
+
which resolves against `templates/assets/lunar_phases/` (bundled, or your own configured `templates` directory's `assets/lunar_phases/` if you have one). The resolved value (e.g. `"Waning Gibbous"`, as published by the [derived-data](https://www.npmjs.com/package/signalk-derived-data) plugin) is normalized to match a filename - lower-cased, punctuation and spaces collapsed to underscores - so `"Waning Gibbous"` picks `waning_gibbous.svg` out of that directory. If the underlying path has no value at all (e.g. the `derived-data` plugin isn't installed), or the value doesn't normalize to any file in the directory, the `<image>` element is omitted from that render - no broken image, no placeholder, nothing shown - and a line is logged to the console so a missing/unmatched value isn't silently invisible.
|
|
161
|
+
|
|
162
|
+
If you don't like the bundled moon phase icons, save your own `<value>.svg` files in the `assets/lunar_phases` sub-directory of your configured `templates` directory - the whole directory is used in place of the bundled one, so add all 8 phases you want to keep, not just the ones you're changing.
|
|
154
163
|
|
|
155
|
-
This is a general mechanism, not specific to moon phases - any `source`/`context`/`path`/`format` combination valid for a `<text>` binding works here too (a `source=resources` value, an explicit `category=`, etc.), the only difference is the required `assets=` directory and the "no match -> no image" behaviour instead of substituted text. To add your own, put a directory of `<value>.svg` files
|
|
164
|
+
This is a general mechanism, not specific to moon phases - any `source`/`context`/`path`/`format` combination valid for a `<text>` binding works here too (a `source=resources` value, an explicit `category=`, etc.), the only difference is the required `assets=` directory and the "no match -> no image" behaviour instead of substituted text. To add your own, put a directory of `<value>.svg` files under an `assets/<name>` sub-directory of your `templates` directory, add an `<image>` element in your SVG editor at the size/position you want, and give it a `<desc>` the same way you would a text field - overriding just the template, just its assets, or both together, all work independently.
|
|
156
165
|
|
|
157
166
|
### Fonts
|
|
158
167
|
|
|
@@ -219,7 +228,7 @@ Additional vendors and devices can be added by a separate npm package that imple
|
|
|
219
228
|
|
|
220
229
|
Templates can be added to the configurable directory. [Inkscape](https://inkscape.org) free, open source, and recommended for editing templates, or your own favourite editor, or by hand in a text editor for hard core (or just tidying up the template side).
|
|
221
230
|
|
|
222
|
-

|
|
231
|
+

|
|
223
232
|
|
|
224
233
|
The object ID and label aren't used by the plugin, only the description is used to define fields. You can also add in ordinary text fields without field definitions, as labels, logos, help text etc.
|
|
225
234
|
|
package/dist/cli/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
5
6
|
const commander_1 = require("commander");
|
|
6
7
|
const xmldom_1 = require("@xmldom/xmldom");
|
|
7
8
|
const registry_1 = require("../devices/registry");
|
|
@@ -10,22 +11,24 @@ const bleDiscovery_1 = require("../devices/bleDiscovery");
|
|
|
10
11
|
const svgRenderer_1 = require("../render/svgRenderer");
|
|
11
12
|
const png_1 = require("../render/png");
|
|
12
13
|
const binding_1 = require("../render/binding");
|
|
14
|
+
const assets_1 = require("../render/assets");
|
|
15
|
+
const config_1 = require("../config");
|
|
13
16
|
const liveContext_1 = require("./liveContext");
|
|
14
17
|
const httpJson_1 = require("../httpJson");
|
|
15
18
|
const log_1 = require("./log");
|
|
16
19
|
(0, registry_1.registerDriver)(new zhsunyco_1.ZhsunycoDriver());
|
|
17
20
|
const VENDOR_IDENTIFY_TIMEOUT_MS = 30000;
|
|
18
21
|
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
19
|
-
const DEFAULT_SIGNALK_URLS = [
|
|
22
|
+
const DEFAULT_SIGNALK_URLS = ["http://localhost", "http://localhost:3000", "https://localhost"];
|
|
20
23
|
const COLOUR_CODES = {
|
|
21
|
-
BW: [
|
|
22
|
-
BWR: [
|
|
23
|
-
BWRY: [
|
|
24
|
+
BW: ["black", "white"],
|
|
25
|
+
BWR: ["black", "white", "red"],
|
|
26
|
+
BWRY: ["black", "white", "red", "yellow"],
|
|
24
27
|
};
|
|
25
28
|
function parseColours(code) {
|
|
26
29
|
const colours = COLOUR_CODES[code.toUpperCase()];
|
|
27
30
|
if (!colours) {
|
|
28
|
-
throw new Error(`unknown --colours value "${code}" - expected one of ${Object.keys(COLOUR_CODES).join(
|
|
31
|
+
throw new Error(`unknown --colours value "${code}" - expected one of ${Object.keys(COLOUR_CODES).join(", ")}`);
|
|
29
32
|
}
|
|
30
33
|
return colours;
|
|
31
34
|
}
|
|
@@ -41,7 +44,7 @@ async function resolveDefaultUrl() {
|
|
|
41
44
|
(0, log_1.logDebug)(`${candidate} did not answer: ${err.message}`);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
throw new Error(`no -u/--url given and none of ${DEFAULT_SIGNALK_URLS.join(
|
|
47
|
+
throw new Error(`no -u/--url given and none of ${DEFAULT_SIGNALK_URLS.join(", ")} answered - specify the server explicitly with -u/--url`);
|
|
45
48
|
}
|
|
46
49
|
/** Shared by every command that takes -u/--url and -e/--example-data - -e wins when both are present; when neither is given, probes DEFAULT_SIGNALK_URLS for a default. */
|
|
47
50
|
async function assembleContext(opts, bindings) {
|
|
@@ -59,7 +62,7 @@ async function identifyVendor(address) {
|
|
|
59
62
|
const device = await (0, bleDiscovery_1.getOrDiscoverDevice)(adapter, address, VENDOR_IDENTIFY_TIMEOUT_MS);
|
|
60
63
|
const name = await device.getName().catch(() => undefined);
|
|
61
64
|
const manufacturerId = await (0, bleDiscovery_1.getManufacturerId)(device);
|
|
62
|
-
(0, log_1.logDebug)(`${address}: advertised name="${name ??
|
|
65
|
+
(0, log_1.logDebug)(`${address}: advertised name="${name ?? ""}" manufacturerId=${manufacturerId ?? "unknown"}`);
|
|
63
66
|
const driver = (0, registry_1.allDrivers)().find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
64
67
|
if (!driver) {
|
|
65
68
|
throw new Error(`no registered vendor driver recognises device "${name ?? address}" - specify --vendor explicitly`);
|
|
@@ -71,50 +74,54 @@ async function identifyVendor(address) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
const program = new commander_1.Command();
|
|
74
|
-
program.name(
|
|
75
|
-
program.option(
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
program.name("esl-cli").description("Local CLI for testing ESL device scan and paint without a SignalK server");
|
|
78
|
+
program.option("-r, --require <module>", "require a module before running, e.g. an npm package that registers a vendor driver (repeatable)",
|
|
79
|
+
// Commander's own typings declare `previous` as always `string[]`, but at runtime it's actually
|
|
80
|
+
// `undefined` on the first invocation when no default is passed to `.option()` - the `| undefined`
|
|
81
|
+
// here keeps that true and makes the `= []` fallback meaningful rather than dead code.
|
|
82
|
+
(value, previous = []) => [...previous, value]);
|
|
83
|
+
program.option("-l, --log-level <level>", "log verbosity: info or debug (e.g. trace which URLs are fetched)", "info");
|
|
84
|
+
program.hook("preAction", () => {
|
|
78
85
|
(0, log_1.setLogLevel)(program.opts().logLevel);
|
|
79
86
|
for (const mod of program.opts().require ?? []) {
|
|
80
87
|
require(mod);
|
|
81
88
|
}
|
|
82
89
|
});
|
|
83
90
|
program
|
|
84
|
-
.command(
|
|
85
|
-
.description(
|
|
91
|
+
.command("vendors")
|
|
92
|
+
.description("List supported vendors and the device models each has confirmed metadata for")
|
|
86
93
|
.action(() => {
|
|
87
|
-
const header = [
|
|
94
|
+
const header = ["vendor", "pid", "hwid", "label", "size", "colours"];
|
|
88
95
|
const rows = [];
|
|
89
96
|
for (const driver of (0, registry_1.allDrivers)()) {
|
|
90
97
|
for (const device of driver.supportedDevices()) {
|
|
91
98
|
rows.push([
|
|
92
99
|
driver.vendor,
|
|
93
|
-
`0x${device.pid.toString(16).padStart(4,
|
|
94
|
-
device.hwVersion ? `0x${device.hwVersion}` :
|
|
100
|
+
`0x${device.pid.toString(16).padStart(4, "0")}`,
|
|
101
|
+
device.hwVersion ? `0x${device.hwVersion}` : "",
|
|
95
102
|
device.label,
|
|
96
103
|
`${device.width}x${device.height}`,
|
|
97
|
-
device.colours.join(
|
|
104
|
+
device.colours.join(","),
|
|
98
105
|
]);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
if (rows.length === 0) {
|
|
102
|
-
console.log(
|
|
109
|
+
console.log("(no confirmed devices yet)");
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
112
|
const widths = header.map((title, col) => Math.max(title.length, ...rows.map((row) => row[col].length)));
|
|
106
|
-
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(
|
|
113
|
+
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" "));
|
|
107
114
|
printRow(header);
|
|
108
115
|
rows.forEach(printRow);
|
|
109
116
|
});
|
|
110
117
|
program
|
|
111
|
-
.command(
|
|
112
|
-
.description(
|
|
113
|
-
.option(
|
|
114
|
-
.option(
|
|
118
|
+
.command("scan")
|
|
119
|
+
.description("Scan for supported BLE ESL devices across all registered vendor drivers")
|
|
120
|
+
.option("-d, --duration <seconds>", "scan duration in seconds", "10")
|
|
121
|
+
.option("-a, --all-devices", "list every nearby BLE device, not just ones a registered driver recognised - unmatched devices show address/name/mfr/rssi only, since there's no driver to do a vendor-specific read like battery")
|
|
115
122
|
.action(async (opts) => {
|
|
116
123
|
const durationMs = Number(opts.duration) * 1000;
|
|
117
|
-
const header = [
|
|
124
|
+
const header = ["vendor", "address", "name", "pid", "hwid", "label", "mfr", "battery", "rssi"];
|
|
118
125
|
const rows = [];
|
|
119
126
|
let matchedCount = 0;
|
|
120
127
|
const drivers = (0, registry_1.allDrivers)();
|
|
@@ -122,25 +129,25 @@ program
|
|
|
122
129
|
await (0, bleDiscovery_1.withDiscovery)(durationMs, async (adapter) => {
|
|
123
130
|
await (0, bleDiscovery_1.forEachAdvertisedDevice)(adapter, async ({ device, address, name, manufacturerId, manufacturerData }) => {
|
|
124
131
|
const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
125
|
-
const mfr = manufacturerId !== undefined ? `0x${manufacturerId.toString(16).padStart(4,
|
|
132
|
+
const mfr = manufacturerId !== undefined ? `0x${manufacturerId.toString(16).padStart(4, "0")}` : "";
|
|
126
133
|
if (!driver) {
|
|
127
134
|
if (opts.allDevices) {
|
|
128
135
|
const rssi = await device
|
|
129
136
|
.getRSSI()
|
|
130
137
|
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
131
138
|
.catch(() => undefined);
|
|
132
|
-
rows.push([
|
|
139
|
+
rows.push(["(unmatched)", address, name ?? "", "", "", "", mfr, "", String(rssi ?? "")]);
|
|
133
140
|
}
|
|
134
141
|
return;
|
|
135
142
|
}
|
|
136
143
|
matchedCount++;
|
|
137
144
|
const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData);
|
|
138
145
|
(0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
|
|
139
|
-
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4,
|
|
140
|
-
const hwid = found.hwVersion ? `0x${found.hwVersion}` :
|
|
141
|
-
const label = found.metadata?.label ??
|
|
142
|
-
const battery = found.batteryMv !== undefined ? `${found.batteryMv}mV` :
|
|
143
|
-
rows.push([driver.vendor, found.address, found.name ??
|
|
146
|
+
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "";
|
|
147
|
+
const hwid = found.hwVersion ? `0x${found.hwVersion}` : "";
|
|
148
|
+
const label = found.metadata?.label ?? "";
|
|
149
|
+
const battery = found.batteryMv !== undefined ? `${found.batteryMv}mV` : "";
|
|
150
|
+
rows.push([driver.vendor, found.address, found.name ?? "", pid, hwid, label, mfr, battery, String(found.rssi ?? "")]);
|
|
144
151
|
});
|
|
145
152
|
});
|
|
146
153
|
if (matchedCount === 0) {
|
|
@@ -150,27 +157,27 @@ program
|
|
|
150
157
|
return;
|
|
151
158
|
}
|
|
152
159
|
const widths = header.map((title, col) => Math.max(title.length, ...rows.map((row) => row[col].length)));
|
|
153
|
-
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(
|
|
160
|
+
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" "));
|
|
154
161
|
printRow(header);
|
|
155
162
|
rows.forEach(printRow);
|
|
156
163
|
});
|
|
157
164
|
program
|
|
158
|
-
.command(
|
|
159
|
-
.description(
|
|
160
|
-
.option(
|
|
161
|
-
.requiredOption(
|
|
162
|
-
.requiredOption(
|
|
163
|
-
.option(
|
|
164
|
-
DEFAULT_SIGNALK_URLS.join(
|
|
165
|
-
|
|
166
|
-
.option(
|
|
167
|
-
.option(
|
|
168
|
-
.option(
|
|
169
|
-
.option(
|
|
170
|
-
.option(
|
|
171
|
-
.option(
|
|
172
|
-
.option(
|
|
173
|
-
.option(
|
|
165
|
+
.command("paint")
|
|
166
|
+
.description("Render a template against a live SignalK server and send it to a device")
|
|
167
|
+
.option("-v, --vendor <vendor>", "vendor driver to use - if omitted, inferred from the device's advertised name")
|
|
168
|
+
.requiredOption("-a, --address <address>", "BLE address of the device")
|
|
169
|
+
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
170
|
+
.option("-u, --url <url>", "SignalK server base URL - resolves the template's source=signalk/resources bindings - if omitted, tries each of " +
|
|
171
|
+
DEFAULT_SIGNALK_URLS.join(", ") +
|
|
172
|
+
" in turn")
|
|
173
|
+
.option("-e, --example-data <dir>", "load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u")
|
|
174
|
+
.option("-k, --aes-key <hex>", "AES-128 key for device authentication, as 32 hex characters - defaults to the vendor's stock key if omitted")
|
|
175
|
+
.option("-w, --width <px>", "render width", "416")
|
|
176
|
+
.option("--height <px>", "render height", "240")
|
|
177
|
+
.option("--voffset <px>", "vertical pixel offset of the panel - overrides the looked-up model for unsupported hardware (requires --colours)", "0")
|
|
178
|
+
.option("--colours <code>", "device colour palette for unsupported hardware: BW, BWR, or BWRY - overrides the looked-up model (uses --width/--height/--voffset)")
|
|
179
|
+
.option("--connect-timeout <seconds>", "BLE connect timeout before giving up on an attempt", "30")
|
|
180
|
+
.option("--retries <n>", "number of paint attempts (including the first) before giving up", "3")
|
|
174
181
|
.action(async (opts) => {
|
|
175
182
|
const vendor = opts.vendor ?? (await identifyVendor(opts.address));
|
|
176
183
|
const driver = (0, registry_1.getDriver)(vendor);
|
|
@@ -179,96 +186,122 @@ program
|
|
|
179
186
|
}
|
|
180
187
|
const modelOverride = opts.colours
|
|
181
188
|
? {
|
|
182
|
-
label:
|
|
189
|
+
label: "manual override",
|
|
183
190
|
width: Number(opts.width),
|
|
184
191
|
height: Number(opts.height),
|
|
185
192
|
voffset: Number(opts.voffset),
|
|
186
193
|
colours: parseColours(opts.colours),
|
|
187
194
|
}
|
|
188
195
|
: undefined;
|
|
189
|
-
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template,
|
|
196
|
+
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, "utf-8"));
|
|
190
197
|
const context = await assembleContext(opts, bindings);
|
|
191
198
|
const renderer = new svgRenderer_1.SvgRenderer();
|
|
192
|
-
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
|
|
199
|
+
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height), (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
|
|
193
200
|
const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
|
|
194
201
|
await (0, bleDiscovery_1.withRetries)(Number(opts.retries), async (attempt) => {
|
|
195
202
|
if (attempt > 1) {
|
|
196
203
|
(0, log_1.logDebug)(`Attempting paint ${attempt}/${opts.retries}`);
|
|
197
204
|
}
|
|
198
|
-
await driver.paint(bitmap, {
|
|
205
|
+
await driver.paint(bitmap, {
|
|
206
|
+
address: opts.address,
|
|
207
|
+
aesKey: opts.aesKey,
|
|
208
|
+
modelOverride,
|
|
209
|
+
connectTimeoutMs,
|
|
210
|
+
});
|
|
199
211
|
});
|
|
200
212
|
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
|
|
201
213
|
});
|
|
202
214
|
program
|
|
203
|
-
.command(
|
|
204
|
-
.description(
|
|
205
|
-
.requiredOption(
|
|
206
|
-
.requiredOption(
|
|
207
|
-
.option(
|
|
208
|
-
DEFAULT_SIGNALK_URLS.join(
|
|
209
|
-
|
|
210
|
-
.option(
|
|
211
|
-
.option(
|
|
212
|
-
.option(
|
|
213
|
-
.option(
|
|
215
|
+
.command("render")
|
|
216
|
+
.description("Render a template against a live SignalK server and write a PNG, without needing a device")
|
|
217
|
+
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
218
|
+
.requiredOption("-o, --output <path>", "output PNG path")
|
|
219
|
+
.option("-u, --url <url>", "SignalK server base URL - resolves the template's source=signalk/resources bindings - if omitted, tries each of " +
|
|
220
|
+
DEFAULT_SIGNALK_URLS.join(", ") +
|
|
221
|
+
" in turn")
|
|
222
|
+
.option("-e, --example-data <dir>", "load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u")
|
|
223
|
+
.option("-w, --width <px>", "render width", "416")
|
|
224
|
+
.option("--height <px>", "render height", "240")
|
|
225
|
+
.option("-f, --font <path>", "override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio",
|
|
226
|
+
// See the -r/--require option above for why `| undefined` is needed here despite commander's typings.
|
|
227
|
+
(value, previous = []) => [...previous, value])
|
|
214
228
|
.action(async (opts) => {
|
|
215
|
-
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template,
|
|
229
|
+
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, "utf-8"));
|
|
216
230
|
const context = await assembleContext(opts, bindings);
|
|
217
231
|
const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
|
|
218
|
-
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
|
|
232
|
+
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height), (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
|
|
219
233
|
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
220
234
|
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
221
235
|
});
|
|
222
236
|
program
|
|
223
|
-
.command(
|
|
224
|
-
.description(
|
|
225
|
-
.requiredOption(
|
|
226
|
-
.option(
|
|
227
|
-
DEFAULT_SIGNALK_URLS.join(
|
|
228
|
-
|
|
229
|
-
.option(
|
|
237
|
+
.command("fields")
|
|
238
|
+
.description("List every <desc> binding in a template by element id, with its source spec and resolved value")
|
|
239
|
+
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
240
|
+
.option("-u, --url <url>", "SignalK server base URL - resolves the template's source=signalk/resources bindings - if omitted, tries each of " +
|
|
241
|
+
DEFAULT_SIGNALK_URLS.join(", ") +
|
|
242
|
+
" in turn")
|
|
243
|
+
.option("-e, --example-data <dir>", "load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u")
|
|
230
244
|
.action(async (opts) => {
|
|
231
|
-
const doc = new xmldom_1.DOMParser().parseFromString(await (0, promises_1.readFile)(opts.template,
|
|
232
|
-
const elements = doc.getElementsByTagName('text');
|
|
245
|
+
const doc = new xmldom_1.DOMParser().parseFromString(await (0, promises_1.readFile)(opts.template, "utf-8"), "image/svg+xml");
|
|
233
246
|
const rows = [];
|
|
234
|
-
for (
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
for (const tag of ["text", "image"]) {
|
|
248
|
+
const elements = doc.getElementsByTagName(tag);
|
|
249
|
+
for (let i = 0; i < elements.length; i++) {
|
|
250
|
+
const element = elements.item(i);
|
|
251
|
+
const desc = element?.getElementsByTagName("desc").item(0);
|
|
252
|
+
if (!element || !desc?.textContent)
|
|
253
|
+
continue;
|
|
254
|
+
const id = element.getAttribute("id") ?? `${tag}#${i}`;
|
|
255
|
+
try {
|
|
256
|
+
rows.push({ id, tag, desc: desc.textContent, binding: (0, binding_1.parseBinding)(desc.textContent) });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
rows.push({ id, tag, desc: desc.textContent, error: err.message });
|
|
260
|
+
}
|
|
245
261
|
}
|
|
246
262
|
}
|
|
247
|
-
const header = [
|
|
263
|
+
const header = ["id", "spec", "value"];
|
|
248
264
|
const table = await Promise.all(rows.map(async (row) => {
|
|
249
265
|
if (row.error || !row.binding)
|
|
250
|
-
return [row.id, row.desc, row.error ??
|
|
266
|
+
return [row.id, row.desc, row.error ?? ""];
|
|
251
267
|
try {
|
|
252
268
|
const context = await assembleContext(opts, [row.binding]);
|
|
253
|
-
|
|
269
|
+
if (row.tag !== "image") {
|
|
270
|
+
return [row.id, row.desc, (0, binding_1.renderBinding)(row.binding, context)];
|
|
271
|
+
}
|
|
272
|
+
// `<image>` bindings don't substitute text - they pick a `.svg` file (see SvgRenderer) -
|
|
273
|
+
// so show what asset that resolves to instead of running it through renderBinding's
|
|
274
|
+
// text formatting, otherwise this row would silently show nothing useful to test against.
|
|
275
|
+
if (!row.binding.assets) {
|
|
276
|
+
return [row.id, row.desc, 'ERROR: an <image> binding requires an "assets" key'];
|
|
277
|
+
}
|
|
278
|
+
const key = (0, assets_1.normalizeAssetKey)((0, binding_1.resolveBinding)(row.binding, context));
|
|
279
|
+
if (!key)
|
|
280
|
+
return [row.id, row.desc, "(no usable value - image would be omitted)"];
|
|
281
|
+
const assetPath = (0, assets_1.resolveAssetPath)((0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR, row.binding.assets, key);
|
|
282
|
+
return [
|
|
283
|
+
row.id,
|
|
284
|
+
row.desc,
|
|
285
|
+
assetPath ? `"${key}" -> ${(0, path_1.basename)(assetPath)}` : `"${key}" -> no matching asset file (image would be omitted)`,
|
|
286
|
+
];
|
|
254
287
|
}
|
|
255
288
|
catch (err) {
|
|
256
289
|
return [row.id, row.desc, `ERROR: ${err.message}`];
|
|
257
290
|
}
|
|
258
291
|
}));
|
|
259
292
|
const widths = header.map((title, col) => Math.max(title.length, ...table.map((cells) => cells[col].length)));
|
|
260
|
-
const printRow = (cells) => console.log(cells.map((cell, col) => cell.padEnd(widths[col])).join(
|
|
293
|
+
const printRow = (cells) => console.log(cells.map((cell, col) => cell.padEnd(widths[col])).join(" "));
|
|
261
294
|
printRow(header);
|
|
262
295
|
table.forEach(printRow);
|
|
263
296
|
});
|
|
264
297
|
program
|
|
265
|
-
.command(
|
|
266
|
-
.description(
|
|
267
|
-
.argument(
|
|
268
|
-
.option(
|
|
269
|
-
DEFAULT_SIGNALK_URLS.join(
|
|
270
|
-
|
|
271
|
-
.option(
|
|
298
|
+
.command("field")
|
|
299
|
+
.description("Resolve a single binding spec directly against a live SignalK server, with no template")
|
|
300
|
+
.argument("<spec>", 'binding spec, e.g. "source=resources,resource=tides,path=station.name" or a bare SignalK path')
|
|
301
|
+
.option("-u, --url <url>", "SignalK server base URL - resolves the spec's source=signalk/resources binding - if omitted, tries each of " +
|
|
302
|
+
DEFAULT_SIGNALK_URLS.join(", ") +
|
|
303
|
+
" in turn")
|
|
304
|
+
.option("-e, --example-data <dir>", "load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u")
|
|
272
305
|
.action(async (spec, opts) => {
|
|
273
306
|
const binding = (0, binding_1.parseBinding)(spec);
|
|
274
307
|
const context = await assembleContext(opts, [binding]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Binding } from
|
|
2
|
-
import { TemplateContext } from
|
|
1
|
+
import { Binding } from "../render/binding";
|
|
2
|
+
import { TemplateContext } from "../render/types";
|
|
3
3
|
/**
|
|
4
4
|
* CLI counterpart to `assembleRawContext` + `considerRepaint`'s `meta` injection in
|
|
5
5
|
* repaintScheduler.ts - same `{ signalk, resources, pathMeta, categories, meta }` shape, but fetched
|
package/dist/cli/liveContext.js
CHANGED
|
@@ -13,10 +13,10 @@ const unitCategories_1 = require("../unitCategories");
|
|
|
13
13
|
const pathMeta_1 = require("../pathMeta");
|
|
14
14
|
const pluginVersion_1 = require("../pluginVersion");
|
|
15
15
|
const log_1 = require("./log");
|
|
16
|
-
const RESOURCES_API_PATH =
|
|
16
|
+
const RESOURCES_API_PATH = "/signalk/v2/api/resources";
|
|
17
17
|
/** `context=self` -> `vessels/self`, `context=vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's full-data-model subtree. */
|
|
18
18
|
function contextPath(context) {
|
|
19
|
-
return context ===
|
|
19
|
+
return context === "self" ? "vessels/self" : context.replace(/\./g, "/");
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* `GET .../vessels/self` (or any other subtree) returns SignalK's full delta-tree shape, where every
|
|
@@ -31,12 +31,12 @@ function contextPath(context) {
|
|
|
31
31
|
* wrapper.
|
|
32
32
|
*/
|
|
33
33
|
function unwrapSignalkTree(node) {
|
|
34
|
-
if (node === null || typeof node !==
|
|
34
|
+
if (node === null || typeof node !== "object")
|
|
35
35
|
return node;
|
|
36
36
|
if (Array.isArray(node))
|
|
37
37
|
return node.map(unwrapSignalkTree);
|
|
38
38
|
const obj = node;
|
|
39
|
-
if (
|
|
39
|
+
if ("value" in obj) {
|
|
40
40
|
return obj.value;
|
|
41
41
|
}
|
|
42
42
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, unwrapSignalkTree(value)]));
|
|
@@ -52,7 +52,7 @@ function unwrapSignalkTree(node) {
|
|
|
52
52
|
*/
|
|
53
53
|
async function assembleLiveContext(signalkUrl, bindings) {
|
|
54
54
|
const signalk = {};
|
|
55
|
-
const contexts = new Set(bindings.filter((binding) => binding.source ===
|
|
55
|
+
const contexts = new Set(bindings.filter((binding) => binding.source === "signalk").map((binding) => binding.context));
|
|
56
56
|
for (const context of contexts) {
|
|
57
57
|
const url = `${signalkUrl}/signalk/v1/api/${contextPath(context)}`;
|
|
58
58
|
(0, log_1.logDebug)(`GET ${url}`);
|
|
@@ -69,7 +69,7 @@ async function assembleLiveContext(signalkUrl, bindings) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
const resources = {};
|
|
72
|
-
const resourceNames = new Set(bindings.filter((binding) => binding.source ===
|
|
72
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
|
|
73
73
|
for (const name of resourceNames) {
|
|
74
74
|
const url = `${signalkUrl}${RESOURCES_API_PATH}/${name}`;
|
|
75
75
|
(0, log_1.logDebug)(`GET ${url}`);
|
|
@@ -89,7 +89,7 @@ async function assembleLiveContext(signalkUrl, bindings) {
|
|
|
89
89
|
async function readJsonFile(filePath) {
|
|
90
90
|
let raw;
|
|
91
91
|
try {
|
|
92
|
-
raw = await (0, promises_1.readFile)(filePath,
|
|
92
|
+
raw = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
93
93
|
}
|
|
94
94
|
catch (err) {
|
|
95
95
|
throw new Error(`could not read example data file ${filePath} - ${err.message}`);
|
|
@@ -116,34 +116,34 @@ async function readJsonFile(filePath) {
|
|
|
116
116
|
*/
|
|
117
117
|
async function assembleExampleContext(examplesDir, bindings) {
|
|
118
118
|
const signalk = {};
|
|
119
|
-
const contexts = new Set(bindings.filter((binding) => binding.source ===
|
|
119
|
+
const contexts = new Set(bindings.filter((binding) => binding.source === "signalk").map((binding) => binding.context));
|
|
120
120
|
if (contexts.size > 0) {
|
|
121
|
-
const vesselsPath = path_1.default.join(examplesDir,
|
|
121
|
+
const vesselsPath = path_1.default.join(examplesDir, "vessels.json");
|
|
122
122
|
(0, log_1.logDebug)(`reading ${vesselsPath}`);
|
|
123
123
|
const vessels = await readJsonFile(vesselsPath);
|
|
124
124
|
for (const context of contexts) {
|
|
125
|
-
const segments = contextPath(context).split(
|
|
125
|
+
const segments = contextPath(context).split("/");
|
|
126
126
|
let node = vessels;
|
|
127
127
|
for (const segment of segments) {
|
|
128
|
-
node = node === null || typeof node !==
|
|
128
|
+
node = node === null || typeof node !== "object" ? undefined : node[segment];
|
|
129
129
|
}
|
|
130
130
|
if (node === undefined) {
|
|
131
|
-
throw new Error(`example data file ${vesselsPath} has no "${segments.join(
|
|
131
|
+
throw new Error(`example data file ${vesselsPath} has no "${segments.join(".")}" subtree for context "${context}"`);
|
|
132
132
|
}
|
|
133
133
|
signalk[context] = unwrapSignalkTree(node);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
const resources = {};
|
|
137
|
-
const resourceNames = new Set(bindings.filter((binding) => binding.source ===
|
|
137
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
|
|
138
138
|
for (const name of resourceNames) {
|
|
139
|
-
const resourcePath = path_1.default.join(examplesDir,
|
|
139
|
+
const resourcePath = path_1.default.join(examplesDir, "resources", `${name}.json`);
|
|
140
140
|
(0, log_1.logDebug)(`reading ${resourcePath}`);
|
|
141
141
|
resources[name] = await readJsonFile(resourcePath);
|
|
142
142
|
}
|
|
143
143
|
const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
|
|
144
144
|
let categories = {};
|
|
145
145
|
if (categoryNames.size > 0) {
|
|
146
|
-
const categoriesPath = path_1.default.join(examplesDir,
|
|
146
|
+
const categoriesPath = path_1.default.join(examplesDir, "categories.json");
|
|
147
147
|
(0, log_1.logDebug)(`reading ${categoriesPath}`);
|
|
148
148
|
categories = (await readJsonFile(categoriesPath));
|
|
149
149
|
for (const category of categoryNames) {
|
package/dist/cli/log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LogLevel =
|
|
1
|
+
export type LogLevel = "info" | "debug";
|
|
2
2
|
/** Set once from the global `--log-level` option (see index.ts's `preAction` hook) - applies to every command. */
|
|
3
3
|
export declare function setLogLevel(level: string): void;
|
|
4
4
|
/** Internal tracing (e.g. which URL is being fetched) - silent unless --log-level debug, so default output stays uncluttered. */
|