@matter/nodejs-ble 0.12.0-alpha.0-20250101-22e7c1044 → 0.12.0-alpha.0-20250107-af5a068c3

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.
Files changed (42) hide show
  1. package/dist/cjs/BleScanner.d.ts +2 -2
  2. package/dist/cjs/BleScanner.d.ts.map +1 -1
  3. package/dist/cjs/BleScanner.js +19 -13
  4. package/dist/cjs/BleScanner.js.map +1 -1
  5. package/dist/cjs/BlenoBleServer.d.ts.map +1 -1
  6. package/dist/cjs/BlenoBleServer.js +4 -4
  7. package/dist/cjs/BlenoBleServer.js.map +1 -1
  8. package/dist/cjs/NobleBleChannel.d.ts +3 -2
  9. package/dist/cjs/NobleBleChannel.d.ts.map +1 -1
  10. package/dist/cjs/NobleBleChannel.js +252 -101
  11. package/dist/cjs/NobleBleChannel.js.map +2 -2
  12. package/dist/cjs/NobleBleClient.d.ts.map +1 -1
  13. package/dist/cjs/NobleBleClient.js +15 -7
  14. package/dist/cjs/NobleBleClient.js.map +1 -1
  15. package/dist/cjs/NodeJsBle.d.ts +1 -2
  16. package/dist/cjs/NodeJsBle.d.ts.map +1 -1
  17. package/dist/cjs/NodeJsBle.js +30 -14
  18. package/dist/cjs/NodeJsBle.js.map +1 -1
  19. package/dist/esm/BleScanner.d.ts +2 -2
  20. package/dist/esm/BleScanner.d.ts.map +1 -1
  21. package/dist/esm/BleScanner.js +19 -13
  22. package/dist/esm/BleScanner.js.map +1 -1
  23. package/dist/esm/BlenoBleServer.d.ts.map +1 -1
  24. package/dist/esm/BlenoBleServer.js +4 -4
  25. package/dist/esm/BlenoBleServer.js.map +1 -1
  26. package/dist/esm/NobleBleChannel.d.ts +3 -2
  27. package/dist/esm/NobleBleChannel.d.ts.map +1 -1
  28. package/dist/esm/NobleBleChannel.js +253 -102
  29. package/dist/esm/NobleBleChannel.js.map +2 -2
  30. package/dist/esm/NobleBleClient.d.ts.map +1 -1
  31. package/dist/esm/NobleBleClient.js +15 -7
  32. package/dist/esm/NobleBleClient.js.map +1 -1
  33. package/dist/esm/NodeJsBle.d.ts +1 -2
  34. package/dist/esm/NodeJsBle.d.ts.map +1 -1
  35. package/dist/esm/NodeJsBle.js +30 -14
  36. package/dist/esm/NodeJsBle.js.map +1 -1
  37. package/package.json +5 -5
  38. package/src/BleScanner.ts +23 -13
  39. package/src/BlenoBleServer.ts +6 -4
  40. package/src/NobleBleChannel.ts +316 -126
  41. package/src/NobleBleClient.ts +17 -7
  42. package/src/NodeJsBle.ts +32 -14
@@ -10,8 +10,10 @@ import {
10
10
  InternalError,
11
11
  Logger,
12
12
  NetInterface,
13
+ NetworkError,
13
14
  ServerAddress,
14
15
  Time,
16
+ Timer,
15
17
  TransportInterface,
16
18
  createPromise,
17
19
  } from "@matter/general";
@@ -24,7 +26,6 @@ import {
24
26
  BTP_CONN_RSP_TIMEOUT_MS,
25
27
  BTP_MAXIMUM_WINDOW_SIZE,
26
28
  BTP_SUPPORTED_VERSIONS,
27
- Ble,
28
29
  BleChannel,
29
30
  BleError,
30
31
  BtpCodec,
@@ -60,157 +61,311 @@ function nobleUuidToUuid(uuid: string): string {
60
61
  return parts.join("-");
61
62
  }
62
63
 
64
+ type BleConnectionGuard = {
65
+ connectTimeout: Timer;
66
+ interviewTimeout: Timer;
67
+ disconnectTimeout: Timer;
68
+ };
69
+
63
70
  export class NobleBleCentralInterface implements NetInterface {
64
- private openChannels: Map<ServerAddress, Peripheral> = new Map();
65
- private onMatterMessageListener: ((socket: Channel<Uint8Array>, data: Uint8Array) => void) | undefined;
71
+ #bleScanner: BleScanner;
72
+ #connectionsInProgress = new Set<ServerAddress>();
73
+ #connectionGuards = new Set<BleConnectionGuard>();
74
+ #openChannels = new Map<ServerAddress, Peripheral>();
75
+ #onMatterMessageListener: ((socket: Channel<Uint8Array>, data: Uint8Array) => void) | undefined;
76
+ #closed = false;
77
+
78
+ constructor(bleScanner: BleScanner) {
79
+ this.#bleScanner = bleScanner;
80
+ }
66
81
 
67
82
  openChannel(address: ServerAddress, tryCount = 1): Promise<Channel<Uint8Array>> {
83
+ if (this.#closed) {
84
+ throw new NetworkError("Network interface is closed");
85
+ }
68
86
  return new Promise((resolve, reject) => {
69
- if (this.onMatterMessageListener === undefined) {
70
- reject(new InternalError(`Network Interface was not added to the system yet.`));
87
+ if (this.#onMatterMessageListener === undefined) {
88
+ reject(new InternalError(`Network Interface was not added to the system yet, so can not connect it.`));
71
89
  return;
72
90
  }
73
91
  if (address.type !== "ble") {
74
92
  reject(new InternalError(`Unsupported address type ${address.type}.`));
75
93
  return;
76
94
  }
77
-
78
- // Get the peripheral by address and connect to it.
79
- const { peripheral, hasAdditionalAdvertisementData } = (
80
- Ble.get().getBleScanner() as BleScanner
81
- ).getDiscoveredDevice(address.peripheralAddress);
82
-
95
+ const { peripheralAddress } = address;
83
96
  if (tryCount > 3) {
84
- reject(new BleError(`Failed to connect to peripheral ${peripheral.address}`));
97
+ reject(new BleError(`Failed to connect to peripheral ${peripheralAddress}`));
85
98
  return;
86
99
  }
87
100
 
88
- logger.debug("BLE peripheral state", peripheral.state);
89
- if (peripheral.state === "connected" || peripheral.state === "connecting") {
101
+ // Get the peripheral by address and connect to it.
102
+ const { peripheral, hasAdditionalAdvertisementData } =
103
+ this.#bleScanner.getDiscoveredDevice(peripheralAddress);
104
+
105
+ if (this.#openChannels.has(address)) {
90
106
  reject(
91
107
  new BleError(
92
- `Peripheral ${address.peripheralAddress} is already connected or connecting. Only one connection supported right now.`,
108
+ `Peripheral ${peripheralAddress} is already connected. Only one connection supported right now.`,
93
109
  ),
94
110
  );
95
111
  return;
96
112
  }
97
- if (this.openChannels.has(address)) {
113
+ if (this.#connectionsInProgress.has(address)) {
114
+ logger.debug(`Connection to peripheral ${peripheralAddress} is already in progress.`);
115
+ return;
116
+ }
117
+
118
+ if (peripheral.state === "error") {
119
+ // Weired state, so better cancel here and try a re-discovery
98
120
  reject(
99
121
  new BleError(
100
- `Peripheral ${address.peripheralAddress} is already connected. Only one connection supported right now.`,
122
+ `Can not connect to peripheral "${peripheralAddress}" because unexpected state "${peripheral.state}"`,
101
123
  ),
102
124
  );
103
125
  return;
104
126
  }
105
- if (peripheral.state !== "disconnected") {
106
- // Try to cleanup strange "in between" states
107
- peripheral.disconnectAsync().then(() => this.openChannel(address, tryCount), reject);
108
- return;
109
- }
110
-
111
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
112
- peripheral.once("connect", async () => {
113
- if (this.onMatterMessageListener === undefined) {
114
- reject(new InternalError(`Network Interface was not added to the system yet.`));
115
- return;
116
- }
117
127
 
118
- const services = await peripheral.discoverServicesAsync([BLE_MATTER_SERVICE_UUID]);
119
- logger.debug(`Found services: ${services.map(s => s.uuid).join(", ")}`);
120
-
121
- for (const service of services) {
122
- logger.debug(`found service: ${service.uuid}`);
123
- if (service.uuid !== BLE_MATTER_SERVICE_UUID) continue;
124
-
125
- // So, discover its characteristics.
126
- const characteristics = await service.discoverCharacteristicsAsync();
127
-
128
- let characteristicC1ForWrite: Characteristic | undefined;
129
- let characteristicC2ForSubscribe: Characteristic | undefined;
130
- let additionalCommissioningRelatedData: Uint8Array | undefined;
131
-
132
- for (const characteristic of characteristics) {
133
- // Loop through each characteristic and match them to the UUIDs that we know about.
134
- logger.debug("found characteristic:", characteristic.uuid, characteristic.properties);
135
-
136
- switch (nobleUuidToUuid(characteristic.uuid)) {
137
- case BLE_MATTER_C1_CHARACTERISTIC_UUID:
138
- logger.debug("found C1 characteristic");
139
- characteristicC1ForWrite = characteristic;
140
- break;
141
-
142
- case BLE_MATTER_C2_CHARACTERISTIC_UUID:
143
- logger.debug("found C2 characteristic");
144
- characteristicC2ForSubscribe = characteristic;
145
- break;
146
-
147
- case BLE_MATTER_C3_CHARACTERISTIC_UUID:
148
- logger.debug("found C3 characteristic");
149
- if (hasAdditionalAdvertisementData) {
150
- logger.debug("reading additional commissioning related data");
151
- const data = await characteristic.readAsync();
152
- additionalCommissioningRelatedData = new Uint8Array(data);
153
- logger.debug("additional data", data);
154
- }
155
- }
156
- }
157
-
158
- if (!characteristicC1ForWrite || !characteristicC2ForSubscribe) {
159
- logger.debug("missing characteristics");
160
- continue;
128
+ // Guard object to indicate if the connection was cancelled. This is used as safe guard in some places
129
+ // if data come in delayed after we already gave up.
130
+ const connectionGuard: BleConnectionGuard = {
131
+ // Timeout when trying to connect to the device because sometimes connect fails and noble does
132
+ // not emit an event. If device does not connect we do not try any longer and reject the promise
133
+ // because a re-discovery is the best option to get teh device into a good state again
134
+ connectTimeout: Time.getTimer("BLE connect timeout", 60_000, () => {
135
+ logger.debug(`Timeout while connecting to peripheral ${peripheralAddress}`);
136
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
137
+ peripheral.removeListener("connect", connectHandler);
138
+ peripheral.removeListener("disconnect", reTryHandler);
139
+ clearConnectionGuard();
140
+ reject(new BleError(`Timeout while connecting to peripheral ${peripheralAddress}`));
141
+ }),
142
+ disconnectTimeout: Time.getTimer("BLE disconnect timeout", 60_000, () => {
143
+ logger.debug(`Timeout while disconnecting to peripheral ${peripheralAddress}`);
144
+ peripheral.removeListener("disconnect", reTryHandler);
145
+ clearConnectionGuard();
146
+ reject(new BleError(`Timeout while disconnecting to peripheral ${peripheralAddress}`));
147
+ }),
148
+ // Timeout when trying to interview the device because sometimes when no response from device
149
+ // comes noble does not resolve promises
150
+ interviewTimeout: Time.getTimer("BLE interview timeout", 60_000, () => {
151
+ logger.debug(`Timeout while interviewing peripheral ${peripheralAddress}`);
152
+ peripheral.removeListener("disconnect", reTryHandler);
153
+ clearConnectionGuard();
154
+ if (peripheral.state === "connected") {
155
+ // We accept the dangling promise potentially because we got a timeout on reading data,
156
+ // so chance is high also disconnect does not work reliably for now
157
+ peripheral
158
+ .disconnectAsync()
159
+ .catch(error => logger.error(`Ignored error while disconnecting`, error));
161
160
  }
161
+ reject(new BleError(`Timeout while interviewing peripheral ${peripheralAddress}`));
162
+ }),
163
+ };
164
+ this.#connectionGuards.add(connectionGuard);
165
+
166
+ const clearConnectionGuard = () => {
167
+ const { connectTimeout, interviewTimeout, disconnectTimeout } = connectionGuard;
168
+ connectTimeout?.stop();
169
+ interviewTimeout?.stop();
170
+ disconnectTimeout?.stop();
171
+ this.#connectionGuards.delete(connectionGuard);
172
+ };
162
173
 
163
- peripheral.removeAllListeners("disconnect");
164
- this.openChannels.set(address, peripheral);
165
- resolve(
166
- await NobleBleChannel.create(
167
- peripheral,
168
- characteristicC1ForWrite,
169
- characteristicC2ForSubscribe,
170
- this.onMatterMessageListener,
171
- additionalCommissioningRelatedData,
172
- ),
173
- );
174
- return;
175
- }
174
+ // Handler to retry the connection. Called on disconnections and errors.
175
+ const reTryHandler = (error?: any) => {
176
+ // Cancel tracking states because we are done in this context
177
+ clearConnectionGuard();
178
+ this.#connectionsInProgress.delete(address);
179
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
180
+ peripheral.removeListener("connect", connectHandler);
181
+ peripheral.removeListener("disconnect", reTryHandler);
176
182
 
177
- peripheral.removeAllListeners("disconnect");
178
- reject(new BleError(`Peripheral ${peripheral.address} does not have the required characteristics`));
179
- });
180
- const reTryHandler = async (error?: any) => {
181
183
  if (error) {
182
184
  logger.error(
183
- `Peripheral ${peripheral.address} disconnected while trying to connect, try again`,
185
+ `Peripheral ${peripheralAddress} disconnected while trying to connect, try again`,
184
186
  error,
185
187
  );
186
188
  } else {
187
- logger.info(`Peripheral ${peripheral.address} disconnected while trying to connect, try gain`);
189
+ logger.info(`Peripheral ${peripheralAddress} disconnected while trying to connect, try again`);
188
190
  }
189
- // Cleanup listeners and try again
190
- peripheral.removeAllListeners("disconnect");
191
- peripheral.removeAllListeners("connect");
191
+
192
+ // Try again and chain promises
192
193
  this.openChannel(address, tryCount + 1)
193
194
  .then(resolve)
194
195
  .catch(reject);
195
196
  };
196
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
197
- peripheral.once("disconnect", reTryHandler);
198
- logger.debug(`Connect to Peripheral now (try ${tryCount})`);
199
- peripheral.connectAsync().catch(reTryHandler);
197
+
198
+ const connectHandler = async (error?: any) => {
199
+ connectionGuard.connectTimeout.stop(); // Connection done, so clear timeout
200
+ if (!this.#connectionGuards.has(connectionGuard)) {
201
+ // Seems that the response was delayed and this process was cancelled in the meantime
202
+ return;
203
+ }
204
+ if (error) {
205
+ clearConnectionGuard();
206
+ reject(new BleError(`Error while connecting to peripheral ${peripheralAddress}`, error));
207
+ return;
208
+ }
209
+ if (this.#onMatterMessageListener === undefined) {
210
+ clearConnectionGuard();
211
+ reject(new InternalError(`Network Interface was not added to the system yet or was cleared.`));
212
+ return;
213
+ }
214
+
215
+ if (this.#connectionsInProgress.has(address)) {
216
+ return;
217
+ }
218
+ this.#connectionsInProgress.add(address);
219
+
220
+ try {
221
+ connectionGuard.interviewTimeout.start();
222
+ const services = await peripheral.discoverServicesAsync([BLE_MATTER_SERVICE_UUID]);
223
+ if (!this.#connectionGuards.has(connectionGuard)) {
224
+ // Seems that the response was delayed and this process was cancelled in the meantime
225
+ return;
226
+ }
227
+ logger.debug(
228
+ `Peripheral ${peripheralAddress}: Found services: ${services.map(s => s.uuid).join(", ")}`,
229
+ );
230
+
231
+ for (const service of services) {
232
+ logger.debug(`Peripheral ${peripheralAddress}: Handle service: ${service.uuid}`);
233
+ if (service.uuid !== BLE_MATTER_SERVICE_UUID) continue;
234
+
235
+ // It's Matter, discover its characteristics.
236
+ const characteristics = await service.discoverCharacteristicsAsync();
237
+ if (!this.#connectionGuards.has(connectionGuard)) {
238
+ // Seems that the response was delayed and this process was cancelled in the meantime
239
+ return;
240
+ }
241
+
242
+ let characteristicC1ForWrite: Characteristic | undefined;
243
+ let characteristicC2ForSubscribe: Characteristic | undefined;
244
+ let additionalCommissioningRelatedData: Uint8Array | undefined;
245
+
246
+ for (const characteristic of characteristics) {
247
+ // Loop through each characteristic and match them to the UUIDs that we know about.
248
+ logger.debug(
249
+ `Peripheral ${peripheralAddress}: Handle characteristic:`,
250
+ characteristic.uuid,
251
+ characteristic.properties,
252
+ );
253
+
254
+ switch (nobleUuidToUuid(characteristic.uuid)) {
255
+ case BLE_MATTER_C1_CHARACTERISTIC_UUID:
256
+ logger.debug(`Peripheral ${peripheralAddress}: Found C1 characteristic`);
257
+ characteristicC1ForWrite = characteristic;
258
+ break;
259
+
260
+ case BLE_MATTER_C2_CHARACTERISTIC_UUID:
261
+ logger.debug(`Peripheral ${peripheralAddress}: Found C2 characteristic`);
262
+ characteristicC2ForSubscribe = characteristic;
263
+ break;
264
+
265
+ case BLE_MATTER_C3_CHARACTERISTIC_UUID:
266
+ logger.debug(`Peripheral ${peripheralAddress}: Found C3 characteristic`);
267
+ if (hasAdditionalAdvertisementData) {
268
+ logger.debug(
269
+ `Peripheral ${peripheralAddress}: Reading additional commissioning related data`,
270
+ );
271
+ const data = await characteristic.readAsync();
272
+ if (!this.#connectionGuards.has(connectionGuard)) {
273
+ // Seems that the response was delayed and this process was cancelled in the meantime
274
+ return;
275
+ }
276
+ additionalCommissioningRelatedData = new Uint8Array(data);
277
+ logger.debug(`Peripheral ${peripheralAddress}: Additional data:`, data);
278
+ }
279
+ }
280
+ }
281
+
282
+ if (!characteristicC1ForWrite || !characteristicC2ForSubscribe) {
283
+ logger.debug(
284
+ `Peripheral ${peripheralAddress}: Missing required Matter characteristics. Ignore.`,
285
+ );
286
+ continue;
287
+ }
288
+
289
+ connectionGuard.interviewTimeout.stop();
290
+ peripheral.removeListener("disconnect", reTryHandler);
291
+ this.#openChannels.set(address, peripheral);
292
+ try {
293
+ resolve(
294
+ await NobleBleChannel.create(
295
+ peripheral,
296
+ characteristicC1ForWrite,
297
+ characteristicC2ForSubscribe,
298
+ this.#onMatterMessageListener,
299
+ additionalCommissioningRelatedData,
300
+ ),
301
+ );
302
+ clearConnectionGuard();
303
+ this.#connectionsInProgress.delete(address);
304
+ } catch (error) {
305
+ this.#connectionsInProgress.delete(address);
306
+ this.#openChannels.delete(address);
307
+ await peripheral.disconnectAsync();
308
+ reTryHandler(error);
309
+ return;
310
+ }
311
+ }
312
+ } finally {
313
+ this.#connectionsInProgress.delete(address);
314
+ clearConnectionGuard();
315
+ }
316
+
317
+ peripheral.removeListener("disconnect", reTryHandler);
318
+ reject(
319
+ new BleError(`Peripheral ${peripheralAddress} does not have the required Matter characteristics`),
320
+ );
321
+ };
322
+
323
+ if (peripheral.state === "connected") {
324
+ logger.debug(`Peripheral ${peripheralAddress}: Already connected`);
325
+ connectHandler().catch(error => logger.warn(`Error while connecting`, error)); // Error should never happen
326
+ } else if (peripheral.state === "disconnecting") {
327
+ logger.debug(`Peripheral ${peripheralAddress}: Disconnect in progress`);
328
+ connectionGuard.disconnectTimeout.start();
329
+ tryCount--;
330
+ peripheral.once("disconnect", reTryHandler);
331
+ } else {
332
+ if (peripheral.state === "connecting") {
333
+ peripheral.cancelConnect(); // Send cancel to noble to make sure we can connect
334
+ peripheral.state = "disconnected"; // Manually fix status because noble does not do it
335
+ }
336
+ // connecting, disconnected
337
+ connectionGuard.connectTimeout.start();
338
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
339
+ peripheral.once("connect", connectHandler);
340
+ peripheral.once("disconnect", reTryHandler);
341
+ logger.debug(`Peripheral ${peripheralAddress}: Connect to Peripheral now (try ${tryCount})`);
342
+ peripheral.connectAsync().catch(error => {
343
+ if (!this.#connectionGuards.has(connectionGuard)) {
344
+ // Seems that the response was delayed and this process was cancelled in the meantime
345
+ return;
346
+ }
347
+ logger.info(`Peripheral ${peripheralAddress}: Error while connecting to peripheral`, error);
348
+ reTryHandler(error);
349
+ });
350
+ }
200
351
  });
201
352
  }
202
353
 
203
354
  onData(listener: (socket: Channel<Uint8Array>, data: Uint8Array) => void): TransportInterface.Listener {
204
- this.onMatterMessageListener = listener;
355
+ this.#onMatterMessageListener = listener;
205
356
  return {
206
357
  close: async () => await this.close(),
207
358
  };
208
359
  }
209
360
 
210
361
  async close() {
211
- for (const peripheral of this.openChannels.values()) {
212
- await peripheral.disconnectAsync();
362
+ this.#closed = true;
363
+ for (const peripheral of this.#openChannels.values()) {
364
+ peripheral
365
+ .disconnectAsync()
366
+ .catch(error => logger.error(`Peripheral ${peripheral.address}: Error while disconnecting`, error));
213
367
  }
368
+ this.#openChannels.clear();
214
369
  }
215
370
 
216
371
  supports(type: ChannelType, _address?: string) {
@@ -229,39 +384,62 @@ export class NobleBleChannel extends BleChannel<Uint8Array> {
229
384
  onMatterMessageListener: (socket: Channel<Uint8Array>, data: Uint8Array) => void,
230
385
  _additionalCommissioningRelatedData?: Uint8Array,
231
386
  ): Promise<NobleBleChannel> {
387
+ const { address: peripheralAddress } = peripheral;
232
388
  let mtu = peripheral.mtu ?? 0;
233
389
  if (mtu > BLE_MAXIMUM_BTP_MTU) {
234
390
  mtu = BLE_MAXIMUM_BTP_MTU;
235
391
  }
236
- logger.debug(`Using MTU=${mtu} (Peripheral MTU=${peripheral.mtu})`);
392
+ logger.debug(
393
+ `Peripheral ${peripheralAddress}: Using MTU=${mtu} bytes (Peripheral supports up to ${peripheral.mtu} bytes)`,
394
+ );
395
+
396
+ const {
397
+ promise: handshakeResponseReceivedPromise,
398
+ resolver: handshakeResolver,
399
+ rejecter: handshakeRejecter,
400
+ } = createPromise<Buffer>();
401
+
402
+ const handshakeHandler = (data: Buffer, isNotification: boolean) => {
403
+ if (data[0] === 0x65 && data[1] === 0x6c && data.length === 6) {
404
+ // Check if the first two bytes and length match the Matter handshake
405
+ logger.info(
406
+ `Peripheral ${peripheralAddress}: Received Matter handshake response: ${data.toString("hex")}.`,
407
+ );
408
+ btpHandshakeTimeout.stop();
409
+ handshakeResolver(data);
410
+ } else {
411
+ logger.debug(
412
+ `Peripheral ${peripheralAddress}: Received first data on C2: ${data.toString("hex")} (isNotification: ${isNotification}) - No handshake response, inforing`,
413
+ );
414
+ }
415
+ };
416
+
417
+ const btpHandshakeTimeout = Time.getTimer("BLE handshake timeout", BTP_CONN_RSP_TIMEOUT_MS, async () => {
418
+ characteristicC2ForSubscribe.removeListener("data", handshakeHandler);
419
+ characteristicC2ForSubscribe
420
+ .unsubscribeAsync()
421
+ .catch(error => logger.error(`Peripheral ${peripheralAddress}: Error while unsubscribing`, error));
422
+ logger.debug(
423
+ `Peripheral ${peripheralAddress}: Handshake Response not received. Disconnected from peripheral`,
424
+ );
425
+
426
+ handshakeRejecter(new BleError(`Peripheral ${peripheralAddress}: Handshake Response not received`));
427
+ }).start();
428
+
237
429
  const btpHandshakeRequest = BtpCodec.encodeBtpHandshakeRequest({
238
430
  versions: BTP_SUPPORTED_VERSIONS,
239
431
  attMtu: mtu,
240
432
  clientWindowSize: BTP_MAXIMUM_WINDOW_SIZE,
241
433
  });
242
- logger.debug(`sending BTP handshake request: ${Logger.toJSON(btpHandshakeRequest)}`);
434
+ logger.debug(
435
+ `Peripheral ${peripheralAddress}: Sending BTP handshake request: ${Logger.toJSON(btpHandshakeRequest)}`,
436
+ );
243
437
  await characteristicC1ForWrite.writeAsync(Buffer.from(btpHandshakeRequest.buffer), false);
244
438
 
245
- const btpHandshakeTimeout = Time.getTimer("BLE handshake timeout", BTP_CONN_RSP_TIMEOUT_MS, async () => {
246
- await peripheral.disconnectAsync();
247
- logger.debug("Handshake Response not received. Disconnected from peripheral");
248
- }).start();
249
-
250
- logger.debug("subscribing to C2 characteristic");
439
+ logger.debug(`Peripheral ${peripheralAddress}: Subscribing to C2 characteristic`);
251
440
  await characteristicC2ForSubscribe.subscribeAsync();
252
441
 
253
- const { promise: handshakeResponseReceivedPromise, resolver } = createPromise<Buffer>();
254
-
255
- characteristicC2ForSubscribe.once("data", (data, isNotification) => {
256
- logger.debug(`received first data on C2: ${data.toString("hex")} (isNotification: ${isNotification})`);
257
-
258
- if (data[0] === 0x65 && data[1] === 0x6c && data.length === 6) {
259
- // Check if the first two bytes and length match the Matter handshake
260
- logger.info(`Received Matter handshake response: ${data.toString("hex")}.`);
261
- btpHandshakeTimeout.stop();
262
- resolver(data);
263
- }
264
- });
442
+ characteristicC2ForSubscribe.once("data", handshakeHandler);
265
443
 
266
444
  const handshakeResponse = await handshakeResponseReceivedPromise;
267
445
 
@@ -275,7 +453,11 @@ export class NobleBleChannel extends BleChannel<Uint8Array> {
275
453
  async () =>
276
454
  void characteristicC2ForSubscribe
277
455
  .unsubscribeAsync()
278
- .then(() => peripheral.disconnectAsync().then(() => logger.debug("disconnected from peripheral"))),
456
+ .then(() =>
457
+ peripheral
458
+ .disconnectAsync()
459
+ .then(() => logger.debug(`Peripheral ${peripheralAddress}: Disconnected from peripheral`)),
460
+ ),
279
461
  // callback to forward decoded and de-assembled Matter messages to ExchangeManager
280
462
  async (data: Uint8Array) => {
281
463
  if (onMatterMessageListener === undefined) {
@@ -286,7 +468,9 @@ export class NobleBleChannel extends BleChannel<Uint8Array> {
286
468
  );
287
469
 
288
470
  characteristicC2ForSubscribe.on("data", (data, isNotification) => {
289
- logger.debug(`received data on C2: ${data.toString("hex")} (isNotification: ${isNotification})`);
471
+ logger.debug(
472
+ `Peripheral ${peripheralAddress}: received data on C2: ${data.toString("hex")} (isNotification: ${isNotification})`,
473
+ );
290
474
 
291
475
  void btpSession.handleIncomingBleData(new Uint8Array(data));
292
476
  });
@@ -316,11 +500,15 @@ export class NobleBleChannel extends BleChannel<Uint8Array> {
316
500
  */
317
501
  async send(data: Uint8Array) {
318
502
  if (!this.connected) {
319
- logger.debug("Cannot send data because not connected to peripheral.");
503
+ logger.debug(
504
+ `Peripheral ${this.peripheral.address}: Cannot send data because not connected to peripheral.`,
505
+ );
320
506
  return;
321
507
  }
322
508
  if (this.btpSession === undefined) {
323
- throw new BtpFlowError(`Cannot send data, no BTP session initialized`);
509
+ throw new BtpFlowError(
510
+ `Peripheral ${this.peripheral.address}: Cannot send data, no BTP session initialized`,
511
+ );
324
512
  }
325
513
  await this.btpSession.sendMatterMessage(data);
326
514
  }
@@ -332,6 +520,8 @@ export class NobleBleChannel extends BleChannel<Uint8Array> {
332
520
 
333
521
  async close() {
334
522
  await this.btpSession.close();
335
- await this.peripheral.disconnectAsync();
523
+ this.peripheral
524
+ .disconnectAsync()
525
+ .catch(error => logger.error(`Peripheral ${this.peripheral.address}: Error while disconnecting`, error));
336
526
  }
337
527
  }
@@ -59,7 +59,14 @@ export class NobleBleClient {
59
59
  }
60
60
  });
61
61
  noble.on("discover", peripheral => this.handleDiscoveredDevice(peripheral));
62
- noble.on("scanStart", () => (this.isScanning = true));
62
+ noble.on("scanStart", () => {
63
+ if (!this.shouldScan) {
64
+ // Noble sometimes emits scanStart when we did not asked for and misses the scanStop event
65
+ // TODO: Remove as soon as Noble fixed this behavior
66
+ return;
67
+ }
68
+ this.isScanning = true;
69
+ });
63
70
  noble.on("scanStop", () => (this.isScanning = false));
64
71
  }
65
72
 
@@ -71,12 +78,14 @@ export class NobleBleClient {
71
78
  }
72
79
 
73
80
  public async startScanning() {
74
- if (this.isScanning) return;
81
+ if (this.isScanning) {
82
+ return;
83
+ }
75
84
 
76
85
  this.shouldScan = true;
77
86
  if (this.nobleState === "poweredOn") {
78
87
  logger.debug("Start BLE scanning for Matter Services ...");
79
- await noble.startScanningAsync([BLE_MATTER_SERVICE_UUID], false);
88
+ await noble.startScanningAsync([BLE_MATTER_SERVICE_UUID], true);
80
89
  } else {
81
90
  logger.debug("noble state is not poweredOn ... delay scanning till poweredOn");
82
91
  }
@@ -94,25 +103,26 @@ export class NobleBleClient {
94
103
  // The advertisement data contains a name, power level (if available), certain advertised service uuids,
95
104
  // as well as manufacturer data.
96
105
  // {"localName":"MATTER-3840","serviceData":[{"uuid":"fff6","data":{"type":"Buffer","data":[0,0,15,241,255,1,128,0]}}],"serviceUuids":["fff6"],"solicitationServiceUuids":[],"serviceSolicitationUuids":[]}
106
+ const address = peripheral.address;
97
107
  logger.debug(
98
- `Found peripheral ${peripheral.address} (${peripheral.advertisement.localName}): ${Logger.toJSON(
108
+ `Found peripheral ${address} (${peripheral.advertisement.localName}): ${Logger.toJSON(
99
109
  peripheral.advertisement,
100
110
  )}`,
101
111
  );
102
112
 
103
113
  if (!peripheral.connectable) {
104
- logger.info(`Peripheral ${peripheral.address} is not connectable ... ignoring`);
114
+ logger.info(`Peripheral ${address} is not connectable ... ignoring`);
105
115
  return;
106
116
  }
107
117
  const matterServiceData = peripheral.advertisement.serviceData.find(
108
118
  serviceData => serviceData.uuid === BLE_MATTER_SERVICE_UUID,
109
119
  );
110
120
  if (matterServiceData === undefined || matterServiceData.data.length !== 8) {
111
- logger.info(`Peripheral ${peripheral.address} does not advertise Matter Service ... ignoring`);
121
+ logger.info(`Peripheral ${address} does not advertise Matter Service ... ignoring`);
112
122
  return;
113
123
  }
114
124
 
115
- this.discoveredPeripherals.set(peripheral.address, { peripheral, matterServiceData: matterServiceData.data });
125
+ this.discoveredPeripherals.set(address, { peripheral, matterServiceData: matterServiceData.data });
116
126
 
117
127
  this.deviceDiscoveredCallback?.(peripheral, matterServiceData.data);
118
128
  }