@rhizomatics/signalk-bluetti-plugin 1.0.7-alpha → 1.1.0-alpha
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/index.js +8 -8
- package/lib/device.js +132 -83
- package/lib/scanner.js +72 -70
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -40,8 +40,8 @@ function builtinModelNames() {
|
|
|
40
40
|
module.exports = function (app) {
|
|
41
41
|
const log = (msg) => app.debug(msg);
|
|
42
42
|
|
|
43
|
-
let scanner
|
|
44
|
-
let activeDevices
|
|
43
|
+
let scanner = null;
|
|
44
|
+
let activeDevices = [];
|
|
45
45
|
let scanResultCache = [];
|
|
46
46
|
|
|
47
47
|
const builtins = builtinModelNames();
|
|
@@ -185,16 +185,16 @@ module.exports = function (app) {
|
|
|
185
185
|
const pending = new Map(devices.map(cfg => [normalise(cfg.address), cfg]));
|
|
186
186
|
const deps = { BluettiDevice, loadCsv, buildDelta, readEncryptionKey };
|
|
187
187
|
|
|
188
|
-
scanner.on('discovered', ({ address, name,
|
|
188
|
+
scanner.on('discovered', ({ address, name, device: bleDevice }) => {
|
|
189
189
|
scanResultCache.push({ address, name });
|
|
190
190
|
const cfg = pending.get(normalise(address));
|
|
191
191
|
if (cfg) {
|
|
192
192
|
pending.delete(normalise(address));
|
|
193
193
|
app.setPluginStatus(`Found ${name} [${address}] — connecting …`);
|
|
194
|
-
//
|
|
195
|
-
//
|
|
194
|
+
// Stop our discovery session before connecting (keeps the poll loop quiet).
|
|
195
|
+
// BlueZ handles scan+connect coexistence at the adapter level.
|
|
196
196
|
scanner.stopScan();
|
|
197
|
-
startDevice(cfg,
|
|
197
|
+
startDevice(cfg, bleDevice, name, deps);
|
|
198
198
|
} else {
|
|
199
199
|
log(`Discovered unconfigured Bluetti device: ${name} [${address}]`);
|
|
200
200
|
}
|
|
@@ -256,7 +256,7 @@ module.exports = function (app) {
|
|
|
256
256
|
return path.join(REGISTERS_DIR, `${builtinModel}.csv`);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
function startDevice(cfg,
|
|
259
|
+
function startDevice(cfg, bleDevice, bleName, { BluettiDevice, loadCsv, buildDelta, readEncryptionKey }) {
|
|
260
260
|
const { address, name, encryptionCsvPath = '', pollIntervalSeconds = 10 } = cfg;
|
|
261
261
|
|
|
262
262
|
let registerPath;
|
|
@@ -299,7 +299,7 @@ module.exports = function (app) {
|
|
|
299
299
|
const device = new BluettiDevice({
|
|
300
300
|
address,
|
|
301
301
|
name,
|
|
302
|
-
|
|
302
|
+
device: bleDevice,
|
|
303
303
|
fields,
|
|
304
304
|
pollIntervalMs: pollIntervalSeconds * 1000,
|
|
305
305
|
xorKey,
|
package/lib/device.js
CHANGED
|
@@ -3,39 +3,40 @@
|
|
|
3
3
|
const EventEmitter = require('events');
|
|
4
4
|
const { buildReadRequest, completeFrameLength, parseReadResponse, applyXor, groupRegisters } = require('./protocol');
|
|
5
5
|
|
|
6
|
-
// UUIDs used by most Bluetti devices
|
|
7
|
-
const SERVICE_UUID
|
|
8
|
-
const NOTIFY_UUID
|
|
9
|
-
const WRITE_UUID
|
|
6
|
+
// UUIDs used by most Bluetti devices
|
|
7
|
+
const SERVICE_UUID = 'ff00';
|
|
8
|
+
const NOTIFY_UUID = 'ff01';
|
|
9
|
+
const WRITE_UUID = 'ff02';
|
|
10
10
|
|
|
11
11
|
// Fallback UUIDs seen on some older/alternate firmwares
|
|
12
12
|
const ALT_SERVICE_UUID = 'ffe0';
|
|
13
13
|
const ALT_CHAR_UUID = 'ffe1';
|
|
14
14
|
|
|
15
|
-
const RECONNECT_DELAYS
|
|
15
|
+
const RECONNECT_DELAYS = [5000, 10000, 20000, 30000, 60000]; // ms, capped at last value
|
|
16
|
+
const CONNECT_TIMEOUT_MS = 30000;
|
|
16
17
|
|
|
17
18
|
class BluettiDevice extends EventEmitter {
|
|
18
|
-
constructor({ address, name,
|
|
19
|
+
constructor({ address, name, device, fields, pollIntervalMs = 10000, xorKey = null, log }) {
|
|
19
20
|
super();
|
|
20
|
-
this._address
|
|
21
|
-
this._name
|
|
22
|
-
this.
|
|
23
|
-
this._fields
|
|
21
|
+
this._address = address;
|
|
22
|
+
this._name = name;
|
|
23
|
+
this._device = device; // node-ble Device (D-Bus backed, persists across disconnects)
|
|
24
|
+
this._fields = fields;
|
|
24
25
|
this._pollIntervalMs = pollIntervalMs;
|
|
25
|
-
this._xorKey
|
|
26
|
-
this._log
|
|
27
|
-
|
|
28
|
-
this._connected
|
|
29
|
-
this._notifyChar
|
|
30
|
-
this._writeChar
|
|
31
|
-
this._rxBuf
|
|
32
|
-
this._currentBatch
|
|
33
|
-
this._batchQueue
|
|
34
|
-
this._pollTimer
|
|
35
|
-
this._reconnectAttempt
|
|
36
|
-
this._stopped
|
|
26
|
+
this._xorKey = xorKey ? Buffer.from(xorKey, 'hex') : null;
|
|
27
|
+
this._log = log;
|
|
28
|
+
|
|
29
|
+
this._connected = false;
|
|
30
|
+
this._notifyChar = null;
|
|
31
|
+
this._writeChar = null;
|
|
32
|
+
this._rxBuf = Buffer.alloc(0);
|
|
33
|
+
this._currentBatch = null; // { start, count } being awaited
|
|
34
|
+
this._batchQueue = [];
|
|
35
|
+
this._pollTimer = null;
|
|
36
|
+
this._reconnectAttempt = 0;
|
|
37
|
+
this._stopped = false;
|
|
38
|
+
this._disconnectHandler = null; // stored so we can remove it on reconnect
|
|
37
39
|
|
|
38
|
-
// Pre-compute register poll batches from field list
|
|
39
40
|
const addrs = [...new Set(fields.flatMap(f => Array.from({ length: f.count }, (_, i) => f.register + i)))];
|
|
40
41
|
this._batches = groupRegisters(addrs);
|
|
41
42
|
this._log(`[${name}] Poll plan: ${this._batches.length} batch(es) covering ${addrs.length} registers`);
|
|
@@ -49,74 +50,123 @@ class BluettiDevice extends EventEmitter {
|
|
|
49
50
|
stop() {
|
|
50
51
|
this._stopped = true;
|
|
51
52
|
this._stopPolling();
|
|
52
|
-
if (this.
|
|
53
|
-
|
|
53
|
+
if (this._disconnectHandler) {
|
|
54
|
+
this._device.removeListener('disconnect', this._disconnectHandler);
|
|
55
|
+
this._disconnectHandler = null;
|
|
56
|
+
}
|
|
57
|
+
if (this._connected) {
|
|
58
|
+
this._device.disconnect().catch(() => {});
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// ── Connection lifecycle ─────────────────────────────────────────────────
|
|
58
63
|
|
|
59
|
-
_connect() {
|
|
64
|
+
async _connect() {
|
|
60
65
|
if (this._stopped) return;
|
|
61
66
|
this._log(`[${this._name}] Connecting to ${this._address} …`);
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// Clear any stale disconnect handler from a previous attempt.
|
|
69
|
+
if (this._disconnectHandler) {
|
|
70
|
+
this._device.removeListener('disconnect', this._disconnectHandler);
|
|
71
|
+
this._disconnectHandler = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let settled = false;
|
|
75
|
+
const connectTimer = setTimeout(() => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
this._log(`[${this._name}] Connect timed out after ${CONNECT_TIMEOUT_MS / 1000}s — will retry`);
|
|
79
|
+
this._scheduleReconnect();
|
|
80
|
+
}, CONNECT_TIMEOUT_MS);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await this._device.connect();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(connectTimer);
|
|
88
|
+
this._log(`[${this._name}] Connect error: ${err.message}`);
|
|
89
|
+
this._scheduleReconnect();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
if (settled) return; // timed out while awaiting
|
|
94
|
+
settled = true;
|
|
95
|
+
clearTimeout(connectTimer);
|
|
96
|
+
|
|
97
|
+
this._log(`[${this._name}] Connected`);
|
|
98
|
+
this._reconnectAttempt = 0;
|
|
99
|
+
|
|
100
|
+
// Register disconnect handler — stored so it can be removed on reconnect.
|
|
101
|
+
this._disconnectHandler = () => {
|
|
75
102
|
if (this._stopped) return;
|
|
76
|
-
this._connected
|
|
77
|
-
this._notifyChar
|
|
78
|
-
this._writeChar
|
|
103
|
+
this._connected = false;
|
|
104
|
+
this._notifyChar = null;
|
|
105
|
+
this._writeChar = null;
|
|
106
|
+
this._disconnectHandler = null;
|
|
79
107
|
this._log(`[${this._name}] Disconnected`);
|
|
80
108
|
this._stopPolling();
|
|
81
109
|
this._scheduleReconnect();
|
|
82
|
-
}
|
|
110
|
+
};
|
|
111
|
+
this._device.once('disconnect', this._disconnectHandler);
|
|
112
|
+
|
|
113
|
+
await this._discoverServices();
|
|
83
114
|
}
|
|
84
115
|
|
|
85
|
-
_discoverServices() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
async _discoverServices() {
|
|
117
|
+
let gatt;
|
|
118
|
+
try {
|
|
119
|
+
gatt = await this._device.gatt();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
this._log(`[${this._name}] GATT error: ${err.message}`);
|
|
122
|
+
this._scheduleReconnect();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let service;
|
|
127
|
+
for (const uuid of [SERVICE_UUID, ALT_SERVICE_UUID]) {
|
|
128
|
+
try {
|
|
129
|
+
service = await gatt.getPrimaryService(uuid);
|
|
130
|
+
break;
|
|
131
|
+
} catch (_) {}
|
|
132
|
+
}
|
|
133
|
+
if (!service) {
|
|
134
|
+
this._log(`[${this._name}] Could not find BLE service (tried ${SERVICE_UUID}, ${ALT_SERVICE_UUID})`);
|
|
135
|
+
this._scheduleReconnect();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let notifyChar;
|
|
140
|
+
for (const uuid of [NOTIFY_UUID, ALT_CHAR_UUID]) {
|
|
141
|
+
try { notifyChar = await service.getCharacteristic(uuid); break; } catch (_) {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let writeChar;
|
|
145
|
+
for (const uuid of [WRITE_UUID, ALT_CHAR_UUID]) {
|
|
146
|
+
try { writeChar = await service.getCharacteristic(uuid); break; } catch (_) {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!notifyChar || !writeChar) {
|
|
150
|
+
this._log(`[${this._name}] Could not find required GATT characteristics`);
|
|
151
|
+
this._scheduleReconnect();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await notifyChar.startNotifications();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
this._log(`[${this._name}] Subscribe error: ${err.message}`);
|
|
159
|
+
this._scheduleReconnect();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
notifyChar.on('valuechanged', (data) => this._onData(data));
|
|
164
|
+
|
|
165
|
+
this._notifyChar = notifyChar;
|
|
166
|
+
this._writeChar = writeChar;
|
|
167
|
+
this._connected = true;
|
|
168
|
+
this.emit('connected');
|
|
169
|
+
this._startPolling();
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
_scheduleReconnect() {
|
|
@@ -124,6 +174,8 @@ class BluettiDevice extends EventEmitter {
|
|
|
124
174
|
const delay = RECONNECT_DELAYS[Math.min(this._reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
|
125
175
|
this._reconnectAttempt++;
|
|
126
176
|
this._log(`[${this._name}] Reconnecting in ${delay / 1000}s (attempt ${this._reconnectAttempt})`);
|
|
177
|
+
// The node-ble Device object is D-Bus backed and persists across disconnects —
|
|
178
|
+
// no need to rescan for a fresh peripheral; just call connect() again.
|
|
127
179
|
setTimeout(() => {
|
|
128
180
|
if (!this._stopped) this._connect();
|
|
129
181
|
}, delay);
|
|
@@ -142,13 +194,12 @@ class BluettiDevice extends EventEmitter {
|
|
|
142
194
|
clearInterval(this._pollTimer);
|
|
143
195
|
this._pollTimer = null;
|
|
144
196
|
}
|
|
145
|
-
this._batchQueue
|
|
197
|
+
this._batchQueue = [];
|
|
146
198
|
this._currentBatch = null;
|
|
147
199
|
}
|
|
148
200
|
|
|
149
201
|
_poll() {
|
|
150
202
|
if (!this._connected || this._batches.length === 0) return;
|
|
151
|
-
// Queue all batches; they'll be sent one at a time as responses arrive
|
|
152
203
|
this._batchQueue = [...this._batches];
|
|
153
204
|
this._rxBuf = Buffer.alloc(0);
|
|
154
205
|
this._sendNextBatch();
|
|
@@ -161,20 +212,19 @@ class BluettiDevice extends EventEmitter {
|
|
|
161
212
|
let frame = buildReadRequest(start, count);
|
|
162
213
|
if (this._xorKey) frame = applyXor(frame, this._xorKey);
|
|
163
214
|
this._rxBuf = Buffer.alloc(0);
|
|
164
|
-
this._writeChar.
|
|
165
|
-
|
|
215
|
+
this._writeChar.writeValueWithoutResponse(frame).catch((err) => {
|
|
216
|
+
this._log(`[${this._name}] Write error: ${err.message}`);
|
|
166
217
|
});
|
|
167
218
|
}
|
|
168
219
|
|
|
169
220
|
// ── Receive ──────────────────────────────────────────────────────────────
|
|
170
221
|
|
|
171
222
|
_onData(data) {
|
|
172
|
-
// Undo XOR on incoming data if encryption is in use
|
|
173
223
|
const chunk = this._xorKey ? applyXor(data, this._xorKey) : data;
|
|
174
224
|
this._rxBuf = Buffer.concat([this._rxBuf, chunk]);
|
|
175
225
|
|
|
176
226
|
const frameLen = completeFrameLength(this._rxBuf);
|
|
177
|
-
if (frameLen === 0) return;
|
|
227
|
+
if (frameLen === 0) return; // waiting for more BLE packets
|
|
178
228
|
|
|
179
229
|
const frame = this._rxBuf.slice(0, frameLen);
|
|
180
230
|
this._rxBuf = this._rxBuf.slice(frameLen);
|
|
@@ -187,7 +237,6 @@ class BluettiDevice extends EventEmitter {
|
|
|
187
237
|
this._log(`[${this._name}] CRC error or unexpected frame, skipping batch`);
|
|
188
238
|
}
|
|
189
239
|
|
|
190
|
-
// Move to next batch
|
|
191
240
|
this._sendNextBatch();
|
|
192
241
|
}
|
|
193
242
|
}
|
package/lib/scanner.js
CHANGED
|
@@ -2,105 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
const EventEmitter = require('events');
|
|
4
4
|
|
|
5
|
-
// Bluetti devices advertise with names matching these prefixes.
|
|
6
|
-
// BT-TH-XXXXXXXXXX is the most common; some models use BLUETTI or model names directly.
|
|
7
5
|
const BLUETTI_NAME_PREFIXES = ['BT-TH-', 'BLUETTI', 'AC', 'EP', 'EB', 'EL'];
|
|
8
6
|
|
|
9
|
-
function isBluettiDevice(
|
|
10
|
-
|
|
7
|
+
function isBluettiDevice(name) {
|
|
8
|
+
if (!name) return false;
|
|
11
9
|
return BLUETTI_NAME_PREFIXES.some(p => name.toUpperCase().startsWith(p.toUpperCase()));
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
class Scanner extends EventEmitter {
|
|
15
13
|
constructor(log) {
|
|
16
14
|
super();
|
|
17
|
-
this._log
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
15
|
+
this._log = log;
|
|
16
|
+
this._bluetooth = null;
|
|
17
|
+
this._destroy = null;
|
|
18
|
+
this._adapter = null;
|
|
19
|
+
this._scanning = false;
|
|
20
|
+
this._scanTimer = null;
|
|
21
|
+
this._pollTimer = null;
|
|
22
|
+
this._found = new Map(); // normalised address → { address, name, device }
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
noble = require('@stoprocent/noble');
|
|
31
|
-
} catch (e) {
|
|
32
|
-
return reject(new Error('@stoprocent/noble not installed — run: npm install'));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this._noble = noble;
|
|
36
|
-
|
|
37
|
-
noble.on('discover', (peripheral) => {
|
|
38
|
-
if (!isBluettiDevice(peripheral)) return;
|
|
39
|
-
const addr = peripheral.address || peripheral.id;
|
|
40
|
-
const name = (peripheral.advertisement.localName || 'unknown').trim();
|
|
41
|
-
if (!this._found.has(addr)) {
|
|
42
|
-
this._found.set(addr, peripheral);
|
|
43
|
-
this._log(`Discovered Bluetti device: ${name} [${addr}]`);
|
|
44
|
-
this.emit('discovered', { address: addr, name, peripheral });
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
noble.on('stateChange', (state) => {
|
|
49
|
-
this._log(`BLE adapter state: ${state}`);
|
|
50
|
-
if (state === 'poweredOn') {
|
|
51
|
-
resolve();
|
|
52
|
-
} else if (state === 'poweredOff' || state === 'unsupported') {
|
|
53
|
-
reject(new Error(`BLE adapter state: ${state}`));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Already powered on (noble initialised before our listener)
|
|
58
|
-
if (noble.state === 'poweredOn') resolve();
|
|
59
|
-
});
|
|
25
|
+
async _init() {
|
|
26
|
+
if (this._adapter) return;
|
|
27
|
+
const { createBluetooth } = require('@naugehyde/node-ble');
|
|
28
|
+
const { bluetooth, destroy } = createBluetooth();
|
|
29
|
+
this._bluetooth = bluetooth;
|
|
30
|
+
this._destroy = destroy;
|
|
31
|
+
this._adapter = await bluetooth.defaultAdapter();
|
|
60
32
|
}
|
|
61
33
|
|
|
62
34
|
async startScan(durationMs = 15000) {
|
|
63
35
|
if (this._scanning) return;
|
|
64
|
-
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await this._init();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
this._log(`BLE adapter init failed: ${err.message}`);
|
|
41
|
+
this.emit('error', err);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
65
45
|
this._found.clear();
|
|
66
46
|
this._scanning = true;
|
|
67
47
|
this._log(`Scanning for Bluetti devices for ${durationMs / 1000}s …`);
|
|
68
|
-
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (!await this._adapter.isDiscovering()) {
|
|
51
|
+
await this._adapter.startDiscovery();
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
this._log(`Failed to start BLE discovery: ${err.message}`);
|
|
55
|
+
this._scanning = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
69
59
|
this._scanTimer = setTimeout(() => this.stopScan(), durationMs);
|
|
60
|
+
this._pollTimer = setInterval(() => this._poll(), 1000);
|
|
61
|
+
this._poll(); // check immediately for already-known devices
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _poll() {
|
|
65
|
+
if (!this._adapter || !this._scanning) return;
|
|
66
|
+
let addresses;
|
|
67
|
+
try {
|
|
68
|
+
addresses = await this._adapter.devices();
|
|
69
|
+
} catch (_) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const addr of addresses) {
|
|
73
|
+
const normAddr = addr.toLowerCase().replace(/:/g, '');
|
|
74
|
+
if (this._found.has(normAddr)) continue;
|
|
75
|
+
try {
|
|
76
|
+
const device = await this._adapter.getDevice(addr);
|
|
77
|
+
const name = await Promise.resolve(device.getName()).catch(() => '') || '';
|
|
78
|
+
if (!isBluettiDevice(name)) continue;
|
|
79
|
+
this._found.set(normAddr, { address: addr, name, device });
|
|
80
|
+
this._log(`Discovered Bluetti device: ${name} [${addr}]`);
|
|
81
|
+
this.emit('discovered', { address: addr, name, device });
|
|
82
|
+
} catch (_) {
|
|
83
|
+
// Device may have disappeared before we could read it; skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
stopScan() {
|
|
73
89
|
if (!this._scanning) return;
|
|
74
90
|
clearTimeout(this._scanTimer);
|
|
91
|
+
clearInterval(this._pollTimer);
|
|
75
92
|
this._scanning = false;
|
|
76
|
-
|
|
93
|
+
// Decrement BlueZ discovery reference count — other plugins' sessions keep scanning.
|
|
94
|
+
if (this._adapter) this._adapter.stopDiscovery().catch(() => {});
|
|
77
95
|
this._log(`Scan complete. Found ${this._found.size} Bluetti device(s).`);
|
|
78
|
-
this.emit('scanComplete', [...this._found.values()].map(
|
|
79
|
-
address: p.address || p.id,
|
|
80
|
-
name: (p.advertisement.localName || 'unknown').trim(),
|
|
81
|
-
})));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Retrieve a previously-discovered peripheral by address (for reconnect).
|
|
85
|
-
getPeripheral(address) {
|
|
86
|
-
return this._found.get(address) || null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Start a persistent background scan (noble keeps discovering; don't auto-stop).
|
|
90
|
-
startPassiveScan() {
|
|
91
|
-
this._initNoble().then(() => {
|
|
92
|
-
this._scanning = true;
|
|
93
|
-
this._noble.startScanning([], true); // allowDuplicates=true so we see RSSI updates
|
|
94
|
-
}).catch(err => {
|
|
95
|
-
this._log(`BLE init failed: ${err.message}`);
|
|
96
|
-
this.emit('error', err);
|
|
97
|
-
});
|
|
96
|
+
this.emit('scanComplete', [...this._found.values()].map(({ address, name }) => ({ address, name })));
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
stopAll() {
|
|
101
100
|
this.stopScan();
|
|
102
|
-
if (this.
|
|
103
|
-
try { this.
|
|
101
|
+
if (this._destroy) {
|
|
102
|
+
try { this._destroy(); } catch (_) {}
|
|
103
|
+
this._destroy = null;
|
|
104
|
+
this._adapter = null;
|
|
105
|
+
this._bluetooth = null;
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-bluetti-plugin",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0-alpha",
|
|
4
4
|
"signalk": {
|
|
5
5
|
"displayName": "Bluetti Monitoring"
|
|
6
6
|
},
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"start": "node index.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@
|
|
33
|
+
"@naugehyde/node-ble": "^1.13.5",
|
|
34
34
|
"csv-parse": "^5.5.6"
|
|
35
35
|
}
|
|
36
36
|
}
|