@rhizomatics/signalk-einklabel-plugin 1.2.3 → 1.3.0-beta1
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 +4 -0
- package/README.md +55 -6
- package/dist/cli/index.js +5 -5
- package/dist/config.d.ts +7 -0
- package/dist/config.js +14 -0
- package/dist/devices/bleBackend.d.ts +21 -0
- package/dist/devices/bleBackend.js +96 -0
- package/dist/devices/bleDiscovery.d.ts +11 -0
- package/dist/devices/bleDiscovery.js +89 -0
- package/dist/devices/discoveryCoordinator.d.ts +7 -6
- package/dist/devices/discoveryCoordinator.js +81 -9
- package/dist/devices/gattConnection.d.ts +9 -0
- package/dist/devices/gattConnection.js +2 -0
- package/dist/devices/gicisky/index.d.ts +7 -2
- package/dist/devices/gicisky/index.js +107 -96
- package/dist/devices/types.d.ts +26 -6
- package/dist/devices/zhsunyco/index.d.ts +8 -2
- package/dist/devices/zhsunyco/index.js +83 -79
- package/dist/plugin.js +27 -9
- package/dist/repaintScheduler.js +4 -2
- package/docs/assets/images/mini_tidal_clock.png +0 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# 1.3.0
|
|
2
|
+
|
|
3
|
+
First implementation of using new SignalK BLE Manager rather than directly using the `bluez` services. Off by default until longer term stability demonstrated.
|
|
4
|
+
|
|
1
5
|
# 1.2.3
|
|
2
6
|
|
|
3
7
|
- Improved example tide template for 2.9" Gicisky, and added blank and error templates
|
package/README.md
CHANGED
|
@@ -24,22 +24,31 @@ Being battery operated, they can be stuck on anywhere without wiring - the only
|
|
|
24
24
|
|
|
25
25
|
Unlike some eInk projects, this plugin doesn't require any physical modification to the labels, or loading any new firmware. It can send an image to a supported shelf label fresh out of the box.
|
|
26
26
|
|
|
27
|
-
Most of requirements below are to make SignalK work with Bluetooth Low Energy, which is good thing to have anyway, since vendors like Victron, Switchbot, Ruuvi and others have BLE enabled hardware that's useful to have on a boat.
|
|
27
|
+
Most of requirements below are to make SignalK work with Bluetooth Low Energy, which is good thing to have anyway, since vendors like Victron, Switchbot, Ruuvi and others have BLE enabled hardware that's useful to have on a boat.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
This plugin can reach BLE hardware two ways - pick whichever fits your setup:
|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- **Direct BlueZ access** (default) - the plugin talks to BlueZ over D-Bus itself. Requirements 1-3 below apply.
|
|
32
|
+
- **SignalK BLE Manager API** (opt-in) - SignalK server >= 2.32.0 ships a [BLE Provider/Consumer API](https://github.com/SignalK/signalk-server/issues/2411) (admin UI: "BLE Manager") that arbitrates adapter access across every BLE-consuming plugin instead of each one grabbing `hci0` for itself, and can source BLE over a remote gateway instead of local hardware at all. Enable the "Use the SignalK BLE Manager API" setting in this plugin's config once it's available (it only appears once the running server has it) - requirements 1-3 below then become the SignalK server's problem, under its own Bluetooth admin settings, not this plugin's.
|
|
33
|
+
|
|
34
|
+
1. A SignalK server, **running Linux** (direct BlueZ mode only - BLE Manager mode with a remote gateway provider has no such requirement)
|
|
35
|
+
|
|
36
|
+
- MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble) in direct BlueZ mode, however they can be used for template development and
|
|
32
37
|
debugging (everything except `scan` and `paint`)
|
|
33
38
|
|
|
34
|
-
2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy).
|
|
39
|
+
2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy) - direct BlueZ mode only; in BLE Manager mode this is whatever the server's own Bluetooth settings provide.
|
|
35
40
|
|
|
36
|
-
- Bluetooth adapters for Linux can be tricky
|
|
41
|
+
- Bluetooth adapters for Linux can be tricky
|
|
42
|
+
- 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
|
|
43
|
+
- 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
|
|
37
44
|
- Some Raspberry Pi models come with suitable Bluetooth built in
|
|
45
|
+
- See advice at [Recommended Bluetooth Adapters for Linux](https://github.com/morrownr/USB-WiFi/blob/main/home/Recommended_Bluetooth_Adapters_for_Linux.md)
|
|
46
|
+
- 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.
|
|
38
47
|
|
|
39
48
|
> - Don't worry about the very latest Bluetooth versions, 4.0 is minimum for BLE, 5.0 is nice
|
|
40
49
|
> - Home Assistant is massively more popular than SignalK, and often also run on Raspberry Pi and similar, so good source of advice
|
|
41
50
|
|
|
42
|
-
3. `bluez` package installed in Linux
|
|
51
|
+
3. `bluez` package installed in Linux - direct BlueZ mode only
|
|
43
52
|
|
|
44
53
|
- No need to do this if you have a Raspberry Pi with recent Raspian version, since bluez comes built in.
|
|
45
54
|
- If you're not running a Raspberry Pi, then ensure that the `dbus` package is installed
|
|
@@ -82,6 +91,10 @@ Use the standard configuration option in the SignalK menu for the plugin.
|
|
|
82
91
|
|
|
83
92
|
Template available as 416x240-BWRY for 3.7" ESLs and a simpler template, sized 250x128, also BWRY, for the cheapest 2.13" labels.
|
|
84
93
|
|
|
94
|
+
This mini tide clock is a 2.9" Gicisky device, less than £10 inc delivery in summer 2026.
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
85
98
|
#### Pre-requisites
|
|
86
99
|
|
|
87
100
|
A _tides_ provider plugin for the Resources API installed and enabled, currently one of:
|
|
@@ -461,6 +474,8 @@ That's the bundled fallback warning, not necessarily an error in this plugin - i
|
|
|
461
474
|
|
|
462
475
|
### SignalK starts before the Bluetooth daemon — does the plugin need `bluetoothd` running at boot?
|
|
463
476
|
|
|
477
|
+
This and the next entry 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.
|
|
478
|
+
|
|
464
479
|
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.
|
|
465
480
|
|
|
466
481
|
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:
|
|
@@ -486,6 +501,40 @@ sudo systemctl restart signalk
|
|
|
486
501
|
|
|
487
502
|
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.
|
|
488
503
|
|
|
504
|
+
### Bluetooth Dongle with "No gpio to reset"
|
|
505
|
+
|
|
506
|
+
Example log:
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
Bluetooth: hci0: No gpio to reset Realtek device, ignoring
|
|
510
|
+
Bluetooth: hci0: Unable to disable scanning: -110
|
|
511
|
+
Bluetooth: hci0: command 0x2042 tx timeout
|
|
512
|
+
Bluetooth: hci0: Opcode 0x2042 failed: -110
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
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.
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
#### Example udev rule fix
|
|
520
|
+
|
|
521
|
+
Following file created at `/etc/udev/rules.d/99-bt500-no-autosuspend.rules`
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
# Disable USB autosuspend for the ASUS USB-BT500 (RTL8761BU, 0b05:190e).
|
|
525
|
+
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0b05", ATTR{idProduct}=="190e", TEST=="power/control", ATTR{power/control}="on"
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
If `tlp` running to minimize power, it may have its own rules trying to suspend the Bluetooth dongle.
|
|
529
|
+
|
|
530
|
+
#### Example tlp fix
|
|
531
|
+
|
|
532
|
+
Following file created at /etc/tlp.d/99-bt500-no-autosuspend.conf
|
|
533
|
+
|
|
534
|
+
```
|
|
535
|
+
USB_DENYLIST="0b05:190e"
|
|
536
|
+
```
|
|
537
|
+
|
|
489
538
|
## Other ESL and General eInk Resources
|
|
490
539
|
|
|
491
540
|
- [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
|
package/dist/cli/index.js
CHANGED
|
@@ -179,18 +179,18 @@ exports.program
|
|
|
179
179
|
await (0, bleDiscovery_1.forEachAdvertisedDevice)(adapter, async ({ device, address, name, manufacturerId, manufacturerData }) => {
|
|
180
180
|
const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
181
181
|
const mfr = manufacturerId !== undefined ? `0x${manufacturerId.toString(16).padStart(4, "0")}` : "";
|
|
182
|
+
const rssi = await device
|
|
183
|
+
.getRSSI()
|
|
184
|
+
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
185
|
+
.catch(() => undefined);
|
|
182
186
|
if (!driver) {
|
|
183
187
|
if (opts.allDevices) {
|
|
184
|
-
const rssi = await device
|
|
185
|
-
.getRSSI()
|
|
186
|
-
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
187
|
-
.catch(() => undefined);
|
|
188
188
|
rows.push(["(unmatched)", address, name ?? "", "", "", "", mfr, "", String(rssi ?? "")]);
|
|
189
189
|
}
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
192
|
matchedCount++;
|
|
193
|
-
const found = await driver.identifyDevice(
|
|
193
|
+
const found = await driver.identifyDevice({ address, name, manufacturerId, manufacturerData, rssi }, () => (0, bleDiscovery_1.connectWithTimeout)(device, VENDOR_IDENTIFY_TIMEOUT_MS).then(() => (0, bleDiscovery_1.openNodeBleGattConnection)(device)));
|
|
194
194
|
(0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
|
|
195
195
|
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "";
|
|
196
196
|
const hwid = found.hwVersion ? `0x${found.hwVersion}` : "";
|
package/dist/config.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface PluginConfig {
|
|
|
75
75
|
scanOnStart: boolean;
|
|
76
76
|
/** How long the startup scan runs, in seconds. */
|
|
77
77
|
scanDurationSeconds: number;
|
|
78
|
+
/**
|
|
79
|
+
* Route BLE access through the SignalK server's BLE Manager API (`app.bleApi`, server >= 2.32.0)
|
|
80
|
+
* instead of connecting to BlueZ directly, so this plugin shares the adapter with other BLE plugins
|
|
81
|
+
* instead of contending for it - see `bleBackend.ts`. Off by default, and only ever offered in the
|
|
82
|
+
* config schema when the running server actually has `app.bleApi` (see `configSchema` below).
|
|
83
|
+
*/
|
|
84
|
+
useBleApi: boolean;
|
|
78
85
|
/** How long to wait for a device to accept a BLE connection before giving up on a repaint attempt, in seconds. */
|
|
79
86
|
paintConnectTimeoutSeconds: number;
|
|
80
87
|
/** How many times to attempt a repaint (including the first try) before giving up and reporting failure. */
|
package/dist/config.js
CHANGED
|
@@ -51,6 +51,7 @@ function defaultConfig() {
|
|
|
51
51
|
templatesDir: "",
|
|
52
52
|
scanOnStart: false,
|
|
53
53
|
scanDurationSeconds: 20,
|
|
54
|
+
useBleApi: false,
|
|
54
55
|
paintConnectTimeoutSeconds: 30,
|
|
55
56
|
paintRetries: 3,
|
|
56
57
|
settleSeconds: 120,
|
|
@@ -61,6 +62,7 @@ const PLUGIN_CONFIG_KEYS = [
|
|
|
61
62
|
"templatesDir",
|
|
62
63
|
"scanOnStart",
|
|
63
64
|
"scanDurationSeconds",
|
|
65
|
+
"useBleApi",
|
|
64
66
|
"paintConnectTimeoutSeconds",
|
|
65
67
|
"paintRetries",
|
|
66
68
|
"settleSeconds",
|
|
@@ -329,6 +331,18 @@ function configSchema(app, discovered = []) {
|
|
|
329
331
|
minimum: 1,
|
|
330
332
|
default: defaults.scanDurationSeconds,
|
|
331
333
|
},
|
|
334
|
+
...(app.bleApi
|
|
335
|
+
? {
|
|
336
|
+
useBleApi: {
|
|
337
|
+
type: "boolean",
|
|
338
|
+
title: "Use the SignalK BLE Manager API",
|
|
339
|
+
description: "Route Bluetooth access through SignalK server's BLE Manager API (server >= 2.32.0) instead of connecting to " +
|
|
340
|
+
"BlueZ directly, so this plugin shares the adapter with other BLE plugins instead of contending for it. Requires " +
|
|
341
|
+
"the server to have a local Bluetooth adapter or BLE gateway available (Server → Settings → Bluetooth).",
|
|
342
|
+
default: defaults.useBleApi,
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
332
346
|
paintConnectTimeoutSeconds: {
|
|
333
347
|
type: "number",
|
|
334
348
|
title: "Paint connect timeout (seconds)",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BLEApi } from "@signalk/server-api";
|
|
2
|
+
import { GattConnection } from "./gattConnection";
|
|
3
|
+
/**
|
|
4
|
+
* Opens a connection to `address` and (for gicisky) re-scans for a fresh advertisement carrying its
|
|
5
|
+
* manufacturer data - the two BLE-hardware-access operations a `paint()` needs beyond the
|
|
6
|
+
* backend-agnostic `GattConnection` a connection itself exposes. `nodeBleBackend()` talks to BlueZ
|
|
7
|
+
* directly; `bleApiBackend()` routes through the SignalK server's BLE Manager API (`app.bleApi`) so
|
|
8
|
+
* this plugin shares the adapter with other BLE plugins instead of contending for it - see
|
|
9
|
+
* `VendorDeviceConfig.gattBackend` in `types.ts` for how a driver picks one.
|
|
10
|
+
*/
|
|
11
|
+
export interface BleBackend {
|
|
12
|
+
connectGatt(address: string, timeoutMs: number): Promise<GattConnection>;
|
|
13
|
+
waitForManufacturerData(address: string, manufacturerId: number, timeoutMs: number): Promise<Buffer | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export declare function nodeBleBackend(): BleBackend;
|
|
16
|
+
/**
|
|
17
|
+
* `bleApi.connectGATT()` has no timeout of its own, same story as node-ble's `Device#connect()` (see
|
|
18
|
+
* `connectWithTimeout` in `bleDiscovery.ts`) - races it against `timeoutMs` and disconnects in the
|
|
19
|
+
* background if it eventually resolves after the caller has already given up.
|
|
20
|
+
*/
|
|
21
|
+
export declare function bleApiBackend(bleApi: BLEApi, pluginId: string): BleBackend;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nodeBleBackend = nodeBleBackend;
|
|
4
|
+
exports.bleApiBackend = bleApiBackend;
|
|
5
|
+
const bleDiscovery_1 = require("./bleDiscovery");
|
|
6
|
+
/** How long to wait for BlueZ to have (or acquire) a `Device` object for the target address before giving up - a separate budget from the connect step itself, matching both drivers' previous hardcoded constant. */
|
|
7
|
+
const DEVICE_DISCOVERY_TIMEOUT_MS = 30000;
|
|
8
|
+
function nodeBleBackend() {
|
|
9
|
+
return {
|
|
10
|
+
async connectGatt(address, timeoutMs) {
|
|
11
|
+
const { bluetooth, destroy } = (0, bleDiscovery_1.createBluetooth)();
|
|
12
|
+
try {
|
|
13
|
+
const adapter = await bluetooth.defaultAdapter();
|
|
14
|
+
const device = await (0, bleDiscovery_1.getOrDiscoverDevice)(adapter, address, DEVICE_DISCOVERY_TIMEOUT_MS);
|
|
15
|
+
await (0, bleDiscovery_1.connectWithTimeout)(device, timeoutMs);
|
|
16
|
+
const conn = (0, bleDiscovery_1.openNodeBleGattConnection)(device);
|
|
17
|
+
return {
|
|
18
|
+
read: conn.read.bind(conn),
|
|
19
|
+
write: conn.write.bind(conn),
|
|
20
|
+
startNotifications: conn.startNotifications.bind(conn),
|
|
21
|
+
stopNotifications: conn.stopNotifications.bind(conn),
|
|
22
|
+
discoverServices: conn.discoverServices.bind(conn),
|
|
23
|
+
onDisconnect: conn.onDisconnect.bind(conn),
|
|
24
|
+
get connected() {
|
|
25
|
+
return conn.connected;
|
|
26
|
+
},
|
|
27
|
+
async disconnect() {
|
|
28
|
+
await conn.disconnect();
|
|
29
|
+
destroy();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
destroy();
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async waitForManufacturerData(address, manufacturerId, timeoutMs) {
|
|
39
|
+
const { bluetooth, destroy } = (0, bleDiscovery_1.createBluetooth)();
|
|
40
|
+
try {
|
|
41
|
+
const adapter = await bluetooth.defaultAdapter();
|
|
42
|
+
const device = await (0, bleDiscovery_1.getOrDiscoverDevice)(adapter, address, DEVICE_DISCOVERY_TIMEOUT_MS);
|
|
43
|
+
return await (0, bleDiscovery_1.waitForManufacturerData)(adapter, device, manufacturerId, timeoutMs);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
destroy();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `bleApi.connectGATT()` has no timeout of its own, same story as node-ble's `Device#connect()` (see
|
|
53
|
+
* `connectWithTimeout` in `bleDiscovery.ts`) - races it against `timeoutMs` and disconnects in the
|
|
54
|
+
* background if it eventually resolves after the caller has already given up.
|
|
55
|
+
*/
|
|
56
|
+
function bleApiBackend(bleApi, pluginId) {
|
|
57
|
+
return {
|
|
58
|
+
async connectGatt(address, timeoutMs) {
|
|
59
|
+
// A previous ungraceful stop (crash, plugin reload mid-paint) can leave a stale GATT claim
|
|
60
|
+
// registered under our own pluginId, which would otherwise block this connect with
|
|
61
|
+
// "already claimed" until the server restarts - see signalk-bluetti-plugin's BleManagerDevice
|
|
62
|
+
// for the same defensive call. A no-op if we don't currently hold the claim.
|
|
63
|
+
await bleApi.releaseGATTDevice(address, pluginId).catch(() => { });
|
|
64
|
+
const connecting = bleApi.connectGATT(address, pluginId);
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
const conn = await Promise.race([connecting, (0, bleDiscovery_1.sleep)(timeoutMs).then(() => void (timedOut = true))]);
|
|
67
|
+
if (timedOut || !conn) {
|
|
68
|
+
connecting.then((c) => c.disconnect()).catch(() => { });
|
|
69
|
+
throw new Error(`connecting to device timed out after ${timeoutMs}ms`);
|
|
70
|
+
}
|
|
71
|
+
return conn;
|
|
72
|
+
},
|
|
73
|
+
async waitForManufacturerData(address, manufacturerId, timeoutMs) {
|
|
74
|
+
// Unlike node-ble's `device.getManufacturerData()`, there's no cached-instant-read path here -
|
|
75
|
+
// `BLEDeviceInfo` (from `getDevices()`/`getDevice()`) carries mac/name/rssi/seenBy but not
|
|
76
|
+
// manufacturer data (see `BLEDeviceInfoSchema` in `@signalk/server-api`'s `ble-schemas.ts`) -
|
|
77
|
+
// only the streamed `BLEAdvertisement` does. So this always actively waits on the stream.
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
unsubscribe();
|
|
81
|
+
resolve(undefined);
|
|
82
|
+
}, timeoutMs);
|
|
83
|
+
const unsubscribe = bleApi.onAdvertisement(pluginId, (adv) => {
|
|
84
|
+
if (adv.mac !== address)
|
|
85
|
+
return;
|
|
86
|
+
const hex = adv.manufacturerData?.[manufacturerId];
|
|
87
|
+
if (hex === undefined)
|
|
88
|
+
return;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
unsubscribe();
|
|
91
|
+
resolve(Buffer.from(hex, "hex"));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Adapter, Bluetooth, Device } from "@naugehyde/node-ble";
|
|
2
|
+
import { GattConnection } from "./gattConnection";
|
|
2
3
|
export declare function sleep(ms: number): Promise<void>;
|
|
3
4
|
/**
|
|
4
5
|
* Drop-in replacement for `node-ble`'s `createBluetooth` that fails fast and clearly when
|
|
@@ -78,3 +79,13 @@ export declare function waitForManufacturerData(adapter: Adapter, device: Device
|
|
|
78
79
|
* leaving it to retry forever in the background after the thing it was waiting to unblock is gone.
|
|
79
80
|
*/
|
|
80
81
|
export declare function waitForAdapter(logDebug: (message: string) => void, cancelled?: () => boolean): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Adapts an already-connected node-ble `Device` to the `GattConnection` facade device drivers
|
|
84
|
+
* (`zhsunyco`/`gicisky`) talk to - the same shape `app.bleApi.connectGATT()` returns natively (see
|
|
85
|
+
* `gattConnection.ts`), so a driver's GATT logic runs unchanged under either backend. node-ble
|
|
86
|
+
* resolves one `GattCharacteristic` object per (service, characteristic) pair and expects callers to
|
|
87
|
+
* hold onto it for reads/writes/notifications - this caches that resolution keyed by
|
|
88
|
+
* `"<serviceUuid>:<charUuid>"` so repeated calls (e.g. one `write` per image chunk) don't re-walk
|
|
89
|
+
* `device.gatt()` every time.
|
|
90
|
+
*/
|
|
91
|
+
export declare function openNodeBleGattConnection(device: Device): GattConnection;
|
|
@@ -10,6 +10,7 @@ exports.connectWithTimeout = connectWithTimeout;
|
|
|
10
10
|
exports.getOrDiscoverDevice = getOrDiscoverDevice;
|
|
11
11
|
exports.waitForManufacturerData = waitForManufacturerData;
|
|
12
12
|
exports.waitForAdapter = waitForAdapter;
|
|
13
|
+
exports.openNodeBleGattConnection = openNodeBleGattConnection;
|
|
13
14
|
const node_ble_1 = require("@naugehyde/node-ble");
|
|
14
15
|
function sleep(ms) {
|
|
15
16
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -236,3 +237,91 @@ async function waitForAdapter(logDebug, cancelled = () => false) {
|
|
|
236
237
|
}
|
|
237
238
|
return false;
|
|
238
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Adapts an already-connected node-ble `Device` to the `GattConnection` facade device drivers
|
|
242
|
+
* (`zhsunyco`/`gicisky`) talk to - the same shape `app.bleApi.connectGATT()` returns natively (see
|
|
243
|
+
* `gattConnection.ts`), so a driver's GATT logic runs unchanged under either backend. node-ble
|
|
244
|
+
* resolves one `GattCharacteristic` object per (service, characteristic) pair and expects callers to
|
|
245
|
+
* hold onto it for reads/writes/notifications - this caches that resolution keyed by
|
|
246
|
+
* `"<serviceUuid>:<charUuid>"` so repeated calls (e.g. one `write` per image chunk) don't re-walk
|
|
247
|
+
* `device.gatt()` every time.
|
|
248
|
+
*/
|
|
249
|
+
function openNodeBleGattConnection(device) {
|
|
250
|
+
const characteristics = new Map();
|
|
251
|
+
const notificationListeners = new Map();
|
|
252
|
+
let gattServer;
|
|
253
|
+
let connected = true;
|
|
254
|
+
const key = (serviceUuid, charUuid) => `${serviceUuid}:${charUuid}`;
|
|
255
|
+
async function resolveCharacteristic(serviceUuid, charUuid) {
|
|
256
|
+
const cacheKey = key(serviceUuid, charUuid);
|
|
257
|
+
const cached = characteristics.get(cacheKey);
|
|
258
|
+
if (cached) {
|
|
259
|
+
return cached;
|
|
260
|
+
}
|
|
261
|
+
gattServer ?? (gattServer = device.gatt());
|
|
262
|
+
const service = await (await gattServer).getPrimaryService(serviceUuid);
|
|
263
|
+
const characteristic = await service.getCharacteristic(charUuid);
|
|
264
|
+
characteristics.set(cacheKey, characteristic);
|
|
265
|
+
return characteristic;
|
|
266
|
+
}
|
|
267
|
+
device.on("disconnect", () => {
|
|
268
|
+
connected = false;
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
async read(serviceUuid, charUuid) {
|
|
272
|
+
const characteristic = await resolveCharacteristic(serviceUuid, charUuid);
|
|
273
|
+
return characteristic.readValue();
|
|
274
|
+
},
|
|
275
|
+
async write(serviceUuid, charUuid, data, withResponse = true) {
|
|
276
|
+
const characteristic = await resolveCharacteristic(serviceUuid, charUuid);
|
|
277
|
+
if (withResponse === false) {
|
|
278
|
+
await characteristic.writeValueWithoutResponse(data);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
await characteristic.writeValueWithResponse(data);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
async startNotifications(serviceUuid, charUuid, callback) {
|
|
285
|
+
const characteristic = await resolveCharacteristic(serviceUuid, charUuid);
|
|
286
|
+
notificationListeners.set(key(serviceUuid, charUuid), callback);
|
|
287
|
+
characteristic.on("valuechanged", callback);
|
|
288
|
+
await characteristic.startNotifications();
|
|
289
|
+
},
|
|
290
|
+
async stopNotifications(serviceUuid, charUuid) {
|
|
291
|
+
const cacheKey = key(serviceUuid, charUuid);
|
|
292
|
+
const characteristic = characteristics.get(cacheKey);
|
|
293
|
+
const listener = notificationListeners.get(cacheKey);
|
|
294
|
+
if (characteristic && listener) {
|
|
295
|
+
characteristic.removeListener("valuechanged", listener);
|
|
296
|
+
notificationListeners.delete(cacheKey);
|
|
297
|
+
await characteristic.stopNotifications().catch(() => { });
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
async discoverServices() {
|
|
301
|
+
gattServer ?? (gattServer = device.gatt());
|
|
302
|
+
const server = await gattServer;
|
|
303
|
+
const services = [];
|
|
304
|
+
for (const serviceUuid of await server.services()) {
|
|
305
|
+
const service = await server.getPrimaryService(serviceUuid);
|
|
306
|
+
const characteristicInfos = [];
|
|
307
|
+
for (const charUuid of await service.characteristics()) {
|
|
308
|
+
const characteristic = await service.getCharacteristic(charUuid);
|
|
309
|
+
characteristics.set(key(serviceUuid, charUuid), characteristic);
|
|
310
|
+
const properties = await characteristic.getFlags().catch(() => []);
|
|
311
|
+
characteristicInfos.push({ uuid: charUuid, properties });
|
|
312
|
+
}
|
|
313
|
+
services.push({ uuid: serviceUuid, characteristics: characteristicInfos });
|
|
314
|
+
}
|
|
315
|
+
return services;
|
|
316
|
+
},
|
|
317
|
+
async disconnect() {
|
|
318
|
+
await device.disconnect();
|
|
319
|
+
},
|
|
320
|
+
get connected() {
|
|
321
|
+
return connected;
|
|
322
|
+
},
|
|
323
|
+
onDisconnect(callback) {
|
|
324
|
+
device.on("disconnect", () => callback());
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -10,10 +10,11 @@ export interface ScanResult {
|
|
|
10
10
|
/** epoch ms a scan was started at, if one is currently running - `undefined` otherwise. */
|
|
11
11
|
export declare function scanInProgressSince(): number | undefined;
|
|
12
12
|
/**
|
|
13
|
-
* Runs one BLE discovery scan and persists what it finds.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Runs one BLE discovery scan and persists what it finds. Neither backend can run two discovery
|
|
14
|
+
* sessions at once (node-ble/BlueZ has no scan-cancellation API; the BLE Manager API tracks one
|
|
15
|
+
* subscription per plugin), so a caller that arrives while a scan is already running (e.g. the
|
|
16
|
+
* startup scan and an on-demand scan for a `device: "ALL"` repaint both wanting to scan at the same
|
|
17
|
+
* moment) is hooked into that *same* in-flight scan's eventual result instead of starting a second
|
|
18
|
+
* one, which would make both fail.
|
|
18
19
|
*/
|
|
19
|
-
export declare function ensureScan(app: ServerAPI, durationSeconds: number): Promise<ScanResult>;
|
|
20
|
+
export declare function ensureScan(app: ServerAPI, durationSeconds: number, useBleApi: boolean): Promise<ScanResult>;
|
|
@@ -3,31 +3,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.scanInProgressSince = scanInProgressSince;
|
|
4
4
|
exports.ensureScan = ensureScan;
|
|
5
5
|
const bleDiscovery_1 = require("./bleDiscovery");
|
|
6
|
+
const bleBackend_1 = require("./bleBackend");
|
|
6
7
|
const discoveredDevicesStore_1 = require("./discoveredDevicesStore");
|
|
7
8
|
const registry_1 = require("./registry");
|
|
9
|
+
const pluginVersion_1 = require("../pluginVersion");
|
|
8
10
|
let inFlight;
|
|
9
11
|
/** epoch ms a scan was started at, if one is currently running - `undefined` otherwise. */
|
|
10
12
|
function scanInProgressSince() {
|
|
11
13
|
return inFlight?.startedAt;
|
|
12
14
|
}
|
|
15
|
+
/** How long a `connect()` closure built during a scan waits for the GATT connect step itself - a scan may be enumerating several devices, so kept short relative to `paint()`'s own connect timeout. */
|
|
16
|
+
const SCAN_GATT_CONNECT_TIMEOUT_MS = 10000;
|
|
13
17
|
/**
|
|
14
|
-
* Runs one BLE discovery scan and persists what it finds.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Runs one BLE discovery scan and persists what it finds. Neither backend can run two discovery
|
|
19
|
+
* sessions at once (node-ble/BlueZ has no scan-cancellation API; the BLE Manager API tracks one
|
|
20
|
+
* subscription per plugin), so a caller that arrives while a scan is already running (e.g. the
|
|
21
|
+
* startup scan and an on-demand scan for a `device: "ALL"` repaint both wanting to scan at the same
|
|
22
|
+
* moment) is hooked into that *same* in-flight scan's eventual result instead of starting a second
|
|
23
|
+
* one, which would make both fail.
|
|
19
24
|
*/
|
|
20
|
-
function ensureScan(app, durationSeconds) {
|
|
25
|
+
function ensureScan(app, durationSeconds, useBleApi) {
|
|
21
26
|
if (inFlight) {
|
|
22
27
|
return inFlight.promise;
|
|
23
28
|
}
|
|
24
|
-
const promise =
|
|
29
|
+
const promise = (useBleApi && !!app.bleApi ? runScanViaBleApi(app, durationSeconds) : runScanViaBlueZ(app, durationSeconds)).finally(() => {
|
|
25
30
|
inFlight = undefined;
|
|
26
31
|
});
|
|
27
32
|
inFlight = { startedAt: Date.now(), promise };
|
|
28
33
|
return promise;
|
|
29
34
|
}
|
|
30
|
-
async function
|
|
35
|
+
async function runScanViaBlueZ(app, durationSeconds) {
|
|
31
36
|
const foundThisScan = [];
|
|
32
37
|
const drivers = (0, registry_1.allDrivers)();
|
|
33
38
|
try {
|
|
@@ -37,7 +42,12 @@ async function runScan(app, durationSeconds) {
|
|
|
37
42
|
if (!driver) {
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
|
-
const
|
|
45
|
+
const rssi = await device
|
|
46
|
+
.getRSSI()
|
|
47
|
+
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
48
|
+
.catch(() => undefined);
|
|
49
|
+
const connect = () => (0, bleDiscovery_1.connectWithTimeout)(device, SCAN_GATT_CONNECT_TIMEOUT_MS).then(() => (0, bleDiscovery_1.openNodeBleGattConnection)(device));
|
|
50
|
+
const found = await driver.identifyDevice({ address, name, manufacturerId, manufacturerData, rssi }, connect).catch((err) => {
|
|
41
51
|
app.debug(`${driver.vendor} scan failed: ${err.message}\n${err.stack ?? ""}`);
|
|
42
52
|
return undefined;
|
|
43
53
|
});
|
|
@@ -57,3 +67,65 @@ async function runScan(app, durationSeconds) {
|
|
|
57
67
|
const merged = (0, discoveredDevicesStore_1.recordScanResults)(app, foundThisScan);
|
|
58
68
|
return { foundThisScan, merged };
|
|
59
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Same as `runScanViaBlueZ` but sourced from the SignalK server's BLE Manager API instead of a direct
|
|
72
|
+
* BlueZ discovery session - subscribes to the merged advertisement stream for `durationSeconds`,
|
|
73
|
+
* seeded with whatever the server already knows about (mirrors `BleManagerScanner` in
|
|
74
|
+
* signalk-bluetti-plugin). `app.bleApi.getDevices()`/advertisements don't carry manufacturer data
|
|
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.
|
|
77
|
+
*/
|
|
78
|
+
async function runScanViaBleApi(app, durationSeconds) {
|
|
79
|
+
const foundThisScan = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const drivers = (0, registry_1.allDrivers)();
|
|
82
|
+
const backend = (0, bleBackend_1.bleApiBackend)(app.bleApi, pluginVersion_1.PLUGIN_NAME);
|
|
83
|
+
const identify = async (mac, name, rssi, manufacturerDataHex) => {
|
|
84
|
+
if (seen.has(mac)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
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
|
+
seen.add(mac);
|
|
95
|
+
const connect = () => backend.connectGatt(mac, SCAN_GATT_CONNECT_TIMEOUT_MS);
|
|
96
|
+
const found = await driver.identifyDevice({ address: mac, name, manufacturerId, manufacturerData, rssi }, connect).catch((err) => {
|
|
97
|
+
app.debug(`${driver.vendor} scan failed: ${err.message}\n${err.stack ?? ""}`);
|
|
98
|
+
return undefined;
|
|
99
|
+
});
|
|
100
|
+
if (!found) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
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
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const unsubscribe = app.bleApi.onAdvertisement(pluginVersion_1.PLUGIN_NAME, (adv) => {
|
|
110
|
+
void identify(adv.mac, adv.name, adv.rssi, adv.manufacturerData);
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
const known = await app.bleApi.getDevices();
|
|
114
|
+
for (const device of known) {
|
|
115
|
+
// BLEDeviceInfo (from getDevices()) doesn't carry manufacturer data - see `bleBackend.ts`'s
|
|
116
|
+
// waitForManufacturerData doc comment. A driver that needs it to identify (e.g. gicisky) will
|
|
117
|
+
// still show up once its advertisement arrives on the stream above during this scan window.
|
|
118
|
+
await identify(device.mac, device.name, device.rssi, undefined);
|
|
119
|
+
}
|
|
120
|
+
await (0, bleDiscovery_1.sleep)(durationSeconds * 1000);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
unsubscribe();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
app.debug(`scan failed: ${err.message}\n${err.stack ?? ""}`);
|
|
128
|
+
}
|
|
129
|
+
const merged = (0, discoveredDevicesStore_1.recordScanResults)(app, foundThisScan);
|
|
130
|
+
return { foundThisScan, merged };
|
|
131
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BLEGattConnection } from "@signalk/server-api";
|
|
2
|
+
/**
|
|
3
|
+
* Service+characteristic-UUID-keyed GATT surface both BLE backends satisfy - `app.bleApi.connectGATT()`
|
|
4
|
+
* already returns exactly this shape (it *is* `BLEGattConnection`), and `openNodeBleGattConnection`
|
|
5
|
+
* (`bleDiscovery.ts`) adapts a node-ble `Device` to it. Device drivers (`zhsunyco`/`gicisky`) talk to
|
|
6
|
+
* this instead of either BLE library directly, so they work unchanged under whichever backend a caller
|
|
7
|
+
* hands them - see `bleBackend.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export type GattConnection = BLEGattConnection;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Device } from "@naugehyde/node-ble";
|
|
2
1
|
import { Bitmap } from "../../render/types";
|
|
3
2
|
import { DeviceMetadata, DiscoveredDevice, VendorDeviceConfig, VendorDriver } from "../types";
|
|
4
3
|
export declare class GiciskyDriver implements VendorDriver {
|
|
@@ -6,6 +5,12 @@ export declare class GiciskyDriver implements VendorDriver {
|
|
|
6
5
|
matchesAdvertisement(_name: string | undefined, manufacturerId: number | undefined): boolean;
|
|
7
6
|
metadataForPid(pid: number): DeviceMetadata | undefined;
|
|
8
7
|
supportedDevices(): DeviceMetadata[];
|
|
9
|
-
identifyDevice(
|
|
8
|
+
identifyDevice({ address, name, manufacturerId, manufacturerData, rssi, }: {
|
|
9
|
+
address: string;
|
|
10
|
+
name: string | undefined;
|
|
11
|
+
manufacturerId: number | undefined;
|
|
12
|
+
manufacturerData: Buffer | undefined;
|
|
13
|
+
rssi: number | undefined;
|
|
14
|
+
}): Promise<DiscoveredDevice>;
|
|
10
15
|
paint(bitmap: Bitmap, config: VendorDeviceConfig): Promise<void>;
|
|
11
16
|
}
|