@rhizomatics/signalk-einklabel-plugin 1.3.0-beta1 → 1.3.0-beta10
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 +25 -1
- package/README.md +42 -3
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +25 -1
- package/dist/config.d.ts +46 -14
- package/dist/config.js +170 -53
- package/dist/devices/bleBackend.d.ts +30 -2
- package/dist/devices/bleBackend.js +179 -7
- package/dist/devices/bleDiscovery.d.ts +9 -2
- package/dist/devices/bleDiscovery.js +12 -3
- package/dist/devices/discoveryCoordinator.d.ts +18 -0
- package/dist/devices/discoveryCoordinator.js +89 -22
- package/dist/devices/gicisky/compression.d.ts +22 -9
- package/dist/devices/gicisky/compression.js +128 -13
- package/dist/devices/gicisky/encode.d.ts +1 -1
- package/dist/devices/gicisky/encode.js +2 -2
- package/dist/devices/gicisky/index.js +19 -8
- package/dist/devices/gicisky/layout.d.ts +13 -1
- package/dist/devices/gicisky/layout.js +21 -0
- package/dist/devices/types.d.ts +22 -0
- package/dist/devices/types.js +2 -0
- package/dist/devices/zhsunyco/compression.d.ts +1 -0
- package/dist/devices/zhsunyco/compression.js +30 -0
- package/dist/devices/zhsunyco/index.js +10 -4
- package/dist/devices/zhsunyco/protocol.d.ts +2 -0
- package/dist/devices/zhsunyco/protocol.js +2 -0
- package/dist/plugin.js +11 -2
- package/dist/render/mirror.d.ts +11 -0
- package/dist/render/mirror.js +24 -0
- package/dist/repaintScheduler.js +27 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
# 1.3.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## BLE Manager
|
|
4
|
+
|
|
5
|
+
First implementation of using new SignalK BLE Manager rather than directly using the `bluez` services.
|
|
6
|
+
|
|
7
|
+
- Off by default until longer term stability demonstrated.
|
|
8
|
+
- It may be less reliable for some devices that can be reached with direct bluez access - try it and fall back to the direct mode if so.
|
|
9
|
+
- BLE Manager use reduces interference between plugins competing for same BLE devices
|
|
10
|
+
- Recommend using SignalK at least release v2.33.0.
|
|
11
|
+
|
|
12
|
+
## Compression
|
|
13
|
+
|
|
14
|
+
Images are now compressed before sending - no change to what's displayed, but send time is massively reduced for labels with lots of empty space, so less battery is used and sends are less likely to fail.
|
|
15
|
+
|
|
16
|
+
- On by default for Zhsunyco labels and Gicisky 7.5"/10.2" labels; can be turned off per label, or with `--no-compress` on the CLI
|
|
17
|
+
- Experimental opt-in _Wire format_ `chunked` sends a Gicisky 4.2" BWR compressed too (`--compression-format chunked` on the CLI)
|
|
18
|
+
|
|
19
|
+
## Advanced Options
|
|
20
|
+
|
|
21
|
+
Connect timeout and retries can now be defined per label.
|
|
22
|
+
|
|
23
|
+
Some labels may need images flipped, so a per-label _Mirror_ option can flip the image horizontally, vertically, or both (rotate 180°, for a label mounted upside down). Also available as `--mirror` on the CLI `paint` and `render` commands.
|
|
24
|
+
|
|
25
|
+
Different 'chunking' options to help with new label models.
|
|
26
|
+
|
|
27
|
+
All the advanced options now grouped together to make the config clearer. Note that if you roll back to a previous version you may have to re-enter these values (they are automatically moved to the new section when upgrading).
|
|
4
28
|
|
|
5
29
|
# 1.2.3
|
|
6
30
|
|
package/README.md
CHANGED
|
@@ -134,10 +134,16 @@ Enable the plugin, and use the large **+** sign to add a label, which opens up t
|
|
|
134
134
|
- If it's a SignalK path, enter it next, for example `environment.tide.state`
|
|
135
135
|
- 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.
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
- _If the render doesn't match the panel size_ - see [Reframing](#reframing)
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
The rest are grouped under _Advanced settings_, and can usually be ignored.
|
|
140
|
+
|
|
141
|
+
- _Compress upload_, _Mirror_ and _Wire format_ - see [Other Image Options](#other-image-options)
|
|
140
142
|
- _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.)
|
|
143
|
+
- _BLE AES key_ - Only needed if the default key doesn't work and you have a better alternative, otherwise ignore
|
|
144
|
+
- _Paint connect timeout_ and _Paint retries_ for this device - leave blank to use the plugin-wide settings, or set them for a label that's slower to respond or further away than the others
|
|
145
|
+
|
|
146
|
+
Configs saved by earlier versions are moved into this layout automatically when the plugin starts.
|
|
141
147
|
|
|
142
148
|
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.
|
|
143
149
|
|
|
@@ -178,6 +184,14 @@ Templates are simply SVG files, to which expressions can be added to use SignalK
|
|
|
178
184
|
|
|
179
185
|
There's some wiggle room with the `reframe` options to use a template that's a bit too small, or too large, for the label, although best results come from a template that's precisely matching the pixel height and width of the label. Next best is template that has the same aspect ratio, so it can be cleanly scaled. `crop` is the 2nd least worst, though if its only a handful of pixels its often not worth worring about a separate template and `crop` is just fine. `scale` is likely to look worst, since it will force an image in regardless of aspect ratio.
|
|
180
186
|
|
|
187
|
+
### Other Image Options
|
|
188
|
+
|
|
189
|
+
Each label has a few more settings for how the image is sent, under _Advanced settings_. The CLI `paint` command has matching options (see [Command Line Interface](#command-line-interface)), so you can try them on a label before changing the plugin config.
|
|
190
|
+
|
|
191
|
+
- _Compress upload_ - on by default. Sends much less data over Bluetooth, so painting is quicker, uses less of the label's battery and is less likely to time out. Works for Zhsunyco labels and Gicisky 7.5"/10.2" labels, and is ignored for others. Turn it off if a label stops updating.
|
|
192
|
+
- _Wire format (Gicisky, experimental)_ - `auto` by default. `chunked` sends a Gicisky 4.2" BWR label compressed, the same way as the 7.5"/10.2". The vendor's own app has been seen doing this, but it hasn't been tested on current firmware. Set it back to `auto` if the label stops updating.
|
|
193
|
+
- _Mirror_ - `none` by default. `horizontal` or `vertical` fixes a label model whose image comes out mirrored. `both` rotates the image 180°, for a label that has to be mounted upside down.
|
|
194
|
+
|
|
181
195
|
### Template Families (multiple panel sizes/colours)
|
|
182
196
|
|
|
183
197
|
A "Template" selection can either be one specific `.svg` file, or a _directory_ holding several versions of the same template for different panel sizes/colour-sets, e.g. `templates/tides/416x240-BWRY.svg` and `templates/tides/250x128-BWRY.svg` both implement the tide clock, just at different sizes.
|
|
@@ -309,6 +323,12 @@ Left unset, both `render` and `paint` default `-w/--width`/`--height` to the tem
|
|
|
309
323
|
|
|
310
324
|
The main SignalK plugin offers the same choice per device (defaulting to `crop` there too) in each device's own config - "If the render doesn't match the panel size".
|
|
311
325
|
|
|
326
|
+
`paint` has matching options for the other per-label image settings too (see [Other Image Options](#other-image-options)):
|
|
327
|
+
|
|
328
|
+
- `--mirror <mode>` - `none` (default), `horizontal`, `vertical`, or `both` (rotate 180°). `render` also takes `--mirror`, to preview the flip as a PNG without a label
|
|
329
|
+
- `--no-compress` - send the image uncompressed, to rule compression out if a label won't update
|
|
330
|
+
- `--compression-format <format>` - `auto` (default) or `chunked`, to try the experimental compressed format on a Gicisky 4.2" BWR
|
|
331
|
+
|
|
312
332
|
`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.
|
|
313
333
|
|
|
314
334
|
( 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` )
|
|
@@ -377,12 +397,24 @@ The label address previously discovered via `esl-cli scan`
|
|
|
377
397
|
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93
|
|
378
398
|
```
|
|
379
399
|
|
|
380
|
-
If the label turns out to be a different size than the template (e.g. it's a 250x128 template on a 416x240 panel),
|
|
400
|
+
If the label turns out to be a different size than the template (e.g. it's a 250x128 template on a 416x240 panel), it's cropped to fit by default - add `--reframe scale` to stretch it instead:
|
|
381
401
|
|
|
382
402
|
```bash
|
|
383
403
|
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --reframe scale
|
|
384
404
|
```
|
|
385
405
|
|
|
406
|
+
If a label doesn't update, try sending it uncompressed to see whether compression is the cause:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --no-compress
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
If the image comes out mirrored, try each `--mirror` mode until it looks right, then set the same _Mirror_ option in the label's config:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --mirror horizontal
|
|
416
|
+
```
|
|
417
|
+
|
|
386
418
|
#### Test Template Without Updating Label
|
|
387
419
|
|
|
388
420
|
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).
|
|
@@ -537,6 +569,8 @@ USB_DENYLIST="0b05:190e"
|
|
|
537
569
|
|
|
538
570
|
## Other ESL and General eInk Resources
|
|
539
571
|
|
|
572
|
+
### Components
|
|
573
|
+
|
|
540
574
|
- [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
|
|
541
575
|
- [zhsunyco-esl](https://github.com/roxburghm/zhsunyco-esl) - Python interface
|
|
542
576
|
- [WoLink](https://github.com/NickWaterton/Wolink) - Python interface and protocol analysis
|
|
@@ -545,9 +579,14 @@ USB_DENYLIST="0b05:190e"
|
|
|
545
579
|
- [esp32-esl-system](https://github.com/giobauermeister/esp32-esl-system) - Docker and ESP32 based system for updating ESLs.
|
|
546
580
|
- [hass-gicisky](https://github.com/eigger/hass-gicisky) - Home Assistant integration for Gicisky ESLs ( a similar vendor to Zhsunyco). Uses [imagespec](https://github.com/eigger/imagespec) for templating.
|
|
547
581
|
- [ha-panda](https://github.com/moryoav/ha-panda) - Home Assistant integration for Panda ESLs ( a similar vendor to Zhsunyco).
|
|
582
|
+
|
|
583
|
+
### Notes and Experiences
|
|
584
|
+
|
|
585
|
+
- [Cabalist Gicisky Image Notes](https://github.com/Cabalist/gicisky_image_notes)
|
|
548
586
|
- [Dmitry.gr](https://dmitry.gr/?r=05.Projects&proj=29.%20eInk%20Price%20Tags) - Personal site of an ESL hacker
|
|
549
587
|
- [Aaron Christobel](https://www.youtube.com/@atc1441) - YouTube channel of an ESL hacker.
|
|
550
588
|
- [rbaron.net](https://rbaron.net/blog/2022/07/29/Daisy-chaining-multiple-electronic-shelf-labels) - Blog of an early ESL hacker.
|
|
589
|
+
### Retail
|
|
551
590
|
- [Pimoroni](https://shop.pimoroni.com/collections/displays?tags=e-ink%20Displays) - All shapes and sizes of eInk displays, aimed at hackers, and with an [inky](https://github.com/pimoroni/inky) GitHub project to support them.
|
|
552
591
|
- [WaveShare](https://www.waveshare.com/product/displays/e-paper.htm) - Wide range of eInk displays for hardware projects, not limited to ESLs.
|
|
553
592
|
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { Colour } from "../devices/types";
|
|
3
|
+
import { Colour, CompressionFormat } from "../devices/types";
|
|
4
4
|
import { ReframeMode } from "../render/reframe";
|
|
5
|
+
import { MirrorMode } from "../render/mirror";
|
|
5
6
|
import { Binding } from "../render/binding";
|
|
6
7
|
import { TemplateContext } from "../render/types";
|
|
7
8
|
/** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
|
|
8
9
|
export declare const DEFAULT_SIGNALK_URLS: string[];
|
|
9
10
|
export declare function parseColours(code: string): Colour[];
|
|
10
11
|
export declare function parseReframeMode(value: string): ReframeMode;
|
|
12
|
+
export declare function parseMirrorMode(value: string): MirrorMode;
|
|
13
|
+
export declare function parseCompressionFormat(value: string): CompressionFormat;
|
|
11
14
|
/** 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
15
|
export declare function assembleContext(opts: {
|
|
13
16
|
url?: string;
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.program = exports.DEFAULT_SIGNALK_URLS = void 0;
|
|
5
5
|
exports.parseColours = parseColours;
|
|
6
6
|
exports.parseReframeMode = parseReframeMode;
|
|
7
|
+
exports.parseMirrorMode = parseMirrorMode;
|
|
8
|
+
exports.parseCompressionFormat = parseCompressionFormat;
|
|
7
9
|
exports.assembleContext = assembleContext;
|
|
8
10
|
const promises_1 = require("fs/promises");
|
|
9
11
|
const path_1 = require("path");
|
|
@@ -13,6 +15,8 @@ const registry_1 = require("../devices/registry");
|
|
|
13
15
|
const zhsunyco_1 = require("../devices/zhsunyco");
|
|
14
16
|
const gicisky_1 = require("../devices/gicisky");
|
|
15
17
|
const bleDiscovery_1 = require("../devices/bleDiscovery");
|
|
18
|
+
const types_1 = require("../devices/types");
|
|
19
|
+
const mirror_1 = require("../render/mirror");
|
|
16
20
|
const svgRenderer_1 = require("../render/svgRenderer");
|
|
17
21
|
const png_1 = require("../render/png");
|
|
18
22
|
const binding_1 = require("../render/binding");
|
|
@@ -45,6 +49,18 @@ function parseReframeMode(value) {
|
|
|
45
49
|
}
|
|
46
50
|
return value;
|
|
47
51
|
}
|
|
52
|
+
function parseMirrorMode(value) {
|
|
53
|
+
if (!mirror_1.MIRROR_MODES.includes(value)) {
|
|
54
|
+
throw new Error(`unknown --mirror value "${value}" - expected one of ${mirror_1.MIRROR_MODES.join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function parseCompressionFormat(value) {
|
|
59
|
+
if (!types_1.COMPRESSION_FORMATS.includes(value)) {
|
|
60
|
+
throw new Error(`unknown --compression-format value "${value}" - expected one of ${types_1.COMPRESSION_FORMATS.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
48
64
|
/** Probes DEFAULT_SIGNALK_URLS in order and returns the first that answers a plain GET - used when -u/--url is omitted. */
|
|
49
65
|
async function resolveDefaultUrl() {
|
|
50
66
|
for (const candidate of exports.DEFAULT_SIGNALK_URLS) {
|
|
@@ -226,6 +242,9 @@ exports.program
|
|
|
226
242
|
.option("--voffset <px>", "vertical pixel offset of the panel - overrides the looked-up model for unsupported hardware (requires --colours)", "0")
|
|
227
243
|
.option("--colours <code>", "device colour palette for unsupported hardware: BW, BWR, or BWRY - overrides the looked-up model (uses --width/--height/--voffset)")
|
|
228
244
|
.option("--reframe <mode>", "how to fit the rendered image onto the device's actual panel size when it doesn't match: crop (default - place at top-left, truncating or leaving the rest blank), scale (stretch the template to the panel), fixed (reject the mismatch instead)", "crop")
|
|
245
|
+
.option("--mirror <mode>", "flip the image before sending: none (default), horizontal, vertical, or both (rotate 180°)", "none")
|
|
246
|
+
.option("--no-compress", 'send the image uncompressed (zhsunyco; gicisky 7.5"/10.2" or --compression-format chunked - compression is on by default)')
|
|
247
|
+
.option("--compression-format <format>", 'gicisky wire format: auto (default - the model\'s usual format) or chunked (experimental - send a 4.2" BWR compressed like the 7.5"/10.2")', "auto")
|
|
229
248
|
.option("--connect-timeout <seconds>", "BLE connect timeout before giving up on an attempt", "30")
|
|
230
249
|
.option("--retries <n>", "number of paint attempts (including the first) before giving up", "3")
|
|
231
250
|
.action(async (opts) => {
|
|
@@ -269,6 +288,9 @@ exports.program
|
|
|
269
288
|
modelOverride,
|
|
270
289
|
connectTimeoutMs,
|
|
271
290
|
reframe: parseReframeMode(opts.reframe),
|
|
291
|
+
mirror: parseMirrorMode(opts.mirror),
|
|
292
|
+
compress: opts.compress,
|
|
293
|
+
compressionFormat: parseCompressionFormat(opts.compressionFormat),
|
|
272
294
|
});
|
|
273
295
|
});
|
|
274
296
|
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
|
|
@@ -287,7 +309,9 @@ exports.program
|
|
|
287
309
|
.option("-f, --font <path>", "override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio",
|
|
288
310
|
// See the -r/--require option above for why `| undefined` is needed here despite commander's typings.
|
|
289
311
|
(value, previous = []) => [...previous, value])
|
|
312
|
+
.option("--mirror <mode>", "flip the PNG the same way paint --mirror would: none (default), horizontal, vertical, or both", "none")
|
|
290
313
|
.action(async (opts) => {
|
|
314
|
+
const mirror = parseMirrorMode(opts.mirror);
|
|
291
315
|
const svgSource = await (0, promises_1.readFile)(opts.template, "utf-8");
|
|
292
316
|
const declared = (0, binding_1.readTemplateDimensions)(svgSource);
|
|
293
317
|
const width = resolveDimension(opts.width, [declared.width], DEFAULT_RENDER_WIDTH);
|
|
@@ -295,7 +319,7 @@ exports.program
|
|
|
295
319
|
const bindings = (0, binding_1.findBindings)(svgSource);
|
|
296
320
|
const context = await assembleContext(opts, bindings);
|
|
297
321
|
const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
|
|
298
|
-
const bitmap = await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
|
|
322
|
+
const bitmap = (0, mirror_1.mirrorBitmap)(await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR), mirror);
|
|
299
323
|
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
300
324
|
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
301
325
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ServerAPI } from "@signalk/server-api";
|
|
2
|
-
import { Colour, DiscoveredDevice } from "./devices/types";
|
|
2
|
+
import { Colour, CompressionFormat, DiscoveredDevice } from "./devices/types";
|
|
3
3
|
import { ReframeMode } from "./render/reframe";
|
|
4
|
+
import { MirrorMode } from "./render/mirror";
|
|
4
5
|
/**
|
|
5
6
|
* Special `device` value meaning "every currently-known discovered device" instead of one specific
|
|
6
7
|
* BLE address - lets a single `DeviceConfig` entry (one template, one trigger) broadcast to every
|
|
@@ -26,8 +27,6 @@ export interface DeviceConfig {
|
|
|
26
27
|
* `./render/templateProviders.ts`) tailoring content to where the label actually sits.
|
|
27
28
|
*/
|
|
28
29
|
description?: string;
|
|
29
|
-
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
30
|
-
aesKey?: string;
|
|
31
30
|
/**
|
|
32
31
|
* Either one specific `.svg` file, or the name of a *template-family* directory holding several
|
|
33
32
|
* same-purpose templates for different panel sizes/colour-sets, named `<width>x<height>-<colours>.svg`
|
|
@@ -48,8 +47,6 @@ export interface DeviceConfig {
|
|
|
48
47
|
intervalHours?: number;
|
|
49
48
|
/** ...at this minute past the hour. */
|
|
50
49
|
intervalMinute?: number;
|
|
51
|
-
/** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
|
|
52
|
-
forceRepaint?: boolean;
|
|
53
50
|
/**
|
|
54
51
|
* How to fit the rendered image onto the device's actual panel size when it doesn't match (see
|
|
55
52
|
* `ReframeMode`) - e.g. a template family with no variant sized for this particular label. Left
|
|
@@ -58,7 +55,34 @@ export interface DeviceConfig {
|
|
|
58
55
|
* showing *something*, even off-size, beats a repaint that just fails outright.
|
|
59
56
|
*/
|
|
60
57
|
reframe?: ReframeMode;
|
|
58
|
+
/** Settings most labels never need, grouped so the admin UI shows them in their own "Advanced settings" box. */
|
|
59
|
+
advanced?: AdvancedDeviceSettings;
|
|
61
60
|
}
|
|
61
|
+
export interface AdvancedDeviceSettings {
|
|
62
|
+
/** Compress the upload (zhsunyco, and gicisky's chunked 7.5"/10.2" panels - ignored otherwise). Unset means on; turn off if a device fails to show compressed images. */
|
|
63
|
+
compress?: boolean;
|
|
64
|
+
/** Flip the image before sending - for a panel whose layout is mirrored, or one mounted upside down (`"both"`). Unset means `"none"`. */
|
|
65
|
+
mirror?: MirrorMode;
|
|
66
|
+
/**
|
|
67
|
+
* Experimental opt-in wire format (gicisky only) - `"chunked"` sends a 4.2" BWR (or another plain
|
|
68
|
+
* two-plane panel) QuickLZ-compressed like the 7.5"/10.2". Unset means `"auto"`, the model's own
|
|
69
|
+
* format. See `CompressionFormat`.
|
|
70
|
+
*/
|
|
71
|
+
compressionFormat?: CompressionFormat;
|
|
72
|
+
/** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
|
|
73
|
+
forceRepaint?: boolean;
|
|
74
|
+
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
75
|
+
aesKey?: string;
|
|
76
|
+
/** Per-device override of `PluginConfig.paintConnectTimeoutSeconds` - unset uses the plugin-wide value. */
|
|
77
|
+
paintConnectTimeoutSeconds?: number;
|
|
78
|
+
/** Per-device override of `PluginConfig.paintRetries` - unset uses the plugin-wide value. */
|
|
79
|
+
paintRetries?: number;
|
|
80
|
+
}
|
|
81
|
+
/** Applies `migrateDeviceConfig` to every device entry - see `healStoredConfig` for persisting the result. */
|
|
82
|
+
export declare function migrateConfig<T extends Partial<PluginConfig>>(config: T): {
|
|
83
|
+
config: T;
|
|
84
|
+
migrated: boolean;
|
|
85
|
+
};
|
|
62
86
|
export interface PluginConfig {
|
|
63
87
|
/**
|
|
64
88
|
* Directory the plugin scans for template files, instead of an upload UI - follows
|
|
@@ -71,6 +95,9 @@ export interface PluginConfig {
|
|
|
71
95
|
* signalk-bluetti-plugin does. Off by default - a `device: ALL_DEVICES` entry scans on demand the
|
|
72
96
|
* first time it has nothing discovered yet (see `resolveTargets` in `repaintScheduler.ts`), and an
|
|
73
97
|
* explicit device selection only ever needed this to populate the dropdown once at initial setup.
|
|
98
|
+
* Also unnecessary with `useBleApi` on, which discovers continuously in the background instead (see
|
|
99
|
+
* `startBleApiDiscoveryListener` in `discoveryCoordinator.ts`) - kept meaningful mainly for direct
|
|
100
|
+
* BlueZ access, which has no continuous-scan equivalent to piggyback on.
|
|
74
101
|
*/
|
|
75
102
|
scanOnStart: boolean;
|
|
76
103
|
/** How long the startup scan runs, in seconds. */
|
|
@@ -137,16 +164,21 @@ export declare const RENDER_FALLBACK_TEMPLATE_NAME = ".error";
|
|
|
137
164
|
export declare function defaultConfig(): PluginConfig;
|
|
138
165
|
export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
|
|
139
166
|
/**
|
|
140
|
-
* Actively rewrites the on-disk file
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* `
|
|
146
|
-
*
|
|
147
|
-
*
|
|
167
|
+
* Actively rewrites the on-disk file when it's in an outdated shape - `readCurrentConfig` alone only
|
|
168
|
+
* fixes things up in memory for callers that go through it, but the admin UI's config form
|
|
169
|
+
* round-trips whatever raw JSON it was handed verbatim. Two shapes are healed:
|
|
170
|
+
*
|
|
171
|
+
* - A nested file (see `readCurrentConfig`'s doc comment): left alone, the UI keeps re-persisting a
|
|
172
|
+
* stray nested `configuration` key it never touches (no schema field maps to it) forever (see
|
|
173
|
+
* `support/signalk-einklabel-plugin.json`).
|
|
174
|
+
* - Advanced device settings saved before they were grouped under `advanced` (see
|
|
175
|
+
* `migrateDeviceConfig`): left alone, the UI would show that group empty, even though the plugin
|
|
176
|
+
* itself still honours the old values.
|
|
177
|
+
*
|
|
178
|
+
* Called once at plugin start, which - unlike `clearForceRepaint` - isn't gated on any device having
|
|
179
|
+
* `forceRepaint` set, so an outdated file gets fixed even if nothing ever triggers that path.
|
|
148
180
|
*/
|
|
149
|
-
export declare function
|
|
181
|
+
export declare function healStoredConfig(app: ServerAPI): void;
|
|
150
182
|
/** See `resolveDir` - `templatesDir`'s own resolution. */
|
|
151
183
|
export declare function resolveTemplatesDir(templatesDir: string | undefined): string;
|
|
152
184
|
export declare function parseDevice(device: string): {
|
package/dist/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RENDER_FALLBACK_TEMPLATE_NAME = exports.BUNDLED_TEMPLATES_DIR = exports.ALL_DEVICES = void 0;
|
|
4
|
+
exports.migrateConfig = migrateConfig;
|
|
4
5
|
exports.defaultConfig = defaultConfig;
|
|
5
6
|
exports.readCurrentConfig = readCurrentConfig;
|
|
6
|
-
exports.
|
|
7
|
+
exports.healStoredConfig = healStoredConfig;
|
|
7
8
|
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
8
9
|
exports.parseDevice = parseDevice;
|
|
9
10
|
exports.resolveTemplatePath = resolveTemplatePath;
|
|
@@ -23,6 +24,49 @@ const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
|
23
24
|
* an on-demand scan itself if nothing's been discovered yet.
|
|
24
25
|
*/
|
|
25
26
|
exports.ALL_DEVICES = "ALL";
|
|
27
|
+
/**
|
|
28
|
+
* Every `AdvancedDeviceSettings` key - these all used to sit directly on `DeviceConfig`, so a config
|
|
29
|
+
* saved before they were grouped still has them there. See `migrateDeviceConfig`.
|
|
30
|
+
*/
|
|
31
|
+
const ADVANCED_DEVICE_KEYS = [
|
|
32
|
+
"compress",
|
|
33
|
+
"mirror",
|
|
34
|
+
"compressionFormat",
|
|
35
|
+
"forceRepaint",
|
|
36
|
+
"aesKey",
|
|
37
|
+
"paintConnectTimeoutSeconds",
|
|
38
|
+
"paintRetries",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Moves any `AdvancedDeviceSettings` key found at the top level of a device entry (the pre-grouping
|
|
42
|
+
* layout) into its `advanced` object, reporting whether anything moved. A value already under
|
|
43
|
+
* `advanced` wins over a legacy top-level one - it can only have got there from the grouped form, so
|
|
44
|
+
* it's the newer of the two.
|
|
45
|
+
*/
|
|
46
|
+
function migrateDeviceConfig(raw) {
|
|
47
|
+
const legacy = {};
|
|
48
|
+
const rest = { ...raw };
|
|
49
|
+
for (const key of ADVANCED_DEVICE_KEYS) {
|
|
50
|
+
if (key in rest) {
|
|
51
|
+
legacy[key] = rest[key];
|
|
52
|
+
delete rest[key];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(legacy).length === 0) {
|
|
56
|
+
return { device: raw, migrated: false };
|
|
57
|
+
}
|
|
58
|
+
return { device: { ...rest, advanced: { ...legacy, ...raw.advanced } }, migrated: true };
|
|
59
|
+
}
|
|
60
|
+
/** Applies `migrateDeviceConfig` to every device entry - see `healStoredConfig` for persisting the result. */
|
|
61
|
+
function migrateConfig(config) {
|
|
62
|
+
if (!Array.isArray(config.devices)) {
|
|
63
|
+
return { config, migrated: false };
|
|
64
|
+
}
|
|
65
|
+
const results = config.devices.map(migrateDeviceConfig);
|
|
66
|
+
return results.some((result) => result.migrated)
|
|
67
|
+
? { config: { ...config, devices: results.map((result) => result.device) }, migrated: true }
|
|
68
|
+
: { config, migrated: false };
|
|
69
|
+
}
|
|
26
70
|
/**
|
|
27
71
|
* The package's own bundled `templates/` directory (ships alongside `dist/`, see
|
|
28
72
|
* package.json's `files`) - templates here are always available, but a same-named template in the
|
|
@@ -108,27 +152,33 @@ function pickKnownKeys(raw) {
|
|
|
108
152
|
}
|
|
109
153
|
function readCurrentConfig(app) {
|
|
110
154
|
const { unwrapped } = unwrapNestedConfiguration(app.readPluginOptions());
|
|
111
|
-
return pickKnownKeys(unwrapped);
|
|
155
|
+
return migrateConfig(pickKnownKeys(unwrapped)).config;
|
|
112
156
|
}
|
|
113
157
|
/**
|
|
114
|
-
* Actively rewrites the on-disk file
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* `
|
|
120
|
-
*
|
|
121
|
-
*
|
|
158
|
+
* Actively rewrites the on-disk file when it's in an outdated shape - `readCurrentConfig` alone only
|
|
159
|
+
* fixes things up in memory for callers that go through it, but the admin UI's config form
|
|
160
|
+
* round-trips whatever raw JSON it was handed verbatim. Two shapes are healed:
|
|
161
|
+
*
|
|
162
|
+
* - A nested file (see `readCurrentConfig`'s doc comment): left alone, the UI keeps re-persisting a
|
|
163
|
+
* stray nested `configuration` key it never touches (no schema field maps to it) forever (see
|
|
164
|
+
* `support/signalk-einklabel-plugin.json`).
|
|
165
|
+
* - Advanced device settings saved before they were grouped under `advanced` (see
|
|
166
|
+
* `migrateDeviceConfig`): left alone, the UI would show that group empty, even though the plugin
|
|
167
|
+
* itself still honours the old values.
|
|
168
|
+
*
|
|
169
|
+
* Called once at plugin start, which - unlike `clearForceRepaint` - isn't gated on any device having
|
|
170
|
+
* `forceRepaint` set, so an outdated file gets fixed even if nothing ever triggers that path.
|
|
122
171
|
*/
|
|
123
|
-
function
|
|
172
|
+
function healStoredConfig(app) {
|
|
124
173
|
const { unwrapped, wasNested } = unwrapNestedConfiguration(app.readPluginOptions());
|
|
125
|
-
|
|
174
|
+
const { config, migrated } = migrateConfig(pickKnownKeys(unwrapped));
|
|
175
|
+
if (!wasNested && !migrated)
|
|
126
176
|
return;
|
|
127
|
-
app.savePluginOptions(
|
|
177
|
+
app.savePluginOptions(config, (err) => {
|
|
128
178
|
if (err)
|
|
129
|
-
app.debug(`failed to
|
|
179
|
+
app.debug(`failed to update the stored plugin config: ${err.message}`);
|
|
130
180
|
else
|
|
131
|
-
app.debug(
|
|
181
|
+
app.debug(`updated the stored plugin config (${[wasNested && "flattened nesting", migrated && "grouped advanced device settings"].filter(Boolean).join(", ")})`);
|
|
132
182
|
});
|
|
133
183
|
}
|
|
134
184
|
/**
|
|
@@ -298,9 +348,34 @@ function resolveTemplatePath(templatesDir, templateName, target) {
|
|
|
298
348
|
const localPath = (0, path_1.join)(templatesDir, templateName);
|
|
299
349
|
return (0, fs_1.existsSync)(localPath) ? localPath : (0, path_1.join)(exports.BUNDLED_TEMPLATES_DIR, templateName);
|
|
300
350
|
}
|
|
301
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* Restricts a string field to `values`, shown as `names` where given - as `oneOf` with `const`/`title`,
|
|
353
|
+
* the form RJSF 5 (the admin UI's form library) supports going forward, rather than the deprecated
|
|
354
|
+
* `enumNames`. JSON Schema forbids an empty `enum`/`oneOf` array, so neither is attached without at
|
|
355
|
+
* least one option - otherwise the whole config schema fails validation.
|
|
356
|
+
*/
|
|
302
357
|
function withEnum(schema, values, names) {
|
|
303
|
-
|
|
358
|
+
if (values.length === 0)
|
|
359
|
+
return schema;
|
|
360
|
+
return names ? { ...schema, oneOf: values.map((value, i) => ({ const: value, title: names[i] ?? value })) } : { ...schema, enum: values };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* An enum-like string field whose options show explanatory labels rather than their raw stored values
|
|
364
|
+
* - `oneOf` with `const`/`title`, which RJSF 5 (the admin UI's form library) renders as each option's
|
|
365
|
+
* label while still saving the bare value, so existing configs are unaffected.
|
|
366
|
+
*
|
|
367
|
+
* Each label is prefixed with an en space: the admin UI renders RJSF's default-theme radio markup under
|
|
368
|
+
* Bootstrap 5, which has no styling for it, so the label otherwise butts straight up against its
|
|
369
|
+
* button - and a plugin can't ship its own CSS. An en space, unlike a plain one, isn't collapsed away
|
|
370
|
+
* by HTML whitespace handling.
|
|
371
|
+
*/
|
|
372
|
+
function choiceField(title, options, extra = {}) {
|
|
373
|
+
return {
|
|
374
|
+
type: "string",
|
|
375
|
+
title,
|
|
376
|
+
...extra,
|
|
377
|
+
oneOf: options.map(([value, label]) => ({ const: value, title: `\u2002${label}` })),
|
|
378
|
+
};
|
|
304
379
|
}
|
|
305
380
|
function configSchema(app, discovered = []) {
|
|
306
381
|
const defaults = defaultConfig();
|
|
@@ -309,6 +384,18 @@ function configSchema(app, discovered = []) {
|
|
|
309
384
|
return {
|
|
310
385
|
type: "object",
|
|
311
386
|
properties: {
|
|
387
|
+
...(app.bleApi
|
|
388
|
+
? {
|
|
389
|
+
useBleApi: {
|
|
390
|
+
type: "boolean",
|
|
391
|
+
title: "Use the SignalK BLE Manager API",
|
|
392
|
+
description: "Route Bluetooth access through SignalK server's BLE Manager API (server >= 2.32.0) instead of connecting to " +
|
|
393
|
+
"BlueZ directly, so this plugin shares the adapter with other BLE plugins instead of contending for it. Requires " +
|
|
394
|
+
"the server to have a local Bluetooth adapter or BLE gateway available (Server → Settings → Bluetooth).",
|
|
395
|
+
default: defaults.useBleApi,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
312
399
|
templatesDir: {
|
|
313
400
|
type: "string",
|
|
314
401
|
title: "Templates directory",
|
|
@@ -321,7 +408,8 @@ function configSchema(app, discovered = []) {
|
|
|
321
408
|
type: "boolean",
|
|
322
409
|
title: "Scan for devices on plugin start",
|
|
323
410
|
description: 'Runs a short BLE scan so discovered devices show up in a device\'s "Device" picker below. ' +
|
|
324
|
-
'Not needed if every device uses "All discovered devices"
|
|
411
|
+
'Not needed if every device uses "All discovered devices" (that scans on demand instead), or if ' +
|
|
412
|
+
'"Use BLE Manager" below is on (that discovers continuously in the background instead of needing a scan).',
|
|
325
413
|
default: defaults.scanOnStart,
|
|
326
414
|
},
|
|
327
415
|
scanDurationSeconds: {
|
|
@@ -331,29 +419,19 @@ function configSchema(app, discovered = []) {
|
|
|
331
419
|
minimum: 1,
|
|
332
420
|
default: defaults.scanDurationSeconds,
|
|
333
421
|
},
|
|
334
|
-
...(app.bleApi
|
|
335
|
-
? {
|
|
336
|
-
useBleApi: {
|
|
337
|
-
type: "boolean",
|
|
338
|
-
title: "Use the SignalK BLE Manager API",
|
|
339
|
-
description: "Route Bluetooth access through SignalK server's BLE Manager API (server >= 2.32.0) instead of connecting to " +
|
|
340
|
-
"BlueZ directly, so this plugin shares the adapter with other BLE plugins instead of contending for it. Requires " +
|
|
341
|
-
"the server to have a local Bluetooth adapter or BLE gateway available (Server → Settings → Bluetooth).",
|
|
342
|
-
default: defaults.useBleApi,
|
|
343
|
-
},
|
|
344
|
-
}
|
|
345
|
-
: {}),
|
|
346
422
|
paintConnectTimeoutSeconds: {
|
|
347
423
|
type: "number",
|
|
348
424
|
title: "Paint connect timeout (seconds)",
|
|
349
|
-
description: "How long to wait for a device to accept a BLE connection before giving up on a repaint attempt."
|
|
425
|
+
description: "How long to wait for a device to accept a BLE connection before giving up on a repaint attempt. " +
|
|
426
|
+
"The default for every device - each device can override it in its own settings below.",
|
|
350
427
|
minimum: 1,
|
|
351
428
|
default: defaults.paintConnectTimeoutSeconds,
|
|
352
429
|
},
|
|
353
430
|
paintRetries: {
|
|
354
431
|
type: "number",
|
|
355
432
|
title: "Paint retries",
|
|
356
|
-
description: "How many times to attempt a repaint (including the first try) before giving up and reporting failure."
|
|
433
|
+
description: "How many times to attempt a repaint (including the first try) before giving up and reporting failure. " +
|
|
434
|
+
"The default for every device - each device can override it in its own settings below.",
|
|
357
435
|
minimum: 1,
|
|
358
436
|
default: defaults.paintRetries,
|
|
359
437
|
},
|
|
@@ -394,11 +472,10 @@ function configSchema(app, discovered = []) {
|
|
|
394
472
|
'in poor light" - available to any template as source=einklabel,path=description or source=label,path=description.',
|
|
395
473
|
},
|
|
396
474
|
templateName: withEnum({ type: "string", title: "Template" }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
397
|
-
repaintTrigger:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
},
|
|
475
|
+
repaintTrigger: choiceField("Repaint trigger", [
|
|
476
|
+
["subscription", "When a SignalK path changes"],
|
|
477
|
+
["interval", "On a timed schedule"],
|
|
478
|
+
]),
|
|
402
479
|
triggerPath: {
|
|
403
480
|
type: "string",
|
|
404
481
|
title: "Trigger SignalK path (if repaint trigger is subscription)",
|
|
@@ -415,22 +492,58 @@ function configSchema(app, discovered = []) {
|
|
|
415
492
|
maximum: 59,
|
|
416
493
|
default: 0,
|
|
417
494
|
},
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
495
|
+
reframe: choiceField("If the render doesn't match the panel size", [
|
|
496
|
+
["crop", "Crop - place at the top-left, cutting off anything too big or leaving the rest blank"],
|
|
497
|
+
["scale", "Scale - stretch to fit exactly (may distort)"],
|
|
498
|
+
["fixed", "Fixed - fail the repaint rather than show an off-size image"],
|
|
499
|
+
], { default: "crop" }),
|
|
500
|
+
advanced: {
|
|
501
|
+
type: "object",
|
|
502
|
+
title: "Advanced settings",
|
|
503
|
+
description: "Most labels never need these.",
|
|
504
|
+
properties: {
|
|
505
|
+
compress: {
|
|
506
|
+
type: "boolean",
|
|
507
|
+
title: 'Compress upload (Zhsunyco, Gicisky 7.5"/10.2")',
|
|
508
|
+
description: "Sends far less data over BLE, so repaints are quicker. Turn off if a label stops updating.",
|
|
509
|
+
default: true,
|
|
510
|
+
},
|
|
511
|
+
mirror: choiceField("Mirror", [
|
|
512
|
+
["none", "No flip"],
|
|
513
|
+
["horizontal", "Flip left to right"],
|
|
514
|
+
["vertical", "Flip top to bottom"],
|
|
515
|
+
["both", "Rotate 180° - for a label mounted upside down"],
|
|
516
|
+
], { description: "Only needed if the image shows up mirrored or upside down on the label.", default: "none" }),
|
|
517
|
+
compressionFormat: choiceField("Wire format (Gicisky, experimental)", [
|
|
518
|
+
["auto", "Auto - the model's usual format"],
|
|
519
|
+
[
|
|
520
|
+
"chunked",
|
|
521
|
+
'Chunked - send compressed like the 7.5"/10.2" panels, e.g. to speed up a 4.2" BWR (untested on current firmware)',
|
|
522
|
+
],
|
|
523
|
+
], { description: "Chunked needs Compress upload on. Switch back to Auto if the label stops updating.", default: "auto" }),
|
|
524
|
+
forceRepaint: {
|
|
525
|
+
type: "boolean",
|
|
526
|
+
title: "Force repaint",
|
|
527
|
+
description: "Repaint even if the data is unchanged - clears itself automatically once that repaint completes",
|
|
528
|
+
default: false,
|
|
529
|
+
},
|
|
530
|
+
aesKey: {
|
|
531
|
+
type: "string",
|
|
532
|
+
title: "BLE AES key (vendor-specific; leave blank to use a default key)",
|
|
533
|
+
},
|
|
534
|
+
paintConnectTimeoutSeconds: {
|
|
535
|
+
type: "number",
|
|
536
|
+
title: "Paint connect timeout for this device (seconds)",
|
|
537
|
+
description: `Leave blank to use the plugin-wide setting (currently ${current.paintConnectTimeoutSeconds}s).`,
|
|
538
|
+
minimum: 1,
|
|
539
|
+
},
|
|
540
|
+
paintRetries: {
|
|
541
|
+
type: "number",
|
|
542
|
+
title: "Paint retries for this device",
|
|
543
|
+
description: `Leave blank to use the plugin-wide setting (currently ${current.paintRetries}).`,
|
|
544
|
+
minimum: 1,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
434
547
|
},
|
|
435
548
|
},
|
|
436
549
|
},
|
|
@@ -445,6 +558,10 @@ function configUiSchema() {
|
|
|
445
558
|
description: { "ui:widget": "textarea" },
|
|
446
559
|
repaintTrigger: { "ui:widget": "radio" },
|
|
447
560
|
reframe: { "ui:widget": "radio" },
|
|
561
|
+
advanced: {
|
|
562
|
+
mirror: { "ui:widget": "radio" },
|
|
563
|
+
compressionFormat: { "ui:widget": "radio" },
|
|
564
|
+
},
|
|
448
565
|
},
|
|
449
566
|
},
|
|
450
567
|
};
|