@rhizomatics/signalk-einklabel-plugin 0.10.1 → 1.0.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 +11 -0
- package/README.md +56 -4
- package/dist/cli/index.d.ts +13 -1
- package/dist/cli/index.js +57 -26
- package/dist/config.d.ts +27 -7
- package/dist/config.js +40 -10
- package/dist/devices/types.d.ts +8 -0
- package/dist/devices/zhsunyco/metadata.js +9 -0
- package/dist/index.d.ts +36 -3
- package/dist/index.js +28 -3
- package/dist/render/binding.d.ts +77 -3
- package/dist/render/binding.js +114 -3
- package/dist/render/formatters.js +18 -0
- package/dist/render/llmPrompt.d.ts +86 -0
- package/dist/render/llmPrompt.js +199 -0
- package/dist/render/templateProviders.d.ts +58 -0
- package/dist/render/templateProviders.js +16 -0
- package/dist/repaintScheduler.js +126 -51
- package/package.json +8 -7
- package/templates/.error/250x128-BWRY.svg +12 -0
- package/templates/.error/416x240-BWRY.svg +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# 1.0.0
|
|
2
|
+
|
|
3
|
+
## Full Release
|
|
4
|
+
|
|
5
|
+
- The `1.0.0` reflects that this plugin has been working in real environment for months, and has been refactored for better extensibility
|
|
6
|
+
|
|
7
|
+
## Extension Support
|
|
8
|
+
|
|
9
|
+
- Custom templates and CLI commands can now be provided by a different plugin
|
|
10
|
+
- Initially this is to support `signalk-einklabel-genai-plugin` which can use a prompt to make an image using a GenAI service like Anthropic or local Ollama
|
|
11
|
+
|
|
1
12
|
# 0.10.1
|
|
2
13
|
|
|
3
14
|
- Test fixes
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com)
|
|
8
8
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
A SignalK plugin to display data from SignalK paths, Resource APIs and plugins on Electronic Shelf Labels (ESL) over a Bluetooth Low Energy (BLE) connection using simple SVG templates.
|
|
10
|
+
A SignalK plugin to display data from SignalK paths, Resource APIs and plugins on Electronic Shelf Labels (ESL) over a Bluetooth Low Energy (BLE) connection using simple SVG templates, or optionally created from a crafted prompt by GenAI if the companion [`@rhizomatics/signalk-einklabel-genai-plugin`](https://github.com/rhizomatics/signalk-einklabel-genai-plugin) is installed.
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
|
@@ -113,7 +113,8 @@ Enable the plugin, and use the large **+** sign to add a label, which opens up t
|
|
|
113
113
|
|
|
114
114
|
- _Friendly Name_ - Give the label any name (word or phrase) you like, for example 'Tide Clock'
|
|
115
115
|
- _Device_ - Unless you have multiple labels, don't bother with pre-scanning or selecting a specific device, instead pick **"All discovered devices"** and it will paint any compatible labels it finds. If you want to pick a specific device, you'll need to wait for a device scan to complete.
|
|
116
|
-
- _Template_ - Choose a built
|
|
116
|
+
- _Template_ - Choose a built-in template, one you've added to the local templates directory, or - if a companion plugin like [`@rhizomatics/signalk-einklabel-genai-plugin`](#genai-rendering) is installed - one of its own contributed entries (shown with a suffix, e.g. "forecast (GenAI)")
|
|
117
|
+
- _Location/description_ - Optional free-text notes on where this label is physically mounted/viewed from, e.g. "chart table, viewed from ~1m in poor light" - available to any template as `source=einklabel,path=description` or `source=label,path=description`
|
|
117
118
|
- _Repaint Trigger_- Do you want this to repaint every few hours (at a chosen minutes past hour), or when a SignalK path changes?
|
|
118
119
|
- If it's a SignalK path, enter it next, for example `environment.tide.state`
|
|
119
120
|
- If it's time based, enter how many hours between repaints, for example 00:00/08:00/16:00 for an 8h schedule, and if you want a specific number of minutes after the hour.
|
|
@@ -207,6 +208,8 @@ Note that for dates and times, the server timezone must be set correctly, for ex
|
|
|
207
208
|
|
|
208
209
|
Additionally, `round=n` can be used to round to limited decimal places.
|
|
209
210
|
|
|
211
|
+
`default=<value>` substitutes `<value>` whenever the resolved value is missing (e.g. an unpublished path), instead of falling through to an empty string - useful anywhere a blank would be misread as a real answer. `default=` with nothing after the `=` still counts as set, deliberately defaulting to an empty string rather than leaving the fallback behaviour unchanged.
|
|
212
|
+
|
|
210
213
|
These can all be combined as in `source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth,round=2`
|
|
211
214
|
|
|
212
215
|
### Non-Textual Fields (Images)
|
|
@@ -231,6 +234,16 @@ Three font types are loaded by default, use the generic font family, or exact fo
|
|
|
231
234
|
- `sans-serif` - `Roboto`
|
|
232
235
|
- `monospace` - `Roboto Mono`
|
|
233
236
|
|
|
237
|
+
## GenAI Rendering
|
|
238
|
+
|
|
239
|
+
As an alternative to hand-designed SVG templates, a device's content can be generated by an LLM instead - this is provided entirely by a separate companion plugin, [`@rhizomatics/signalk-einklabel-genai-plugin`](https://github.com/rhizomatics/signalk-einklabel-genai-plugin), not by this core plugin. The base plugin (BLE painting, SVG templates) has nothing to do with any LLM SDK or API key, so installing it never pulls in GenAI dependencies unless you also explicitly install and enable the companion plugin - the split is deliberate, for anyone on a small/constrained install or who'd simply rather not have any GenAI code in their install at all.
|
|
240
|
+
|
|
241
|
+
Once the companion plugin is installed and enabled, its own config screen handles provider/model/API-key selection (OpenAI, Anthropic, Google, xAI, DeepSeek, Moonshot AI, Ollama, or any other OpenAI-compatible endpoint), and its prompts show up as ordinary entries in this plugin's "Template" dropdown, suffixed (e.g. "forecast (GenAI)") - pick one exactly like you'd pick a `.svg` file. Under the hood it's a `TemplateProvider` (see [Extending](#extending) below) - the same extension mechanism a new vendor's hardware driver uses - so from this plugin's point of view it's just another way `templateName` gets resolved to an image.
|
|
242
|
+
|
|
243
|
+
If the LLM call fails (network/API error, exhausted retries) or its response isn't renderable, this core plugin pushes its own bundled warning template instead of leaving the previous, possibly now-wrong, content on screen - so a stale weather prompt never quietly shows yesterday's forecast through today's storm. This is a generic safety net, not GenAI-specific: the exact same fallback covers a broken hand-authored template failing to render at all. The next scheduled repaint retries automatically. Since each repaint may call a (usually paid) LLM API, a GenAI-backed device should use Repaint Trigger `interval`, not `subscription`.
|
|
244
|
+
|
|
245
|
+
See the companion plugin's own README for installing it, writing prompts, and its `esl-cli prompt`/`generate` commands for testing prompts without a device.
|
|
246
|
+
|
|
234
247
|
## Architecture
|
|
235
248
|
|
|
236
249
|
The primary things managed and provided by the plugin are:
|
|
@@ -259,6 +272,8 @@ See also the commands useful for debugging under [Developing Templates]
|
|
|
259
272
|
|
|
260
273
|
The width, height, vertical offset and colour palette for the device are taken from the internal register of devices, however can be overridden on the command line. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
|
|
261
274
|
|
|
275
|
+
`esl-cli` can also be extended with new subcommands by a `-r/--require`'d package - see [Extending](#extending) below - which is how [`@rhizomatics/signalk-einklabel-genai-plugin`](#genai-rendering) adds its own `prompt`/`generate` commands for testing prompts without a device.
|
|
276
|
+
|
|
262
277
|
( The CLI can also be run from a checked out module, or by opening a terminal shell at `~/.signalk/node_modules/@rhizomatics/signalk-einklabel-plugin`, as `npx esl-cli command --args` )
|
|
263
278
|
|
|
264
279
|
### Scans from CLI
|
|
@@ -278,7 +293,13 @@ Additional vendors and devices can be added by a separate npm package that imple
|
|
|
278
293
|
|
|
279
294
|
- `import esl from '@rhizomatics/signalk-einklabel-plugin'; esl.registerVendorDriver(myDriver)`
|
|
280
295
|
- In the SignalK runtime, call this from the extension's own plugin `start()`. In the CLI, load the extension with `esl-cli --require <module> <command>`.
|
|
281
|
-
- Declare this package as a `
|
|
296
|
+
- Declare this package as a regular npm `dependency` in the extension package - **not** a `peerDependency`, per SignalK's own guidance that npm's peer-dependency resolution interacts poorly with the server's plugin install layout - and declare the SignalK-level relationship via `"signalk": { "requires": ["@rhizomatics/signalk-einklabel-plugin"] }` in the extension's own `package.json` instead, so the App Store can install/report it.
|
|
297
|
+
|
|
298
|
+
### Template Providers
|
|
299
|
+
|
|
300
|
+
An alternative way to produce a device's content, alongside hand-authored SVG templates, can be added the same way - `esl.registerTemplateProvider({ suffix, listTemplates, render })`, from a separate package's own plugin `start()` (or `esl-cli --require <module>`). This is what [`@rhizomatics/signalk-einklabel-genai-plugin`](#genai-rendering) is: `listTemplates()` returns the entries it currently offers (already including its own `suffix`, e.g. `"forecast (GenAI)"`) for the "Template" dropdown, and `render(request)` - given the same signalk/resources/label context any SVG template's bindings get - returns a `Bitmap`, exactly as `SvgRenderer.render()` would. Rejecting from `render()` routes the repaint through this plugin's own generic fallback-warning template, the same as a broken hand-authored template failing to render.
|
|
301
|
+
|
|
302
|
+
`esl.SvgRenderer`, `esl.buildLabel`, `esl.findBindingsInText`, and `esl.substituteBindingsInText` are also exported for a `TemplateProvider` extension to reuse - e.g. to resolve its own `{...}`-style placeholders against the request's context, or to rasterize whatever SVG it produces into the `Bitmap` its `render()` must return.
|
|
282
303
|
|
|
283
304
|
### Developing Templates
|
|
284
305
|
|
|
@@ -380,7 +401,38 @@ If you have an SVG viewer extension, this will show the image rather than allowi
|
|
|
380
401
|
|
|
381
402
|
Check if the text boxes are normal text or flowed text, and correct to normal text.
|
|
382
403
|
|
|
383
|
-
|
|
404
|
+
### My label just shows "CONTENT UNAVAILABLE"
|
|
405
|
+
|
|
406
|
+
That's the bundled fallback warning, not necessarily an error in this plugin - it means the most recent repaint failed, whatever produced the content (a broken hand-authored template, or a `TemplateProvider` extension like [`@rhizomatics/signalk-einklabel-genai-plugin`](#genai-rendering) - e.g. its LLM call failing on no network/API access, an invalid API key, or a response that wasn't a renderable SVG, after using up its configured retries). Check the SignalK server logs (debug logging on for this plugin) for the specific error, and if it's a GenAI device, check that plugin's own provider/API key/model settings. This plugin deliberately never leaves old content on screen when a repaint fails - it retries automatically at the next scheduled interval.
|
|
407
|
+
|
|
408
|
+
### SignalK starts before the Bluetooth daemon — does the plugin need `bluetoothd` running at boot?
|
|
409
|
+
|
|
410
|
+
The plugin retries BLE adapter initialisation with backoff (starting at 2s, capping at 30s) if `bluetoothd`/D-Bus isn't up yet when the plugin starts, so a slow-starting Bluetooth stack on boot will no longer strand it — it keeps retrying until the adapter appears rather than failing once and giving up. You'll see `BLE adapter not ready … — retrying in Ns …` in the SignalK logs in the meantime.
|
|
411
|
+
|
|
412
|
+
That said, it's cleaner to fix the boot ordering at the systemd level so the plugin finds the adapter ready on its first attempt. If SignalK runs as a systemd service (`systemctl status signalk`) and its unit file has no `[Unit]` section (check with `systemctl cat signalk`), add one:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
sudo systemctl edit signalk.service
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
This opens an override file — add:
|
|
419
|
+
|
|
420
|
+
```ini
|
|
421
|
+
[Unit]
|
|
422
|
+
After=bluetooth.target
|
|
423
|
+
Wants=bluetooth.target
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Save and exit, then:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
sudo systemctl daemon-reload
|
|
430
|
+
sudo systemctl restart signalk
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
This tells systemd to start `bluetoothd` first and wait for it before starting SignalK, rather than relying on both racing to start in parallel at boot.
|
|
434
|
+
|
|
435
|
+
## Other ESL and General eInk Resources
|
|
384
436
|
|
|
385
437
|
- [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
|
|
386
438
|
- [zhsunyco-esl](https://github.com/roxburghm/zhsunyco-esl) - Python interface
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { Colour } from "../devices/types";
|
|
4
|
+
import { Binding } from "../render/binding";
|
|
5
|
+
import { TemplateContext } from "../render/types";
|
|
6
|
+
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
7
|
+
export declare const DEFAULT_SIGNALK_URLS: string[];
|
|
8
|
+
export declare function parseColours(code: string): Colour[];
|
|
9
|
+
/** 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. */
|
|
10
|
+
export declare function assembleContext(opts: {
|
|
11
|
+
url?: string;
|
|
12
|
+
exampleData?: string;
|
|
13
|
+
}, bindings: Binding[]): Promise<TemplateContext>;
|
|
14
|
+
export declare const program: Command;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.program = exports.DEFAULT_SIGNALK_URLS = void 0;
|
|
5
|
+
exports.parseColours = parseColours;
|
|
6
|
+
exports.assembleContext = assembleContext;
|
|
4
7
|
const promises_1 = require("fs/promises");
|
|
5
8
|
const path_1 = require("path");
|
|
6
9
|
const commander_1 = require("commander");
|
|
@@ -19,7 +22,7 @@ const log_1 = require("./log");
|
|
|
19
22
|
(0, registry_1.registerDriver)(new zhsunyco_1.ZhsunycoDriver());
|
|
20
23
|
const VENDOR_IDENTIFY_TIMEOUT_MS = 30000;
|
|
21
24
|
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
22
|
-
|
|
25
|
+
exports.DEFAULT_SIGNALK_URLS = ["http://localhost", "http://localhost:3000", "https://localhost"];
|
|
23
26
|
const COLOUR_CODES = {
|
|
24
27
|
BW: ["black", "white"],
|
|
25
28
|
BWR: ["black", "white", "red"],
|
|
@@ -34,7 +37,7 @@ function parseColours(code) {
|
|
|
34
37
|
}
|
|
35
38
|
/** Probes DEFAULT_SIGNALK_URLS in order and returns the first that answers a plain GET - used when -u/--url is omitted. */
|
|
36
39
|
async function resolveDefaultUrl() {
|
|
37
|
-
for (const candidate of DEFAULT_SIGNALK_URLS) {
|
|
40
|
+
for (const candidate of exports.DEFAULT_SIGNALK_URLS) {
|
|
38
41
|
try {
|
|
39
42
|
(0, log_1.logDebug)(`probing ${candidate} as a default SignalK server`);
|
|
40
43
|
await (0, httpJson_1.fetchJson)(`${candidate}/signalk`);
|
|
@@ -44,7 +47,7 @@ async function resolveDefaultUrl() {
|
|
|
44
47
|
(0, log_1.logDebug)(`${candidate} did not answer: ${err.message}`);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
throw new Error(`no -u/--url given and none of ${DEFAULT_SIGNALK_URLS.join(", ")} answered - specify the server explicitly with -u/--url`);
|
|
50
|
+
throw new Error(`no -u/--url given and none of ${exports.DEFAULT_SIGNALK_URLS.join(", ")} answered - specify the server explicitly with -u/--url`);
|
|
48
51
|
}
|
|
49
52
|
/** 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. */
|
|
50
53
|
async function assembleContext(opts, bindings) {
|
|
@@ -73,21 +76,49 @@ async function identifyVendor(address) {
|
|
|
73
76
|
destroy();
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Values passed via `-r`/`--require` (repeatable, `--require=value` form included), read directly from
|
|
81
|
+
* `process.argv` rather than through commander - see the doc comment below on why.
|
|
82
|
+
*/
|
|
83
|
+
function scanRequireModules(argv) {
|
|
84
|
+
const modules = [];
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const arg = argv[i];
|
|
87
|
+
if (arg === "-r" || arg === "--require") {
|
|
88
|
+
if (argv[i + 1])
|
|
89
|
+
modules.push(argv[i + 1]);
|
|
90
|
+
}
|
|
91
|
+
else if (arg.startsWith("--require=")) {
|
|
92
|
+
modules.push(arg.slice("--require=".length));
|
|
93
|
+
}
|
|
88
94
|
}
|
|
95
|
+
return modules;
|
|
96
|
+
}
|
|
97
|
+
// `program` is created (and exported) before anything is `require()`'d below, and before any built-in
|
|
98
|
+
// `.command(...)` is defined - a `-r`'d extension module commonly `require()`s this same compiled file
|
|
99
|
+
// (e.g. `require(".../dist/cli/index.js")`) to get at `program`/`assembleContext`/etc; since that's a
|
|
100
|
+
// circular require back into the module currently being loaded, Node hands it back this module's
|
|
101
|
+
// *exports object as it exists so far*, not the finished one - so `program` has to already be assigned
|
|
102
|
+
// by this point, or the extension would see it as `undefined`.
|
|
103
|
+
exports.program = new commander_1.Command();
|
|
104
|
+
exports.program.name("esl-cli").description("Local CLI for testing ESL device scan and paint without a SignalK server");
|
|
105
|
+
exports.program.option("-r, --require <module>", "require a module before running, e.g. an npm package that registers a vendor driver/template provider or contributes its own subcommands (repeatable) - loaded before any command is parsed", (value, previous = []) => [...previous, value]);
|
|
106
|
+
exports.program.option("-l, --log-level <level>", "log verbosity: info or debug (e.g. trace which URLs are fetched)", "info");
|
|
107
|
+
exports.program.hook("preAction", () => {
|
|
108
|
+
(0, log_1.setLogLevel)(exports.program.opts().logLevel);
|
|
89
109
|
});
|
|
90
|
-
program
|
|
110
|
+
// Loaded here - after `program` exists (see above) but before any built-in `.command(...)` is defined,
|
|
111
|
+
// and well before `.parseAsync()` at the bottom of this file - not in a commander `preAction` hook (as
|
|
112
|
+
// this used to work), which only runs *after* commander has already resolved which command was invoked
|
|
113
|
+
// and parsed its arguments. That timing is fine for a `-r`'d module that only registers a vendor driver
|
|
114
|
+
// or template provider (an already-defined command's own action merely consults those registries when
|
|
115
|
+
// it runs), but too late for one to contribute a brand-new *subcommand* (e.g. a companion plugin adding
|
|
116
|
+
// its own `prompt`/`generate`) to this same invocation - by the time `preAction` fires, commander has
|
|
117
|
+
// already decided there's no such command.
|
|
118
|
+
for (const mod of scanRequireModules(process.argv)) {
|
|
119
|
+
require(mod);
|
|
120
|
+
}
|
|
121
|
+
exports.program
|
|
91
122
|
.command("vendors")
|
|
92
123
|
.description("List supported vendors and the device models each has confirmed metadata for")
|
|
93
124
|
.action(() => {
|
|
@@ -114,7 +145,7 @@ program
|
|
|
114
145
|
printRow(header);
|
|
115
146
|
rows.forEach(printRow);
|
|
116
147
|
});
|
|
117
|
-
program
|
|
148
|
+
exports.program
|
|
118
149
|
.command("scan")
|
|
119
150
|
.description("Scan for supported BLE ESL devices across all registered vendor drivers")
|
|
120
151
|
.option("-d, --duration <seconds>", "scan duration in seconds", "10")
|
|
@@ -161,14 +192,14 @@ program
|
|
|
161
192
|
printRow(header);
|
|
162
193
|
rows.forEach(printRow);
|
|
163
194
|
});
|
|
164
|
-
program
|
|
195
|
+
exports.program
|
|
165
196
|
.command("paint")
|
|
166
197
|
.description("Render a template against a live SignalK server and send it to a device")
|
|
167
198
|
.option("-v, --vendor <vendor>", "vendor driver to use - if omitted, inferred from the device's advertised name")
|
|
168
199
|
.requiredOption("-a, --address <address>", "BLE address of the device")
|
|
169
200
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
170
201
|
.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(", ") +
|
|
202
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
172
203
|
" in turn")
|
|
173
204
|
.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
205
|
.option("-k, --aes-key <hex>", "AES-128 key for device authentication, as 32 hex characters - defaults to the vendor's stock key if omitted")
|
|
@@ -211,13 +242,13 @@ program
|
|
|
211
242
|
});
|
|
212
243
|
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
|
|
213
244
|
});
|
|
214
|
-
program
|
|
245
|
+
exports.program
|
|
215
246
|
.command("render")
|
|
216
247
|
.description("Render a template against a live SignalK server and write a PNG, without needing a device")
|
|
217
248
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
218
249
|
.requiredOption("-o, --output <path>", "output PNG path")
|
|
219
250
|
.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(", ") +
|
|
251
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
221
252
|
" in turn")
|
|
222
253
|
.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
254
|
.option("-w, --width <px>", "render width", "416")
|
|
@@ -233,12 +264,12 @@ program
|
|
|
233
264
|
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
234
265
|
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
235
266
|
});
|
|
236
|
-
program
|
|
267
|
+
exports.program
|
|
237
268
|
.command("fields")
|
|
238
269
|
.description("List every <desc> binding in a template by element id, with its source spec and resolved value")
|
|
239
270
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
240
271
|
.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(", ") +
|
|
272
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
242
273
|
" in turn")
|
|
243
274
|
.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")
|
|
244
275
|
.action(async (opts) => {
|
|
@@ -296,12 +327,12 @@ program
|
|
|
296
327
|
printRow(header);
|
|
297
328
|
table.forEach(printRow);
|
|
298
329
|
});
|
|
299
|
-
program
|
|
330
|
+
exports.program
|
|
300
331
|
.command("field")
|
|
301
332
|
.description("Resolve a single binding spec directly against a live SignalK server, with no template")
|
|
302
333
|
.argument("<spec>", 'binding spec, e.g. "source=resources,resource=tides,path=station.name" or a bare SignalK path')
|
|
303
334
|
.option("-u, --url <url>", "SignalK server base URL - resolves the spec's source=signalk/resources binding - if omitted, tries each of " +
|
|
304
|
-
DEFAULT_SIGNALK_URLS.join(", ") +
|
|
335
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
305
336
|
" in turn")
|
|
306
337
|
.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")
|
|
307
338
|
.action(async (spec, opts) => {
|
|
@@ -309,7 +340,7 @@ program
|
|
|
309
340
|
const context = await assembleContext(opts, [binding]);
|
|
310
341
|
console.log((0, binding_1.renderBinding)(binding, context));
|
|
311
342
|
});
|
|
312
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
343
|
+
exports.program.parseAsync(process.argv).catch((err) => {
|
|
313
344
|
console.error(err.message);
|
|
314
345
|
process.exitCode = 1;
|
|
315
346
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface DeviceConfig {
|
|
|
17
17
|
* live BLE read) and the BLE address, or the special value `ALL_DEVICES` (see above).
|
|
18
18
|
*/
|
|
19
19
|
device: string;
|
|
20
|
+
/**
|
|
21
|
+
* Free-text notes about where this label is physically mounted/viewed from, e.g. "chart table,
|
|
22
|
+
* viewed from ~1m in poor light" - purely descriptive. Available to any template via
|
|
23
|
+
* `source=einklabel,path=description` or `source=label,path=description` (see `buildLabelContext` in
|
|
24
|
+
* `./render/binding.ts`) if it wants it - e.g. useful to a `TemplateProvider` extension (see
|
|
25
|
+
* `./render/templateProviders.ts`) tailoring content to where the label actually sits.
|
|
26
|
+
*/
|
|
27
|
+
description?: string;
|
|
20
28
|
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
21
29
|
aesKey?: string;
|
|
22
30
|
/**
|
|
@@ -25,8 +33,13 @@ export interface DeviceConfig {
|
|
|
25
33
|
* (e.g. `templates/tides/416x240-BWRY.svg`) - `pickBestVariant` then picks whichever file inside it
|
|
26
34
|
* best matches this entry's actual device(s), so one `DeviceConfig` (and one `ALL_DEVICES` entry in
|
|
27
35
|
* particular) works across different panel sizes without the user picking a file for each.
|
|
36
|
+
*
|
|
37
|
+
* Can also name an entry a registered `TemplateProvider` offers (see `./render/templateProviders.ts`)
|
|
38
|
+
* instead of a file - e.g. `signalk-einklabel-genai-plugin` contributing `"forecast (GenAI)"` -
|
|
39
|
+
* `considerRepaint` (`./repaintScheduler.ts`) checks the provider registry before falling back to
|
|
40
|
+
* resolving this as a file path.
|
|
28
41
|
*/
|
|
29
|
-
templateName
|
|
42
|
+
templateName?: string;
|
|
30
43
|
repaintTrigger: "subscription" | "interval";
|
|
31
44
|
/** SignalK path to subscribe to when `repaintTrigger` is `subscription` - a repaint is considered on every delta. */
|
|
32
45
|
triggerPath?: string;
|
|
@@ -93,6 +106,18 @@ export interface PluginConfig {
|
|
|
93
106
|
* `templates/.assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
94
107
|
*/
|
|
95
108
|
export declare const BUNDLED_TEMPLATES_DIR: string;
|
|
109
|
+
/**
|
|
110
|
+
* Bundled template family pushed instead of a device's real content whenever a repaint fails - a broken
|
|
111
|
+
* hand-authored template, or a registered `TemplateProvider`'s `render()` rejecting (e.g.
|
|
112
|
+
* `@rhizomatics/signalk-einklabel-genai-plugin`'s LLM call failing) - see `considerRepaint` in
|
|
113
|
+
* `./repaintScheduler.ts`. Generic on purpose: it has no idea *why* the render failed, only that it
|
|
114
|
+
* did, and that the previous, possibly now-wrong, content must never be left on screen unmarked.
|
|
115
|
+
*
|
|
116
|
+
* Dot-prefixed like `.assets`/`.blank` so `listTemplateFamilies` excludes it from the "Template"
|
|
117
|
+
* dropdown - it's an internal fallback mechanism, not something a user should ever pick as a device's
|
|
118
|
+
* own template.
|
|
119
|
+
*/
|
|
120
|
+
export declare const RENDER_FALLBACK_TEMPLATE_NAME = ".error";
|
|
96
121
|
export declare function defaultConfig(): PluginConfig;
|
|
97
122
|
export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
|
|
98
123
|
/**
|
|
@@ -106,12 +131,7 @@ export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>
|
|
|
106
131
|
* flattened even if nothing ever triggers that path.
|
|
107
132
|
*/
|
|
108
133
|
export declare function healNestedConfig(app: ServerAPI): void;
|
|
109
|
-
/**
|
|
110
|
-
* Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
|
|
111
|
-
* signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
|
|
112
|
-
* path is resolved against `~/.signalk` (where SignalK itself stores its config by default), and
|
|
113
|
-
* an absolute path is used as-is.
|
|
114
|
-
*/
|
|
134
|
+
/** See `resolveDir` - `templatesDir`'s own resolution. */
|
|
115
135
|
export declare function resolveTemplatesDir(templatesDir: string | undefined): string;
|
|
116
136
|
export declare function parseDevice(device: string): {
|
|
117
137
|
vendor: string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BUNDLED_TEMPLATES_DIR = exports.ALL_DEVICES = void 0;
|
|
3
|
+
exports.RENDER_FALLBACK_TEMPLATE_NAME = exports.BUNDLED_TEMPLATES_DIR = exports.ALL_DEVICES = void 0;
|
|
4
4
|
exports.defaultConfig = defaultConfig;
|
|
5
5
|
exports.readCurrentConfig = readCurrentConfig;
|
|
6
6
|
exports.healNestedConfig = healNestedConfig;
|
|
@@ -12,6 +12,7 @@ exports.configUiSchema = configUiSchema;
|
|
|
12
12
|
const fs_1 = require("fs");
|
|
13
13
|
const os_1 = require("os");
|
|
14
14
|
const path_1 = require("path");
|
|
15
|
+
const templateProviders_1 = require("./render/templateProviders");
|
|
15
16
|
const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
16
17
|
/**
|
|
17
18
|
* Special `device` value meaning "every currently-known discovered device" instead of one specific
|
|
@@ -31,6 +32,18 @@ exports.ALL_DEVICES = "ALL";
|
|
|
31
32
|
* `templates/.assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
32
33
|
*/
|
|
33
34
|
exports.BUNDLED_TEMPLATES_DIR = (0, path_1.join)(__dirname, "..", "templates");
|
|
35
|
+
/**
|
|
36
|
+
* Bundled template family pushed instead of a device's real content whenever a repaint fails - a broken
|
|
37
|
+
* hand-authored template, or a registered `TemplateProvider`'s `render()` rejecting (e.g.
|
|
38
|
+
* `@rhizomatics/signalk-einklabel-genai-plugin`'s LLM call failing) - see `considerRepaint` in
|
|
39
|
+
* `./repaintScheduler.ts`. Generic on purpose: it has no idea *why* the render failed, only that it
|
|
40
|
+
* did, and that the previous, possibly now-wrong, content must never be left on screen unmarked.
|
|
41
|
+
*
|
|
42
|
+
* Dot-prefixed like `.assets`/`.blank` so `listTemplateFamilies` excludes it from the "Template"
|
|
43
|
+
* dropdown - it's an internal fallback mechanism, not something a user should ever pick as a device's
|
|
44
|
+
* own template.
|
|
45
|
+
*/
|
|
46
|
+
exports.RENDER_FALLBACK_TEMPLATE_NAME = ".error";
|
|
34
47
|
const SIGNALK_HOME_DIR = (0, path_1.join)((0, os_1.homedir)(), ".signalk");
|
|
35
48
|
const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, "einklabel", "templates");
|
|
36
49
|
function defaultConfig() {
|
|
@@ -117,18 +130,21 @@ function healNestedConfig(app) {
|
|
|
117
130
|
});
|
|
118
131
|
}
|
|
119
132
|
/**
|
|
120
|
-
* Resolves
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* an absolute path is used as-is.
|
|
133
|
+
* Resolves a user-facing `<x>Dir` setting to an actual directory, mirroring signalk-parquet's
|
|
134
|
+
* `outputDirectory` convention: empty means `defaultDir`, a relative path is resolved against
|
|
135
|
+
* `~/.signalk` (where SignalK itself stores its config by default), and an absolute path is used as-is.
|
|
124
136
|
*/
|
|
125
|
-
function
|
|
126
|
-
const trimmed =
|
|
137
|
+
function resolveDir(dir, defaultDir) {
|
|
138
|
+
const trimmed = dir?.trim();
|
|
127
139
|
if (!trimmed) {
|
|
128
|
-
return
|
|
140
|
+
return defaultDir;
|
|
129
141
|
}
|
|
130
142
|
return (0, path_1.isAbsolute)(trimmed) ? trimmed : (0, path_1.join)(SIGNALK_HOME_DIR, trimmed);
|
|
131
143
|
}
|
|
144
|
+
/** See `resolveDir` - `templatesDir`'s own resolution. */
|
|
145
|
+
function resolveTemplatesDir(templatesDir) {
|
|
146
|
+
return resolveDir(templatesDir, DEFAULT_TEMPLATES_DIR);
|
|
147
|
+
}
|
|
132
148
|
/**
|
|
133
149
|
* Enum for the combined "device" field, built from recently scanned devices - including ones a
|
|
134
150
|
* driver identified as its vendor but whose PID isn't in its metadata table yet (clearly labelled,
|
|
@@ -219,11 +235,18 @@ function listTemplateFamilies(dir) {
|
|
|
219
235
|
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && listTemplateVariants((0, path_1.join)(dir, entry.name)).length > 0)
|
|
220
236
|
.map((entry) => entry.name);
|
|
221
237
|
}
|
|
222
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Local templates (files or template-family directories) take priority over a same-named bundled one;
|
|
240
|
+
* both show up as options, followed by every registered `TemplateProvider`'s own entries (e.g.
|
|
241
|
+
* `signalk-einklabel-genai-plugin` contributing `"forecast (GenAI)"` - see `./render/templateProviders.ts`)
|
|
242
|
+
* - so a provider-backed "template" is picked from this exact same dropdown, distinguished only by its
|
|
243
|
+
* `suffix`, with no separate render-mode field at all.
|
|
244
|
+
*/
|
|
223
245
|
function templateNameOptions(templatesDir) {
|
|
224
246
|
const local = [...listSvgFiles(templatesDir), ...listTemplateFamilies(templatesDir)];
|
|
225
247
|
const bundled = [...listSvgFiles(exports.BUNDLED_TEMPLATES_DIR), ...listTemplateFamilies(exports.BUNDLED_TEMPLATES_DIR)].filter((name) => !local.includes(name));
|
|
226
|
-
|
|
248
|
+
const provided = (0, templateProviders_1.allTemplateProviders)().flatMap((provider) => provider.listTemplates());
|
|
249
|
+
return [...local, ...bundled, ...provided];
|
|
227
250
|
}
|
|
228
251
|
function sameColours(a, b) {
|
|
229
252
|
if (a.length !== b.length)
|
|
@@ -350,6 +373,12 @@ function configSchema(app, discovered = []) {
|
|
|
350
373
|
'address, or "All discovered devices" to paint this same template/trigger to every device the plugin currently ' +
|
|
351
374
|
"knows about - simplest for a single label, and also covers several identical labels without listing each one.",
|
|
352
375
|
}, deviceValues, deviceLabels),
|
|
376
|
+
description: {
|
|
377
|
+
type: "string",
|
|
378
|
+
title: "Location/description (optional)",
|
|
379
|
+
description: 'Free-text notes about where this label is physically mounted/viewed from, e.g. "chart table, viewed from ~1m ' +
|
|
380
|
+
'in poor light" - available to any template as source=einklabel,path=description or source=label,path=description.',
|
|
381
|
+
},
|
|
353
382
|
templateName: withEnum({ type: "string", title: "Template" }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
354
383
|
repaintTrigger: {
|
|
355
384
|
type: "string",
|
|
@@ -392,6 +421,7 @@ function configUiSchema() {
|
|
|
392
421
|
return {
|
|
393
422
|
devices: {
|
|
394
423
|
items: {
|
|
424
|
+
description: { "ui:widget": "textarea" },
|
|
395
425
|
repaintTrigger: { "ui:widget": "radio" },
|
|
396
426
|
},
|
|
397
427
|
},
|
package/dist/devices/types.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface DeviceMetadata {
|
|
|
13
13
|
* that should be treated as the default/fallback for that PID.
|
|
14
14
|
*/
|
|
15
15
|
hwVersion?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Real-world brand name (e.g. "Zhsunyco") - distinct from `VendorDriver.vendor` (e.g. also
|
|
18
|
+
* "zhsunyco"), which is this codebase's internal BLE-protocol driver key rather than a name meant
|
|
19
|
+
* for display. Optional since a CLI/config `DeviceModelOverride` for unsupported hardware has no
|
|
20
|
+
* table entry to source it from - `considerLlmPromptRepaint` (repaintScheduler.ts) falls back to
|
|
21
|
+
* the driver's own `vendor` key when unset, for a `source=label,path=manufacturer` prompt binding.
|
|
22
|
+
*/
|
|
23
|
+
manufacturer?: string;
|
|
16
24
|
label: string;
|
|
17
25
|
width: number;
|
|
18
26
|
height: number;
|
|
@@ -22,6 +22,7 @@ exports.ZHSUNYCO_PID_METADATA = void 0;
|
|
|
22
22
|
exports.ZHSUNYCO_PID_METADATA = [
|
|
23
23
|
{
|
|
24
24
|
pid: 0x0008,
|
|
25
|
+
manufacturer: "Zhsunyco",
|
|
25
26
|
label: '1.54"',
|
|
26
27
|
width: 200,
|
|
27
28
|
height: 200,
|
|
@@ -30,6 +31,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
33
|
pid: 0x000a,
|
|
34
|
+
manufacturer: "Zhsunyco",
|
|
33
35
|
label: '2.13"',
|
|
34
36
|
width: 250,
|
|
35
37
|
height: 128,
|
|
@@ -38,6 +40,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
38
40
|
},
|
|
39
41
|
{
|
|
40
42
|
pid: 0x000e,
|
|
43
|
+
manufacturer: "Zhsunyco",
|
|
41
44
|
label: '3.7"',
|
|
42
45
|
width: 416,
|
|
43
46
|
height: 240,
|
|
@@ -47,6 +50,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
47
50
|
{
|
|
48
51
|
pid: 0x000e,
|
|
49
52
|
hwVersion: "0103",
|
|
53
|
+
manufacturer: "Zhsunyco",
|
|
50
54
|
label: '2.13"',
|
|
51
55
|
width: 250,
|
|
52
56
|
height: 128,
|
|
@@ -56,6 +60,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
56
60
|
{
|
|
57
61
|
pid: 0x000e,
|
|
58
62
|
hwVersion: "0201",
|
|
63
|
+
manufacturer: "Zhsunyco",
|
|
59
64
|
label: '3.5"',
|
|
60
65
|
width: 384,
|
|
61
66
|
height: 184,
|
|
@@ -65,6 +70,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
65
70
|
{
|
|
66
71
|
pid: 0x000e,
|
|
67
72
|
hwVersion: "0203",
|
|
73
|
+
manufacturer: "Zhsunyco",
|
|
68
74
|
label: '7.5"',
|
|
69
75
|
width: 800,
|
|
70
76
|
height: 480,
|
|
@@ -73,6 +79,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
73
79
|
},
|
|
74
80
|
{
|
|
75
81
|
pid: 0x0012,
|
|
82
|
+
manufacturer: "Zhsunyco",
|
|
76
83
|
label: '2.9"',
|
|
77
84
|
width: 296,
|
|
78
85
|
height: 128,
|
|
@@ -81,6 +88,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
81
88
|
},
|
|
82
89
|
{
|
|
83
90
|
pid: 0x0016,
|
|
91
|
+
manufacturer: "Zhsunyco",
|
|
84
92
|
label: '4.2"',
|
|
85
93
|
width: 400,
|
|
86
94
|
height: 300,
|
|
@@ -89,6 +97,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
89
97
|
},
|
|
90
98
|
{
|
|
91
99
|
pid: 0x001a,
|
|
100
|
+
manufacturer: "Zhsunyco",
|
|
92
101
|
label: '5.8"',
|
|
93
102
|
width: 648,
|
|
94
103
|
height: 480,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ServerAPI, Plugin } from "@signalk/server-api";
|
|
2
2
|
import { registerDriver, getDriver, allDrivers } from "./devices/registry";
|
|
3
|
+
import { registerTemplateProvider as registerTemplateProviderImpl, findTemplateProvider as findTemplateProviderImpl, allTemplateProviders as allTemplateProvidersImpl } from "./render/templateProviders";
|
|
4
|
+
import { SvgRenderer } from "./render/svgRenderer";
|
|
5
|
+
import { bitmapToPng as bitmapToPngImpl } from "./render/png";
|
|
6
|
+
import { buildLabelContext, findTextBindings, substituteTextBindings } from "./render/binding";
|
|
3
7
|
import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadataType, DiscoveredDevice as DiscoveredDeviceType, VendorDeviceConfig as VendorDeviceConfigType, Colour as ColourType } from "./devices/types";
|
|
8
|
+
import type { TemplateProvider as TemplateProviderType, TemplateRenderRequest as TemplateRenderRequestType } from "./render/templateProviders";
|
|
9
|
+
import type { LabelMeta as LabelMetaType } from "./render/binding";
|
|
10
|
+
import type { Bitmap as BitmapType, TemplateContext as TemplateContextType } from "./render/types";
|
|
4
11
|
/**
|
|
5
12
|
* Public extension point for vendor packages. A package that adds support for a new
|
|
6
13
|
* ESL vendor (e.g. `signalk-esl-shoplabelcorp-plugin`) imports this module and calls
|
|
@@ -8,19 +15,45 @@ import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadata
|
|
|
8
15
|
* `start()` (or at module load time). There's no scanning of installed packages -
|
|
9
16
|
* registration is always an explicit call by the extension's own code.
|
|
10
17
|
*
|
|
11
|
-
* Declare this package as a `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* Declare this package as a regular `dependency` in the extension package (**not** a
|
|
19
|
+
* `peerDependency` - the SignalK team's own guidance is that npm's peer-dependency resolution
|
|
20
|
+
* interacts poorly with the server's plugin install layout), and declare the SignalK-level
|
|
21
|
+
* relationship via `"signalk": { "requires": ["@rhizomatics/signalk-einklabel-plugin"] }` in the
|
|
22
|
+
* extension's own package.json instead, so the App Store can install/report the dependency.
|
|
14
23
|
*/
|
|
15
24
|
declare function plugin(app: ServerAPI): Plugin;
|
|
16
25
|
declare namespace plugin {
|
|
17
26
|
const registerVendorDriver: typeof registerDriver;
|
|
18
27
|
const getVendorDriver: typeof getDriver;
|
|
19
28
|
const allVendorDrivers: typeof allDrivers;
|
|
29
|
+
/**
|
|
30
|
+
* Public extension point for a package offering an alternative to hand-authored SVG templates - e.g.
|
|
31
|
+
* `@rhizomatics/signalk-einklabel-genai-plugin` generating content from an LLM prompt - see
|
|
32
|
+
* `TemplateProvider`'s own doc comment (`./render/templateProviders.ts`) for the full contract. Same
|
|
33
|
+
* regular-`dependency`-plus-`signalk.requires` convention as `registerVendorDriver` above.
|
|
34
|
+
*/
|
|
35
|
+
const registerTemplateProvider: typeof registerTemplateProviderImpl;
|
|
36
|
+
const getTemplateProvider: typeof findTemplateProviderImpl;
|
|
37
|
+
const allTemplateProviders: typeof allTemplateProvidersImpl;
|
|
38
|
+
/** Rasterizes an SVG string to a `Bitmap` - a template provider needs this to turn whatever SVG it produces (e.g. an LLM's response) into paintable pixels, exactly as a bundled template is rendered. */
|
|
39
|
+
const Renderer: typeof SvgRenderer;
|
|
40
|
+
/** Encodes a `Bitmap` as PNG bytes - useful for a CLI extension writing a preview file, same as the core plugin's own `render`/`generate` commands. */
|
|
41
|
+
const bitmapToPng: typeof bitmapToPngImpl;
|
|
42
|
+
/** Facts about one physical label (`source=label,path=...` bindings resolve against these) - see `./render/binding.ts`. */
|
|
43
|
+
const buildLabel: typeof buildLabelContext;
|
|
44
|
+
/** Every `{...}` placeholder referenced across one or more text fragments, parsed as bindings - see `./render/binding.ts`. */
|
|
45
|
+
const findBindingsInText: typeof findTextBindings;
|
|
46
|
+
/** Substitutes every `{...}` placeholder in `text` with its resolved binding value - see `./render/binding.ts`. */
|
|
47
|
+
const substituteBindingsInText: typeof substituteTextBindings;
|
|
20
48
|
type VendorDriver = VendorDriverType;
|
|
21
49
|
type DeviceMetadata = DeviceMetadataType;
|
|
22
50
|
type DiscoveredDevice = DiscoveredDeviceType;
|
|
23
51
|
type VendorDeviceConfig = VendorDeviceConfigType;
|
|
24
52
|
type Colour = ColourType;
|
|
53
|
+
type TemplateProvider = TemplateProviderType;
|
|
54
|
+
type TemplateRenderRequest = TemplateRenderRequestType;
|
|
55
|
+
type LabelMeta = LabelMetaType;
|
|
56
|
+
type Bitmap = BitmapType;
|
|
57
|
+
type TemplateContext = TemplateContextType;
|
|
25
58
|
}
|
|
26
59
|
export = plugin;
|