@rhizomatics/signalk-einklabel-plugin 0.4.0 → 0.4.2
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 +6 -0
- package/README.md +4 -2
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +244 -0
- package/dist/cli/liveContext.d.ts +12 -0
- package/dist/cli/liveContext.js +76 -0
- package/dist/cli/log.d.ts +5 -0
- package/dist/cli/log.js +18 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +193 -0
- package/dist/devices/bleDiscovery.d.ts +54 -0
- package/dist/devices/bleDiscovery.js +134 -0
- package/dist/devices/registry.d.ts +4 -0
- package/dist/devices/registry.js +15 -0
- package/dist/devices/types.d.ts +70 -0
- package/dist/devices/types.js +2 -0
- package/dist/devices/zhsunyco/encode.d.ts +12 -0
- package/dist/devices/zhsunyco/encode.js +47 -0
- package/dist/devices/zhsunyco/index.d.ts +11 -0
- package/dist/devices/zhsunyco/index.js +148 -0
- package/dist/devices/zhsunyco/metadata.d.ts +20 -0
- package/dist/devices/zhsunyco/metadata.js +98 -0
- package/dist/devices/zhsunyco/protocol.d.ts +49 -0
- package/dist/devices/zhsunyco/protocol.js +86 -0
- package/dist/httpJson.d.ts +6 -0
- package/dist/httpJson.js +40 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +23 -0
- package/dist/pathMeta.d.ts +16 -0
- package/dist/pathMeta.js +21 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +99 -0
- package/dist/render/binding.d.ts +54 -0
- package/dist/render/binding.js +175 -0
- package/dist/render/fonts.d.ts +9 -0
- package/dist/render/fonts.js +16 -0
- package/dist/render/formatters.d.ts +18 -0
- package/dist/render/formatters.js +91 -0
- package/dist/render/png.d.ts +3 -0
- package/dist/render/png.js +10 -0
- package/dist/render/svgRenderer.d.ts +32 -0
- package/dist/render/svgRenderer.js +88 -0
- package/dist/render/types.d.ts +26 -0
- package/dist/render/types.js +2 -0
- package/dist/repaintScheduler.d.ts +6 -0
- package/dist/repaintScheduler.js +193 -0
- package/dist/resolveApiUrl.d.ts +28 -0
- package/dist/resolveApiUrl.js +62 -0
- package/dist/unitCategories.d.ts +11 -0
- package/dist/unitCategories.js +46 -0
- package/docs/assets/icons/icon_tmp.png +0 -0
- package/docs/assets/screenshots/example_tidal_clock.png +0 -0
- package/package.json +6 -4
- package/templates/tide.svg +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# 0.4.2
|
|
2
|
+
- Fix High/Low display for tide clock template
|
|
3
|
+
- Fix last repaint time for display being hour out
|
|
4
|
+
- Change default name of template directory to 'einklabel/templates'
|
|
5
|
+
# 0.4.1
|
|
6
|
+
- Packaging fixes
|
|
1
7
|
# 0.4.0
|
|
2
8
|
- Added `einklabel` as a `source` for template fields, and `repainted` as path
|
|
3
9
|
- Added `local_datetime_short` as a datetime format option, for `27 Jun 26 18:05` style output
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# eInk Labels for SignalK
|
|
2
2
|
|
|
3
3
|
** BETA - basic functionality, limited vendor/product support **
|
|
4
4
|
|
|
@@ -30,7 +30,7 @@ In the `description` of the SVG text box, use a comma separated set of key value
|
|
|
30
30
|
|
|
31
31
|
For example, `path=environment.forecast.description` uses the default data source (the `self` vessel context) and the named SignalK path. A bare path with no key/value pairs at all, e.g. just `environment.forecast.description`, is shorthand for the same thing. Overriding the default context can be done with `path=environment.forecast.description,context=vessels.urn:mrn:imo:mmsi:232345678` - the `context` value must match a real SignalK context exactly as it appears in the Data Browser.
|
|
32
32
|
|
|
33
|
-
####
|
|
33
|
+
#### SignalK REST APIs
|
|
34
34
|
|
|
35
35
|
The source can be overridden to use the SignalK server's Resources API instead. For example, `source=resources,resource=tides,path=station.name` picks the `tides` resource and pulls the `station.name` path out of the JSON response - this works for any resource type (`tides`, `waypoints`, `routes`, ...), and needs nothing configured: the plugin reaches the Resources API directly. Where a resource is specified, it will be fetched once for that render, and subsequent fields sourced from the same resource use that cached response.
|
|
36
36
|
|
|
@@ -51,6 +51,8 @@ A `format` can be specified to make the value easier to understand. The supporte
|
|
|
51
51
|
|
|
52
52
|
SignalK's unit preferences are used to automatically convert a `signalk`-sourced numeric value to its preferred display unit, and append a unit symbol like `kt` or `m`, unless `format=raw` is specified to switch that off. However, when using plugin or API data there may be no path metadata to convert from (for example `signalk-tides` publishes tide data to the Resources API, and `level` is a raw metre value with no SignalK path of its own) - in these cases an explicit `category` can be given instead, and the unit preferences will be applied the same way, for example `category=depth` for the tides level figure.
|
|
53
53
|
|
|
54
|
+
Note that for dates and times, the server timezone must be set correctly, for example `Europe/London` rather than the default `Etc/UTC`. This can be done on Linux using `timedatectl` or `raspi-config` if using a Raspberry Pi with Raspian.
|
|
55
|
+
|
|
54
56
|
Common categories:
|
|
55
57
|
|
|
56
58
|
* `depth` - Use the SignalK preferred depth unit, make the conversion if needed, and tack on the unit name as a suffix
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const commander_1 = require("commander");
|
|
6
|
+
const xmldom_1 = require("@xmldom/xmldom");
|
|
7
|
+
const registry_1 = require("../devices/registry");
|
|
8
|
+
const zhsunyco_1 = require("../devices/zhsunyco");
|
|
9
|
+
const bleDiscovery_1 = require("../devices/bleDiscovery");
|
|
10
|
+
const svgRenderer_1 = require("../render/svgRenderer");
|
|
11
|
+
const png_1 = require("../render/png");
|
|
12
|
+
const binding_1 = require("../render/binding");
|
|
13
|
+
const liveContext_1 = require("./liveContext");
|
|
14
|
+
const log_1 = require("./log");
|
|
15
|
+
(0, registry_1.registerDriver)(new zhsunyco_1.ZhsunycoDriver());
|
|
16
|
+
const VENDOR_IDENTIFY_TIMEOUT_MS = 30000;
|
|
17
|
+
const DEFAULT_SIGNALK_URL = 'http://localhost:3000';
|
|
18
|
+
const COLOUR_CODES = {
|
|
19
|
+
BW: ['black', 'white'],
|
|
20
|
+
BWR: ['black', 'white', 'red'],
|
|
21
|
+
BWRY: ['black', 'white', 'red', 'yellow'],
|
|
22
|
+
};
|
|
23
|
+
function parseColours(code) {
|
|
24
|
+
const colours = COLOUR_CODES[code.toUpperCase()];
|
|
25
|
+
if (!colours) {
|
|
26
|
+
throw new Error(`unknown --colours value "${code}" - expected one of ${Object.keys(COLOUR_CODES).join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
return colours;
|
|
29
|
+
}
|
|
30
|
+
/** Connects long enough to read the advertised name and manufacturer ID, then matches against registered drivers. */
|
|
31
|
+
async function identifyVendor(address) {
|
|
32
|
+
const { bluetooth, destroy } = (0, bleDiscovery_1.createBluetooth)();
|
|
33
|
+
try {
|
|
34
|
+
(0, log_1.logDebug)(`connecting to ${address} to identify its vendor (timeout ${VENDOR_IDENTIFY_TIMEOUT_MS}ms)`);
|
|
35
|
+
const adapter = await bluetooth.defaultAdapter();
|
|
36
|
+
const device = await (0, bleDiscovery_1.getOrDiscoverDevice)(adapter, address, VENDOR_IDENTIFY_TIMEOUT_MS);
|
|
37
|
+
const name = await device.getName().catch(() => undefined);
|
|
38
|
+
const manufacturerId = await (0, bleDiscovery_1.getManufacturerId)(device);
|
|
39
|
+
(0, log_1.logDebug)(`${address}: advertised name="${name ?? ''}" manufacturerId=${manufacturerId ?? 'unknown'}`);
|
|
40
|
+
const driver = (0, registry_1.allDrivers)().find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
41
|
+
if (!driver) {
|
|
42
|
+
throw new Error(`no registered vendor driver recognises device "${name ?? address}" - specify --vendor explicitly`);
|
|
43
|
+
}
|
|
44
|
+
return driver.vendor;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
destroy();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const program = new commander_1.Command();
|
|
51
|
+
program.name('esl-cli').description('Local CLI for testing ESL device scan and paint without a SignalK server');
|
|
52
|
+
program.option('-r, --require <module>', 'require a module before running, e.g. an npm package that registers a vendor driver (repeatable)', (value, previous = []) => [...previous, value]);
|
|
53
|
+
program.option('-l, --log-level <level>', 'log verbosity: info or debug (e.g. trace which URLs are fetched)', 'info');
|
|
54
|
+
program.hook('preAction', () => {
|
|
55
|
+
(0, log_1.setLogLevel)(program.opts().logLevel);
|
|
56
|
+
for (const mod of program.opts().require ?? []) {
|
|
57
|
+
require(mod);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('vendors')
|
|
62
|
+
.description('List supported vendors and the device models each has confirmed metadata for')
|
|
63
|
+
.action(() => {
|
|
64
|
+
const header = ['vendor', 'pid', 'hwid', 'label', 'size', 'colours'];
|
|
65
|
+
const rows = [];
|
|
66
|
+
for (const driver of (0, registry_1.allDrivers)()) {
|
|
67
|
+
for (const device of driver.supportedDevices()) {
|
|
68
|
+
rows.push([
|
|
69
|
+
driver.vendor,
|
|
70
|
+
`0x${device.pid.toString(16).padStart(4, '0')}`,
|
|
71
|
+
device.hwVersion ? `0x${device.hwVersion}` : '',
|
|
72
|
+
device.label,
|
|
73
|
+
`${device.width}x${device.height}`,
|
|
74
|
+
device.colours.join(','),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (rows.length === 0) {
|
|
79
|
+
console.log('(no confirmed devices yet)');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const widths = header.map((title, col) => Math.max(title.length, ...rows.map((row) => row[col].length)));
|
|
83
|
+
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(' '));
|
|
84
|
+
printRow(header);
|
|
85
|
+
rows.forEach(printRow);
|
|
86
|
+
});
|
|
87
|
+
program
|
|
88
|
+
.command('scan')
|
|
89
|
+
.description('Scan for supported BLE ESL devices across all registered vendor drivers')
|
|
90
|
+
.option('-d, --duration <seconds>', 'scan duration in seconds', '10')
|
|
91
|
+
.option('-a, --all-devices', 'list every nearby BLE device, not just ones a registered driver recognised - unmatched devices show address/name/mfr/rssi only, since there\'s no driver to do a vendor-specific read like battery')
|
|
92
|
+
.action(async (opts) => {
|
|
93
|
+
const durationMs = Number(opts.duration) * 1000;
|
|
94
|
+
const header = ['vendor', 'address', 'name', 'pid', 'label', 'mfr', 'battery', 'rssi'];
|
|
95
|
+
const rows = [];
|
|
96
|
+
let matchedCount = 0;
|
|
97
|
+
const drivers = (0, registry_1.allDrivers)();
|
|
98
|
+
(0, log_1.logDebug)(`scanning for ${durationMs}ms`);
|
|
99
|
+
await (0, bleDiscovery_1.withDiscovery)(durationMs, async (adapter) => {
|
|
100
|
+
await (0, bleDiscovery_1.forEachAdvertisedDevice)(adapter, async ({ device, address, name, manufacturerId, manufacturerData }) => {
|
|
101
|
+
const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
102
|
+
const mfr = manufacturerId !== undefined ? `0x${manufacturerId.toString(16).padStart(4, '0')}` : '';
|
|
103
|
+
if (!driver) {
|
|
104
|
+
if (opts.allDevices) {
|
|
105
|
+
const rssi = await device
|
|
106
|
+
.getRSSI()
|
|
107
|
+
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
108
|
+
.catch(() => undefined);
|
|
109
|
+
rows.push(['(unmatched)', address, name ?? '', '', '', mfr, '', String(rssi ?? '')]);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
matchedCount++;
|
|
114
|
+
const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData);
|
|
115
|
+
(0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
|
|
116
|
+
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, '0')}` : '';
|
|
117
|
+
const label = found.metadata?.label ?? '';
|
|
118
|
+
const battery = found.batteryMv !== undefined ? `${found.batteryMv}mV` : '';
|
|
119
|
+
rows.push([driver.vendor, found.address, found.name ?? '', pid, label, mfr, battery, String(found.rssi ?? '')]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
if (matchedCount === 0) {
|
|
123
|
+
console.log(`no devices found in ${opts.duration}s - try a longer scan with -d, e.g. "-d 30"`);
|
|
124
|
+
}
|
|
125
|
+
if (rows.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const widths = header.map((title, col) => Math.max(title.length, ...rows.map((row) => row[col].length)));
|
|
129
|
+
const printRow = (row) => console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(' '));
|
|
130
|
+
printRow(header);
|
|
131
|
+
rows.forEach(printRow);
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command('paint')
|
|
135
|
+
.description('Render a template against a live SignalK server and send it to a device')
|
|
136
|
+
.option('-v, --vendor <vendor>', 'vendor driver to use - if omitted, inferred from the device\'s advertised name')
|
|
137
|
+
.requiredOption('-a, --address <address>', 'BLE address of the device')
|
|
138
|
+
.requiredOption('-t, --template <path>', 'path to SVG template')
|
|
139
|
+
.option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
|
|
140
|
+
.option('-k, --aes-key <hex>', 'AES-128 key for device authentication, as 32 hex characters - defaults to the vendor\'s stock key if omitted')
|
|
141
|
+
.option('-w, --width <px>', 'render width', '416')
|
|
142
|
+
.option('--height <px>', 'render height', '240')
|
|
143
|
+
.option('--voffset <px>', 'vertical pixel offset of the panel - overrides the looked-up model for unsupported hardware (requires --colours)', '0')
|
|
144
|
+
.option('--colours <code>', 'device colour palette for unsupported hardware: BW, BWR, or BWRY - overrides the looked-up model (uses --width/--height/--voffset)')
|
|
145
|
+
.option('--connect-timeout <seconds>', 'BLE connect timeout before giving up on an attempt', '30')
|
|
146
|
+
.option('--retries <n>', 'number of paint attempts (including the first) before giving up', '3')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
const vendor = opts.vendor ?? (await identifyVendor(opts.address));
|
|
149
|
+
const driver = (0, registry_1.getDriver)(vendor);
|
|
150
|
+
if (!driver) {
|
|
151
|
+
throw new Error(`no driver registered for vendor "${vendor}"`);
|
|
152
|
+
}
|
|
153
|
+
const modelOverride = opts.colours
|
|
154
|
+
? {
|
|
155
|
+
label: 'manual override',
|
|
156
|
+
width: Number(opts.width),
|
|
157
|
+
height: Number(opts.height),
|
|
158
|
+
voffset: Number(opts.voffset),
|
|
159
|
+
colours: parseColours(opts.colours),
|
|
160
|
+
}
|
|
161
|
+
: undefined;
|
|
162
|
+
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, 'utf-8'));
|
|
163
|
+
const context = await (0, liveContext_1.assembleLiveContext)(opts.url, bindings);
|
|
164
|
+
const renderer = new svgRenderer_1.SvgRenderer();
|
|
165
|
+
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
|
|
166
|
+
const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
|
|
167
|
+
await (0, bleDiscovery_1.withRetries)(Number(opts.retries), async (attempt) => {
|
|
168
|
+
if (attempt > 1) {
|
|
169
|
+
(0, log_1.logDebug)(`paint attempt ${attempt}/${opts.retries}`);
|
|
170
|
+
}
|
|
171
|
+
await driver.paint(bitmap, { address: opts.address, aesKey: opts.aesKey, modelOverride, connectTimeoutMs });
|
|
172
|
+
});
|
|
173
|
+
console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height})`);
|
|
174
|
+
});
|
|
175
|
+
program
|
|
176
|
+
.command('render')
|
|
177
|
+
.description('Render a template against a live SignalK server and write a PNG, without needing a device')
|
|
178
|
+
.requiredOption('-t, --template <path>', 'path to SVG template')
|
|
179
|
+
.requiredOption('-o, --output <path>', 'output PNG path')
|
|
180
|
+
.option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
|
|
181
|
+
.option('-w, --width <px>', 'render width', '416')
|
|
182
|
+
.option('--height <px>', 'render height', '240')
|
|
183
|
+
.option('-f, --font <path>', 'override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio', (value, previous = []) => [...previous, value])
|
|
184
|
+
.action(async (opts) => {
|
|
185
|
+
const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, 'utf-8'));
|
|
186
|
+
const context = await (0, liveContext_1.assembleLiveContext)(opts.url, bindings);
|
|
187
|
+
const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
|
|
188
|
+
const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
|
|
189
|
+
await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
|
|
190
|
+
console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
|
|
191
|
+
});
|
|
192
|
+
program
|
|
193
|
+
.command('fields')
|
|
194
|
+
.description('List every <desc> binding in a template by element id, with its source spec and resolved value')
|
|
195
|
+
.requiredOption('-t, --template <path>', 'path to SVG template')
|
|
196
|
+
.option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
|
|
197
|
+
.action(async (opts) => {
|
|
198
|
+
const doc = new xmldom_1.DOMParser().parseFromString(await (0, promises_1.readFile)(opts.template, 'utf-8'), 'image/svg+xml');
|
|
199
|
+
const elements = doc.getElementsByTagName('text');
|
|
200
|
+
const rows = [];
|
|
201
|
+
for (let i = 0; i < elements.length; i++) {
|
|
202
|
+
const element = elements.item(i);
|
|
203
|
+
const desc = element?.getElementsByTagName('desc').item(0);
|
|
204
|
+
if (!element || !desc?.textContent)
|
|
205
|
+
continue;
|
|
206
|
+
const id = element.getAttribute('id') ?? `#${i}`;
|
|
207
|
+
try {
|
|
208
|
+
rows.push({ id, desc: desc.textContent, binding: (0, binding_1.parseBinding)(desc.textContent) });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
rows.push({ id, desc: desc.textContent, error: err.message });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const header = ['id', 'spec', 'value'];
|
|
215
|
+
const table = await Promise.all(rows.map(async (row) => {
|
|
216
|
+
if (row.error || !row.binding)
|
|
217
|
+
return [row.id, row.desc, row.error ?? ''];
|
|
218
|
+
try {
|
|
219
|
+
const context = await (0, liveContext_1.assembleLiveContext)(opts.url, [row.binding]);
|
|
220
|
+
return [row.id, row.desc, (0, binding_1.renderBinding)(row.binding, context)];
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return [row.id, row.desc, `ERROR: ${err.message}`];
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
const widths = header.map((title, col) => Math.max(title.length, ...table.map((cells) => cells[col].length)));
|
|
227
|
+
const printRow = (cells) => console.log(cells.map((cell, col) => cell.padEnd(widths[col])).join(' '));
|
|
228
|
+
printRow(header);
|
|
229
|
+
table.forEach(printRow);
|
|
230
|
+
});
|
|
231
|
+
program
|
|
232
|
+
.command('field')
|
|
233
|
+
.description('Resolve a single binding spec directly against a live SignalK server, with no template')
|
|
234
|
+
.argument('<spec>', 'binding spec, e.g. "source=resources,resource=tides,path=station.name" or a bare SignalK path')
|
|
235
|
+
.option('-u, --url <url>', 'SignalK server base URL - resolves the spec\'s source=signalk/resources binding', DEFAULT_SIGNALK_URL)
|
|
236
|
+
.action(async (spec, opts) => {
|
|
237
|
+
const binding = (0, binding_1.parseBinding)(spec);
|
|
238
|
+
const context = await (0, liveContext_1.assembleLiveContext)(opts.url, [binding]);
|
|
239
|
+
console.log((0, binding_1.renderBinding)(binding, context));
|
|
240
|
+
});
|
|
241
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
242
|
+
console.error(err.message);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Binding } from '../render/binding';
|
|
2
|
+
import { TemplateContext } from '../render/types';
|
|
3
|
+
/**
|
|
4
|
+
* CLI counterpart to `assembleRawContext` + `considerRepaint`'s `meta` injection in
|
|
5
|
+
* repaintScheduler.ts - same `{ signalk, resources, pathMeta, categories, meta }` shape, but fetched
|
|
6
|
+
* entirely over plain HTTP (the CLI has no live `ServerAPI` to call `getSelfPath`/`getPath`/
|
|
7
|
+
* `resourcesApi` on, and per-path metadata/`category=` resolution has no in-process equivalent at all
|
|
8
|
+
* - see `repaintScheduler.ts`) against a real or test SignalK server's REST API. Fetches each
|
|
9
|
+
* referenced context's/resource's *whole* subtree once and lets the existing binding resolver
|
|
10
|
+
* navigate `path` within it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assembleLiveContext(signalkUrl: string, bindings: Binding[]): Promise<TemplateContext>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assembleLiveContext = assembleLiveContext;
|
|
4
|
+
const httpJson_1 = require("../httpJson");
|
|
5
|
+
const unitCategories_1 = require("../unitCategories");
|
|
6
|
+
const pathMeta_1 = require("../pathMeta");
|
|
7
|
+
const log_1 = require("./log");
|
|
8
|
+
const RESOURCES_API_PATH = '/signalk/v2/api/resources';
|
|
9
|
+
/** `context=self` -> `vessels/self`, `context=vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's full-data-model subtree. */
|
|
10
|
+
function contextPath(context) {
|
|
11
|
+
return context === 'self' ? 'vessels/self' : context.replace(/\./g, '/');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `GET .../vessels/self` (or any other subtree) returns SignalK's full delta-tree shape, where every
|
|
15
|
+
* leaf is wrapped as `{ value, $source, timestamp, ... }` rather than the bare value - unlike
|
|
16
|
+
* `app.getSelfPath`/`getPath` in the live plugin (repaintScheduler.ts), which already return bare
|
|
17
|
+
* values. Recursively unwraps every such leaf so `path=` bindings resolve the same way over HTTP as
|
|
18
|
+
* they do live. Only applied to `signalk` fetches - `resources` responses have no such wrapper.
|
|
19
|
+
*
|
|
20
|
+
* Keys off `value` alone, not also requiring `timestamp`/`$source` - not every server includes both on
|
|
21
|
+
* every leaf, and a `value` key is otherwise meaningless on a SignalK data-model node (it isn't a
|
|
22
|
+
* regular vessel property name), so there's no real risk of unwrapping something that isn't this
|
|
23
|
+
* wrapper.
|
|
24
|
+
*/
|
|
25
|
+
function unwrapSignalkTree(node) {
|
|
26
|
+
if (node === null || typeof node !== 'object')
|
|
27
|
+
return node;
|
|
28
|
+
if (Array.isArray(node))
|
|
29
|
+
return node.map(unwrapSignalkTree);
|
|
30
|
+
const obj = node;
|
|
31
|
+
if ('value' in obj) {
|
|
32
|
+
return obj.value;
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, unwrapSignalkTree(value)]));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* CLI counterpart to `assembleRawContext` + `considerRepaint`'s `meta` injection in
|
|
38
|
+
* repaintScheduler.ts - same `{ signalk, resources, pathMeta, categories, meta }` shape, but fetched
|
|
39
|
+
* entirely over plain HTTP (the CLI has no live `ServerAPI` to call `getSelfPath`/`getPath`/
|
|
40
|
+
* `resourcesApi` on, and per-path metadata/`category=` resolution has no in-process equivalent at all
|
|
41
|
+
* - see `repaintScheduler.ts`) against a real or test SignalK server's REST API. Fetches each
|
|
42
|
+
* referenced context's/resource's *whole* subtree once and lets the existing binding resolver
|
|
43
|
+
* navigate `path` within it.
|
|
44
|
+
*/
|
|
45
|
+
async function assembleLiveContext(signalkUrl, bindings) {
|
|
46
|
+
const signalk = {};
|
|
47
|
+
const contexts = new Set(bindings.filter((binding) => binding.source === 'signalk').map((binding) => binding.context));
|
|
48
|
+
for (const context of contexts) {
|
|
49
|
+
const url = `${signalkUrl}/signalk/v1/api/${contextPath(context)}`;
|
|
50
|
+
(0, log_1.logDebug)(`GET ${url}`);
|
|
51
|
+
signalk[context] = unwrapSignalkTree(await (0, httpJson_1.fetchJson)(url));
|
|
52
|
+
}
|
|
53
|
+
const pathMeta = {};
|
|
54
|
+
for (const context of contexts) {
|
|
55
|
+
try {
|
|
56
|
+
(0, log_1.logDebug)(`GET ${signalkUrl}/signalk/v1/api/${contextPath(context)}/meta`);
|
|
57
|
+
pathMeta[context] = await (0, pathMeta_1.fetchPathMeta)(signalkUrl, context);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(`warning: could not fetch path metadata for context "${context}" (${err.message}) - automatic unit conversion will show raw values`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const resources = {};
|
|
64
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === 'resources').map((binding) => binding.resource));
|
|
65
|
+
for (const name of resourceNames) {
|
|
66
|
+
const url = `${signalkUrl}${RESOURCES_API_PATH}/${name}`;
|
|
67
|
+
(0, log_1.logDebug)(`GET ${url}`);
|
|
68
|
+
resources[name] = await (0, httpJson_1.fetchJson)(url);
|
|
69
|
+
}
|
|
70
|
+
const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
|
|
71
|
+
const categories = await (0, unitCategories_1.fetchCategoryDisplayUnits)(signalkUrl, categoryNames);
|
|
72
|
+
// Matches `considerRepaint` in repaintScheduler.ts - the CLI has no real device repaint to time, so
|
|
73
|
+
// a `source=einklabel,path=repainted` binding just resolves to "now", same as a live render would.
|
|
74
|
+
const meta = { repainted: new Date().toISOString() };
|
|
75
|
+
return { signalk, resources, pathMeta, categories, meta };
|
|
76
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type LogLevel = 'info' | 'debug';
|
|
2
|
+
/** Set once from the global `--log-level` option (see index.ts's `preAction` hook) - applies to every command. */
|
|
3
|
+
export declare function setLogLevel(level: string): void;
|
|
4
|
+
/** Internal tracing (e.g. which URL is being fetched) - silent unless --log-level debug, so default output stays uncluttered. */
|
|
5
|
+
export declare function logDebug(message: string): void;
|
package/dist/cli/log.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setLogLevel = setLogLevel;
|
|
4
|
+
exports.logDebug = logDebug;
|
|
5
|
+
const LEVELS = ['info', 'debug'];
|
|
6
|
+
let currentLevel = 'info';
|
|
7
|
+
/** Set once from the global `--log-level` option (see index.ts's `preAction` hook) - applies to every command. */
|
|
8
|
+
function setLogLevel(level) {
|
|
9
|
+
if (!LEVELS.includes(level)) {
|
|
10
|
+
throw new Error(`unknown --log-level "${level}" - expected one of ${LEVELS.join(', ')}`);
|
|
11
|
+
}
|
|
12
|
+
currentLevel = level;
|
|
13
|
+
}
|
|
14
|
+
/** Internal tracing (e.g. which URL is being fetched) - silent unless --log-level debug, so default output stays uncluttered. */
|
|
15
|
+
function logDebug(message) {
|
|
16
|
+
if (currentLevel === 'debug')
|
|
17
|
+
console.error(`[debug] ${message}`);
|
|
18
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ServerAPI } from '@signalk/server-api';
|
|
2
|
+
import { DiscoveredDevice } from './devices/types';
|
|
3
|
+
export interface DeviceConfig {
|
|
4
|
+
friendlyName: string;
|
|
5
|
+
/**
|
|
6
|
+
* `"<vendor>:<pid>[:<hwVersion>]@<address>"`, picked from a combined enum of recently
|
|
7
|
+
* scanned devices so one selection sets both the model (width/height/colours, known
|
|
8
|
+
* without a live BLE read) and the BLE address.
|
|
9
|
+
*/
|
|
10
|
+
device: string;
|
|
11
|
+
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
12
|
+
aesKey?: string;
|
|
13
|
+
templateName: string;
|
|
14
|
+
repaintTrigger: 'subscription' | 'interval';
|
|
15
|
+
/** SignalK path to subscribe to when `repaintTrigger` is `subscription` - a repaint is considered on every delta. */
|
|
16
|
+
triggerPath?: string;
|
|
17
|
+
/** When `repaintTrigger` is `interval`: repaint every N hours... */
|
|
18
|
+
intervalHours?: number;
|
|
19
|
+
/** ...at this minute past the hour. */
|
|
20
|
+
intervalMinute?: number;
|
|
21
|
+
/** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
|
|
22
|
+
forceRepaint?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface PluginConfig {
|
|
25
|
+
/**
|
|
26
|
+
* Directory the plugin scans for template files, instead of an upload UI - follows
|
|
27
|
+
* signalk-parquet's convention: empty for the default, a relative path resolved against
|
|
28
|
+
* `~/.signalk`, or an absolute path. Use `resolveTemplatesDir` to turn this into an actual path.
|
|
29
|
+
*/
|
|
30
|
+
templatesDir: string;
|
|
31
|
+
/** Run a short BLE scan on plugin start and report discoveries via plugin status, like signalk-bluetti-plugin does. */
|
|
32
|
+
scanOnStart: boolean;
|
|
33
|
+
/** How long the startup scan runs, in seconds. */
|
|
34
|
+
scanDurationSeconds: number;
|
|
35
|
+
/** How long to wait for a device to accept a BLE connection before giving up on a repaint attempt, in seconds. */
|
|
36
|
+
paintConnectTimeoutSeconds: number;
|
|
37
|
+
/** How many times to attempt a repaint (including the first try) before giving up and reporting failure. */
|
|
38
|
+
paintRetries: number;
|
|
39
|
+
/**
|
|
40
|
+
* Base URL of this SignalK server, used for: (1) a `signalk`-sourced numeric value's automatic unit
|
|
41
|
+
* conversion (`GET .../vessels/<context>/meta`, see `../pathMeta.ts`) unless `format=raw`, and (2) an
|
|
42
|
+
* explicit `category=` binding (e.g. `category=depth` on a resource-sourced value with no path
|
|
43
|
+
* metadata of its own, see `../unitCategories.ts`). Neither has an in-process equivalent reachable via
|
|
44
|
+
* the plugin API - confirmed against the signalk-server source, this resolution only happens in its
|
|
45
|
+
* REST layer.
|
|
46
|
+
*
|
|
47
|
+
* Always the local loopback address - the plugin runs on the same host as the server, so it's
|
|
48
|
+
* reachable regardless of any external reverse proxy. Left unset, the plugin probes
|
|
49
|
+
* `SIGNALK_API_URL_OPTIONS` at startup (in likelihood order: 3000 for a bare `npm install`, then
|
|
50
|
+
* 80/443 for container/systemd installs) and uses whichever responds - see `./resolveApiUrl.ts`. Set
|
|
51
|
+
* explicitly only to skip probing or to confirm a specific one is reachable; either way, it must allow
|
|
52
|
+
* anonymous read access - the plugin has no login flow.
|
|
53
|
+
*/
|
|
54
|
+
signalkApiUrl?: string;
|
|
55
|
+
devices: DeviceConfig[];
|
|
56
|
+
}
|
|
57
|
+
export declare function defaultConfig(): PluginConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
|
|
60
|
+
* signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
|
|
61
|
+
* path is resolved against `~/.signalk` (where SignalK itself stores its config by default), and
|
|
62
|
+
* an absolute path is used as-is.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveTemplatesDir(templatesDir: string | undefined): string;
|
|
65
|
+
export declare function parseDevice(device: string): {
|
|
66
|
+
vendor: string;
|
|
67
|
+
pid: number;
|
|
68
|
+
hwVersion?: string;
|
|
69
|
+
address: string;
|
|
70
|
+
} | undefined;
|
|
71
|
+
/** Resolves a template name to an actual file path - a local template overrides the bundled one of the same name. */
|
|
72
|
+
export declare function resolveTemplatePath(templatesDir: string, templateName: string): string;
|
|
73
|
+
export declare function configSchema(app: ServerAPI, discovered?: DiscoveredDevice[]): object;
|
|
74
|
+
export declare function configUiSchema(): object;
|