@rhizomatics/signalk-einklabel-plugin 1.3.0-beta12 → 1.3.0-beta13
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/README.md +14 -556
- package/dist/config.d.ts +16 -0
- package/dist/config.js +13 -10
- package/dist/docs/templateReference.d.ts +18 -0
- package/dist/docs/templateReference.js +207 -0
- package/docs/bluetooth.md +109 -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 +40 -0
- package/docs/getting-started.md +111 -0
- package/docs/templates.md +142 -0
- package/package.json +5 -2
package/dist/config.js
CHANGED
|
@@ -7,6 +7,9 @@ exports.readCurrentConfig = readCurrentConfig;
|
|
|
7
7
|
exports.healStoredConfig = healStoredConfig;
|
|
8
8
|
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
9
9
|
exports.parseDevice = parseDevice;
|
|
10
|
+
exports.listSvgFiles = listSvgFiles;
|
|
11
|
+
exports.listTemplateVariants = listTemplateVariants;
|
|
12
|
+
exports.listTemplateFamilies = listTemplateFamilies;
|
|
10
13
|
exports.resolveTemplatePath = resolveTemplatePath;
|
|
11
14
|
exports.configSchema = configSchema;
|
|
12
15
|
exports.configUiSchema = configUiSchema;
|
|
@@ -362,9 +365,9 @@ function withEnum(schema, values, names) {
|
|
|
362
365
|
}
|
|
363
366
|
/** The plugin's documentation site - its sections' anchors are the README's own headings (see `site/scripts/sync-readme.mjs`). */
|
|
364
367
|
const DOCS_URL = "https://signalk-einklabel.rhizomatics.org.uk/";
|
|
365
|
-
/** A Markdown link to a docs section, for a field description rendered with `ui:enableMarkdownInDescription` (see `configUiSchema`). */
|
|
366
|
-
function docsLink(
|
|
367
|
-
return `[${text}](${DOCS_URL}
|
|
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})`;
|
|
368
371
|
}
|
|
369
372
|
/**
|
|
370
373
|
* An enum-like string field whose options show explanatory labels rather than their raw stored values
|
|
@@ -481,12 +484,12 @@ function configSchema(app, discovered = []) {
|
|
|
481
484
|
title: "Location/description (optional)",
|
|
482
485
|
description: 'Free-text notes about where this label is physically mounted/viewed from, e.g. "chart table, viewed from ~1m ' +
|
|
483
486
|
'in poor light" - available to any template as `source=label,path=description`. ' +
|
|
484
|
-
docsLink("label-details"),
|
|
487
|
+
docsLink("templates/#label-details"),
|
|
485
488
|
},
|
|
486
489
|
templateName: withEnum({
|
|
487
490
|
type: "string",
|
|
488
491
|
title: "Template",
|
|
489
|
-
description: `A bundled template, or one from your templates directory. ${docsLink("
|
|
492
|
+
description: `A bundled template, or one from your templates directory. ${docsLink("examples/")}`,
|
|
490
493
|
}, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
491
494
|
repaintTrigger: choiceField("Repaint trigger", [
|
|
492
495
|
["subscription", "When a SignalK path changes"],
|
|
@@ -501,11 +504,11 @@ function configSchema(app, discovered = []) {
|
|
|
501
504
|
["crop", "Crop - place at the top-left, cutting off anything too big or leaving the rest blank"],
|
|
502
505
|
["scale", "Scale - stretch to fit exactly (may distort)"],
|
|
503
506
|
["fixed", "Fixed - fail the repaint rather than show an off-size image"],
|
|
504
|
-
], { description: docsLink("reframing"), default: "crop" }),
|
|
507
|
+
], { description: docsLink("templates/#reframing"), default: "crop" }),
|
|
505
508
|
compress: {
|
|
506
509
|
type: "boolean",
|
|
507
510
|
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. ${docsLink("other-image-options")}`,
|
|
511
|
+
description: `Sends far less data over BLE, so repaints are quicker. Turn off if a label stops updating. ${docsLink("templates/#other-image-options")}`,
|
|
509
512
|
default: true,
|
|
510
513
|
},
|
|
511
514
|
mirror: choiceField("Mirror", [
|
|
@@ -514,7 +517,7 @@ function configSchema(app, discovered = []) {
|
|
|
514
517
|
["vertical", "Flip top to bottom"],
|
|
515
518
|
["both", "Rotate 180° - for a label mounted upside down"],
|
|
516
519
|
], {
|
|
517
|
-
description: `Only needed if the image shows up mirrored or upside down on the label. ${docsLink("other-image-options")}`,
|
|
520
|
+
description: `Only needed if the image shows up mirrored or upside down on the label. ${docsLink("templates/#other-image-options")}`,
|
|
518
521
|
default: "none",
|
|
519
522
|
}),
|
|
520
523
|
compressionFormat: choiceField("Wire format (Gicisky, experimental)", [
|
|
@@ -524,7 +527,7 @@ function configSchema(app, discovered = []) {
|
|
|
524
527
|
'Chunked - send compressed like the 7.5"/10.2" panels, e.g. to speed up a 4.2" BWR (untested on current firmware)',
|
|
525
528
|
],
|
|
526
529
|
], {
|
|
527
|
-
description: `Chunked needs Compress upload on. Switch back to Auto if the label stops updating. ${docsLink("other-image-options")}`,
|
|
530
|
+
description: `Chunked needs Compress upload on. Switch back to Auto if the label stops updating. ${docsLink("templates/#other-image-options")}`,
|
|
528
531
|
default: "auto",
|
|
529
532
|
}),
|
|
530
533
|
forceRepaint: {
|
|
@@ -616,7 +619,7 @@ function configUiSchema() {
|
|
|
616
619
|
"advanced",
|
|
617
620
|
],
|
|
618
621
|
friendlyName: { "ui:placeholder": "e.g. Tide clock" },
|
|
619
|
-
description: { "ui:widget": "textarea", "ui:placeholder": "e.g.
|
|
622
|
+
description: { "ui:widget": "textarea", "ui:placeholder": "e.g. at companionway", ...MARKDOWN },
|
|
620
623
|
templateName: MARKDOWN,
|
|
621
624
|
repaintTrigger: { "ui:widget": "radio" },
|
|
622
625
|
triggerPath: { "ui:placeholder": "e.g. environment.tide.state" },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The reference block for one bundled template name, e.g. `tides` (a family) or `tide.svg`. */
|
|
2
|
+
export declare function templateReference(templateName: string, templatesDir?: string): string;
|
|
3
|
+
/** Every bundled template name a user can pick - plain `.svg` files and family directories, not dot-prefixed internals. */
|
|
4
|
+
export declare function bundledTemplateNames(templatesDir?: string): string[];
|
|
5
|
+
/** A one-row-per-template summary, linking each to the example page whose reference block covers it. */
|
|
6
|
+
export declare function templatesSummary(pagesByTemplate: Map<string, string>, templatesDir?: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Regenerates every marked block under `examplesDir`, returning each file's current and regenerated
|
|
9
|
+
* content. `templates-summary` blocks link to whichever page holds a template's `template-reference`
|
|
10
|
+
* block, so all pages are scanned for those first.
|
|
11
|
+
*/
|
|
12
|
+
export declare function regenerateExampleDocs(examplesDir: string, templatesDir?: string): {
|
|
13
|
+
path: string;
|
|
14
|
+
current: string;
|
|
15
|
+
updated: string;
|
|
16
|
+
stale: boolean;
|
|
17
|
+
}[];
|
|
18
|
+
export declare const EXAMPLES_DOCS_DIR: string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXAMPLES_DOCS_DIR = void 0;
|
|
4
|
+
exports.templateReference = templateReference;
|
|
5
|
+
exports.bundledTemplateNames = bundledTemplateNames;
|
|
6
|
+
exports.templatesSummary = templatesSummary;
|
|
7
|
+
exports.regenerateExampleDocs = regenerateExampleDocs;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const binding_1 = require("../render/binding");
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const zhsunyco_1 = require("../devices/zhsunyco");
|
|
13
|
+
const gicisky_1 = require("../devices/gicisky");
|
|
14
|
+
/**
|
|
15
|
+
* Generates the "Template reference" blocks in the docs' example pages from the bundled templates
|
|
16
|
+
* themselves - sizes, aspect ratios, colours, which supported labels each size fits, and the data
|
|
17
|
+
* fields each one reads - so that reference can't drift from the templates it describes. Blocks sit
|
|
18
|
+
* between `<!-- BEGIN GENERATED: <kind> [<template>] -->` and `<!-- END GENERATED -->` markers in
|
|
19
|
+
* hand-written pages; everything outside them is left alone. Run `npm run docs:templates` to update,
|
|
20
|
+
* and `templateReference.test.ts` fails when a block is stale.
|
|
21
|
+
*/
|
|
22
|
+
const TEMPLATE_SOURCE_URL = "https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates";
|
|
23
|
+
const MARKER = /<!-- BEGIN GENERATED: ([\w-]+)(?: ([^\s>]+))? -->[\s\S]*?<!-- END GENERATED -->/g;
|
|
24
|
+
const COLOUR_LETTER = { black: "B", white: "W", red: "R", yellow: "Y" };
|
|
25
|
+
function supportedLabels() {
|
|
26
|
+
return [new zhsunyco_1.ZhsunycoDriver(), new gicisky_1.GiciskyDriver()].flatMap((driver) => driver.supportedDevices());
|
|
27
|
+
}
|
|
28
|
+
/** Every file a bundled template name covers - one for a plain `.svg`, one per variant for a family directory. */
|
|
29
|
+
function templateFiles(templatesDir, templateName) {
|
|
30
|
+
const path = (0, path_1.join)(templatesDir, templateName);
|
|
31
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
32
|
+
throw new Error(`template "${templateName}" not found in ${templatesDir}`);
|
|
33
|
+
}
|
|
34
|
+
const read = (relativePath, colours) => {
|
|
35
|
+
const source = (0, fs_1.readFileSync)((0, path_1.join)(templatesDir, relativePath), "utf-8");
|
|
36
|
+
return { path: relativePath, ...(0, binding_1.readTemplateDimensions)(source), colours, bindings: (0, binding_1.findBindings)(source) };
|
|
37
|
+
};
|
|
38
|
+
if ((0, fs_1.statSync)(path).isDirectory()) {
|
|
39
|
+
return (0, config_1.listTemplateVariants)(path)
|
|
40
|
+
.sort((a, b) => b.width - a.width || b.height - a.height)
|
|
41
|
+
.map((variant) => read(`${templateName}/${variant.fileName}`, variant.colours));
|
|
42
|
+
}
|
|
43
|
+
return [read(templateName)];
|
|
44
|
+
}
|
|
45
|
+
function variantName(file) {
|
|
46
|
+
return file.path
|
|
47
|
+
.split("/")
|
|
48
|
+
.pop()
|
|
49
|
+
.replace(/\.svg$/, "");
|
|
50
|
+
}
|
|
51
|
+
function aspectRatio(width, height) {
|
|
52
|
+
return `${(width / height).toFixed(2)} : 1`;
|
|
53
|
+
}
|
|
54
|
+
function matchingLabels(file, labels) {
|
|
55
|
+
const matches = labels
|
|
56
|
+
.filter((label) => label.width === file.width && label.height - label.voffset === file.height)
|
|
57
|
+
.map((label) => {
|
|
58
|
+
const colours = label.colours.map((colour) => COLOUR_LETTER[colour]).join("");
|
|
59
|
+
// Some models' own label already names their colours (e.g. Gicisky's `2.9" BWR`).
|
|
60
|
+
const name = label.label.includes(colours) ? label.label : `${label.label} ${colours}`;
|
|
61
|
+
return `${label.manufacturer ?? ""} ${name}`.trim();
|
|
62
|
+
});
|
|
63
|
+
return matches.length > 0 ? [...new Set(matches)].join(", ") : "none exactly - see [Reframing](../templates.md#reframing)";
|
|
64
|
+
}
|
|
65
|
+
function describeSource(binding) {
|
|
66
|
+
switch (binding.source) {
|
|
67
|
+
case "signalk":
|
|
68
|
+
return binding.context === "self" ? "Signal K path" : `Signal K path (${binding.context})`;
|
|
69
|
+
case "resources":
|
|
70
|
+
return `\`${binding.resource}\` resource${binding.provider ? ` (provider \`${binding.provider}\`)` : ""}`;
|
|
71
|
+
case "einklabel":
|
|
72
|
+
return "Plugin";
|
|
73
|
+
case "label":
|
|
74
|
+
return "Label details";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function describeOptions(binding) {
|
|
78
|
+
return [
|
|
79
|
+
binding.assets && `image from \`${binding.assets}\``,
|
|
80
|
+
binding.format && `format \`${binding.format}\``,
|
|
81
|
+
binding.category && `category \`${binding.category}\``,
|
|
82
|
+
binding.round !== undefined && `round ${binding.round}`,
|
|
83
|
+
binding.default !== undefined && `default \`${binding.default}\``,
|
|
84
|
+
].filter((option) => Boolean(option));
|
|
85
|
+
}
|
|
86
|
+
/** A table row per distinct field - array indexes collapsed, so `extremes[0].time` and `extremes[2].time` are one row. */
|
|
87
|
+
function fieldsTable(files) {
|
|
88
|
+
const rows = new Map();
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
for (const binding of file.bindings) {
|
|
91
|
+
const source = describeSource(binding);
|
|
92
|
+
const path = binding.path.replace(/\[\d+\]/g, "[n]");
|
|
93
|
+
const key = `${source}\u0000${path}`;
|
|
94
|
+
const row = rows.get(key) ?? { source, path, options: new Set(), usedIn: new Set() };
|
|
95
|
+
describeOptions(binding).forEach((option) => row.options.add(option));
|
|
96
|
+
row.usedIn.add(variantName(file));
|
|
97
|
+
rows.set(key, row);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const showUsedIn = files.length > 1;
|
|
101
|
+
const header = showUsedIn ? "| Source | Path | Options | Used in |\n|---|---|---|---|" : "| Source | Path | Options |\n|---|---|---|";
|
|
102
|
+
const sorted = [...rows.values()].sort((a, b) => a.source.localeCompare(b.source) || a.path.localeCompare(b.path));
|
|
103
|
+
const lines = sorted.map((row) => {
|
|
104
|
+
const usedIn = row.usedIn.size === files.length ? "all" : [...row.usedIn].join(", ");
|
|
105
|
+
const cells = [row.source, `\`${row.path}\``, [...row.options].join(", ") || "-", ...(showUsedIn ? [usedIn] : [])];
|
|
106
|
+
return `| ${cells.join(" | ")} |`;
|
|
107
|
+
});
|
|
108
|
+
return [header, ...lines].join("\n");
|
|
109
|
+
}
|
|
110
|
+
/** The reference block for one bundled template name, e.g. `tides` (a family) or `tide.svg`. */
|
|
111
|
+
function templateReference(templateName, templatesDir = config_1.BUNDLED_TEMPLATES_DIR) {
|
|
112
|
+
const files = templateFiles(templatesDir, templateName);
|
|
113
|
+
const labels = supportedLabels();
|
|
114
|
+
const sizes = files.map((file) => {
|
|
115
|
+
const size = file.width && file.height ? `${file.width} × ${file.height}` : "-";
|
|
116
|
+
const ratio = file.width && file.height ? aspectRatio(file.width, file.height) : "-";
|
|
117
|
+
const colours = file.colours?.join(", ") ?? "-";
|
|
118
|
+
return `| [\`${file.path}\`](${TEMPLATE_SOURCE_URL}/${file.path}) | ${size} | ${ratio} | ${colours} | ${matchingLabels(file, labels)} |`;
|
|
119
|
+
});
|
|
120
|
+
return [
|
|
121
|
+
`**Template:** \`${templateName}\``,
|
|
122
|
+
"",
|
|
123
|
+
"| File | Size (px) | Aspect ratio | Colours | Fits these labels exactly |",
|
|
124
|
+
"|---|---|---|---|---|",
|
|
125
|
+
...sizes,
|
|
126
|
+
"",
|
|
127
|
+
"**Data used**",
|
|
128
|
+
"",
|
|
129
|
+
fieldsTable(files),
|
|
130
|
+
].join("\n");
|
|
131
|
+
}
|
|
132
|
+
/** Every bundled template name a user can pick - plain `.svg` files and family directories, not dot-prefixed internals. */
|
|
133
|
+
function bundledTemplateNames(templatesDir = config_1.BUNDLED_TEMPLATES_DIR) {
|
|
134
|
+
return [...(0, config_1.listTemplateFamilies)(templatesDir), ...(0, config_1.listSvgFiles)(templatesDir)].sort();
|
|
135
|
+
}
|
|
136
|
+
/** A one-row-per-template summary, linking each to the example page whose reference block covers it. */
|
|
137
|
+
function templatesSummary(pagesByTemplate, templatesDir = config_1.BUNDLED_TEMPLATES_DIR) {
|
|
138
|
+
const rows = bundledTemplateNames(templatesDir).map((name) => {
|
|
139
|
+
const files = templateFiles(templatesDir, name);
|
|
140
|
+
const sizes = files.map((file) => (file.width && file.height ? `${file.width}×${file.height}` : "?")).join(", ");
|
|
141
|
+
// Only what has to come from outside the plugin - its own and the label's details are always there.
|
|
142
|
+
const external = files
|
|
143
|
+
.flatMap((file) => file.bindings)
|
|
144
|
+
.filter((binding) => binding.source === "signalk" || binding.source === "resources");
|
|
145
|
+
const resources = external.filter((binding) => binding.source === "resources").map(describeSource);
|
|
146
|
+
const paths = external.filter((binding) => binding.source === "signalk").map((binding) => `\`${binding.path}\``);
|
|
147
|
+
const sources = [...new Set(resources), ...(paths.length > 0 ? [`Signal K ${[...new Set(paths)].sort().join(", ")}`] : [])];
|
|
148
|
+
const page = pagesByTemplate.get(name);
|
|
149
|
+
return `| ${page ? `[\`${name}\`](${page})` : `\`${name}\``} | ${sizes} | ${sources.join(", ") || "-"} |`;
|
|
150
|
+
});
|
|
151
|
+
return ["| Template | Sizes | Data needed |", "|---|---|---|", ...rows].join("\n");
|
|
152
|
+
}
|
|
153
|
+
function markdownFiles(dir) {
|
|
154
|
+
return (0, fs_1.readdirSync)(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
155
|
+
const path = (0, path_1.join)(dir, entry.name);
|
|
156
|
+
if (entry.isDirectory())
|
|
157
|
+
return markdownFiles(path);
|
|
158
|
+
return entry.name.endsWith(".md") ? [path] : [];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Regenerates every marked block under `examplesDir`, returning each file's current and regenerated
|
|
163
|
+
* content. `templates-summary` blocks link to whichever page holds a template's `template-reference`
|
|
164
|
+
* block, so all pages are scanned for those first.
|
|
165
|
+
*/
|
|
166
|
+
function regenerateExampleDocs(examplesDir, templatesDir = config_1.BUNDLED_TEMPLATES_DIR) {
|
|
167
|
+
const pages = markdownFiles(examplesDir).map((path) => ({ path, current: (0, fs_1.readFileSync)(path, "utf-8") }));
|
|
168
|
+
const pagesByTemplate = new Map();
|
|
169
|
+
for (const page of pages) {
|
|
170
|
+
for (const match of page.current.matchAll(MARKER)) {
|
|
171
|
+
if (match[1] === "template-reference" && match[2]) {
|
|
172
|
+
pagesByTemplate.set(match[2], page.path.slice(examplesDir.length + 1));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return pages.map((page) => {
|
|
177
|
+
const updated = page.current.replace(MARKER, (_block, kind, arg) => {
|
|
178
|
+
const body = kind === "template-reference" && arg
|
|
179
|
+
? templateReference(arg, templatesDir)
|
|
180
|
+
: kind === "templates-summary"
|
|
181
|
+
? templatesSummary(pagesByTemplate, templatesDir)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (body === undefined)
|
|
184
|
+
throw new Error(`${page.path}: unknown generated block "${kind}${arg ? ` ${arg}` : ""}"`);
|
|
185
|
+
return `<!-- BEGIN GENERATED: ${kind}${arg ? ` ${arg}` : ""} -->\n<!-- Generated from the bundled templates by \`npm run docs:templates\` - do not edit by hand. -->\n\n${body}\n\n<!-- END GENERATED -->`;
|
|
186
|
+
});
|
|
187
|
+
return { ...page, updated, stale: ignoringTableAlignment(updated) !== ignoringTableAlignment(page.current) };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The formatter (`oxfmt`, run by `npm run docs:templates` after this) pads Markdown table columns to
|
|
192
|
+
* line up, which this generator doesn't - so compare with that padding stripped, or a freshly
|
|
193
|
+
* formatted page would always look stale.
|
|
194
|
+
*/
|
|
195
|
+
function ignoringTableAlignment(markdown) {
|
|
196
|
+
return markdown
|
|
197
|
+
.split("\n")
|
|
198
|
+
.map((line) => (line.startsWith("|") ? line.replace(/\s*\|\s*/g, "|").replace(/-{3,}/g, "---") : line))
|
|
199
|
+
.join("\n");
|
|
200
|
+
}
|
|
201
|
+
exports.EXAMPLES_DOCS_DIR = (0, path_1.join)(__dirname, "..", "..", "docs", "examples");
|
|
202
|
+
if (require.main === module) {
|
|
203
|
+
const changed = regenerateExampleDocs(exports.EXAMPLES_DOCS_DIR).filter((page) => page.stale);
|
|
204
|
+
for (const page of changed)
|
|
205
|
+
(0, fs_1.writeFileSync)(page.path, page.updated);
|
|
206
|
+
console.log(changed.length > 0 ? `updated ${changed.map((page) => page.path).join(", ")}` : "template docs already up to date");
|
|
207
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Bluetooth
|
|
2
|
+
|
|
3
|
+
Advice for getting a reliable Bluetooth Low Energy (BLE) connection between the SignalK server and your labels.
|
|
4
|
+
|
|
5
|
+
Most of this applies to direct BlueZ mode only. If the "Use the SignalK BLE Manager API" setting is enabled, the adapter and its lifecycle are managed by the SignalK server, under its own Bluetooth admin settings, once for every BLE-consuming plugin.
|
|
6
|
+
|
|
7
|
+
## Choosing a Bluetooth Adapter
|
|
8
|
+
|
|
9
|
+
Only needed in direct BlueZ mode - in BLE Manager mode the adapter is whatever the SignalK server's own Bluetooth settings provide.
|
|
10
|
+
|
|
11
|
+
- Bluetooth adapters for Linux can be tricky
|
|
12
|
+
- TP-Link UB400 and Asus USB-BT500 are two well-known and available ones, though the ASUS USB-BT500 one can have problems with some Pi type boards (see [Adapter stops responding](#adapter-stops-responding-no-gpio-to-reset))
|
|
13
|
+
- CSR4.0 dongles (CSR8510 chip) have had kernel support for years, and there are well known work arounds for some of them, including in the Linux kernel since v5.17
|
|
14
|
+
- Some Raspberry Pi models come with suitable Bluetooth built in
|
|
15
|
+
- See advice at [Recommended Bluetooth Adapters for Linux](https://github.com/morrownr/USB-WiFi/blob/main/home/Recommended_Bluetooth_Adapters_for_Linux.md)
|
|
16
|
+
- Bluetooth adapters typically prefer being in USB2.0 ports rather than USB3.0 ports, since often the USB3.0 implementation leaks radio energy on the same 2.4Ghz spectrum as Bluetooth. If no USB2.0 port available, try a shielded USB2.0 extension lead to distance the dongle from the port. Some dongle manufacturees seems to do a better job at shielding for this than others.
|
|
17
|
+
|
|
18
|
+
> - Don't worry about the very latest Bluetooth versions, 4.0 is minimum for BLE, 5.0 is nice
|
|
19
|
+
> - Home Assistant is massively more popular than SignalK, and often also run on Raspberry Pi and similar, so good source of advice
|
|
20
|
+
|
|
21
|
+
## Weak Signal
|
|
22
|
+
|
|
23
|
+
If the label is too far from the SignalK server's adapter, try a BLE proxy device - ESP32 is popular for this - or, with the BLE Manager API, a remote BLE gateway.
|
|
24
|
+
|
|
25
|
+
## Bluetooth Plugins Impacting Each Other
|
|
26
|
+
|
|
27
|
+
Bluetooth plugins can kick off scanning, and otherwise interfere with each other. Worst case is when plugins attempt to connect directly to Bluetooth adapters. Better is when they connect using `bluez` and `dbus` Linux components, and best of all when they use the SignalK BLE Manager added in 2026.
|
|
28
|
+
|
|
29
|
+
If you're having problems with Bluetooth connections, make sure other plugins are well behaved, using BLE Manager where they can, and consider temporarily switching them off if needed to debug label connections.
|
|
30
|
+
|
|
31
|
+
## Stuck Bluetooth Adapters
|
|
32
|
+
|
|
33
|
+
Sometime Bluetooth adapters, and/or the Linux services that use them, can get into a 'stuck' state, where the only solution is to reboot the server (although unplugging and plugging the dongle may help). The best way to avoid this is using a known good dongle, and using BLE Manager in SignalK wherever possible.
|
|
34
|
+
|
|
35
|
+
## Tuning Bluetooth Connections
|
|
36
|
+
|
|
37
|
+
Labels spend most of their time asleep, so they can be slow to accept a connection, and slow to answer while an image is being sent to them. Linux's Bluetooth defaults are set with phones, headphones and sensors in mind, so if connections to labels regularly time out, or drop partway through a repaint (for example with GATT or connection-abort errors in the log), some Bluetooth settings on the server may need adjusting:
|
|
38
|
+
|
|
39
|
+
- **The plugin's own timeouts** - the _Paint connect timeout_ and _Paint retries_, set plugin-wide or per label (see [Setting up a Label](getting-started.md#setting-up-a-label)). Try these first, since they only affect this plugin.
|
|
40
|
+
- **BlueZ's connection settings**, in the `[LE]` section of `/etc/bluetooth/main.conf` - the connection interval range (`MinConnectionInterval`/`MaxConnectionInterval`), how long a quiet connection is kept before it's dropped (`ConnectionSupervisionTimeout`), and how long a connection attempt waits (`Autoconnecttimeout`). The file's own comments describe each one. Restart the Bluetooth service after changing it.
|
|
41
|
+
- **The kernel's Bluetooth settings** for the adapter, under `/sys/kernel/debug/bluetooth/hci0/` - such as `supervision_timeout`, `conn_min_interval` and `conn_max_interval`. Values written here are lost on reboot, unless something re-applies them at startup.
|
|
42
|
+
|
|
43
|
+
These apply whenever the server's Bluetooth goes through BlueZ, including the SignalK BLE Manager with a local adapter. They affect every Bluetooth device the server talks to, not just labels, so change one thing at a time and check that your other Bluetooth equipment still works.
|
|
44
|
+
|
|
45
|
+
No particular values are recommended here - what works depends on the adapter, the labels and whatever else is using Bluetooth. Get advice before changing them, for example from the [SignalK community](https://signalk.org) or Home Assistant's Bluetooth community, where many of the same adapters and Linux setups are used.
|
|
46
|
+
|
|
47
|
+
## SignalK starts before the Bluetooth daemon
|
|
48
|
+
|
|
49
|
+
This and the next section are about direct BlueZ mode only - if the "Use the SignalK BLE Manager API" setting is enabled, adapter/dongle lifecycle is the SignalK server's problem to manage once, for every BLE-consuming plugin, not this plugin's.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
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:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sudo systemctl edit signalk.service
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This opens an override file — add:
|
|
60
|
+
|
|
61
|
+
```ini
|
|
62
|
+
[Unit]
|
|
63
|
+
After=bluetooth.target
|
|
64
|
+
Wants=bluetooth.target
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Save and exit, then:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
sudo systemctl daemon-reload
|
|
71
|
+
sudo systemctl restart signalk
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Adapter stops responding: "No gpio to reset"
|
|
77
|
+
|
|
78
|
+
Example log:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Bluetooth: hci0: No gpio to reset Realtek device, ignoring
|
|
82
|
+
Bluetooth: hci0: Unable to disable scanning: -110
|
|
83
|
+
Bluetooth: hci0: command 0x2042 tx timeout
|
|
84
|
+
Bluetooth: hci0: Opcode 0x2042 failed: -110
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This happens when USB autosuspend cycles the dongle in and out of low-power suspend while idle. When bluetoothd sends an HCI command while the device is suspended or mid-resume, it never gets answered — adapters like the popular ASUS USB-500 lack a GPIO to allow reset and its stuck, and spams logs.
|
|
88
|
+
|
|
89
|
+
### Example udev rule fix
|
|
90
|
+
|
|
91
|
+
> [!Note]
|
|
92
|
+
> In these examples the dongle is for vendor `0b05` and product `190e`, adapt for your own devices, use `lsusb` to find out, and if there's no `lsusb` command, install the `usbutils` package.
|
|
93
|
+
|
|
94
|
+
Following file created at `/etc/udev/rules.d/99-bt500-no-autosuspend.rules`
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
# Disable USB autosuspend for the ASUS USB-BT500 (RTL8761BU, 0b05:190e).
|
|
98
|
+
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0b05", ATTR{idProduct}=="190e", TEST=="power/control", ATTR{power/control}="on"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Example tlp fix
|
|
102
|
+
|
|
103
|
+
If `tlp` running to minimize power, it may have its own rules trying to suspend the Bluetooth dongle.
|
|
104
|
+
|
|
105
|
+
Following file created at /etc/tlp.d/99-bt500-no-autosuspend.conf
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
USB_DENYLIST="0b05:190e"
|
|
109
|
+
```
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Command Line Interface
|
|
2
|
+
|
|
3
|
+
To get fast feedback on templates and shelf devices without updating and configuring SignalK, a CLI call `esl-cli` is provided when the module is manually installed (see [Using Outside of SignalK](getting-started.md#using-outside-of-signalk)) that has these commands. Use `--help` to get all the options.
|
|
4
|
+
|
|
5
|
+
- `vendors` - list supported vendors
|
|
6
|
+
- `scan` - report supported devices found from a BLE scan
|
|
7
|
+
- `render` - transform an SVG template and data into a PNG
|
|
8
|
+
- `paint` - render an SVG template and data to a selected ESL
|
|
9
|
+
- `fields` and `field` - see [Debugging Templates](#debugging-templates)
|
|
10
|
+
|
|
11
|
+
The width, height, vertical offset and colour palette for the device are taken from the internal register of devices, however can be overridden on the command line with `-w/--width`, `--height`, `--voffset` and `--colours`. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
|
|
12
|
+
|
|
13
|
+
Left unset, both `render` and `paint` default `-w/--width`/`--height` to the template's own declared `width`/`height` (or `viewBox`) - neither command connects to a device just to size the render, since that would mean an extra BLE connect ahead of `paint`'s own, and doing two back-to-back is exactly the kind of churn that trips real BLE hardware.
|
|
14
|
+
|
|
15
|
+
`paint` also takes `--reframe <mode>`, applied once it has connected and identified the device, for when the rendered image doesn't come out the same size as its actual panel (see [Reframing](templates.md#reframing)):
|
|
16
|
+
|
|
17
|
+
- `crop` (default) - keeps pixels 1:1, placed from the top-left; a bigger render is truncated to fit, a smaller one leaves the extra panel space blank
|
|
18
|
+
- `scale` - stretches the rendered image onto the panel's exact dimensions (independently per axis, not preserving aspect ratio)
|
|
19
|
+
- `fixed` - no adjustment; rejects a size mismatch with an error instead
|
|
20
|
+
|
|
21
|
+
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".
|
|
22
|
+
|
|
23
|
+
`paint` has matching options for the other per-label image settings too (see [Other Image Options](templates.md#other-image-options)):
|
|
24
|
+
|
|
25
|
+
- `--mirror <mode>` - `none` (default), `horizontal`, `vertical`, or `both` (rotate 180°). `render` also takes `--mirror`, to preview the flip as a PNG without a label
|
|
26
|
+
- `--no-compress` - send the image uncompressed, to rule compression out if a label won't update
|
|
27
|
+
- `--compression-format <format>` - `auto` (default) or `chunked`, to try the experimental compressed format on a Gicisky 4.2" BWR
|
|
28
|
+
|
|
29
|
+
`esl-cli` can also be extended with new subcommands - see [Extending](extending.md#cli-commands).
|
|
30
|
+
|
|
31
|
+
( 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` )
|
|
32
|
+
|
|
33
|
+
## Scans from CLI
|
|
34
|
+
|
|
35
|
+
The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
|
|
36
|
+
|
|
37
|
+
- Scan for longer, in this example 90 seconds
|
|
38
|
+
- `npx esl-cli scan -d 90`
|
|
39
|
+
- Scan for all BLE devices, whatever they are
|
|
40
|
+
- `npx esl-cli scan -a`
|
|
41
|
+
|
|
42
|
+
## Debugging Templates
|
|
43
|
+
|
|
44
|
+
The `esl-cli` can be used to debug and validate templates quickly:
|
|
45
|
+
|
|
46
|
+
- `render` - Render templates with SignalK data and write to a local PNG file
|
|
47
|
+
- `paint` - Render templates with SignalK data and send to selected ESL device
|
|
48
|
+
- `fields` - List the fields in the template, with the source specification and the rendered data value
|
|
49
|
+
- `field` - Accept a source specification (outside of any template context) and return the rendered value if available
|
|
50
|
+
|
|
51
|
+
Use `--help` to get the full set of arguments for any of the commands.
|
|
52
|
+
|
|
53
|
+
## CLI Examples
|
|
54
|
+
|
|
55
|
+
### Paint Image Directly
|
|
56
|
+
|
|
57
|
+
The label address previously discovered via `esl-cli scan`
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
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:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --reframe scale
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If a label doesn't update, try sending it uncompressed to see whether compression is the cause:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --no-compress
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --mirror horizontal
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Test Template Without Updating Label
|
|
82
|
+
|
|
83
|
+
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).
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx esl-cli render -t templates/tides/250x128-BWRY.svg -o example.png -u http://localhost
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
and this version will work even without a running SignalK server, using some pre-packaged example data:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx esl-cli render -t templates/tides/250x128-BWRY.svg -o example.png -e examples
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### List all Fields and Rendered Values
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx esl-cli fields -t templates/tide.svg -u http://localhost
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
id spec value
|
|
103
|
+
station.name source=resources,resource=tides,provider=tides,path=station.name Tobermory
|
|
104
|
+
source.name. source=resources,resource=tides,provider=tides,path=station.source.name TICON-4
|
|
105
|
+
last_repaint source=einklabel,path=repainted,format=local_datetime_short 30 Jun 26 00:08
|
|
106
|
+
extremes.0 source=resources,resource=tides,provider=tides,path=extremes[0].label Low
|
|
107
|
+
extremes.1 source=resources,resource=tides,provider=tides,path=extremes[1].label High
|
|
108
|
+
extremes.2 source=resources,resource=tides,provider=tides,path=extremes[2].label Low
|
|
109
|
+
timezoneRegion source=einklabel,path=local_zone BST
|
|
110
|
+
lat source=resources,resource=tides,provider=tides,path=station.datums.LAT,category=depth 0.2m
|
|
111
|
+
hat source=resources,resource=tides,provider=tides,path=station.datums.HAT,category=depth 5.2m
|
|
112
|
+
extremes.2.level source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth 1.1m
|
|
113
|
+
extremes.2.time source=resources,resource=tides,provider=tides,path=extremes[2].time,format=local_time 13:15
|
|
114
|
+
extremes.1.level source=resources,resource=tides,provider=tides,path=extremes[1].level,category=depth 3.8m
|
|
115
|
+
extremes.1.time source=resources,resource=tides,provider=tides,path=extremes[1].time,format=local_time 07:05
|
|
116
|
+
extremes.0.time source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time 01:21
|
|
117
|
+
extremes.0.time-8 source=resources,resource=tides,provider=tides,path=extremes[0].time,format=day_mon 30 Jun
|
|
118
|
+
extremes.0.time-8-5 source=resources,resource=tides,provider=tides,path=extremes[1].time,format=day_mon 30 Jun
|
|
119
|
+
extremes.0.time-8-9 source=resources,resource=tides,provider=tides,path=extremes[2].time,format=day_mon 30 Jun
|
|
120
|
+
extremes.0.level source=resources,resource=tides,provider=tides,path=extremes[0].level,category=depth 1.3m
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Offline Working
|
|
124
|
+
|
|
125
|
+
`render` and `paint` need a `--url` argument to point to the SignalK server to retrieve data. If you don't have access to one, you can use `--example-data` or `-e` to point to a directory of example data, which is bundled with the plugin or available in GitHub at [examples](https://github.com/rhizomatics/signalk-einklabel-plugin/tree/main/examples). This also allows you to write templates for resource APIs that aren't available yet.
|
|
126
|
+
|
|
127
|
+
- `vessels.json` - The standard SignalK vessel paths
|
|
128
|
+
- `resources/xxxx.json` - The output of the `xxxx` resources API call
|
|
129
|
+
- `categories.json` - SignalK unit categories needed for `category=depth` type formatting
|
|
130
|
+
|
|
131
|
+
For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show all the field data that will be populated from the example API, vessel and category data in the `examples` local directory.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
The plugin comes with ready-made templates for these examples. Pick one as a label's _Template_ in the plugin config, or copy it into your own templates directory as a starting point - see [Templates](../templates.md).
|
|
4
|
+
|
|
5
|
+
- [Tide Clock](tide-clock.md) - the next few high and low tides, with the moon phase
|
|
6
|
+
- [Watch Schedule](watch-schedule.md) - who's on watch now and next
|
|
7
|
+
|
|
8
|
+
## Bundled Templates
|
|
9
|
+
|
|
10
|
+
Every bundled template, with its sizes and the data it needs. Each example's page lists the exact labels each size fits and every field it reads.
|
|
11
|
+
|
|
12
|
+
<!-- BEGIN GENERATED: templates-summary -->
|
|
13
|
+
<!-- Generated from the bundled templates by `npm run docs:templates` - do not edit by hand. -->
|
|
14
|
+
|
|
15
|
+
| Template | Sizes | Data needed |
|
|
16
|
+
| ---------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| [`tide.svg`](tide-clock.md) | 416×240 | `tides` resource (provider `tides`), Signal K `environment.moon.phaseName` |
|
|
18
|
+
| [`tides`](tide-clock.md) | 416×240, 296×128, 250×128 | `tides` resource (provider `tides`), Signal K `environment.moon.phaseName` |
|
|
19
|
+
| [`watch`](watch-schedule.md) | 416×240 | Signal K `watch.current.endTime`, `watch.current.startTime`, `watch.current.teamName`, `watch.next.endTime`, `watch.next.startTime`, `watch.next.teamName`, `watch.system.name` |
|
|
20
|
+
|
|
21
|
+
<!-- END GENERATED -->
|
|
22
|
+
|
|
23
|
+
## Trying Examples Without a Boat
|
|
24
|
+
|
|
25
|
+
Each template can be rendered to a PNG with the CLI using the bundled example data, with no SignalK server or label needed - see [Test Template Without Updating Label](../cli.md#test-template-without-updating-label).
|