@rhizomatics/signalk-bluetti-plugin 1.3.0 → 1.3.1
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/README.md +30 -1
- package/lib/scanner.js +69 -5
- package/package.json +1 -1
- package/registers/el100v2.csv +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|

|
|
7
7
|
[](https://github.com/rhizomatics/signalk-bluetti-plugin/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
A SignalK plugin to display data from Bluetti power stations over a Bluetooth Low Energy (BLE) connection.
|
|
9
|
+
A SignalK plugin to display data from Bluetti power stations over a Bluetooth Low Energy (BLE) connection. BETA status
|
|
10
10
|
|
|
11
11
|
## Pre-requisites
|
|
12
12
|
|
|
@@ -79,3 +79,32 @@ node cli.js info aa:bb:cc:dd:ee:ff --registers ./my-device-registers.csv --encry
|
|
|
79
79
|
|
|
80
80
|
node cli.js info aa:bb:cc:dd:ee:ff --registers ac200p --timeout 30 # discovery timeout if device isn't already known to BlueZ (default: 20)
|
|
81
81
|
```
|
|
82
|
+
|
|
83
|
+
## FAQ
|
|
84
|
+
|
|
85
|
+
### SignalK starts before the Bluetooth daemon — does the plugin need `bluetoothd` running at boot?
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
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:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
sudo systemctl edit signalk.service
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This opens an override file — add:
|
|
96
|
+
|
|
97
|
+
```ini
|
|
98
|
+
[Unit]
|
|
99
|
+
After=bluetooth.target
|
|
100
|
+
Wants=bluetooth.target
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Save and exit, then:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
sudo systemctl daemon-reload
|
|
107
|
+
sudo systemctl restart signalk
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
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.
|
package/lib/scanner.js
CHANGED
|
@@ -4,6 +4,13 @@ const EventEmitter = require("events");
|
|
|
4
4
|
|
|
5
5
|
const BLUETTI_NAME_PREFIXES = ["BT-TH-", "BLUETTI", "AC", "EP", "EB", "EL"];
|
|
6
6
|
|
|
7
|
+
// Retry/backoff for BLE adapter init — covers the case where SignalK (and this
|
|
8
|
+
// plugin) starts before bluetoothd/D-Bus is up, e.g. on boot when service
|
|
9
|
+
// ordering isn't configured. Retries forever with capped exponential backoff
|
|
10
|
+
// rather than giving up after one attempt.
|
|
11
|
+
const ADAPTER_INIT_RETRY_INITIAL_MS = 2000;
|
|
12
|
+
const ADAPTER_INIT_RETRY_MAX_MS = 30000;
|
|
13
|
+
|
|
7
14
|
function isBluettiDevice(name) {
|
|
8
15
|
if (!name) return false;
|
|
9
16
|
return BLUETTI_NAME_PREFIXES.some((p) => name.toUpperCase().startsWith(p.toUpperCase()));
|
|
@@ -21,17 +28,71 @@ class Scanner extends EventEmitter {
|
|
|
21
28
|
this._scanTimer = null;
|
|
22
29
|
this._pollTimer = null;
|
|
23
30
|
this._found = new Map(); // normalised address → { address, name, device, isBluetti }
|
|
31
|
+
this._stopped = false;
|
|
32
|
+
this._retryTimer = null;
|
|
33
|
+
this._retryResolve = null;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
async _init() {
|
|
27
37
|
if (this._adapter) return;
|
|
28
38
|
const { createBluetooth } = require("@naugehyde/node-ble");
|
|
29
39
|
const bt = createBluetooth();
|
|
40
|
+
try {
|
|
41
|
+
this._adapter = await bt.bluetooth.defaultAdapter();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// Don't leak the D-Bus connection on a failed attempt — a retry will
|
|
44
|
+
// create a fresh one.
|
|
45
|
+
try {
|
|
46
|
+
bt.destroy();
|
|
47
|
+
} catch {}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
30
50
|
this._bluetooth = bt.bluetooth;
|
|
31
51
|
// Call through `bt` rather than destructuring — destroy() is a method on the
|
|
32
52
|
// returned object and must keep its receiver.
|
|
33
53
|
this._destroy = () => bt.destroy();
|
|
34
|
-
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Retries _init() with capped exponential backoff until it succeeds or
|
|
57
|
+
// stopAll()/stopScan() cancels the wait (this._stopped).
|
|
58
|
+
async _initWithRetry() {
|
|
59
|
+
let delay = ADAPTER_INIT_RETRY_INITIAL_MS;
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
await this._init();
|
|
63
|
+
return;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (this._stopped) throw err;
|
|
66
|
+
this._log(`BLE adapter not ready (${err.message}) — retrying in ${Math.round(delay / 1000)}s …`);
|
|
67
|
+
await this._delay(delay);
|
|
68
|
+
if (this._stopped) throw new Error("scanner stopped while waiting for BLE adapter");
|
|
69
|
+
delay = Math.min(delay * 2, ADAPTER_INIT_RETRY_MAX_MS);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_delay(ms) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
this._retryResolve = resolve;
|
|
77
|
+
this._retryTimer = setTimeout(() => {
|
|
78
|
+
this._retryTimer = null;
|
|
79
|
+
this._retryResolve = null;
|
|
80
|
+
resolve();
|
|
81
|
+
}, ms);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Cancels a pending _delay() wait immediately, if one is in flight.
|
|
86
|
+
_cancelRetryWait() {
|
|
87
|
+
if (this._retryTimer) {
|
|
88
|
+
clearTimeout(this._retryTimer);
|
|
89
|
+
this._retryTimer = null;
|
|
90
|
+
}
|
|
91
|
+
if (this._retryResolve) {
|
|
92
|
+
const resolve = this._retryResolve;
|
|
93
|
+
this._retryResolve = null;
|
|
94
|
+
resolve();
|
|
95
|
+
}
|
|
35
96
|
}
|
|
36
97
|
|
|
37
98
|
// `durationMs` — auto-stop (and release BlueZ discovery) after this long.
|
|
@@ -40,14 +101,15 @@ class Scanner extends EventEmitter {
|
|
|
40
101
|
// the adapter's shared discovery session (see stopScan()).
|
|
41
102
|
async startScan(durationMs = 15000) {
|
|
42
103
|
if (this._scanning) return;
|
|
104
|
+
this._stopped = false;
|
|
43
105
|
|
|
44
106
|
try {
|
|
45
|
-
await this.
|
|
46
|
-
} catch
|
|
47
|
-
|
|
48
|
-
this.emit("error", err);
|
|
107
|
+
await this._initWithRetry();
|
|
108
|
+
} catch {
|
|
109
|
+
// Cancelled via stopAll()/stopScan() while waiting for the adapter.
|
|
49
110
|
return;
|
|
50
111
|
}
|
|
112
|
+
if (this._stopped) return;
|
|
51
113
|
|
|
52
114
|
this._found.clear();
|
|
53
115
|
this._scanning = true;
|
|
@@ -119,6 +181,8 @@ class Scanner extends EventEmitter {
|
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
stopAll() {
|
|
184
|
+
this._stopped = true;
|
|
185
|
+
this._cancelRetryWait();
|
|
122
186
|
this.stopScan();
|
|
123
187
|
if (this._destroy) {
|
|
124
188
|
try {
|
package/package.json
CHANGED
package/registers/el100v2.csv
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Note: DEVICE_TYPE (110) and DEVICE_SN (116) are string/serial fields — omitted.
|
|
6
6
|
#
|
|
7
7
|
field_name,register_address,register_count,data_type,scale,offset,unit,signalk_path
|
|
8
|
-
battery_soc,102,1,uint16,
|
|
8
|
+
battery_soc,102,1,uint16,1,0,%,electrical.batteries.{name}.stateOfCharge
|
|
9
9
|
dc_output_power,140,1,uint16,1,0,W,electrical.dc.{name}.power
|
|
10
10
|
ac_output_power,142,1,uint16,1,0,W,electrical.inverters.{name}.ac.power
|
|
11
11
|
dc_input_power,144,1,uint16,1,0,W,electrical.solar.{name}.panelPower
|