@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 CHANGED
@@ -6,7 +6,7 @@
6
6
  ![code style: oxfmt](https://img.shields.io/badge/code_style-oxfmt-blue.svg)
7
7
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](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. ALPHA status
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
- this._adapter = await bt.bluetooth.defaultAdapter();
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._init();
46
- } catch (err) {
47
- this._log(`BLE adapter init failed: ${err.message}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-bluetti-plugin",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "SignalK plugin for Bluetti power station monitoring via Bluetooth LE. Beta, use with caution.",
5
5
  "keywords": [
6
6
  "battery",
@@ -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,0.01,0,%,electrical.batteries.{name}.stateOfCharge
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