@rhizomatics/signalk-einklabel-plugin 1.3.0-beta1 → 1.3.0-beta11
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 +29 -1
- package/README.md +42 -3
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +25 -1
- package/dist/config.d.ts +46 -14
- package/dist/config.js +170 -53
- package/dist/devices/bleBackend.d.ts +30 -2
- package/dist/devices/bleBackend.js +179 -7
- package/dist/devices/bleDiscovery.d.ts +9 -2
- package/dist/devices/bleDiscovery.js +12 -3
- package/dist/devices/discoveryCoordinator.d.ts +18 -0
- package/dist/devices/discoveryCoordinator.js +89 -22
- package/dist/devices/gicisky/compression.d.ts +22 -9
- package/dist/devices/gicisky/compression.js +128 -13
- package/dist/devices/gicisky/encode.d.ts +1 -1
- package/dist/devices/gicisky/encode.js +2 -2
- package/dist/devices/gicisky/index.js +19 -8
- package/dist/devices/gicisky/layout.d.ts +13 -1
- package/dist/devices/gicisky/layout.js +21 -0
- package/dist/devices/types.d.ts +22 -0
- package/dist/devices/types.js +2 -0
- package/dist/devices/zhsunyco/compression.d.ts +1 -0
- package/dist/devices/zhsunyco/compression.js +30 -0
- package/dist/devices/zhsunyco/index.js +10 -4
- package/dist/devices/zhsunyco/protocol.d.ts +2 -0
- package/dist/devices/zhsunyco/protocol.js +2 -0
- package/dist/plugin.js +11 -2
- package/dist/render/mirror.d.ts +11 -0
- package/dist/render/mirror.js +24 -0
- package/dist/repaintScheduler.js +27 -12
- package/package.json +15 -9
|
@@ -15,6 +15,8 @@ export declare const WOLINK_CHARACTERISTIC_UUIDS: {
|
|
|
15
15
|
export declare const COMMAND: {
|
|
16
16
|
readonly uploadBlock: 42240;
|
|
17
17
|
readonly refreshUncompressed: 42241;
|
|
18
|
+
/** Refresh after uploading a `compressWolinkBlocks` payload rather than the raw buffer. */
|
|
19
|
+
readonly refreshCompressed: 42242;
|
|
18
20
|
};
|
|
19
21
|
export interface AdvertisedDeviceInfo {
|
|
20
22
|
pid: number;
|
|
@@ -25,6 +25,8 @@ exports.WOLINK_CHARACTERISTIC_UUIDS = {
|
|
|
25
25
|
exports.COMMAND = {
|
|
26
26
|
uploadBlock: 0xa500,
|
|
27
27
|
refreshUncompressed: 0xa501,
|
|
28
|
+
/** Refresh after uploading a `compressWolinkBlocks` payload rather than the raw buffer. */
|
|
29
|
+
refreshCompressed: 0xa502,
|
|
28
30
|
};
|
|
29
31
|
/** Decodes the 8-byte header shared by the advertising mirror and config characteristic. */
|
|
30
32
|
function decodeAdvertisedInfo(data) {
|
package/dist/plugin.js
CHANGED
|
@@ -9,6 +9,7 @@ const discoveryCoordinator_1 = require("./devices/discoveryCoordinator");
|
|
|
9
9
|
const bleDiscovery_1 = require("./devices/bleDiscovery");
|
|
10
10
|
const discoveredDevicesStore_1 = require("./devices/discoveredDevicesStore");
|
|
11
11
|
const repaintScheduler_1 = require("./repaintScheduler");
|
|
12
|
+
const pluginVersion_1 = require("./pluginVersion");
|
|
12
13
|
/** Mirrors signalk-bluetti-plugin's convention: scan briefly, report finds via plugin status for the user to copy-paste. */
|
|
13
14
|
async function runStartupScan(app, durationSeconds, useBleApi) {
|
|
14
15
|
const alreadyRunning = (0, discoveryCoordinator_1.scanInProgressSince)();
|
|
@@ -41,6 +42,7 @@ function createPlugin(app) {
|
|
|
41
42
|
// enough to have added it - checked at runtime rather than trusted from the type.
|
|
42
43
|
const bleApiAvailable = !!app.bleApi;
|
|
43
44
|
let scheduler;
|
|
45
|
+
let stopDiscoveryListener;
|
|
44
46
|
let stopped = false;
|
|
45
47
|
const plugin = {
|
|
46
48
|
id: "signalk-einklabel-plugin",
|
|
@@ -54,10 +56,10 @@ function createPlugin(app) {
|
|
|
54
56
|
start(config) {
|
|
55
57
|
const pluginConfig = {
|
|
56
58
|
...(0, config_1.defaultConfig)(),
|
|
57
|
-
...config,
|
|
59
|
+
...(0, config_1.migrateConfig)(config).config,
|
|
58
60
|
};
|
|
59
61
|
app.debug(`starting with ${pluginConfig.devices.length} configured device(s)`);
|
|
60
|
-
(0, config_1.
|
|
62
|
+
(0, config_1.healStoredConfig)(app);
|
|
61
63
|
stopped = false;
|
|
62
64
|
const useBleApi = pluginConfig.useBleApi && bleApiAvailable;
|
|
63
65
|
if (pluginConfig.useBleApi && !bleApiAvailable) {
|
|
@@ -73,6 +75,11 @@ function createPlugin(app) {
|
|
|
73
75
|
scheduler = (0, repaintScheduler_1.startRepaintScheduler)(app, pluginConfig);
|
|
74
76
|
};
|
|
75
77
|
if (useBleApi) {
|
|
78
|
+
// BLE Manager already runs its own continuous scan server-side (that's what populates its own
|
|
79
|
+
// admin UI device list) - unlike direct BlueZ access below, discovery here doesn't need a
|
|
80
|
+
// bounded scan window at all, so this listens for as long as the plugin runs rather than only
|
|
81
|
+
// during `scanOnStart`/an on-demand `ALL_DEVICES` scan. See its own doc comment.
|
|
82
|
+
stopDiscoveryListener = (0, discoveryCoordinator_1.startBleApiDiscoveryListener)(app, pluginVersion_1.PLUGIN_NAME);
|
|
76
83
|
// The server/provider governs its own local-adapter readiness once BLE Manager mode owns
|
|
77
84
|
// `hci0` (or has none at all, behind a remote gateway) - waiting on a *local* BlueZ adapter
|
|
78
85
|
// here would be waiting on something this mode may never even need.
|
|
@@ -90,6 +97,8 @@ function createPlugin(app) {
|
|
|
90
97
|
stopped = true;
|
|
91
98
|
scheduler?.stop();
|
|
92
99
|
scheduler = undefined;
|
|
100
|
+
stopDiscoveryListener?.();
|
|
101
|
+
stopDiscoveryListener = undefined;
|
|
93
102
|
app.debug("stopped");
|
|
94
103
|
},
|
|
95
104
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Bitmap } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Flips the rendered image before it's encoded for the panel - for hardware whose RAM layout is
|
|
4
|
+
* mirrored relative to what a driver's encoder assumes (the Wolink HA integration found the 2.9"
|
|
5
|
+
* and 3.5" panels differ from the 3.7" this plugin's zhsunyco encoder was confirmed against), or a
|
|
6
|
+
* label that's simply mounted upside down (`"both"` is a 180° rotation).
|
|
7
|
+
*/
|
|
8
|
+
export type MirrorMode = "none" | "horizontal" | "vertical" | "both";
|
|
9
|
+
export declare const MIRROR_MODES: MirrorMode[];
|
|
10
|
+
/** Returns `bitmap` unchanged for `"none"`, otherwise a flipped copy. */
|
|
11
|
+
export declare function mirrorBitmap(bitmap: Bitmap, mode: MirrorMode): Bitmap;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MIRROR_MODES = void 0;
|
|
4
|
+
exports.mirrorBitmap = mirrorBitmap;
|
|
5
|
+
exports.MIRROR_MODES = ["none", "horizontal", "vertical", "both"];
|
|
6
|
+
/** Returns `bitmap` unchanged for `"none"`, otherwise a flipped copy. */
|
|
7
|
+
function mirrorBitmap(bitmap, mode) {
|
|
8
|
+
if (mode === "none") {
|
|
9
|
+
return bitmap;
|
|
10
|
+
}
|
|
11
|
+
const { width, height } = bitmap;
|
|
12
|
+
const flipX = mode === "horizontal" || mode === "both";
|
|
13
|
+
const flipY = mode === "vertical" || mode === "both";
|
|
14
|
+
const data = new Uint8Array(bitmap.data.length);
|
|
15
|
+
for (let y = 0; y < height; y++) {
|
|
16
|
+
const srcY = flipY ? height - 1 - y : y;
|
|
17
|
+
for (let x = 0; x < width; x++) {
|
|
18
|
+
const srcX = flipX ? width - 1 - x : x;
|
|
19
|
+
const srcOffset = (srcY * width + srcX) * 4;
|
|
20
|
+
data.set(bitmap.data.subarray(srcOffset, srcOffset + 4), (y * width + x) * 4);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { width, height, data };
|
|
24
|
+
}
|
package/dist/repaintScheduler.js
CHANGED
|
@@ -23,6 +23,8 @@ const pluginVersion_1 = require("./pluginVersion");
|
|
|
23
23
|
const httpJson_1 = require("./httpJson");
|
|
24
24
|
const INTERVAL_POLL_MS = 60000;
|
|
25
25
|
const SUBSCRIPTION_DEBOUNCE_MS = 2000;
|
|
26
|
+
/** Pause between paint attempts - see `withRetries`' `delayMs`. */
|
|
27
|
+
const PAINT_RETRY_DELAY_MS = 5000;
|
|
26
28
|
const RESOURCES_API_PATH = "/signalk/v2/api/resources";
|
|
27
29
|
/**
|
|
28
30
|
* The most recent wall-clock instant at or before `now` that an `interval`-triggered device's own
|
|
@@ -173,7 +175,7 @@ async function assembleRawContext(app, apiUrl, bindings) {
|
|
|
173
175
|
}
|
|
174
176
|
function clearForceRepaint(app, friendlyName) {
|
|
175
177
|
const current = (0, config_1.readCurrentConfig)(app);
|
|
176
|
-
const devices = (current.devices ?? []).map((device) => device.friendlyName === friendlyName ? { ...device, forceRepaint: false } : device);
|
|
178
|
+
const devices = (current.devices ?? []).map((device) => device.friendlyName === friendlyName ? { ...device, advanced: { ...device.advanced, forceRepaint: false } } : device);
|
|
177
179
|
app.savePluginOptions({ ...current, devices }, (err) => {
|
|
178
180
|
if (err)
|
|
179
181
|
app.debug(`failed to clear forceRepaint for "${friendlyName}": ${err.message}`);
|
|
@@ -307,7 +309,7 @@ async function considerRepaint(app, config, device, target, state, getApiUrl) {
|
|
|
307
309
|
// scheduled tick" (e.g. a regenerated forecast) is the whole point of one - which is exactly why a
|
|
308
310
|
// provider-backed device should use `repaintTrigger: "interval"`, not `subscription`, for
|
|
309
311
|
// cost/battery reasons (each repaint may be a paid API call on the provider's side).
|
|
310
|
-
if (!provider && !templateChanged && !dataChanged && !device.forceRepaint) {
|
|
312
|
+
if (!provider && !templateChanged && !dataChanged && !device.advanced?.forceRepaint) {
|
|
311
313
|
app.debug(`${label}: data unchanged, skipping repaint`);
|
|
312
314
|
return;
|
|
313
315
|
}
|
|
@@ -325,7 +327,7 @@ async function considerRepaint(app, config, device, target, state, getApiUrl) {
|
|
|
325
327
|
? await provider.render({ templateName: device.templateName, context: renderContext, width, height, colours: metadata.colours })
|
|
326
328
|
: await renderer.render(templatePath, renderContext, width, height, templatesDir, config_1.BUNDLED_TEMPLATES_DIR);
|
|
327
329
|
succeeded = true;
|
|
328
|
-
repaintReason = device.forceRepaint
|
|
330
|
+
repaintReason = device.advanced?.forceRepaint
|
|
329
331
|
? "forced"
|
|
330
332
|
: provider
|
|
331
333
|
? "provider-rendered"
|
|
@@ -344,16 +346,29 @@ async function considerRepaint(app, config, device, target, state, getApiUrl) {
|
|
|
344
346
|
});
|
|
345
347
|
bitmap = await renderer.render(fallbackPath, { meta: { repainted: new Date().toISOString(), plugin_version: pluginVersion_1.PLUGIN_VERSION, description: device.description ?? "" } }, width, height, templatesDir, config_1.BUNDLED_TEMPLATES_DIR);
|
|
346
348
|
}
|
|
347
|
-
const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
349
|
+
const connectTimeoutMs = (device.advanced?.paintConnectTimeoutSeconds ?? config.paintConnectTimeoutSeconds) * 1000;
|
|
350
|
+
const gattBackend = config.useBleApi && app.bleApi ? (0, bleBackend_1.bleApiBackend)(app.bleApi, pluginVersion_1.PLUGIN_NAME) : undefined;
|
|
351
|
+
const attempts = Math.max(1, device.advanced?.paintRetries ?? config.paintRetries);
|
|
352
|
+
const paintWithRetries = () => (0, bleDiscovery_1.withRetries)(attempts, async (attempt) => {
|
|
353
|
+
app.debug(`${label}: attempting paint ${attempt}/${attempts}`);
|
|
352
354
|
const startedAt = Date.now();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
await driver.paint(bitmap, {
|
|
356
|
+
address,
|
|
357
|
+
pid: target.pid,
|
|
358
|
+
aesKey: device.advanced?.aesKey,
|
|
359
|
+
connectTimeoutMs,
|
|
360
|
+
reframe: device.reframe,
|
|
361
|
+
mirror: device.advanced?.mirror,
|
|
362
|
+
compress: device.advanced?.compress,
|
|
363
|
+
compressionFormat: device.advanced?.compressionFormat,
|
|
364
|
+
gattBackend,
|
|
365
|
+
});
|
|
355
366
|
paintDurationMs = Date.now() - startedAt;
|
|
367
|
+
}, {
|
|
368
|
+
delayMs: PAINT_RETRY_DELAY_MS,
|
|
369
|
+
onError: (err, attempt) => app.debug(`${label}: paint attempt ${attempt}/${attempts} failed: ${err.message}`),
|
|
356
370
|
});
|
|
371
|
+
await (gattBackend ? (0, bleBackend_1.exclusiveBleManagerAccess)(paintWithRetries) : paintWithRetries());
|
|
357
372
|
(0, discoveredDevicesStore_1.touchDiscoveredDevice)(app, { address, vendor: target.vendor, pid: target.pid, hwVersion: target.hwVersion, metadata });
|
|
358
373
|
// Only a successful render counts as "repainted" for dedup/catch-up purposes - a failure must not be
|
|
359
374
|
// recorded here (see this function's doc comment on why).
|
|
@@ -393,7 +408,7 @@ function startRepaintScheduler(app, config) {
|
|
|
393
408
|
// A single `forceRepaint` flag covers every target under `ALL_DEVICES` too - only clear it once
|
|
394
409
|
// every target has actually succeeded, so a target that failed still gets forced again next time
|
|
395
410
|
// instead of quietly reverting to ordinary hash-based dedup.
|
|
396
|
-
if (device.forceRepaint && results.every((result) => result.status === "fulfilled")) {
|
|
411
|
+
if (device.advanced?.forceRepaint && results.every((result) => result.status === "fulfilled")) {
|
|
397
412
|
clearForceRepaint(app, device.friendlyName);
|
|
398
413
|
}
|
|
399
414
|
};
|
|
@@ -440,7 +455,7 @@ function startRepaintScheduler(app, config) {
|
|
|
440
455
|
// still avoids a redundant paint once targets are resolved.
|
|
441
456
|
const startupCheckTimer = setTimeout(() => {
|
|
442
457
|
for (const device of config.devices) {
|
|
443
|
-
if (device.repaintTrigger === "interval" && !device.forceRepaint && device.device !== config_1.ALL_DEVICES) {
|
|
458
|
+
if (device.repaintTrigger === "interval" && !device.advanced?.forceRepaint && device.device !== config_1.ALL_DEVICES) {
|
|
444
459
|
const hours = device.intervalHours ?? 1;
|
|
445
460
|
const minute = device.intervalMinute ?? 0;
|
|
446
461
|
const dueSlot = mostRecentScheduledSlot(new Date(), hours, minute);
|
package/package.json
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-plugin",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-beta11",
|
|
4
4
|
"description": "Display SignalK data on eInk Electronic Shelf Labels, includes working examples for tide clock and watch schedule.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ble",
|
|
7
7
|
"display",
|
|
8
|
+
"e-ink",
|
|
9
|
+
"e-paper",
|
|
8
10
|
"eink",
|
|
11
|
+
"epaper",
|
|
9
12
|
"esl",
|
|
10
13
|
"gicisky",
|
|
11
14
|
"instrument",
|
|
15
|
+
"marine",
|
|
16
|
+
"shelf-label",
|
|
12
17
|
"signalk",
|
|
13
18
|
"signalk-category-hardware",
|
|
14
19
|
"signalk-category-instruments",
|
|
15
20
|
"signalk-node-server-plugin",
|
|
16
21
|
"tides",
|
|
17
22
|
"watch-schedule",
|
|
23
|
+
"wolink",
|
|
18
24
|
"zhsunyco"
|
|
19
25
|
],
|
|
20
26
|
"homepage": "https://signalk-einklabel.rhizomatics.org.uk/",
|
|
@@ -56,7 +62,7 @@
|
|
|
56
62
|
"@fontsource/roboto-serif": "^5.3.0",
|
|
57
63
|
"@naugehyde/node-ble": "^1.13.5",
|
|
58
64
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
59
|
-
"@xmldom/xmldom": "^0.9.
|
|
65
|
+
"@xmldom/xmldom": "^0.9.12",
|
|
60
66
|
"commander": "^15.0.0",
|
|
61
67
|
"form-data": "^4.0.6",
|
|
62
68
|
"luxon": "^3.7.2",
|
|
@@ -65,17 +71,17 @@
|
|
|
65
71
|
},
|
|
66
72
|
"devDependencies": {
|
|
67
73
|
"@signalk/server-api": "^2.32.0",
|
|
68
|
-
"@types/luxon": "^3.7.
|
|
69
|
-
"@types/node": "^26.
|
|
74
|
+
"@types/luxon": "^3.7.5",
|
|
75
|
+
"@types/node": "^26.6.0",
|
|
70
76
|
"@types/pngjs": "^6.0.5",
|
|
71
|
-
"oxfmt": "^0.
|
|
72
|
-
"oxlint": "^1.
|
|
77
|
+
"oxfmt": "^0.70.0",
|
|
78
|
+
"oxlint": "^1.85.0",
|
|
73
79
|
"oxlint-tsgolint": "^7.0.2001",
|
|
74
80
|
"ts-node": "^10.9.2",
|
|
75
81
|
"typescript": "^5.5.0"
|
|
76
82
|
},
|
|
77
83
|
"engines": {
|
|
78
|
-
"node": ">=
|
|
84
|
+
"node": ">=22"
|
|
79
85
|
},
|
|
80
86
|
"signalk": {
|
|
81
87
|
"displayName": "eInk Label Displays",
|
|
@@ -87,9 +93,9 @@
|
|
|
87
93
|
"docs/assets/screenshots/label_config.png"
|
|
88
94
|
],
|
|
89
95
|
"recommends": [
|
|
96
|
+
"@rhizomatics/signalk-einklabel-genai-plugin",
|
|
90
97
|
"signalk-watch-schedule",
|
|
91
|
-
"signalk-tides"
|
|
92
|
-
"@rhizomatics/signalk-einklabel-genai-plugin"
|
|
98
|
+
"signalk-tides"
|
|
93
99
|
]
|
|
94
100
|
}
|
|
95
101
|
}
|