@rhizomatics/signalk-einklabel-plugin 1.3.0-beta5 → 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.
- package/CHANGELOG.md +0 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +2 -1
- package/dist/devices/bleBackend.js +5 -5
- 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/index.js +16 -6
- package/dist/devices/types.d.ts +7 -0
- package/dist/plugin.js +9 -0
- package/dist/repaintScheduler.js +16 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -71,6 +71,9 @@ export interface PluginConfig {
|
|
|
71
71
|
* signalk-bluetti-plugin does. Off by default - a `device: ALL_DEVICES` entry scans on demand the
|
|
72
72
|
* first time it has nothing discovered yet (see `resolveTargets` in `repaintScheduler.ts`), and an
|
|
73
73
|
* explicit device selection only ever needed this to populate the dropdown once at initial setup.
|
|
74
|
+
* Also unnecessary with `useBleApi` on, which discovers continuously in the background instead (see
|
|
75
|
+
* `startBleApiDiscoveryListener` in `discoveryCoordinator.ts`) - kept meaningful mainly for direct
|
|
76
|
+
* BlueZ access, which has no continuous-scan equivalent to piggyback on.
|
|
74
77
|
*/
|
|
75
78
|
scanOnStart: boolean;
|
|
76
79
|
/** How long the startup scan runs, in seconds. */
|
package/dist/config.js
CHANGED
|
@@ -333,7 +333,8 @@ function configSchema(app, discovered = []) {
|
|
|
333
333
|
type: "boolean",
|
|
334
334
|
title: "Scan for devices on plugin start",
|
|
335
335
|
description: 'Runs a short BLE scan so discovered devices show up in a device\'s "Device" picker below. ' +
|
|
336
|
-
'Not needed if every device uses "All discovered devices"
|
|
336
|
+
'Not needed if every device uses "All discovered devices" (that scans on demand instead), or if ' +
|
|
337
|
+
'"Use BLE Manager" below is on (that discovers continuously in the background instead of needing a scan).',
|
|
337
338
|
default: defaults.scanOnStart,
|
|
338
339
|
},
|
|
339
340
|
scanDurationSeconds: {
|
|
@@ -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;
|
|
@@ -18,3 +18,21 @@ export declare function scanInProgressSince(): number | undefined;
|
|
|
18
18
|
* one, which would make both fail.
|
|
19
19
|
*/
|
|
20
20
|
export declare function ensureScan(app: ServerAPI, durationSeconds: number, useBleApi: boolean): Promise<ScanResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Keeps the "Device" picker (and `ALL_DEVICES`) current for as long as the plugin runs, without any
|
|
23
|
+
* bounded "scan" step at all - unlike `runScanViaBleApi`/`runScanViaBlueZ` above, which only ever look
|
|
24
|
+
* for new devices during an explicit, timed window (`scanOnStart`, or an on-demand `ALL_DEVICES` scan).
|
|
25
|
+
* BLE Manager already runs its own continuous scan server-side to build the device list the admin UI's
|
|
26
|
+
* "BLE Manager" page shows - a device that's visible there but never appears in *this* plugin's own
|
|
27
|
+
* picker unless `scanOnStart` happens to be ticked is exactly that mismatch: nothing was ever listening
|
|
28
|
+
* for BLE Manager's advertisements outside a bounded window. This instead subscribes once, for good,
|
|
29
|
+
* so a device shows up here as soon as BLE Manager itself has seen it - no manual scan required.
|
|
30
|
+
*
|
|
31
|
+
* Only runs a genuinely new (or long-expired, see `DISCOVERED_DEVICE_TTL_MS`) address through
|
|
32
|
+
* `driver.identifyDevice()` - a real GATT connect+read. A device already in the persisted store just
|
|
33
|
+
* gets `lastSeenAt` bumped via `touchDiscoveredDevice`, since re-identifying it on every ~1s
|
|
34
|
+
* advertisement would otherwise hammer it (and the shared GATT slots) for no reason. `pending` guards
|
|
35
|
+
* against two advertisements for the same brand-new device starting a second identify attempt while
|
|
36
|
+
* the first is still connecting.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startBleApiDiscoveryListener(app: ServerAPI, pluginId: string): () => void;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scanInProgressSince = scanInProgressSince;
|
|
4
4
|
exports.ensureScan = ensureScan;
|
|
5
|
+
exports.startBleApiDiscoveryListener = startBleApiDiscoveryListener;
|
|
5
6
|
const bleDiscovery_1 = require("./bleDiscovery");
|
|
6
7
|
const bleBackend_1 = require("./bleBackend");
|
|
7
8
|
const discoveredDevicesStore_1 = require("./discoveredDevicesStore");
|
|
@@ -67,43 +68,55 @@ async function runScanViaBlueZ(app, durationSeconds) {
|
|
|
67
68
|
const merged = (0, discoveredDevicesStore_1.recordScanResults)(app, foundThisScan);
|
|
68
69
|
return { foundThisScan, merged };
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The `matchesAdvertisement`+`identifyDevice` half of processing one BLE Manager sighting - shared by
|
|
73
|
+
* `runScanViaBleApi`'s bounded scan and `startBleApiDiscoveryListener`'s indefinite one below. Doesn't
|
|
74
|
+
* decide *whether* to attempt identification (a bounded scan's own per-call `seen` set vs. the
|
|
75
|
+
* listener's persisted-store check need different answers to that) - just does the attempt and reports
|
|
76
|
+
* what it found, or `undefined` for "no matching driver"/"identify failed". `adv.manufacturerData`
|
|
77
|
+
* (from either an advertisement or, seeded, a `getDevices()` entry) is `Record<number, string>`
|
|
78
|
+
* (decimal company ID -> hex-encoded payload) rather than node-ble's `Buffer` - converted here so
|
|
79
|
+
* drivers never see the difference.
|
|
80
|
+
*/
|
|
81
|
+
async function identifyOne(app, backend, mac, name, rssi, manufacturerDataHex) {
|
|
82
|
+
const [manufacturerIdKey] = Object.keys(manufacturerDataHex ?? {});
|
|
83
|
+
const manufacturerId = manufacturerIdKey === undefined ? undefined : Number(manufacturerIdKey);
|
|
84
|
+
const manufacturerData = manufacturerId === undefined ? undefined : Buffer.from(manufacturerDataHex[manufacturerId], "hex");
|
|
85
|
+
const driver = (0, registry_1.allDrivers)().find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
86
|
+
if (!driver) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const connect = () => backend.connectGatt(mac, SCAN_GATT_CONNECT_TIMEOUT_MS);
|
|
90
|
+
const found = await driver.identifyDevice({ address: mac, name, manufacturerId, manufacturerData, rssi }, connect).catch((err) => {
|
|
91
|
+
app.debug(`${driver.vendor} discovery failed for [${mac}]: ${err.message}\n${err.stack ?? ""}`);
|
|
92
|
+
return undefined;
|
|
93
|
+
});
|
|
94
|
+
if (found) {
|
|
95
|
+
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "unknown";
|
|
96
|
+
const hwid = found.hwVersion ?? "unknown";
|
|
97
|
+
app.debug(`discovered ${driver.vendor} device "${found.name ?? ""}" [${found.address}] pid=${pid} hwid=${hwid}`);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
70
101
|
/**
|
|
71
102
|
* Same as `runScanViaBlueZ` but sourced from the SignalK server's BLE Manager API instead of a direct
|
|
72
103
|
* BlueZ discovery session - subscribes to the merged advertisement stream for `durationSeconds`,
|
|
73
104
|
* seeded with whatever the server already knows about (mirrors `BleManagerScanner` in
|
|
74
|
-
* signalk-bluetti-plugin).
|
|
75
|
-
* directly comparable to node-ble's `Buffer` - `adv.manufacturerData` is `Record<number, string>`
|
|
76
|
-
* (decimal company ID -> hex-encoded payload), converted here so drivers never see the difference.
|
|
105
|
+
* signalk-bluetti-plugin).
|
|
77
106
|
*/
|
|
78
107
|
async function runScanViaBleApi(app, durationSeconds) {
|
|
79
108
|
const foundThisScan = [];
|
|
80
109
|
const seen = new Set();
|
|
81
|
-
const drivers = (0, registry_1.allDrivers)();
|
|
82
110
|
const backend = (0, bleBackend_1.bleApiBackend)(app.bleApi, pluginVersion_1.PLUGIN_NAME);
|
|
83
111
|
const identify = async (mac, name, rssi, manufacturerDataHex) => {
|
|
84
112
|
if (seen.has(mac)) {
|
|
85
113
|
return;
|
|
86
114
|
}
|
|
87
|
-
const [manufacturerIdKey] = Object.keys(manufacturerDataHex ?? {});
|
|
88
|
-
const manufacturerId = manufacturerIdKey === undefined ? undefined : Number(manufacturerIdKey);
|
|
89
|
-
const manufacturerData = manufacturerId === undefined ? undefined : Buffer.from(manufacturerDataHex[manufacturerId], "hex");
|
|
90
|
-
const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
91
|
-
if (!driver) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
115
|
seen.add(mac);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return undefined;
|
|
99
|
-
});
|
|
100
|
-
if (!found) {
|
|
101
|
-
return;
|
|
116
|
+
const found = await identifyOne(app, backend, mac, name, rssi, manufacturerDataHex);
|
|
117
|
+
if (found) {
|
|
118
|
+
foundThisScan.push(found);
|
|
102
119
|
}
|
|
103
|
-
foundThisScan.push(found);
|
|
104
|
-
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "unknown";
|
|
105
|
-
const hwid = found.hwVersion ?? "unknown";
|
|
106
|
-
app.debug(`discovered ${driver.vendor} device "${found.name ?? ""}" [${found.address}] pid=${pid} hwid=${hwid}`);
|
|
107
120
|
};
|
|
108
121
|
try {
|
|
109
122
|
const unsubscribe = app.bleApi.onAdvertisement(pluginVersion_1.PLUGIN_NAME, (adv) => {
|
|
@@ -129,3 +142,57 @@ async function runScanViaBleApi(app, durationSeconds) {
|
|
|
129
142
|
const merged = (0, discoveredDevicesStore_1.recordScanResults)(app, foundThisScan);
|
|
130
143
|
return { foundThisScan, merged };
|
|
131
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Keeps the "Device" picker (and `ALL_DEVICES`) current for as long as the plugin runs, without any
|
|
147
|
+
* bounded "scan" step at all - unlike `runScanViaBleApi`/`runScanViaBlueZ` above, which only ever look
|
|
148
|
+
* for new devices during an explicit, timed window (`scanOnStart`, or an on-demand `ALL_DEVICES` scan).
|
|
149
|
+
* BLE Manager already runs its own continuous scan server-side to build the device list the admin UI's
|
|
150
|
+
* "BLE Manager" page shows - a device that's visible there but never appears in *this* plugin's own
|
|
151
|
+
* picker unless `scanOnStart` happens to be ticked is exactly that mismatch: nothing was ever listening
|
|
152
|
+
* for BLE Manager's advertisements outside a bounded window. This instead subscribes once, for good,
|
|
153
|
+
* so a device shows up here as soon as BLE Manager itself has seen it - no manual scan required.
|
|
154
|
+
*
|
|
155
|
+
* Only runs a genuinely new (or long-expired, see `DISCOVERED_DEVICE_TTL_MS`) address through
|
|
156
|
+
* `driver.identifyDevice()` - a real GATT connect+read. A device already in the persisted store just
|
|
157
|
+
* gets `lastSeenAt` bumped via `touchDiscoveredDevice`, since re-identifying it on every ~1s
|
|
158
|
+
* advertisement would otherwise hammer it (and the shared GATT slots) for no reason. `pending` guards
|
|
159
|
+
* against two advertisements for the same brand-new device starting a second identify attempt while
|
|
160
|
+
* the first is still connecting.
|
|
161
|
+
*/
|
|
162
|
+
function startBleApiDiscoveryListener(app, pluginId) {
|
|
163
|
+
const backend = (0, bleBackend_1.bleApiBackend)(app.bleApi, pluginId);
|
|
164
|
+
const pending = new Set();
|
|
165
|
+
const identify = async (mac, name, rssi, manufacturerDataHex) => {
|
|
166
|
+
const existing = (0, discoveredDevicesStore_1.loadDiscoveredDevices)(app)[mac];
|
|
167
|
+
if (existing) {
|
|
168
|
+
if (Date.now() - existing.lastSeenAt < discoveredDevicesStore_1.DISCOVERED_DEVICE_TTL_MS) {
|
|
169
|
+
(0, discoveredDevicesStore_1.touchDiscoveredDevice)(app, existing);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (pending.has(mac)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
pending.add(mac);
|
|
177
|
+
try {
|
|
178
|
+
const found = await identifyOne(app, backend, mac, name, rssi, manufacturerDataHex);
|
|
179
|
+
if (found) {
|
|
180
|
+
(0, discoveredDevicesStore_1.recordScanResults)(app, [found]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
pending.delete(mac);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const unsubscribe = app.bleApi.onAdvertisement(pluginId, (adv) => {
|
|
188
|
+
void identify(adv.mac, adv.name, adv.rssi, adv.manufacturerData);
|
|
189
|
+
});
|
|
190
|
+
// Seeds from whatever BLE Manager already knows about (e.g. a device it saw before this plugin
|
|
191
|
+
// subscribed) - same manufacturer-data caveat as `runScanViaBleApi`'s equivalent seed loop; a driver
|
|
192
|
+
// that needs it (rather than matching on name) picks it up once a fresh advertisement arrives above.
|
|
193
|
+
void app.bleApi
|
|
194
|
+
.getDevices()
|
|
195
|
+
.then((known) => Promise.all(known.map((device) => identify(device.mac, device.name, device.rssi, undefined))))
|
|
196
|
+
.catch((err) => app.debug(`discovery listener: could not read known devices: ${err.message}`));
|
|
197
|
+
return unsubscribe;
|
|
198
|
+
}
|
|
@@ -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/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",
|
|
@@ -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
|
};
|
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",
|