@rhizomatics/signalk-einklabel-plugin 0.10.1 → 1.2.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 +22 -0
- package/README.md +111 -9
- package/dist/cli/index.d.ts +15 -1
- package/dist/cli/index.js +103 -37
- package/dist/config.d.ts +27 -7
- package/dist/config.js +40 -10
- package/dist/devices/gicisky/compression.d.ts +16 -0
- package/dist/devices/gicisky/compression.js +34 -0
- package/dist/devices/gicisky/encode.d.ts +10 -0
- package/dist/devices/gicisky/encode.js +130 -0
- package/dist/devices/gicisky/index.d.ts +11 -0
- package/dist/devices/gicisky/index.js +164 -0
- package/dist/devices/gicisky/layout.d.ts +35 -0
- package/dist/devices/gicisky/layout.js +35 -0
- package/dist/devices/gicisky/metadata.d.ts +16 -0
- package/dist/devices/gicisky/metadata.js +127 -0
- package/dist/devices/gicisky/protocol.d.ts +53 -0
- package/dist/devices/gicisky/protocol.js +88 -0
- package/dist/devices/types.d.ts +11 -0
- package/dist/devices/zhsunyco/index.js +3 -1
- package/dist/devices/zhsunyco/metadata.js +9 -0
- package/dist/index.d.ts +36 -3
- package/dist/index.js +28 -3
- package/dist/plugin.js +2 -0
- package/dist/render/binding.d.ts +86 -3
- package/dist/render/binding.js +135 -3
- package/dist/render/formatters.js +18 -0
- package/dist/render/reframe.d.ts +12 -0
- package/dist/render/reframe.js +36 -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 +15 -12
- package/templates/.error/250x128-BWRY.svg +12 -0
- package/templates/.error/416x240-BWRY.svg +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
# 1.2.0
|
|
2
|
+
|
|
3
|
+
- Added support for Gicisky labels (also known by other brands), tested with a 2.9" BWRY model
|
|
4
|
+
- CLI render command use template height and width as defaults, can still be overridden by arguments to preview particular size of label
|
|
5
|
+
- CLI paint commands use label height and width as defaults, overridden by arguments if needed
|
|
6
|
+
- CLI paint command now has a `--reframe` option for labels that don't match template size, so can quickly test a label without having perfect template right away
|
|
7
|
+
|
|
8
|
+
# 1.1.0
|
|
9
|
+
|
|
10
|
+
- Label images can now optionally be emailed to an address every time they change, including a table of fields and values used to complete the template, or the prompt if genai based
|
|
11
|
+
|
|
12
|
+
# 1.0.0
|
|
13
|
+
|
|
14
|
+
## Full Release
|
|
15
|
+
|
|
16
|
+
- The `1.0.0` reflects that this plugin has been working in real environment for months, and has been refactored for better extensibility
|
|
17
|
+
|
|
18
|
+
## Extension Support
|
|
19
|
+
|
|
20
|
+
- Custom templates and CLI commands can now be provided by a different plugin
|
|
21
|
+
- 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
|
|
22
|
+
|
|
1
23
|
# 0.10.1
|
|
2
24
|
|
|
3
25
|
- Test fixes
|
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
[](https://codecov.io/gh/rhizomatics/signalk-einklabel-plugin)
|
|
7
7
|
[](https://github.com)
|
|
8
8
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
9
|
+
[](https://boat-tech-directory.rhizomatics.org.uk)
|
|
9
10
|
|
|
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.
|
|
11
|
+
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
12
|
|
|
12
13
|

|
|
13
14
|
|
|
@@ -17,7 +18,7 @@ Electronic Shelf Labels are [eInk](https://en.wikipedia.org/wiki/E_Ink) devices
|
|
|
17
18
|
|
|
18
19
|
Since they are designed to be used in large quantity in small shops, they are cheap and simple devices. Earlier models required dedicated controllers, or updates over Wifi or NFC, whereas many modern ones are standalone BLE devices that can be updated from a phone or server.
|
|
19
20
|
|
|
20
|
-
Being battery operated, they can be stuck on anywhere without wiring - the only location constraints are bluetooth range, visibility (they need ambient light since the display is more like paper than a traditional lit-up electronic display) and out of the weather
|
|
21
|
+
Being battery operated, they can be stuck on anywhere without wiring - the only location constraints are bluetooth range, visibility (they need ambient light since the display is more like paper than a traditional lit-up electronic display) and, for some labels, being out of the weather if they are not waterproof, although IP65 labels are available.
|
|
21
22
|
|
|
22
23
|
## Pre-requisites
|
|
23
24
|
|
|
@@ -34,8 +35,9 @@ Most of requirements below are to make SignalK work with Bluetooth Low Energy, w
|
|
|
34
35
|
|
|
35
36
|
- Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
|
|
36
37
|
- Some Raspberry Pi models come with suitable Bluetooth built in
|
|
37
|
-
|
|
38
|
-
-
|
|
38
|
+
|
|
39
|
+
> - Don't worry about the very latest Bluetooth versions, 4.0 is minimum for BLE, 5.0 is nice
|
|
40
|
+
> - Home Assistant is massively more popular than SignalK, and often also run on Raspberry Pi and similar, so good source of advice
|
|
39
41
|
|
|
40
42
|
3. `bluez` package installed in Linux
|
|
41
43
|
|
|
@@ -44,7 +46,7 @@ Most of requirements below are to make SignalK work with Bluetooth Low Energy, w
|
|
|
44
46
|
|
|
45
47
|
4. One or more supported Electronic Shelf Labels
|
|
46
48
|
|
|
47
|
-
- The
|
|
49
|
+
- The labels used for testing this are the [Zhsunyco 3.7" BWRY](https://www.aliexpress.com/item/1005010050104435.html) and a [Gicisky 2.9" BWRY](https://www.aliexpress.com/item/1005012933325056.html)
|
|
48
50
|
|
|
49
51
|
5. Correct time zone set on server if local time is to be shown on display
|
|
50
52
|
|
|
@@ -113,7 +115,8 @@ Enable the plugin, and use the large **+** sign to add a label, which opens up t
|
|
|
113
115
|
|
|
114
116
|
- _Friendly Name_ - Give the label any name (word or phrase) you like, for example 'Tide Clock'
|
|
115
117
|
- _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
|
|
118
|
+
- _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)")
|
|
119
|
+
- _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
120
|
- _Repaint Trigger_- Do you want this to repaint every few hours (at a chosen minutes past hour), or when a SignalK path changes?
|
|
118
121
|
- If it's a SignalK path, enter it next, for example `environment.tide.state`
|
|
119
122
|
- 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.
|
|
@@ -146,6 +149,14 @@ Also known as 'Suny' and 'WOLink'.
|
|
|
146
149
|
|
|
147
150
|
Python code for a variety of their labels at https://github.com/roxburghm/zhsunyco-esl and https://github.com/NickWaterton/Wolink
|
|
148
151
|
|
|
152
|
+
### Gicisky
|
|
153
|
+
|
|
154
|
+
Known by other names, e.g. 'Picksmart', and with white label brands
|
|
155
|
+
|
|
156
|
+
- BLE ESLs
|
|
157
|
+
- Official store is on [AliExpress](https://www.aliexpress.com/store/911771479/pages/all-items.html?productGroupId=40000001654819&spm=a2g0o.store_pc_home.pcShopHead_6000727597996.1_1)
|
|
158
|
+
- Cheapest labels under £10 GBP / $13 USD
|
|
159
|
+
|
|
149
160
|
## Templating
|
|
150
161
|
|
|
151
162
|
Templates are simply SVG files, to which expressions can be added to use SignalK data, with options to make it easier to read, like rounding or simplifying dates and times. The template can have sample data in the placeholder, so is easy to layout and visualize.
|
|
@@ -207,6 +218,8 @@ Note that for dates and times, the server timezone must be set correctly, for ex
|
|
|
207
218
|
|
|
208
219
|
Additionally, `round=n` can be used to round to limited decimal places.
|
|
209
220
|
|
|
221
|
+
`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.
|
|
222
|
+
|
|
210
223
|
These can all be combined as in `source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth,round=2`
|
|
211
224
|
|
|
212
225
|
### Non-Textual Fields (Images)
|
|
@@ -231,6 +244,16 @@ Three font types are loaded by default, use the generic font family, or exact fo
|
|
|
231
244
|
- `sans-serif` - `Roboto`
|
|
232
245
|
- `monospace` - `Roboto Mono`
|
|
233
246
|
|
|
247
|
+
## GenAI Rendering
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
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`.
|
|
254
|
+
|
|
255
|
+
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.
|
|
256
|
+
|
|
234
257
|
## Architecture
|
|
235
258
|
|
|
236
259
|
The primary things managed and provided by the plugin are:
|
|
@@ -257,7 +280,17 @@ See also the commands useful for debugging under [Developing Templates]
|
|
|
257
280
|
- `render` - transform an SVG template and data into a PNG
|
|
258
281
|
- `paint` - render an SVG template and data to a selected ESL
|
|
259
282
|
|
|
260
|
-
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
|
|
283
|
+
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 with `-w/--width`, `--height`, `--voffset` and `--colours`. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
|
|
284
|
+
|
|
285
|
+
Left unset, both `render` and `paint` default `-w/--width`/`--height` to the template's own declared `width`/`height` (or `viewBox`) - neither command connects to a device just to size the render, since that would mean an extra BLE connect ahead of `paint`'s own, and doing two back-to-back is exactly the kind of churn that trips real BLE hardware.
|
|
286
|
+
|
|
287
|
+
`paint` also takes `--reframe <mode>`, applied once it has connected and identified the device, for when the rendered image doesn't come out the same size as its actual panel:
|
|
288
|
+
|
|
289
|
+
- `fixed` (default) - no adjustment; a size mismatch is rejected with an error, as it always has been
|
|
290
|
+
- `scale` - stretches the rendered image onto the panel's exact dimensions (independently per axis, not preserving aspect ratio)
|
|
291
|
+
- `crop` - keeps pixels 1:1, placed from the top-left; a bigger render is truncated to fit, a smaller one leaves the extra panel space blank
|
|
292
|
+
|
|
293
|
+
`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.
|
|
261
294
|
|
|
262
295
|
( 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
296
|
|
|
@@ -278,7 +311,13 @@ Additional vendors and devices can be added by a separate npm package that imple
|
|
|
278
311
|
|
|
279
312
|
- `import esl from '@rhizomatics/signalk-einklabel-plugin'; esl.registerVendorDriver(myDriver)`
|
|
280
313
|
- 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 `
|
|
314
|
+
- 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.
|
|
315
|
+
|
|
316
|
+
### Template Providers
|
|
317
|
+
|
|
318
|
+
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.
|
|
319
|
+
|
|
320
|
+
`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
321
|
|
|
283
322
|
### Developing Templates
|
|
284
323
|
|
|
@@ -307,8 +346,40 @@ The `esl-cli` can be used to debug and validate templates quickly:
|
|
|
307
346
|
- `fields` - List the fields in the template, with the source specification and the rendered data value
|
|
308
347
|
- `field` - Accept a source specification (outside of any template context) and return the rendered value if available
|
|
309
348
|
|
|
349
|
+
Use `--help` to get the full set of arguments for any of the commands.
|
|
350
|
+
|
|
310
351
|
### Examples
|
|
311
352
|
|
|
353
|
+
#### Paint Image Directly
|
|
354
|
+
|
|
355
|
+
The label address previously discovered via `esl-cli scan`
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
If the label turns out to be a different size than the template (e.g. it's a 250x128 template on a 416x240 panel), that's normally rejected as a mismatch - add `--reframe` to fit it instead:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --reframe scale
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Test Template Without Updating Label
|
|
368
|
+
|
|
369
|
+
This will work even if you don't have a label, or even bluetooth. (The `-u` can be left out if your SignalK server running locally on default ports).
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npx esl-cli render -t templates/tides/250x128-BWRY.svg -o example.png -u http://localhost
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
and this version will work even without a running SignalK server, using some pre-packaged example data:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
npx esl-cli render -t templates/tides/250x128-BWRY.svg -o example.png -e examples
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
312
383
|
#### List all Fields and Rendered Values
|
|
313
384
|
|
|
314
385
|
```bash
|
|
@@ -380,7 +451,38 @@ If you have an SVG viewer extension, this will show the image rather than allowi
|
|
|
380
451
|
|
|
381
452
|
Check if the text boxes are normal text or flowed text, and correct to normal text.
|
|
382
453
|
|
|
383
|
-
|
|
454
|
+
### My label just shows "CONTENT UNAVAILABLE"
|
|
455
|
+
|
|
456
|
+
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.
|
|
457
|
+
|
|
458
|
+
### SignalK starts before the Bluetooth daemon — does the plugin need `bluetoothd` running at boot?
|
|
459
|
+
|
|
460
|
+
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.
|
|
461
|
+
|
|
462
|
+
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:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
sudo systemctl edit signalk.service
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
This opens an override file — add:
|
|
469
|
+
|
|
470
|
+
```ini
|
|
471
|
+
[Unit]
|
|
472
|
+
After=bluetooth.target
|
|
473
|
+
Wants=bluetooth.target
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Save and exit, then:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
sudo systemctl daemon-reload
|
|
480
|
+
sudo systemctl restart signalk
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
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.
|
|
484
|
+
|
|
485
|
+
## Other ESL and General eInk Resources
|
|
384
486
|
|
|
385
487
|
- [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
|
|
386
488
|
- [zhsunyco-esl](https://github.com/roxburghm/zhsunyco-esl) - Python interface
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { Colour } from "../devices/types";
|
|
4
|
+
import { ReframeMode } from "../render/reframe";
|
|
5
|
+
import { Binding } from "../render/binding";
|
|
6
|
+
import { TemplateContext } from "../render/types";
|
|
7
|
+
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
8
|
+
export declare const DEFAULT_SIGNALK_URLS: string[];
|
|
9
|
+
export declare function parseColours(code: string): Colour[];
|
|
10
|
+
export declare function parseReframeMode(value: string): ReframeMode;
|
|
11
|
+
/** 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. */
|
|
12
|
+
export declare function assembleContext(opts: {
|
|
13
|
+
url?: string;
|
|
14
|
+
exampleData?: string;
|
|
15
|
+
}, bindings: Binding[]): Promise<TemplateContext>;
|
|
16
|
+
export declare const program: Command;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
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.parseReframeMode = parseReframeMode;
|
|
7
|
+
exports.assembleContext = assembleContext;
|
|
4
8
|
const promises_1 = require("fs/promises");
|
|
5
9
|
const path_1 = require("path");
|
|
6
10
|
const commander_1 = require("commander");
|
|
7
11
|
const xmldom_1 = require("@xmldom/xmldom");
|
|
8
12
|
const registry_1 = require("../devices/registry");
|
|
9
13
|
const zhsunyco_1 = require("../devices/zhsunyco");
|
|
14
|
+
const gicisky_1 = require("../devices/gicisky");
|
|
10
15
|
const bleDiscovery_1 = require("../devices/bleDiscovery");
|
|
11
16
|
const svgRenderer_1 = require("../render/svgRenderer");
|
|
12
17
|
const png_1 = require("../render/png");
|
|
@@ -17,9 +22,10 @@ const liveContext_1 = require("./liveContext");
|
|
|
17
22
|
const httpJson_1 = require("../httpJson");
|
|
18
23
|
const log_1 = require("./log");
|
|
19
24
|
(0, registry_1.registerDriver)(new zhsunyco_1.ZhsunycoDriver());
|
|
25
|
+
(0, registry_1.registerDriver)(new gicisky_1.GiciskyDriver());
|
|
20
26
|
const VENDOR_IDENTIFY_TIMEOUT_MS = 30000;
|
|
21
27
|
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
22
|
-
|
|
28
|
+
exports.DEFAULT_SIGNALK_URLS = ["http://localhost", "http://localhost:3000", "https://localhost"];
|
|
23
29
|
const COLOUR_CODES = {
|
|
24
30
|
BW: ["black", "white"],
|
|
25
31
|
BWR: ["black", "white", "red"],
|
|
@@ -32,9 +38,16 @@ function parseColours(code) {
|
|
|
32
38
|
}
|
|
33
39
|
return colours;
|
|
34
40
|
}
|
|
41
|
+
const REFRAME_MODES = ["fixed", "scale", "crop"];
|
|
42
|
+
function parseReframeMode(value) {
|
|
43
|
+
if (!REFRAME_MODES.includes(value)) {
|
|
44
|
+
throw new Error(`unknown --reframe value "${value}" - expected one of ${REFRAME_MODES.join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
35
48
|
/** Probes DEFAULT_SIGNALK_URLS in order and returns the first that answers a plain GET - used when -u/--url is omitted. */
|
|
36
49
|
async function resolveDefaultUrl() {
|
|
37
|
-
for (const candidate of DEFAULT_SIGNALK_URLS) {
|
|
50
|
+
for (const candidate of exports.DEFAULT_SIGNALK_URLS) {
|
|
38
51
|
try {
|
|
39
52
|
(0, log_1.logDebug)(`probing ${candidate} as a default SignalK server`);
|
|
40
53
|
await (0, httpJson_1.fetchJson)(`${candidate}/signalk`);
|
|
@@ -44,7 +57,15 @@ async function resolveDefaultUrl() {
|
|
|
44
57
|
(0, log_1.logDebug)(`${candidate} did not answer: ${err.message}`);
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
|
-
throw new Error(`no -u/--url given and none of ${DEFAULT_SIGNALK_URLS.join(", ")} answered - specify the server explicitly with -u/--url`);
|
|
60
|
+
throw new Error(`no -u/--url given and none of ${exports.DEFAULT_SIGNALK_URLS.join(", ")} answered - specify the server explicitly with -u/--url`);
|
|
61
|
+
}
|
|
62
|
+
const DEFAULT_RENDER_WIDTH = 416;
|
|
63
|
+
const DEFAULT_RENDER_HEIGHT = 240;
|
|
64
|
+
/** Shared by every command's -w/--width and --height: an explicit flag always wins; otherwise the first defined value in `sources` (in order); otherwise a generic last-resort default. */
|
|
65
|
+
function resolveDimension(explicit, sources, fallback) {
|
|
66
|
+
if (explicit !== undefined)
|
|
67
|
+
return Number(explicit);
|
|
68
|
+
return sources.find((value) => value !== undefined) ?? fallback;
|
|
48
69
|
}
|
|
49
70
|
/** 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
71
|
async function assembleContext(opts, bindings) {
|
|
@@ -73,21 +94,49 @@ async function identifyVendor(address) {
|
|
|
73
94
|
destroy();
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Values passed via `-r`/`--require` (repeatable, `--require=value` form included), read directly from
|
|
99
|
+
* `process.argv` rather than through commander - see the doc comment below on why.
|
|
100
|
+
*/
|
|
101
|
+
function scanRequireModules(argv) {
|
|
102
|
+
const modules = [];
|
|
103
|
+
for (let i = 0; i < argv.length; i++) {
|
|
104
|
+
const arg = argv[i];
|
|
105
|
+
if (arg === "-r" || arg === "--require") {
|
|
106
|
+
if (argv[i + 1])
|
|
107
|
+
modules.push(argv[i + 1]);
|
|
108
|
+
}
|
|
109
|
+
else if (arg.startsWith("--require=")) {
|
|
110
|
+
modules.push(arg.slice("--require=".length));
|
|
111
|
+
}
|
|
88
112
|
}
|
|
113
|
+
return modules;
|
|
114
|
+
}
|
|
115
|
+
// `program` is created (and exported) before anything is `require()`'d below, and before any built-in
|
|
116
|
+
// `.command(...)` is defined - a `-r`'d extension module commonly `require()`s this same compiled file
|
|
117
|
+
// (e.g. `require(".../dist/cli/index.js")`) to get at `program`/`assembleContext`/etc; since that's a
|
|
118
|
+
// circular require back into the module currently being loaded, Node hands it back this module's
|
|
119
|
+
// *exports object as it exists so far*, not the finished one - so `program` has to already be assigned
|
|
120
|
+
// by this point, or the extension would see it as `undefined`.
|
|
121
|
+
exports.program = new commander_1.Command();
|
|
122
|
+
exports.program.name("esl-cli").description("Local CLI for testing ESL device scan and paint without a SignalK server");
|
|
123
|
+
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]);
|
|
124
|
+
exports.program.option("-l, --log-level <level>", "log verbosity: info or debug (e.g. trace which URLs are fetched)", "info");
|
|
125
|
+
exports.program.hook("preAction", () => {
|
|
126
|
+
(0, log_1.setLogLevel)(exports.program.opts().logLevel);
|
|
89
127
|
});
|
|
90
|
-
program
|
|
128
|
+
// Loaded here - after `program` exists (see above) but before any built-in `.command(...)` is defined,
|
|
129
|
+
// and well before `.parseAsync()` at the bottom of this file - not in a commander `preAction` hook (as
|
|
130
|
+
// this used to work), which only runs *after* commander has already resolved which command was invoked
|
|
131
|
+
// and parsed its arguments. That timing is fine for a `-r`'d module that only registers a vendor driver
|
|
132
|
+
// or template provider (an already-defined command's own action merely consults those registries when
|
|
133
|
+
// it runs), but too late for one to contribute a brand-new *subcommand* (e.g. a companion plugin adding
|
|
134
|
+
// its own `prompt`/`generate`) to this same invocation - by the time `preAction` fires, commander has
|
|
135
|
+
// already decided there's no such command.
|
|
136
|
+
for (const mod of scanRequireModules(process.argv)) {
|
|
137
|
+
require(mod);
|
|
138
|
+
}
|
|
139
|
+
exports.program
|
|
91
140
|
.command("vendors")
|
|
92
141
|
.description("List supported vendors and the device models each has confirmed metadata for")
|
|
93
142
|
.action(() => {
|
|
@@ -114,10 +163,10 @@ program
|
|
|
114
163
|
printRow(header);
|
|
115
164
|
rows.forEach(printRow);
|
|
116
165
|
});
|
|
117
|
-
program
|
|
166
|
+
exports.program
|
|
118
167
|
.command("scan")
|
|
119
168
|
.description("Scan for supported BLE ESL devices across all registered vendor drivers")
|
|
120
|
-
.option("-d, --duration <seconds>", "scan duration in seconds", "
|
|
169
|
+
.option("-d, --duration <seconds>", "scan duration in seconds", "30")
|
|
121
170
|
.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")
|
|
122
171
|
.action(async (opts) => {
|
|
123
172
|
const durationMs = Number(opts.duration) * 1000;
|
|
@@ -161,21 +210,22 @@ program
|
|
|
161
210
|
printRow(header);
|
|
162
211
|
rows.forEach(printRow);
|
|
163
212
|
});
|
|
164
|
-
program
|
|
213
|
+
exports.program
|
|
165
214
|
.command("paint")
|
|
166
215
|
.description("Render a template against a live SignalK server and send it to a device")
|
|
167
216
|
.option("-v, --vendor <vendor>", "vendor driver to use - if omitted, inferred from the device's advertised name")
|
|
168
217
|
.requiredOption("-a, --address <address>", "BLE address of the device")
|
|
169
218
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
170
219
|
.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(", ") +
|
|
220
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
172
221
|
" in turn")
|
|
173
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")
|
|
174
223
|
.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
|
|
176
|
-
.option("--height <px>", "render height
|
|
224
|
+
.option("-w, --width <px>", "render width - defaults to the template's declared width/viewBox - see --reframe for fitting onto a differently-sized panel")
|
|
225
|
+
.option("--height <px>", "render height - defaults to the template's declared height/viewBox - see --reframe for fitting onto a differently-sized panel")
|
|
177
226
|
.option("--voffset <px>", "vertical pixel offset of the panel - overrides the looked-up model for unsupported hardware (requires --colours)", "0")
|
|
178
227
|
.option("--colours <code>", "device colour palette for unsupported hardware: BW, BWR, or BWRY - overrides the looked-up model (uses --width/--height/--voffset)")
|
|
228
|
+
.option("--reframe <mode>", "how to fit the rendered image onto the device's actual panel size when it doesn't match: fixed (default - reject the mismatch, as always), scale (stretch the template to the panel), crop (place at top-left, truncating or leaving the rest blank)", "fixed")
|
|
179
229
|
.option("--connect-timeout <seconds>", "BLE connect timeout before giving up on an attempt", "30")
|
|
180
230
|
.option("--retries <n>", "number of paint attempts (including the first) before giving up", "3")
|
|
181
231
|
.action(async (opts) => {
|
|
@@ -184,19 +234,30 @@ program
|
|
|
184
234
|
if (!driver) {
|
|
185
235
|
throw new Error(`no driver registered for vendor "${vendor}"`);
|
|
186
236
|
}
|
|
237
|
+
const svgSource = await (0, promises_1.readFile)(opts.template, "utf-8");
|
|
238
|
+
// Defaults from the template's own declared size, same as `render` - not from the device's real
|
|
239
|
+
// panel size, which would need its own identify connect ahead of driver.paint()'s own connect;
|
|
240
|
+
// two back-to-back BLE connects on the same device is exactly the kind of churn that trips BlueZ
|
|
241
|
+
// (confirmed: identifyDevice() does a full connect+disconnect for zhsunyco, to read its PID off a
|
|
242
|
+
// GATT characteristic - immediately followed by paint()'s own connect crashed with a raw D-Bus
|
|
243
|
+
// "recipient disconnected from message bus without replying" on real hardware). --reframe below
|
|
244
|
+
// covers a mismatch against the real panel size within paint()'s own single connect instead.
|
|
245
|
+
const declared = (0, binding_1.readTemplateDimensions)(svgSource);
|
|
246
|
+
const width = resolveDimension(opts.width, [declared.width], DEFAULT_RENDER_WIDTH);
|
|
247
|
+
const height = resolveDimension(opts.height, [declared.height], DEFAULT_RENDER_HEIGHT);
|
|
187
248
|
const modelOverride = opts.colours
|
|
188
249
|
? {
|
|
189
250
|
label: "manual override",
|
|
190
|
-
width
|
|
191
|
-
height
|
|
251
|
+
width,
|
|
252
|
+
height,
|
|
192
253
|
voffset: Number(opts.voffset),
|
|
193
254
|
colours: parseColours(opts.colours),
|
|
194
255
|
}
|
|
195
256
|
: undefined;
|
|
196
|
-
const bindings = (0, binding_1.findBindings)(
|
|
257
|
+
const bindings = (0, binding_1.findBindings)(svgSource);
|
|
197
258
|
const context = await assembleContext(opts, bindings);
|
|
198
259
|
const renderer = new svgRenderer_1.SvgRenderer();
|
|
199
|
-
const bitmap = await renderer.render(opts.template, context,
|
|
260
|
+
const bitmap = await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
|
|
200
261
|
const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
|
|
201
262
|
await (0, bleDiscovery_1.withRetries)(Number(opts.retries), async (attempt) => {
|
|
202
263
|
if (attempt > 1) {
|
|
@@ -207,38 +268,43 @@ program
|
|
|
207
268
|
aesKey: opts.aesKey,
|
|
208
269
|
modelOverride,
|
|
209
270
|
connectTimeoutMs,
|
|
271
|
+
reframe: parseReframeMode(opts.reframe),
|
|
210
272
|
});
|
|
211
273
|
});
|
|
212
274
|
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
|
|
213
275
|
});
|
|
214
|
-
program
|
|
276
|
+
exports.program
|
|
215
277
|
.command("render")
|
|
216
278
|
.description("Render a template against a live SignalK server and write a PNG, without needing a device")
|
|
217
279
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
218
280
|
.requiredOption("-o, --output <path>", "output PNG path")
|
|
219
281
|
.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(", ") +
|
|
282
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
221
283
|
" in turn")
|
|
222
284
|
.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
|
|
224
|
-
.option("--height <px>", "render height
|
|
285
|
+
.option("-w, --width <px>", "render width - defaults to the template's declared width/viewBox")
|
|
286
|
+
.option("--height <px>", "render height - defaults to the template's declared height/viewBox")
|
|
225
287
|
.option("-f, --font <path>", "override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio",
|
|
226
288
|
// See the -r/--require option above for why `| undefined` is needed here despite commander's typings.
|
|
227
289
|
(value, previous = []) => [...previous, value])
|
|
228
290
|
.action(async (opts) => {
|
|
229
|
-
const
|
|
291
|
+
const svgSource = await (0, promises_1.readFile)(opts.template, "utf-8");
|
|
292
|
+
const declared = (0, binding_1.readTemplateDimensions)(svgSource);
|
|
293
|
+
const width = resolveDimension(opts.width, [declared.width], DEFAULT_RENDER_WIDTH);
|
|
294
|
+
const height = resolveDimension(opts.height, [declared.height], DEFAULT_RENDER_HEIGHT);
|
|
295
|
+
const bindings = (0, binding_1.findBindings)(svgSource);
|
|
230
296
|
const context = await assembleContext(opts, bindings);
|
|
231
297
|
const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
|
|
232
|
-
const bitmap = await renderer.render(opts.template, context,
|
|
298
|
+
const bitmap = await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
|
|
233
299
|
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
234
300
|
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
235
301
|
});
|
|
236
|
-
program
|
|
302
|
+
exports.program
|
|
237
303
|
.command("fields")
|
|
238
304
|
.description("List every <desc> binding in a template by element id, with its source spec and resolved value")
|
|
239
305
|
.requiredOption("-t, --template <path>", "path to SVG template")
|
|
240
306
|
.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(", ") +
|
|
307
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
242
308
|
" in turn")
|
|
243
309
|
.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
310
|
.action(async (opts) => {
|
|
@@ -296,12 +362,12 @@ program
|
|
|
296
362
|
printRow(header);
|
|
297
363
|
table.forEach(printRow);
|
|
298
364
|
});
|
|
299
|
-
program
|
|
365
|
+
exports.program
|
|
300
366
|
.command("field")
|
|
301
367
|
.description("Resolve a single binding spec directly against a live SignalK server, with no template")
|
|
302
368
|
.argument("<spec>", 'binding spec, e.g. "source=resources,resource=tides,path=station.name" or a bare SignalK path')
|
|
303
369
|
.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(", ") +
|
|
370
|
+
exports.DEFAULT_SIGNALK_URLS.join(", ") +
|
|
305
371
|
" in turn")
|
|
306
372
|
.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
373
|
.action(async (spec, opts) => {
|
|
@@ -309,7 +375,7 @@ program
|
|
|
309
375
|
const context = await assembleContext(opts, [binding]);
|
|
310
376
|
console.log((0, binding_1.renderBinding)(binding, context));
|
|
311
377
|
});
|
|
312
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
378
|
+
exports.program.parseAsync(process.argv).catch((err) => {
|
|
313
379
|
console.error(err.message);
|
|
314
380
|
process.exitCode = 1;
|
|
315
381
|
});
|
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;
|