@rhizomatics/signalk-einklabel-plugin 1.3.0-beta6 → 1.3.0-beta7
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.
|
@@ -205,11 +205,11 @@ function bleApiBackend(bleApi, pluginId) {
|
|
|
205
205
|
// whatever the claim currently is, connected or still connecting, regardless of how the
|
|
206
206
|
// original `connectGATT()` promise eventually settles - disconnecting it too if it does still
|
|
207
207
|
// resolve afterwards, harmlessly, since a connection the server already released is a no-op to
|
|
208
|
-
// disconnect again.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
// disconnect again. The release is awaited (only the late disconnect is left in the
|
|
209
|
+
// background) so a caller retrying straight away - `withRetries` - can't race it with a new
|
|
210
|
+
// `connectGATT()` and get rejected with "has a GATT claim in progress" for its trouble.
|
|
211
|
+
await bleApi.releaseGATTDevice(address, pluginId).catch(() => { });
|
|
212
|
+
void connecting.then((c) => c.disconnect()).catch(() => { });
|
|
213
213
|
throw new Error(`connecting to device timed out after ${timeoutMs}ms`);
|
|
214
214
|
}
|
|
215
215
|
return withSessionWatchdog(conn, GATT_SESSION_WATCHDOG_MS, async () => {
|
|
@@ -34,9 +34,16 @@ export declare function forEachAdvertisedDevice(adapter: Adapter, fn: (advertise
|
|
|
34
34
|
/**
|
|
35
35
|
* Retries `fn` up to `attempts` times (including the first try), returning on the first success -
|
|
36
36
|
* shared by the repaint scheduler and the CLI's `paint` command so one flaky BLE connection
|
|
37
|
-
* doesn't fail a whole repaint after a single bad attempt.
|
|
37
|
+
* doesn't fail a whole repaint after a single bad attempt. `delayMs` pauses between attempts, giving
|
|
38
|
+
* the adapter (and any BLE Manager claim the failed attempt held) a moment to settle rather than
|
|
39
|
+
* hitting the device again in the same tick. `onError` sees every failed attempt's error, not just the
|
|
40
|
+
* last one that's eventually thrown - an early attempt's error is often the real cause, with later
|
|
41
|
+
* ones just fallout from it.
|
|
38
42
|
*/
|
|
39
|
-
export declare function withRetries<T>(attempts: number, fn: (attempt: number) => Promise<T
|
|
43
|
+
export declare function withRetries<T>(attempts: number, fn: (attempt: number) => Promise<T>, { delayMs, onError }?: {
|
|
44
|
+
delayMs?: number;
|
|
45
|
+
onError?: (err: unknown, attempt: number) => void;
|
|
46
|
+
}): Promise<T>;
|
|
40
47
|
/**
|
|
41
48
|
* Opens exactly one BLE discovery window and one D-Bus/BlueZ session, then hands the adapter to
|
|
42
49
|
* `fn` - shared by `plugin.ts`'s startup scan and the CLI's `scan` command so scanning across
|
|
@@ -68,16 +68,25 @@ async function forEachAdvertisedDevice(adapter, fn) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Retries `fn` up to `attempts` times (including the first try), returning on the first success -
|
|
70
70
|
* shared by the repaint scheduler and the CLI's `paint` command so one flaky BLE connection
|
|
71
|
-
* doesn't fail a whole repaint after a single bad attempt.
|
|
71
|
+
* doesn't fail a whole repaint after a single bad attempt. `delayMs` pauses between attempts, giving
|
|
72
|
+
* the adapter (and any BLE Manager claim the failed attempt held) a moment to settle rather than
|
|
73
|
+
* hitting the device again in the same tick. `onError` sees every failed attempt's error, not just the
|
|
74
|
+
* last one that's eventually thrown - an early attempt's error is often the real cause, with later
|
|
75
|
+
* ones just fallout from it.
|
|
72
76
|
*/
|
|
73
|
-
async function withRetries(attempts, fn) {
|
|
77
|
+
async function withRetries(attempts, fn, { delayMs = 0, onError } = {}) {
|
|
78
|
+
const total = Math.max(1, attempts);
|
|
74
79
|
let lastErr;
|
|
75
|
-
for (let attempt = 1; attempt <=
|
|
80
|
+
for (let attempt = 1; attempt <= total; attempt++) {
|
|
76
81
|
try {
|
|
77
82
|
return await fn(attempt);
|
|
78
83
|
}
|
|
79
84
|
catch (err) {
|
|
80
85
|
lastErr = err;
|
|
86
|
+
onError?.(err, attempt);
|
|
87
|
+
if (attempt < total && delayMs > 0) {
|
|
88
|
+
await sleep(delayMs);
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
throw lastErr;
|
|
@@ -53,19 +53,23 @@ class GiciskyDriver {
|
|
|
53
53
|
*/
|
|
54
54
|
const manufacturerData = await backend.waitForManufacturerData(config.address, protocol_1.GICISKY_MANUFACTURER_ID, MANUFACTURER_DATA_RESCAN_TIMEOUT_MS);
|
|
55
55
|
const info = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
|
|
56
|
+
// A device that's quiet right now (e.g. still refreshing from the last paint) can still be painted
|
|
57
|
+
// when its PID is already known from config/a previous scan - the connect below doesn't need the
|
|
58
|
+
// advertisement, only this lookup does.
|
|
59
|
+
const pid = info?.deviceId ?? config.pid;
|
|
56
60
|
const metadata = config.modelOverride
|
|
57
|
-
? { pid:
|
|
58
|
-
:
|
|
59
|
-
? this.metadataForPid(
|
|
61
|
+
? { pid: pid ?? 0, ...config.modelOverride }
|
|
62
|
+
: pid !== undefined
|
|
63
|
+
? this.metadataForPid(pid)
|
|
60
64
|
: undefined;
|
|
61
65
|
if (!metadata) {
|
|
62
|
-
throw new Error(
|
|
66
|
+
throw new Error(pid === undefined
|
|
63
67
|
? "gicisky device isn't advertising - rescanned but got nothing back (out of range, asleep, or already connected " +
|
|
64
68
|
"elsewhere) - pass --width/--height/--voffset/--colours to describe it manually"
|
|
65
|
-
: `gicisky device reports unrecognised deviceId 0x${
|
|
69
|
+
: `gicisky device reports unrecognised deviceId 0x${pid.toString(16).padStart(4, "0")} - ` +
|
|
66
70
|
"pass --width/--height/--voffset/--colours to describe it manually");
|
|
67
71
|
}
|
|
68
|
-
const layout = (
|
|
72
|
+
const layout = (pid !== undefined && layout_1.GICISKY_PID_LAYOUT[pid]) || (0, layout_1.defaultLayoutFor)(metadata.colours);
|
|
69
73
|
const framed = (0, reframe_1.reframeBitmap)(bitmap, metadata.width, metadata.height, config.reframe ?? "crop");
|
|
70
74
|
const payload = (0, encode_1.encodeBitmap)(framed, metadata, layout);
|
|
71
75
|
const conn = await backend.connectGatt(config.address, config.connectTimeoutMs ?? DEFAULT_PAINT_CONNECT_TIMEOUT_MS);
|
|
@@ -89,6 +93,12 @@ class GiciskyDriver {
|
|
|
89
93
|
const chunk = payload.subarray(part * chunkSize, Math.min(part * chunkSize + chunkSize, payload.length));
|
|
90
94
|
const ackData = await writeAndAwaitAck(conn, imgServiceUuid, imgUuid, (0, protocol_1.imageChunkPacket)(part, chunk), ack);
|
|
91
95
|
const decoded = (0, protocol_1.decodeTransferAck)(ackData);
|
|
96
|
+
if (decoded && !decoded.ok && part * chunkSize + chunk.length >= payload.length) {
|
|
97
|
+
// The device answers the final chunk with a non-zero status (seen: `05 08 00000000`) once
|
|
98
|
+
// it has the whole image and starts refreshing - the transfer's done, not failed. Matches
|
|
99
|
+
// hass-gicisky's writer, which ends the transfer on any non-zero status rather than erroring.
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
92
102
|
if (!decoded?.ok) {
|
|
93
103
|
throw new Error(`gicisky device reported an error transferring image part ${part}: ${ackData.toString("hex")}`);
|
|
94
104
|
}
|
package/dist/devices/types.d.ts
CHANGED
|
@@ -53,6 +53,13 @@ export type DeviceModelOverride = Omit<DeviceMetadata, "pid">;
|
|
|
53
53
|
/** Per-device settings the user supplies when registering a device, beyond what's in DeviceMetadata. */
|
|
54
54
|
export interface VendorDeviceConfig {
|
|
55
55
|
address: string;
|
|
56
|
+
/**
|
|
57
|
+
* The PID already known for this device (from the configured `"<vendor>:<pid>@<address>"` or a
|
|
58
|
+
* previous scan) - a fallback for drivers that otherwise only learn it from a fresh advertisement
|
|
59
|
+
* (gicisky), so a device that's quiet at paint time can still be painted. A live advertised PID
|
|
60
|
+
* takes precedence when there is one.
|
|
61
|
+
*/
|
|
62
|
+
pid?: number;
|
|
56
63
|
/** AES key for vendors that need it, entered by the user. If omitted, vendors that have one may fall back to a stock/manufacturer-default key instead of failing. */
|
|
57
64
|
aesKey?: string;
|
|
58
65
|
/** Forces the device model facts instead of looking up the advertised PID - for hardware not yet in the driver's table. */
|
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
|
|
@@ -346,13 +348,22 @@ async function considerRepaint(app, config, device, target, state, getApiUrl) {
|
|
|
346
348
|
}
|
|
347
349
|
const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
|
|
348
350
|
const gattBackend = config.useBleApi && app.bleApi ? (0, bleBackend_1.bleApiBackend)(app.bleApi, pluginVersion_1.PLUGIN_NAME) : undefined;
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
351
|
+
const attempts = Math.max(1, config.paintRetries);
|
|
352
|
+
const paintWithRetries = () => (0, bleDiscovery_1.withRetries)(attempts, async (attempt) => {
|
|
353
|
+
app.debug(`${label}: attempting paint ${attempt}/${attempts}`);
|
|
353
354
|
const startedAt = Date.now();
|
|
354
|
-
await driver.paint(bitmap, {
|
|
355
|
+
await driver.paint(bitmap, {
|
|
356
|
+
address,
|
|
357
|
+
pid: target.pid,
|
|
358
|
+
aesKey: device.aesKey,
|
|
359
|
+
connectTimeoutMs,
|
|
360
|
+
reframe: device.reframe,
|
|
361
|
+
gattBackend,
|
|
362
|
+
});
|
|
355
363
|
paintDurationMs = Date.now() - startedAt;
|
|
364
|
+
}, {
|
|
365
|
+
delayMs: PAINT_RETRY_DELAY_MS,
|
|
366
|
+
onError: (err, attempt) => app.debug(`${label}: paint attempt ${attempt}/${attempts} failed: ${err.message}`),
|
|
356
367
|
});
|
|
357
368
|
await (gattBackend ? (0, bleBackend_1.exclusiveBleManagerAccess)(paintWithRetries) : paintWithRetries());
|
|
358
369
|
(0, discoveredDevicesStore_1.touchDiscoveredDevice)(app, { address, vendor: target.vendor, pid: target.pid, hwVersion: target.hwVersion, metadata });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-plugin",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-beta7",
|
|
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",
|