@rhizomatics/signalk-einklabel-plugin 1.3.0-beta8 → 1.3.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 +36 -1
- package/README.md +21 -511
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +26 -1
- package/dist/config.d.ts +62 -15
- package/dist/config.js +252 -60
- package/dist/devices/bleBackend.js +44 -7
- package/dist/devices/bleDiscovery.d.ts +5 -0
- package/dist/devices/bleDiscovery.js +18 -0
- 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 +9 -3
- package/dist/devices/gicisky/layout.d.ts +13 -1
- package/dist/devices/gicisky/layout.js +21 -0
- package/dist/devices/types.d.ts +26 -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 +44 -9
- package/dist/devices/zhsunyco/protocol.d.ts +2 -0
- package/dist/devices/zhsunyco/protocol.js +2 -0
- package/dist/docs/templateReference.d.ts +18 -0
- package/dist/docs/templateReference.js +207 -0
- package/dist/email/emailSender.d.ts +27 -0
- package/dist/email/emailSender.js +66 -0
- package/dist/plugin.js +2 -2
- package/dist/render/fieldsTable.d.ts +15 -0
- package/dist/render/fieldsTable.js +52 -0
- package/dist/render/llmPrompt.d.ts +86 -0
- package/dist/render/llmPrompt.js +199 -0
- package/dist/render/mirror.d.ts +11 -0
- package/dist/render/mirror.js +24 -0
- package/dist/repaintScheduler.d.ts +8 -0
- package/dist/repaintScheduler.js +86 -29
- package/dist/resolveApiUrl.js +3 -0
- package/docs/bluetooth.md +162 -0
- package/docs/cli.md +131 -0
- package/docs/examples/README.md +25 -0
- package/docs/examples/tide-clock.md +93 -0
- package/docs/examples/watch-schedule.md +37 -0
- package/docs/extending.md +33 -0
- package/docs/faq.md +44 -0
- package/docs/getting-started.md +111 -0
- package/docs/templates.md +142 -0
- package/package.json +16 -7
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,10 @@ 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),
|
|
294
|
+
log: log_1.logDebug,
|
|
272
295
|
});
|
|
273
296
|
});
|
|
274
297
|
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
|
|
@@ -287,7 +310,9 @@ exports.program
|
|
|
287
310
|
.option("-f, --font <path>", "override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio",
|
|
288
311
|
// See the -r/--require option above for why `| undefined` is needed here despite commander's typings.
|
|
289
312
|
(value, previous = []) => [...previous, value])
|
|
313
|
+
.option("--mirror <mode>", "flip the PNG the same way paint --mirror would: none (default), horizontal, vertical, or both", "none")
|
|
290
314
|
.action(async (opts) => {
|
|
315
|
+
const mirror = parseMirrorMode(opts.mirror);
|
|
291
316
|
const svgSource = await (0, promises_1.readFile)(opts.template, "utf-8");
|
|
292
317
|
const declared = (0, binding_1.readTemplateDimensions)(svgSource);
|
|
293
318
|
const width = resolveDimension(opts.width, [declared.width], DEFAULT_RENDER_WIDTH);
|
|
@@ -295,7 +320,7 @@ exports.program
|
|
|
295
320
|
const bindings = (0, binding_1.findBindings)(svgSource);
|
|
296
321
|
const context = await assembleContext(opts, bindings);
|
|
297
322
|
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);
|
|
323
|
+
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
324
|
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
300
325
|
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
301
326
|
});
|
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
|
|
@@ -21,13 +22,11 @@ export interface DeviceConfig {
|
|
|
21
22
|
/**
|
|
22
23
|
* Free-text notes about where this label is physically mounted/viewed from, e.g. "chart table,
|
|
23
24
|
* viewed from ~1m in poor light" - purely descriptive. Available to any template via
|
|
24
|
-
* `source=
|
|
25
|
+
* `source=label,path=description` (see `buildLabelContext` in
|
|
25
26
|
* `./render/binding.ts`) if it wants it - e.g. useful to a `TemplateProvider` extension (see
|
|
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,10 @@ export interface DeviceConfig {
|
|
|
48
47
|
intervalHours?: number;
|
|
49
48
|
/** ...at this minute past the hour. */
|
|
50
49
|
intervalMinute?: number;
|
|
51
|
-
/**
|
|
52
|
-
|
|
50
|
+
/** Settings most labels never need, grouped so the admin UI shows them in their own "Advanced settings" box. */
|
|
51
|
+
advanced?: AdvancedDeviceSettings;
|
|
52
|
+
}
|
|
53
|
+
export interface AdvancedDeviceSettings {
|
|
53
54
|
/**
|
|
54
55
|
* How to fit the rendered image onto the device's actual panel size when it doesn't match (see
|
|
55
56
|
* `ReframeMode`) - e.g. a template family with no variant sized for this particular label. Left
|
|
@@ -58,7 +59,32 @@ export interface DeviceConfig {
|
|
|
58
59
|
* showing *something*, even off-size, beats a repaint that just fails outright.
|
|
59
60
|
*/
|
|
60
61
|
reframe?: ReframeMode;
|
|
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
|
+
/** Send the image without waiting for each write to be acknowledged (zhsunyco) - see `VendorDeviceConfig.writeWithoutResponse`. Unset means off. */
|
|
73
|
+
writeWithoutResponse?: boolean;
|
|
74
|
+
/** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
|
|
75
|
+
forceRepaint?: boolean;
|
|
76
|
+
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
77
|
+
aesKey?: string;
|
|
78
|
+
/** Per-device override of `PluginConfig.paintConnectTimeoutSeconds` - unset uses the plugin-wide value. */
|
|
79
|
+
paintConnectTimeoutSeconds?: number;
|
|
80
|
+
/** Per-device override of `PluginConfig.paintRetries` - unset uses the plugin-wide value. */
|
|
81
|
+
paintRetries?: number;
|
|
61
82
|
}
|
|
83
|
+
/** Applies `migrateDeviceConfig` to every device entry - see `healStoredConfig` for persisting the result. */
|
|
84
|
+
export declare function migrateConfig<T extends Partial<PluginConfig>>(config: T): {
|
|
85
|
+
config: T;
|
|
86
|
+
migrated: boolean;
|
|
87
|
+
};
|
|
62
88
|
export interface PluginConfig {
|
|
63
89
|
/**
|
|
64
90
|
* Directory the plugin scans for template files, instead of an upload UI - follows
|
|
@@ -140,16 +166,21 @@ export declare const RENDER_FALLBACK_TEMPLATE_NAME = ".error";
|
|
|
140
166
|
export declare function defaultConfig(): PluginConfig;
|
|
141
167
|
export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
|
|
142
168
|
/**
|
|
143
|
-
* Actively rewrites the on-disk file
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
169
|
+
* Actively rewrites the on-disk file when it's in an outdated shape - `readCurrentConfig` alone only
|
|
170
|
+
* fixes things up in memory for callers that go through it, but the admin UI's config form
|
|
171
|
+
* round-trips whatever raw JSON it was handed verbatim. Two shapes are healed:
|
|
172
|
+
*
|
|
173
|
+
* - A nested file (see `readCurrentConfig`'s doc comment): left alone, the UI keeps re-persisting a
|
|
174
|
+
* stray nested `configuration` key it never touches (no schema field maps to it) forever (see
|
|
175
|
+
* `support/signalk-einklabel-plugin.json`).
|
|
176
|
+
* - Advanced device settings saved before they were grouped under `advanced` (see
|
|
177
|
+
* `migrateDeviceConfig`): left alone, the UI would show that group empty, even though the plugin
|
|
178
|
+
* itself still honours the old values.
|
|
179
|
+
*
|
|
180
|
+
* Called once at plugin start, which - unlike `clearForceRepaint` - isn't gated on any device having
|
|
181
|
+
* `forceRepaint` set, so an outdated file gets fixed even if nothing ever triggers that path.
|
|
151
182
|
*/
|
|
152
|
-
export declare function
|
|
183
|
+
export declare function healStoredConfig(app: ServerAPI): void;
|
|
153
184
|
/** See `resolveDir` - `templatesDir`'s own resolution. */
|
|
154
185
|
export declare function resolveTemplatesDir(templatesDir: string | undefined): string;
|
|
155
186
|
export declare function parseDevice(device: string): {
|
|
@@ -158,6 +189,22 @@ export declare function parseDevice(device: string): {
|
|
|
158
189
|
hwVersion?: string;
|
|
159
190
|
address: string;
|
|
160
191
|
} | undefined;
|
|
192
|
+
export declare function listSvgFiles(dir: string): string[];
|
|
193
|
+
export interface TemplateVariant {
|
|
194
|
+
fileName: string;
|
|
195
|
+
width: number;
|
|
196
|
+
height: number;
|
|
197
|
+
colours: Colour[];
|
|
198
|
+
}
|
|
199
|
+
export declare function listTemplateVariants(dir: string): TemplateVariant[];
|
|
200
|
+
/**
|
|
201
|
+
* A directory only counts as a template-family option if it actually has at least one parseable
|
|
202
|
+
* variant file in it - otherwise it's something else entirely, e.g. `.assets`. Dot-prefixed
|
|
203
|
+
* directories (e.g. `.assets`, `.blank`) are always excluded, even if they happen to contain
|
|
204
|
+
* parseable variant files, since they're reserved for non-template-option use (asset bundles,
|
|
205
|
+
* work-in-progress templates not ready to appear in the dropdown, etc).
|
|
206
|
+
*/
|
|
207
|
+
export declare function listTemplateFamilies(dir: string): string[];
|
|
161
208
|
/**
|
|
162
209
|
* Resolves a template name to an actual file path - a local template overrides the bundled one of the
|
|
163
210
|
* same name. `templateName` can also name a template-family *directory* (see `pickBestVariant`), in
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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;
|
|
10
|
+
exports.listSvgFiles = listSvgFiles;
|
|
11
|
+
exports.listTemplateVariants = listTemplateVariants;
|
|
12
|
+
exports.listTemplateFamilies = listTemplateFamilies;
|
|
9
13
|
exports.resolveTemplatePath = resolveTemplatePath;
|
|
10
14
|
exports.configSchema = configSchema;
|
|
11
15
|
exports.configUiSchema = configUiSchema;
|
|
@@ -23,6 +27,50 @@ const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
|
23
27
|
* an on-demand scan itself if nothing's been discovered yet.
|
|
24
28
|
*/
|
|
25
29
|
exports.ALL_DEVICES = "ALL";
|
|
30
|
+
/**
|
|
31
|
+
* Every `AdvancedDeviceSettings` key - these all used to sit directly on `DeviceConfig`, so a config
|
|
32
|
+
* saved before they were grouped still has them there. See `migrateDeviceConfig`.
|
|
33
|
+
*/
|
|
34
|
+
const ADVANCED_DEVICE_KEYS = [
|
|
35
|
+
"reframe",
|
|
36
|
+
"compress",
|
|
37
|
+
"mirror",
|
|
38
|
+
"compressionFormat",
|
|
39
|
+
"forceRepaint",
|
|
40
|
+
"aesKey",
|
|
41
|
+
"paintConnectTimeoutSeconds",
|
|
42
|
+
"paintRetries",
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Moves any `AdvancedDeviceSettings` key found at the top level of a device entry (the pre-grouping
|
|
46
|
+
* layout) into its `advanced` object, reporting whether anything moved. A value already under
|
|
47
|
+
* `advanced` wins over a legacy top-level one - it can only have got there from the grouped form, so
|
|
48
|
+
* it's the newer of the two.
|
|
49
|
+
*/
|
|
50
|
+
function migrateDeviceConfig(raw) {
|
|
51
|
+
const legacy = {};
|
|
52
|
+
const rest = { ...raw };
|
|
53
|
+
for (const key of ADVANCED_DEVICE_KEYS) {
|
|
54
|
+
if (key in rest) {
|
|
55
|
+
legacy[key] = rest[key];
|
|
56
|
+
delete rest[key];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(legacy).length === 0) {
|
|
60
|
+
return { device: raw, migrated: false };
|
|
61
|
+
}
|
|
62
|
+
return { device: { ...rest, advanced: { ...legacy, ...raw.advanced } }, migrated: true };
|
|
63
|
+
}
|
|
64
|
+
/** Applies `migrateDeviceConfig` to every device entry - see `healStoredConfig` for persisting the result. */
|
|
65
|
+
function migrateConfig(config) {
|
|
66
|
+
if (!Array.isArray(config.devices)) {
|
|
67
|
+
return { config, migrated: false };
|
|
68
|
+
}
|
|
69
|
+
const results = config.devices.map(migrateDeviceConfig);
|
|
70
|
+
return results.some((result) => result.migrated)
|
|
71
|
+
? { config: { ...config, devices: results.map((result) => result.device) }, migrated: true }
|
|
72
|
+
: { config, migrated: false };
|
|
73
|
+
}
|
|
26
74
|
/**
|
|
27
75
|
* The package's own bundled `templates/` directory (ships alongside `dist/`, see
|
|
28
76
|
* package.json's `files`) - templates here are always available, but a same-named template in the
|
|
@@ -108,27 +156,33 @@ function pickKnownKeys(raw) {
|
|
|
108
156
|
}
|
|
109
157
|
function readCurrentConfig(app) {
|
|
110
158
|
const { unwrapped } = unwrapNestedConfiguration(app.readPluginOptions());
|
|
111
|
-
return pickKnownKeys(unwrapped);
|
|
159
|
+
return migrateConfig(pickKnownKeys(unwrapped)).config;
|
|
112
160
|
}
|
|
113
161
|
/**
|
|
114
|
-
* Actively rewrites the on-disk file
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* `
|
|
120
|
-
*
|
|
121
|
-
*
|
|
162
|
+
* Actively rewrites the on-disk file when it's in an outdated shape - `readCurrentConfig` alone only
|
|
163
|
+
* fixes things up in memory for callers that go through it, but the admin UI's config form
|
|
164
|
+
* round-trips whatever raw JSON it was handed verbatim. Two shapes are healed:
|
|
165
|
+
*
|
|
166
|
+
* - A nested file (see `readCurrentConfig`'s doc comment): left alone, the UI keeps re-persisting a
|
|
167
|
+
* stray nested `configuration` key it never touches (no schema field maps to it) forever (see
|
|
168
|
+
* `support/signalk-einklabel-plugin.json`).
|
|
169
|
+
* - Advanced device settings saved before they were grouped under `advanced` (see
|
|
170
|
+
* `migrateDeviceConfig`): left alone, the UI would show that group empty, even though the plugin
|
|
171
|
+
* itself still honours the old values.
|
|
172
|
+
*
|
|
173
|
+
* Called once at plugin start, which - unlike `clearForceRepaint` - isn't gated on any device having
|
|
174
|
+
* `forceRepaint` set, so an outdated file gets fixed even if nothing ever triggers that path.
|
|
122
175
|
*/
|
|
123
|
-
function
|
|
176
|
+
function healStoredConfig(app) {
|
|
124
177
|
const { unwrapped, wasNested } = unwrapNestedConfiguration(app.readPluginOptions());
|
|
125
|
-
|
|
178
|
+
const { config, migrated } = migrateConfig(pickKnownKeys(unwrapped));
|
|
179
|
+
if (!wasNested && !migrated)
|
|
126
180
|
return;
|
|
127
|
-
app.savePluginOptions(
|
|
181
|
+
app.savePluginOptions(config, (err) => {
|
|
128
182
|
if (err)
|
|
129
|
-
app.debug(`failed to
|
|
183
|
+
app.debug(`failed to update the stored plugin config: ${err.message}`);
|
|
130
184
|
else
|
|
131
|
-
app.debug(
|
|
185
|
+
app.debug(`updated the stored plugin config (${[wasNested && "flattened nesting", migrated && "grouped advanced device settings"].filter(Boolean).join(", ")})`);
|
|
132
186
|
});
|
|
133
187
|
}
|
|
134
188
|
/**
|
|
@@ -298,9 +352,40 @@ function resolveTemplatePath(templatesDir, templateName, target) {
|
|
|
298
352
|
const localPath = (0, path_1.join)(templatesDir, templateName);
|
|
299
353
|
return (0, fs_1.existsSync)(localPath) ? localPath : (0, path_1.join)(exports.BUNDLED_TEMPLATES_DIR, templateName);
|
|
300
354
|
}
|
|
301
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* Restricts a string field to `values`, shown as `names` where given - as `oneOf` with `const`/`title`,
|
|
357
|
+
* the form RJSF 5 (the admin UI's form library) supports going forward, rather than the deprecated
|
|
358
|
+
* `enumNames`. JSON Schema forbids an empty `enum`/`oneOf` array, so neither is attached without at
|
|
359
|
+
* least one option - otherwise the whole config schema fails validation.
|
|
360
|
+
*/
|
|
302
361
|
function withEnum(schema, values, names) {
|
|
303
|
-
|
|
362
|
+
if (values.length === 0)
|
|
363
|
+
return schema;
|
|
364
|
+
return names ? { ...schema, oneOf: values.map((value, i) => ({ const: value, title: names[i] ?? value })) } : { ...schema, enum: values };
|
|
365
|
+
}
|
|
366
|
+
/** The plugin's documentation site - its sections' anchors are the README's own headings (see `site/scripts/sync-readme.mjs`). */
|
|
367
|
+
const DOCS_URL = "https://signalk-einklabel.rhizomatics.org.uk/";
|
|
368
|
+
/** A Markdown link to a docs page (and optional section, e.g. `templates/#reframing`), for a field description rendered with `ui:enableMarkdownInDescription` (see `configUiSchema`). */
|
|
369
|
+
function docsLink(page, text = "More in the docs") {
|
|
370
|
+
return `[${text}](${DOCS_URL}${page})`;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* An enum-like string field whose options show explanatory labels rather than their raw stored values
|
|
374
|
+
* - `oneOf` with `const`/`title`, which RJSF 5 (the admin UI's form library) renders as each option's
|
|
375
|
+
* label while still saving the bare value, so existing configs are unaffected.
|
|
376
|
+
*
|
|
377
|
+
* Each label is prefixed with an en space: the admin UI renders RJSF's default-theme radio markup under
|
|
378
|
+
* Bootstrap 5, which has no styling for it, so the label otherwise butts straight up against its
|
|
379
|
+
* button - and a plugin can't ship its own CSS. An en space, unlike a plain one, isn't collapsed away
|
|
380
|
+
* by HTML whitespace handling.
|
|
381
|
+
*/
|
|
382
|
+
function choiceField(title, options, extra = {}) {
|
|
383
|
+
return {
|
|
384
|
+
type: "string",
|
|
385
|
+
title,
|
|
386
|
+
...extra,
|
|
387
|
+
oneOf: options.map(([value, label]) => ({ const: value, title: `\u2002${label}` })),
|
|
388
|
+
};
|
|
304
389
|
}
|
|
305
390
|
function configSchema(app, discovered = []) {
|
|
306
391
|
const defaults = defaultConfig();
|
|
@@ -347,14 +432,16 @@ function configSchema(app, discovered = []) {
|
|
|
347
432
|
paintConnectTimeoutSeconds: {
|
|
348
433
|
type: "number",
|
|
349
434
|
title: "Paint connect timeout (seconds)",
|
|
350
|
-
description: "How long to wait for a device to accept a BLE connection before giving up on a repaint attempt."
|
|
435
|
+
description: "How long to wait for a device to accept a BLE connection before giving up on a repaint attempt. " +
|
|
436
|
+
"The default for every device - each device can override it in its own settings below.",
|
|
351
437
|
minimum: 1,
|
|
352
438
|
default: defaults.paintConnectTimeoutSeconds,
|
|
353
439
|
},
|
|
354
440
|
paintRetries: {
|
|
355
441
|
type: "number",
|
|
356
442
|
title: "Paint retries",
|
|
357
|
-
description: "How many times to attempt a repaint (including the first try) before giving up and reporting failure."
|
|
443
|
+
description: "How many times to attempt a repaint (including the first try) before giving up and reporting failure. " +
|
|
444
|
+
"The default for every device - each device can override it in its own settings below.",
|
|
358
445
|
minimum: 1,
|
|
359
446
|
default: defaults.paintRetries,
|
|
360
447
|
},
|
|
@@ -370,8 +457,12 @@ function configSchema(app, discovered = []) {
|
|
|
370
457
|
signalkApiUrl: {
|
|
371
458
|
type: "string",
|
|
372
459
|
title: "SignalK API base URL (leave blank to auto-detect)",
|
|
373
|
-
description: "Used for plugin access to SignalK REST APIs not yet integrated for direct plugin access. Left blank, the plugin probes the likely options at startup (3000, 80, 443
|
|
374
|
-
|
|
460
|
+
description: "Used for plugin access to SignalK REST APIs not yet integrated for direct plugin access. Left blank, the plugin probes the likely options at startup (3000, 80, 443) - " +
|
|
461
|
+
"only set this to skip probing, or for a server on another port or host, e.g. http://localhost:3001. Anonymous read access is required.",
|
|
462
|
+
// Free text with the probed options as suggestions - `examples` renders as the input's
|
|
463
|
+
// autocomplete list in the admin UI's form library (RJSF 5), where `enum` would forbid anything else.
|
|
464
|
+
examples: resolveApiUrl_1.SIGNALK_API_URL_OPTIONS,
|
|
465
|
+
pattern: "^(\\s*|\\s*https?://\\S+\\s*)$",
|
|
375
466
|
},
|
|
376
467
|
devices: {
|
|
377
468
|
type: "array",
|
|
@@ -392,46 +483,120 @@ function configSchema(app, discovered = []) {
|
|
|
392
483
|
type: "string",
|
|
393
484
|
title: "Location/description (optional)",
|
|
394
485
|
description: 'Free-text notes about where this label is physically mounted/viewed from, e.g. "chart table, viewed from ~1m ' +
|
|
395
|
-
'in poor light" - available to any template as source=
|
|
396
|
-
|
|
397
|
-
templateName: withEnum({ type: "string", title: "Template" }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
398
|
-
repaintTrigger: {
|
|
399
|
-
type: "string",
|
|
400
|
-
title: "Repaint trigger",
|
|
401
|
-
enum: ["subscription", "interval"],
|
|
486
|
+
'in poor light" - available to any template as `source=label,path=description`. ' +
|
|
487
|
+
docsLink("templates/#label-details"),
|
|
402
488
|
},
|
|
403
|
-
|
|
489
|
+
templateName: withEnum({
|
|
404
490
|
type: "string",
|
|
405
|
-
title: "
|
|
491
|
+
title: "Template",
|
|
492
|
+
description: `A bundled template, or one from your templates directory. ${docsLink("examples/")}`,
|
|
493
|
+
}, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
494
|
+
repaintTrigger: choiceField("Repaint trigger", [
|
|
495
|
+
["subscription", "When a SignalK path changes"],
|
|
496
|
+
["interval", "On a timed schedule"],
|
|
497
|
+
]),
|
|
498
|
+
advanced: {
|
|
499
|
+
type: "object",
|
|
500
|
+
title: "Advanced settings",
|
|
501
|
+
description: "Most labels never need these.",
|
|
502
|
+
properties: {
|
|
503
|
+
reframe: choiceField("If the render doesn't match the panel size", [
|
|
504
|
+
["crop", "Crop - place at the top-left, cutting off anything too big or leaving the rest blank"],
|
|
505
|
+
["scale", "Scale - stretch to fit exactly (may distort)"],
|
|
506
|
+
["fixed", "Fixed - fail the repaint rather than show an off-size image"],
|
|
507
|
+
], { description: docsLink("templates/#reframing"), default: "crop" }),
|
|
508
|
+
compress: {
|
|
509
|
+
type: "boolean",
|
|
510
|
+
title: 'Compress upload (Zhsunyco, Gicisky 7.5"/10.2")',
|
|
511
|
+
description: `Sends far less data over BLE, so repaints are quicker. Turn off if a label stops updating. ${docsLink("templates/#other-image-options")}`,
|
|
512
|
+
default: true,
|
|
513
|
+
},
|
|
514
|
+
mirror: choiceField("Mirror", [
|
|
515
|
+
["none", "No flip"],
|
|
516
|
+
["horizontal", "Flip left to right"],
|
|
517
|
+
["vertical", "Flip top to bottom"],
|
|
518
|
+
["both", "Rotate 180° - for a label mounted upside down"],
|
|
519
|
+
], {
|
|
520
|
+
description: `Only needed if the image shows up mirrored or upside down on the label. ${docsLink("templates/#other-image-options")}`,
|
|
521
|
+
default: "none",
|
|
522
|
+
}),
|
|
523
|
+
compressionFormat: choiceField("Wire format (Gicisky, experimental)", [
|
|
524
|
+
["auto", "Auto - the model's usual format"],
|
|
525
|
+
[
|
|
526
|
+
"chunked",
|
|
527
|
+
'Chunked - send compressed like the 7.5"/10.2" panels, e.g. to speed up a 4.2" BWR (untested on current firmware)',
|
|
528
|
+
],
|
|
529
|
+
], {
|
|
530
|
+
description: `Chunked needs Compress upload on. Switch back to Auto if the label stops updating. ${docsLink("templates/#other-image-options")}`,
|
|
531
|
+
default: "auto",
|
|
532
|
+
}),
|
|
533
|
+
writeWithoutResponse: {
|
|
534
|
+
type: "boolean",
|
|
535
|
+
title: "Send image without waiting for each write (Zhsunyco)",
|
|
536
|
+
description: "Turn on if a Zhsunyco label fails with ATT error 0x0e when using the SignalK BLE Manager - its writes that " +
|
|
537
|
+
`wait for acknowledgement are rejected by these labels (SignalK 2.33 and earlier). ${docsLink("bluetooth/#zhsunyco-labels-and-the-ble-manager")}`,
|
|
538
|
+
default: false,
|
|
539
|
+
},
|
|
540
|
+
forceRepaint: {
|
|
541
|
+
type: "boolean",
|
|
542
|
+
title: "Force repaint",
|
|
543
|
+
description: "Repaint even if the data is unchanged - clears itself automatically once that repaint completes",
|
|
544
|
+
default: false,
|
|
545
|
+
},
|
|
546
|
+
aesKey: {
|
|
547
|
+
type: "string",
|
|
548
|
+
title: "BLE AES key (Zhsunyco)",
|
|
549
|
+
description: "32 hex characters. Leave blank to use the default key, which works for most labels.",
|
|
550
|
+
pattern: "^([0-9a-fA-F]{32})?$",
|
|
551
|
+
},
|
|
552
|
+
paintConnectTimeoutSeconds: {
|
|
553
|
+
type: "number",
|
|
554
|
+
title: "Paint connect timeout for this device (seconds)",
|
|
555
|
+
description: `Leave blank to use the plugin-wide setting (currently ${current.paintConnectTimeoutSeconds}s).`,
|
|
556
|
+
minimum: 1,
|
|
557
|
+
},
|
|
558
|
+
paintRetries: {
|
|
559
|
+
type: "number",
|
|
560
|
+
title: "Paint retries for this device",
|
|
561
|
+
description: `Leave blank to use the plugin-wide setting (currently ${current.paintRetries}).`,
|
|
562
|
+
minimum: 1,
|
|
563
|
+
},
|
|
564
|
+
},
|
|
406
565
|
},
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
566
|
+
},
|
|
567
|
+
// Shows only the fields for the chosen trigger. RJSF keeps a hidden field's value, so switching
|
|
568
|
+
// trigger and back doesn't lose it - and the scheduler only reads the ones matching the trigger.
|
|
569
|
+
dependencies: {
|
|
570
|
+
repaintTrigger: {
|
|
571
|
+
oneOf: [
|
|
572
|
+
{
|
|
573
|
+
properties: {
|
|
574
|
+
repaintTrigger: { const: "subscription" },
|
|
575
|
+
triggerPath: {
|
|
576
|
+
type: "string",
|
|
577
|
+
title: "Trigger SignalK path",
|
|
578
|
+
description: "Repaints whenever this path's value changes.",
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
properties: {
|
|
584
|
+
repaintTrigger: { const: "interval" },
|
|
585
|
+
intervalHours: {
|
|
586
|
+
type: "number",
|
|
587
|
+
title: "Repaint every N hours",
|
|
588
|
+
minimum: 1,
|
|
589
|
+
},
|
|
590
|
+
intervalMinute: {
|
|
591
|
+
type: "number",
|
|
592
|
+
title: "Minutes past the hour",
|
|
593
|
+
minimum: 0,
|
|
594
|
+
maximum: 59,
|
|
595
|
+
default: 0,
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
],
|
|
435
600
|
},
|
|
436
601
|
},
|
|
437
602
|
},
|
|
@@ -439,13 +604,40 @@ function configSchema(app, discovered = []) {
|
|
|
439
604
|
},
|
|
440
605
|
};
|
|
441
606
|
}
|
|
607
|
+
/** Lets a field's `description` include Markdown - used for its `docsLink`. */
|
|
608
|
+
const MARKDOWN = { "ui:enableMarkdownInDescription": true };
|
|
442
609
|
function configUiSchema() {
|
|
443
610
|
return {
|
|
611
|
+
signalkApiUrl: { "ui:placeholder": "Auto-detect, or e.g. http://localhost:3001" },
|
|
444
612
|
devices: {
|
|
445
613
|
items: {
|
|
446
|
-
|
|
614
|
+
// Keeps the trigger's own fields (added by the schema's `dependencies`) next to it, rather than
|
|
615
|
+
// after every other field, and Advanced settings last.
|
|
616
|
+
"ui:order": [
|
|
617
|
+
"friendlyName",
|
|
618
|
+
"device",
|
|
619
|
+
"description",
|
|
620
|
+
"templateName",
|
|
621
|
+
"repaintTrigger",
|
|
622
|
+
"triggerPath",
|
|
623
|
+
"intervalHours",
|
|
624
|
+
"intervalMinute",
|
|
625
|
+
"*",
|
|
626
|
+
"advanced",
|
|
627
|
+
],
|
|
628
|
+
friendlyName: { "ui:placeholder": "e.g. Tide clock" },
|
|
629
|
+
description: { "ui:widget": "textarea", "ui:placeholder": "e.g. at companionway", ...MARKDOWN },
|
|
630
|
+
templateName: MARKDOWN,
|
|
447
631
|
repaintTrigger: { "ui:widget": "radio" },
|
|
448
|
-
|
|
632
|
+
triggerPath: { "ui:placeholder": "e.g. environment.tide.state" },
|
|
633
|
+
advanced: {
|
|
634
|
+
reframe: { "ui:widget": "radio", ...MARKDOWN },
|
|
635
|
+
compress: MARKDOWN,
|
|
636
|
+
mirror: { "ui:widget": "radio", ...MARKDOWN },
|
|
637
|
+
compressionFormat: { "ui:widget": "radio", ...MARKDOWN },
|
|
638
|
+
writeWithoutResponse: MARKDOWN,
|
|
639
|
+
aesKey: { "ui:placeholder": "e.g. 00112233445566778899aabbccddeeff" },
|
|
640
|
+
},
|
|
449
641
|
},
|
|
450
642
|
},
|
|
451
643
|
};
|