@rhizomatics/signalk-einklabel-plugin 0.10.0 → 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 +20 -2
- package/README.md +74 -22
- 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 +48 -12
- 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/docs/assets/images/real_tidal_clock.jpg +0 -0
- package/package.json +13 -8
- package/templates/.error/250x128-BWRY.svg +12 -0
- package/templates/.error/416x240-BWRY.svg +12 -0
- package/templates/watch/416x240-BWRY.svg +1 -1
- /package/templates/{blank → .blank}/250x128-BWRY.svg +0 -0
- /package/templates/{blank → .blank}/416x240-BWRY.svg +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
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
|
|
|
14
|
+
- Test fixes
|
|
15
|
+
- Moved blank templates to `.blank` directory and suppressed from drop down selection
|
|
16
|
+
- Added recommendations for `signalk-tides` and `signalk-watch-schedule` plugins
|
|
17
|
+
|
|
18
|
+
# 0.10.0
|
|
19
|
+
|
|
3
20
|
## Watch Schedule Example
|
|
21
|
+
|
|
4
22
|
- New template to show current and next crew watch, using `signalk-watch-schedule` plugin
|
|
5
23
|
|
|
6
24
|
## CLI / Debugging
|
|
25
|
+
|
|
7
26
|
- `fields` command now reports where text field definitions can't be interpreted
|
|
8
27
|
- Epoch style timestamps now automatically handled as if proper date time for format options
|
|
9
28
|
|
|
@@ -21,8 +40,7 @@
|
|
|
21
40
|
|
|
22
41
|
## Fixes
|
|
23
42
|
|
|
24
|
-
- Fix configuration JSON holding older versions of itself as subentries
|
|
25
|
-
and data changes
|
|
43
|
+
- Fix configuration JSON holding older versions of itself as subentries and data changes
|
|
26
44
|
- Fix most cases of scanned devices not appearing in device dropdown choice
|
|
27
45
|
|
|
28
46
|
## Improvements
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://github.com)
|
|
8
8
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
9
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, 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
|
|
|
14
14
|
## What is an ESL?
|
|
15
15
|
|
|
@@ -27,13 +27,13 @@ Most of requirements below are to make SignalK work with Bluetooth Low Energy, w
|
|
|
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
|
+
- MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble), however they can be used for template development and
|
|
31
31
|
debugging (everything except `scan` and `paint`)
|
|
32
32
|
|
|
33
33
|
2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy).
|
|
34
34
|
|
|
35
35
|
- Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
|
|
36
|
-
- Some Raspberry Pi models come with suitable Bluetooth
|
|
36
|
+
- Some Raspberry Pi models come with suitable Bluetooth built in
|
|
37
37
|
- Don't worry about the very latest Bluetooth versions, 4.0 is minimum for BLE, 5.0 is nice
|
|
38
38
|
- Home Assistant is massively more popular than SignalK, and often also run on Raspberry Pi and similar, so good source of advice
|
|
39
39
|
|
|
@@ -44,14 +44,14 @@ Most of requirements below are to make SignalK work with Bluetooth Low Energy, w
|
|
|
44
44
|
|
|
45
45
|
4. One or more supported Electronic Shelf Labels
|
|
46
46
|
|
|
47
|
-
- The label used for testing this is the [
|
|
47
|
+
- The label used for testing this is the [Zhsunyco 3.7" BWRY](https://www.aliexpress.com/item/1005010050104435.html)
|
|
48
48
|
|
|
49
49
|
5. Correct time zone set on server if local time is to be shown on display
|
|
50
50
|
|
|
51
51
|
- See [FAQ](#faq-timezone)
|
|
52
52
|
- If not set, everything will work, but you may see the wrong zone or not have daylight savings applied
|
|
53
53
|
|
|
54
|
-
Once you have all of that, it may be worth also installing [signalk-victron-ble](https://github.com/stefanor/signalk-victron-ble),[signalk-ruuvitag-plugin](https://github.com/vokkim/signalk-ruuvitag-plugin) or [bt-sensors-plugin](https://github.com/naugehyde/bt-sensors-plugin-sk) to pull in data from other sensors and equipment.
|
|
54
|
+
Once you have all of that, it may be worth also installing [signalk-victron-ble](https://github.com/stefanor/signalk-victron-ble), [signalk-ruuvitag-plugin](https://github.com/vokkim/signalk-ruuvitag-plugin) or [bt-sensors-plugin](https://github.com/naugehyde/bt-sensors-plugin-sk) to pull in data from other sensors and equipment.
|
|
55
55
|
|
|
56
56
|
## Installation
|
|
57
57
|
|
|
@@ -89,7 +89,7 @@ A _tides_ provider plugin for the Resources API installed and enabled, currently
|
|
|
89
89
|
|
|
90
90
|
The [tides](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tides/) templates can be customized to run with any tide provider, a specific one, or switch to other APIs or SignalK data paths.
|
|
91
91
|
|
|
92
|
-
- In the template it uses a SVG description like `source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time` to get the first tide time, ensures
|
|
92
|
+
- In the template it uses a SVG description like `source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time` to get the first tide time, ensures it's the preferred `signalk-tides` provider and makes it a simple local time rather than a UTC date-time.
|
|
93
93
|
|
|
94
94
|
To show the lunar phase, the `environment.moon.phaseName` path is required, which can
|
|
95
95
|
be easily achieved by installing and configuring the `derived-data` plugin.
|
|
@@ -107,29 +107,30 @@ Template available as 416x240-BWRY for 3.7" ESLs.
|
|
|
107
107
|
|
|
108
108
|
## Setting up a Label
|
|
109
109
|
|
|
110
|
-
Enable the plugin, and use the large **+** sign to add a label, which
|
|
110
|
+
Enable the plugin, and use the large **+** sign to add a label, which opens up these fields.
|
|
111
111
|
|
|
112
112
|

|
|
113
113
|
|
|
114
114
|
- _Friendly Name_ - Give the label any name (word or phrase) you like, for example 'Tide Clock'
|
|
115
|
-
- _Device_ - Unless you have multiple labels, don't bother with pre-scanning or selecting a
|
|
116
|
-
- _Template_ - Choose a built
|
|
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-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
|
-
- If
|
|
119
|
-
- If
|
|
119
|
+
- If it's a SignalK path, enter it next, for example `environment.tide.state`
|
|
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.
|
|
120
121
|
|
|
121
122
|
There are also two more advanced options, which can usually be ignored.
|
|
122
123
|
|
|
123
124
|
- _BLE AES key_ - Only needed if the default key doesn't work and you have a better alternative, otherwise ignore
|
|
124
125
|
- _Force Repaint_ - Next time the label is due to be painted, update even if the data or template hasn't changed (this flag will automatically be cleared after this.)
|
|
125
126
|
|
|
126
|
-
When the plugin starts, it will automatically re-paint the label if
|
|
127
|
+
When the plugin starts, it will automatically re-paint the label if it's new, or the last timed slot was missed and the data has changed.
|
|
127
128
|
|
|
128
129
|
### Scanning for Devices
|
|
129
130
|
|
|
130
131
|
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.
|
|
131
132
|
|
|
132
|
-
One other quirk is that some devices respond with a different name at different times, for example the
|
|
133
|
+
One other quirk is that some devices respond with a different name at different times, for example the generic `WOESL` sometimes and model specific `WL17500C74` other times. However, the MAC address, e.g. `66:66:17:50:0D:2B` is constant, and this is what's tracked by the plugin.
|
|
133
134
|
|
|
134
135
|
The plugin can optionally re-scan whenever it starts up (off by default), although this isn't essential once a label has been configured. Devices found by any scan are remembered across restarts - see the FAQ below.
|
|
135
136
|
|
|
@@ -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:
|
|
@@ -257,7 +270,9 @@ See also the commands useful for debugging under [Developing Templates]
|
|
|
257
270
|
- `render` - transform an SVG template and data into a PNG
|
|
258
271
|
- `paint` - render an SVG template and data to a selected ESL
|
|
259
272
|
|
|
260
|
-
The width, height, vertical offset and colour palette for the device
|
|
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.
|
|
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.
|
|
261
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
|
|
|
@@ -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
|
|
|
@@ -351,7 +372,7 @@ For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show al
|
|
|
351
372
|
|
|
352
373
|
### I can't see my device as a choice on the drop-down list after scan
|
|
353
374
|
|
|
354
|
-
SignalK plugins lack ability to self-update after something like a scan, so first time round you may have to close the config and reload it to see this. Subsequently the plugin will remember all scanned devices, and only drop previously seen ones if it goes 24 hours without a positive scan or with failed paint attempts.
|
|
375
|
+
SignalK plugins lack the ability to self-update after something like a scan, so first time round you may have to close the config and reload it to see this. Subsequently the plugin will remember all scanned devices, and only drop previously seen ones if it goes 24 hours without a positive scan or with failed paint attempts.
|
|
355
376
|
|
|
356
377
|
Easiest way to solve this is to choose 'All Discovered Devices' in the device configuration, and it will paint any compatible devices it finds on future scans.
|
|
357
378
|
|
|
@@ -359,12 +380,12 @@ Easiest way to solve this is to choose 'All Discovered Devices' in the device co
|
|
|
359
380
|
|
|
360
381
|
If the plugin repaints a display at server startup, then the plugin that provides the data may not have started ( or in the case of `derived-data` the plugin that the plugin depends on! ) and unlike Home Assistant, there's no good way of sequencing the start of plugins.
|
|
361
382
|
|
|
362
|
-
Use the _settle_ time, to impose a minimum wait between the eInk Label plugin being initialized, and it attempting to paint any displays, and increase this value if
|
|
383
|
+
Use the _settle_ time, to impose a minimum wait between the eInk Label plugin being initialized, and it attempting to paint any displays, and increase this value if it's still missing data.
|
|
363
384
|
|
|
364
385
|
### Times are showing incorrectly {#faq-timezone}
|
|
365
386
|
|
|
366
387
|
If times are in the wrong timezone, or don't have daylight savings applied correctly,
|
|
367
|
-
then check that the server itself (at the Linux level, not SignalK, which doesn't know)is configured for your timezone, assuming of course that you're a coastal sailor. Use `raspi-config` on a Raspberry Pi, or `timedatectl` on a Linux server.
|
|
388
|
+
then check that the server itself (at the Linux level, not SignalK, which doesn't know) is configured for your timezone, assuming of course that you're a coastal sailor. Use `raspi-config` on a Raspberry Pi, or `timedatectl` on a Linux server.
|
|
368
389
|
|
|
369
390
|
If you're a global cruiser, then use something like [signalk-set-gps-timezone](https://github.com/hoeken/signalk-set-gps-timezone) to set the value in the operating system.
|
|
370
391
|
|
|
@@ -374,16 +395,47 @@ Try a BLE proxy device, ESP32 is popular for this.
|
|
|
374
395
|
|
|
375
396
|
### Can't edit the text contents of SVG template in VSCode
|
|
376
397
|
|
|
377
|
-
If you have an SVG viewer extension, this
|
|
398
|
+
If you have an SVG viewer extension, this will show the image rather than allowing editing of text. To solve, right click on the file in VSCode _Explorer_ view and choose to edit with _Text Editor_.
|
|
378
399
|
|
|
379
400
|
### Description is set in InkScape but doesn't render
|
|
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
|
|
387
439
|
- [WoLink](https://github.com/NickWaterton/Wolink) - Python interface and protocol analysis
|
|
388
440
|
- [e-ink dashboard for Signal K](https://github.com/meri-imperiumi/dashboard) - Waveshare display based multi instrument display.
|
|
389
441
|
- [eInk Dashboard Modern SK](https://github.com/VladimirKalachikhin/e-inkDashboardModernSK) - SignalK dashboard for non-ESL eInk display.
|
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;
|